Compare commits

..

297 Commits

Author SHA1 Message Date
Sebastian Sebbie Silbermann
7806ec2fe1 19.0.4
Some checks failed
(Runtime) Build and Test / Discover flow inline configs (push) Has been cancelled
(Runtime) Build and Test / Confirm generated inline Fizz runtime is up to date (push) Has been cancelled
(Runtime) Build and Test / Check flags (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 9) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 9) (push) Has been cancelled
(Runtime) Build and Test / Download base build for sizebot (push) Has been cancelled
(Runtime) Build and Test / Flow check ${{ matrix.flow_inline_config_shortname }} (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / Process artifacts combined (push) Has been cancelled
(Runtime) Build and Test / Search build artifacts for unminified errors (push) Has been cancelled
(Runtime) Build and Test / Check release dependencies (push) Has been cancelled
(Runtime) Build and Test / Check fixtures DOM (stable) (push) Has been cancelled
(Runtime) Build and Test / Run fixtures Flight tests (push) Has been cancelled
(Runtime) Build and Test / Build DevTools and process artifacts (push) Has been cancelled
(Runtime) Build and Test / Run DevTools e2e tests (push) Has been cancelled
(Runtime) Build and Test / Run sizebot (push) Has been cancelled
2026-01-26 17:47:12 +01:00
Sebastian Markbage
de58cca00c Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com>
Co-authored-by: Janka Uryga <lolzatu2@gmail.com>
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-01-26 16:45:51 +01:00
Sebastian Sebbie Silbermann
d8946cb6b8 Version 19.0.3
Some checks failed
(Runtime) Build and Test / Discover flow inline configs (push) Has been cancelled
(Runtime) Build and Test / Confirm generated inline Fizz runtime is up to date (push) Has been cancelled
(Runtime) Build and Test / Check flags (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 9) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 9) (push) Has been cancelled
(Runtime) Build and Test / Download base build for sizebot (push) Has been cancelled
(Runtime) Build and Test / Flow check ${{ matrix.flow_inline_config_shortname }} (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / Process artifacts combined (push) Has been cancelled
(Runtime) Build and Test / Search build artifacts for unminified errors (push) Has been cancelled
(Runtime) Build and Test / Check release dependencies (push) Has been cancelled
(Runtime) Build and Test / Check fixtures DOM (stable) (push) Has been cancelled
(Runtime) Build and Test / Run fixtures Flight tests (push) Has been cancelled
(Runtime) Build and Test / Build DevTools and process artifacts (push) Has been cancelled
(Runtime) Build and Test / Run DevTools e2e tests (push) Has been cancelled
(Runtime) Build and Test / Run sizebot (push) Has been cancelled
2025-12-11 23:36:48 +01:00
Sebastian Markbage
bd4289b116 Add extra protection 2025-12-11 23:25:26 +01:00
Sebastian Sebbie Silbermann
61c95d04a8 Version 19.0.2
Some checks failed
(Runtime) Build and Test / Discover flow inline configs (push) Has been cancelled
(Runtime) Build and Test / Confirm generated inline Fizz runtime is up to date (push) Has been cancelled
(Runtime) Build and Test / Check flags (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=experimental --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development --persistent (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=development (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=stable --env=production (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-classic --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=www-modern --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=development --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=false (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 1/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 2/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 3/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 4/5) (push) Has been cancelled
(Runtime) Build and Test / yarn test -r=xplat --env=production --variant=true (Shard 5/5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (experimental, 9) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 0) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 1) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 10) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 11) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 12) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 13) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 14) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 15) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 16) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 17) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 18) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 19) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 2) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 3) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 4) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 5) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 6) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 7) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 8) (push) Has been cancelled
(Runtime) Build and Test / yarn build and lint (stable, 9) (push) Has been cancelled
(Runtime) Build and Test / Download base build for sizebot (push) Has been cancelled
(Runtime) Build and Test / Flow check ${{ matrix.flow_inline_config_shortname }} (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (1/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (2/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, --project=devtools -r=experimental) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=experimental --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=experimental --env=production) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=stable --env=development) (push) Has been cancelled
(Runtime) Build and Test / yarn test-build (3/3, -r=stable --env=production) (push) Has been cancelled
(Runtime) Build and Test / Process artifacts combined (push) Has been cancelled
(Runtime) Build and Test / Search build artifacts for unminified errors (push) Has been cancelled
(Runtime) Build and Test / Check release dependencies (push) Has been cancelled
(Runtime) Build and Test / Check fixtures DOM (stable) (push) Has been cancelled
(Runtime) Build and Test / Run fixtures Flight tests (push) Has been cancelled
(Runtime) Build and Test / Build DevTools and process artifacts (push) Has been cancelled
(Runtime) Build and Test / Run DevTools e2e tests (push) Has been cancelled
(Runtime) Build and Test / Run sizebot (push) Has been cancelled
2025-12-11 21:21:02 +01:00
Sebastian Markbåge
ac60e73588 Patch Promise cycles and toString on Server Functions
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-12-11 21:20:56 +01:00
Sebastian "Sebbie" Silbermann
6bc6b1c747 Run CI for backport releases (#35313) 2025-12-08 16:13:37 +01:00
Sebastian Markbage
bbed0b0ee6 Bring ReactFlightClient fixes to FlightReplyServer
We did a bunch of refactors to ReactFlightClient in PRs like #29823 and #33664.

This brings a bunch of those related refactors to the equivalent FlightReplyServer. Such as deep resolution of cycles and deferred error handling.
2025-12-03 10:00:46 +01:00
Sebastian Sebbie Silbermann
f3e3f6fbac Version 19.0.1 2025-12-02 23:12:51 +01:00
Sebastian "Sebbie" Silbermann
154f4b15e2 [release] Allow building single release channel with processed versions (#35270) 2025-12-02 22:54:07 +01:00
Andrew Clark
7aa5dda3b3 Bump RC version to 19.0.0-rc.1 (#31542) 2024-11-14 11:48:33 -05:00
Andrew Clark
988e217670 Turn on enableSiblingPrerendering in canary (#31541)
In preparation for the next RC, I set this feature flag to true
everywhere. I did not delete the feature flag yet, in case there are yet
more bugs to be discovered.

I also didn't remove the dynamic feature flag from the Meta builds; I'll
let the Meta folks handle that.
2024-11-14 11:48:14 -05:00
lauren
380f5d675d Fix sizebot (#31535)
Our CI workflows generally cache `**/node_modules` (note the glob, it
caches all transitive node_module directories) to speed up startup for
new jobs that don't change any dependencies. However it seems like one
of our caches got into a weird state (not sure how it happened) where
the `build` directory (used in various other scripts as the directory
for compiled React packages) would contain a `node_modules` directory as
well. This made sizebot size change messages very big since it would try
to compare every single file in `build/node_modules`.

The fix is to ensure we always clean the `build` directory before doing
anything with it. We can also delete that one problematic cache but this
PR is a little more resilient to other weird behavior with that
directory.
2024-11-13 15:13:46 -05:00
Sebastian Markbåge
7ac8e61211 Only log component level profiling for components that actually performed work (#31522)
This provides less context but skips a lot of noise.

Previously we were including parent components to provide context about
what is rendering but this turns out to be:

1) Very expensive due to the overhead of `performance.measure()` while
profiling.
2) Unactionable noise in the profile that hurt more than it added in
real apps with large trees.

This approach instead just add performance.measure calls for each
component that was marked as PerformedWork (which was used for this
purpose by React Profiler) or had any Effects.

Not everything gets marked with PerformedWork though. E.g. DOM nodes do
not but they can have significant render times since creating them takes
time. We might consider including them if a self-time threshold is met.

Because there is little to no context about the component anymore it
becomes really essential to get a feature from Chrome DevTools that can
link to something with more context like React DevTools.
2024-11-13 10:57:15 -05:00
mofeiZ
3770c11011 [compiler] repro for reactive ref.current accesses (#31519)
See test fixture
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31519).
* #31521
* __->__ #31519
2024-11-12 14:04:54 -05:00
Sebastian Markbåge
d9b3841ca6 Revert "Performance tracks are sorted by start time" hack (#31518)
This reverts commit d3bf32a95806b6d583ef041b8d83781cd686cfd8 which was
part of #30983

When you have very deep trees this trick can cause the top levels to
skew way too much from the real numbers. Creating unbalanced trees.

The bug should have been fixed in Chrome Canary now so that entries
added later are sorted to go first which should've addressed this issue.
2024-11-12 12:46:55 -05:00
mofeiZ
2ec26bc432 [compiler] Repro for mutable range edge case (#31479)
See test fixtures
2024-11-11 18:04:29 -05:00
Ricky
b836de613d Fix continuation bug (#31434)
## Overview

In `scheduleTaskForRootDuringMicrotask` we clear `root.callbackNode` if
the work loop is [suspended waiting on
data](ac3ca097ae/packages/react-reconciler/src/ReactFiberRootScheduler.js (L338)).

But we don't null check `root.callbackNode` before returning a
continuation in `performWorkOnRootViaSchedulerTask` where
`scheduleTaskForRootDuringMicrotask` is synchronously called, causing an
infinite loop when the only thing in the queue is something suspended
waiting on data.

This essentially restores the behavior from here:
https://github.com/facebook/react/pull/26328/files#diff-72ff2175ae3569037f0b16802a41b0cda2b2d66bb97f2bda78ed8445ed487b58L1168

Found by investigating the failures for
https://github.com/facebook/react/pull/31417

## TODO
- add a test

---------

Co-authored-by: Joe Savona <joesavona@fb.com>
2024-11-11 17:25:37 -05:00
Josh Story
ed15d5007c update flight readme wording (#31466) 2024-11-10 11:58:52 -08:00
Henry Q. Dineen
6e29479bff [devtools] allow non-coercible objects in formatConsoleArgumentsToSingleString (#31444)
## Summary

We have been getting unhandled `TypeError: Cannot convert object to
primitive value` errors in development that only occur when using
devtools. I tracked it down to `console.error()` calls coming from
Apollo Client where one of the arguments is an object without a
prototype (created with `Object.create(null)`). This causes
`formatConsoleArgumentsToSingleString()` in React's devtools to error as
the function does not defend against `String()` throwing an error.

My attempted fix is to introduce a `safeToString` function (naming
suggestions appreciated) which expects `String()` to throw on certain
object and in that case falls back to returning `[object Object]`, which
is what `String({})` would return.

## How did you test this change?

Added a new unit test.
2024-11-10 19:24:15 +00:00
Josh Story
ff595de29a [Flight] Add initial readme to react-server package (#31464)
This readme documents React Server Components from `react-server`
package enough to get an implementer started. It's not comprehensive but
it's a beginning point and crucially adds documentation for the
`prerender` API for Flight.
2024-11-08 16:07:37 -08:00
Jack Pope
989af12f72 Make prerendering always non-blocking with fix (#31452)
We've previously failed to land this change due to some internal apps
seeing infinite render loops due to external store state updates during
render. It turns out that since the `renderWasConcurrent` var was moved
into the do block, the sync render triggered from the external store
check was stuck with a `RootSuspended` `exitStatus`. So this is not
unique to sibling prerendering but more generally related to how we
handle update to a sync external store during render.

We've tested this build against local repros which now render without
crashes. We will try to add a unit test to cover the scenario as well.

---------

Co-authored-by: Andrew Clark <git@andrewclark.io>
Co-authored-by: Rick Hanlon <rickhanlonii@fb.com>
2024-11-08 12:38:41 -05:00
Alex Hunt
5c56b873ef Update React Native shims to use export syntax (#31426)
## Summary

I'm working to get the main `react-native` package parsable by modern
Flow tooling (both `flow-bundler`, `flow-api-translator`), and one
blocker is legacy `module.exports` syntax. This diff updates files which
are [synced to
`react-native`](https://github.com/facebook/react-native/tree/main/packages/react-native/Libraries/Renderer/shims)
from this repo.

## How did you test this change?

Files were pasted into `react-native-github` under fbsource, where Flow
validates .
2024-11-07 14:53:44 +00:00
Jan Kassens
682a103cde [www] set disableLegacyMode to true (#31439) 2024-11-07 09:05:31 -05:00
Jan Kassens
e1378902bb [string-refs] cleanup string ref code (#31443) 2024-11-06 14:00:10 -05:00
Sathya Gunasekaran
a88b9e5f68 [compiler] Outline JSX with non-jsx children (#31442)
Previously, we bailed out on outlining jsx that had children that were
not part of the outlined jsx.

Now, we add support for children by treating as attributes.
2024-11-06 17:54:44 +00:00
Sathya Gunasekaran
09197bb786 [compiler] Outline jsx with duplicate attributes (#31441)
Previously, we would skip outlining jsx expressions that had duplicate
jsx attributes as we would not rename them causing incorrect
compilation.

In this PR, we add outlining support for duplicate jsx attributes by
renaming them.
2024-11-06 17:50:13 +00:00
Sathya Gunasekaran
2df8f61885 [compiler] Store original and new prop names (#31440)
Previously, we'd directly store the original attributes from the jsx
expressions. But this isn't enough as we want to rename duplicate
attributes.

This PR refactors the prop collection logic to store both the original
and new names for jsx attributes in the newly outlined jsx expression.

For now, both the new and old names are the same. In the future, they
will be different when we add support for outlining expressions with
duplicate attribute names.
2024-11-06 17:44:52 +00:00
Jan Kassens
a7b83e7ceb [www] set disableStringRefs to true (#31438) 2024-11-06 12:13:43 -05:00
Sophie Alpert
66855b9637 Remove unused lastFullyObservedContext (#31435) 2024-11-06 07:35:23 -08:00
Jan Kassens
314968561b Back out "[bundles] stop building legacy Paper renderer (#31429)" (#31437)
Backs out the 2 related commits:
-
f8f6e1a21a
-
6c0f37f94b

Since I only realized when syncing that we need the version of `react`
and the legacy renderer to match.

While I investigate if there's anything we can do to work around that
while preserving the legacy renderer, this unblocks the sync.
2024-11-06 09:41:18 -05:00
Jan Kassens
d1f04722d6 [string-refs] remove enableLogStringRefsProd flag (#31414)
We no longer need this production logging.
2024-11-06 09:00:49 -05:00
Jan Kassens
3dc1e4820e Followup: remove dead test code from #30346 (#31415)
I missed that this was a constant false check when making the broader
cleanup changes in https://github.com/facebook/react/pull/30346
2024-11-06 08:58:36 -05:00
mofeiZ
c3570b158d [compiler] Collect temporaries and optional chains from inner functions (#31346)
Recursively collect identifier / property loads and optional chains from
inner functions. This PR is in preparation for #31200

Previously, we only did this in `collectHoistablePropertyLoads` to
understand hoistable property loads from inner functions.
1. collectTemporariesSidemap
2. collectOptionalChainSidemap
3. collectHoistablePropertyLoads
- ^ this recursively calls `collectTemporariesSidemap`,
`collectOptionalChainSidemap`, and `collectOptionalChainSidemap` on
inner functions
4. collectDependencies

Now, we have
1. collectTemporariesSidemap
- recursively record identifiers in inner functions. Note that we track
all temporaries in the same map as `IdentifierIds` are currently unique
across functions
2. collectOptionalChainSidemap
    - recursively records optional chain sidemaps in inner functions
3. collectHoistablePropertyLoads
    - (unchanged, except to remove recursive collection of temporaries)
4. collectDependencies
- unchanged: to be modified to recursively collect dependencies in next
PR

'
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31346).
* #31202
* #31203
* #31201
* #31200
* __->__ #31346
* #31199
2024-11-05 19:25:05 -05:00
mofeiZ
fd018af617 [compiler] Delete propagateScopeDeps (non-hir) (#31199)
`enablePropagateScopeDepsHIR` is now used extensively in Meta. This has
been tested for over two weeks in our e2e tests and production.

The rest of this stack deletes `LoweredFunction.dependencies`, which the
non-hir version of `PropagateScopeDeps` depends on. To avoid a more
forked HIR (non-hir with dependencies and hir with no dependencies),
let's go ahead and clean up the non-hir version of
PropagateScopeDepsHIR.

Note that all fixture changes in this PR were previously reviewed when
they were copied to `propagate-scope-deps-hir-fork`. Will clean up /
merge these duplicate fixtures in a later PR

'
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31199).
* #31202
* #31203
* #31201
* #31200
* #31346
* __->__ #31199
2024-11-05 19:22:04 -05:00
mofeiZ
f2f002c7c1 [compiler][be] Stabilize compiler output: sort deps and decls by name (#31362)
All dependencies and declarations of a reactive scope can be reordered
to scope start/end. i.e. generated code does not depend on conditional
short-circuiting logic as dependencies are inferred to have no side
effects.

Sorting these by name helps us get higher signal compilation snapshot
diffs when upgrading the compiler and testing PRs
2024-11-05 18:26:50 -05:00
mofeiZ
792fa065ca [compiler][ez] Clean up pragma parsing for tests + playground (#31347)
Move environment config parsing for `inlineJsxTransform`,
`lowerContextAccess`, and some dev-only options out of snap (test
fixture). These should now be available for playground via
`@inlineJsxTransform` and `lowerContextAccess`.

Other small change:
Changed zod fields from `nullish()` -> `nullable().default(null)`.
[`nullish`](https://zod.dev/?id=nullish) fields accept `null |
undefined` and default to `undefined`. We don't distinguish between null
and undefined for any of these options, so let's only accept null +
default to null. This also makes EnvironmentConfig in the playground
more accurate. Previously, some fields just didn't show up as
`prettyFormat({field: undefined})` does not print `field`.
2024-11-05 18:19:44 -05:00
mofeiZ
33195602ea [compiler][ez] tsconfig: treat all snap fixtures as modules (#31350)
Qol improvement. Currently, typescript lints treat test fixtures without
an export as a 'global script' (see
[docs](https://www.typescriptlang.org/docs/handbook/2/modules.html#how-javascript-modules-are-defined)).
This gives confusing lints for duplicate declarations (in the global
scope)
2024-11-05 17:57:18 -05:00
mofeiZ
5ca2bc6d63 [compiler][ez] Fixture repro for function hoisting bug (#31349)
Repro for bug reported by @alexmckenley
2024-11-05 17:56:53 -05:00
mofeiZ
bddb7c9b5c [compiler] Add fixture for objectexpr computed key bug (#31348)
We were bailing out on complex computed-key syntax (prior to #31344) as
we assumed that this caused bugs (due to inferring computed key rvalues
to have `freeze` effects).

This fixture shows that this bailout is unrelated to the underlying bug
2024-11-05 17:56:36 -05:00
Jan Kassens
f8f6e1a21a [bundles] update GitHub actions for commit build branch (#31432)
This is a followup to
6c0f37f94b
to unblock the job.
2024-11-05 16:04:02 -05:00
Jan Kassens
6c0f37f94b [bundles] stop building legacy Paper renderer (#31429) 2024-11-05 15:49:20 -05:00
mofeiZ
527bcaa83d [compiler] patch: rewrite scope dep/decl in inlineJsxTransform (#31431)
This bugfix is needed to land #31199 PropagateScopeDepsHIR infers scope
declarations for the `inline-jsx-transform` test fixture (the non-hir
version does not).

These declarations must get the rewritten phi identifiers
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31431).
* #31204
* #31202
* #31203
* #31201
* #31200
* #31346
* #31199
* __->__ #31431
* #31345
* #31197
2024-11-05 15:27:39 -05:00
mofeiZ
e7e269b726 [compiler] bugfix for hoistable deps for nested functions (#31345)
`PropertyPathRegistry` is responsible for uniqueing identifier and
property paths. This is necessary for the hoistability CFG merging logic
which takes unions and intersections of these nodes to determine a basic
block's hoistable reads, as a function of its neighbors. We also depend
on this to merge optional chained and non-optional chained property
paths

This fixes a small bug in #31066 in which we create a new registry for
nested functions. Now, we use the same registry for a component / hook
and all its inner functions

'
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31345).
* #31204
* #31202
* #31203
* #31201
* #31200
* #31346
* #31199
* #31431
* __->__ #31345
* #31197
2024-11-05 15:25:54 -05:00
mofeiZ
dd1a021bad [compiler][ez] Patch hoistability for ObjectMethods (#31197)
Extends #31066 to ObjectMethods (somehow missed this before).

'
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31197).
* #31204
* #31202
* #31203
* #31201
* #31200
* #31346
* #31199
* #31431
* #31345
* __->__ #31197
2024-11-05 15:25:39 -05:00
Sebastian Markbåge
156eab2f7b Fork the "empty" prepareStackTrace case for Server builds (#31427)
We don't actually want the source mapped version of `.stack` from errors
because that would cause us to not be able to associate it with a source
map in the UIs that need it. The strategy in browsers is more correct
where the display is responsible for source maps.

That's why we disable any custom `prepareStackTrace` like the ones added
by `source-map`. We reset it to `undefined`.

However, when running node with `--enable-source-maps` the default for
`prepareStackTrace` which is a V8 feature (but may exist elsewhere too
like Bun) is a source mapped version of the stack. In those environments
we need to reset it to a default implementation that doesn't apply
source maps.

We already did this in Flight using the `ReactFlightStackConfigV8.js`
config. However, we need this more generally in the
`shared/ReactComponentStackFrame` implementation.

We could always set it to the default implementation instead of
`undefined` but that's unnecessary code in browser builds and it might
lead to slightly different results. For safety and code size, this PR
does it with a fork instead.

All builds specific to `node` or `edge` (or `markup` which is a server
feature) gets the default implementation where as everything else (e.g.
browsers) get `undefined` since it's expected that this is not source
mapped. We don't have to do anything about the equivalent in React
DevTools since React DevTools doesn't run on the server.
2024-11-05 15:05:04 -05:00
lauren
b81e6dd2da [cleanup] Remove compiler runtime-compat fixture library (#31430)
There's no real reason to keep this around anymore now that the compiler
beta is released and we have validated that react-compiler-runtime is
[usable by
libraries](https://www.npmjs.com/package/react-compiler-runtime?activeTab=dependents).

Let's clean this up for now.
2024-11-05 14:14:39 -05:00
Ricky
33c7bd9ae3 Remove trim_trailing_whitespace from editorconfig (#31413)
This setting breaks `toMatchInlineSnapshot` by removing whitespace in
snapshots.
2024-11-04 15:30:02 -05:00
Jan Kassens
07aa494432 Remove enableRefAsProp feature flag (#30346)
The flag is fully rolled out.
2024-11-04 14:30:58 -05:00
Jack Pope
543eb09321 [compiler] Wrap inline jsx transform codegen in conditional (#31267)
JSX inlining is a prod-only optimization. We want to enforce this while
maintaining the same compiler output in DEV and PROD.

Here we add a conditional to the transform that only replaces JSX with
object literals outside of DEV. Then a later build step can handle DCE
based on the value of `__DEV__`
2024-11-04 13:19:05 -05:00
Timothy Yung
4d577fd216 More Unit Tests for Refs in Hidden Subtrees (#31404)
## Summary

While fixing ref lifecycles in hidden subtrees in
https://github.com/facebook/react/pull/31379, @rickhanlonii noticed that
we could also add more unit tests for other types of tags to prevent
future regressions during code refactors.

This PR adds more unit tests in the same vein as those added in
https://github.com/facebook/react/pull/31379.

## How did you test this change?

Verified unit tests pass:

```
$ yarn
$ yarn test ReactFreshIntegration-test.js
```
2024-11-04 10:46:28 -05:00
Josh Story
7c8e5e7ab8 Reenable lazy context propagation (#31405)
Reverts facebook/react#31403 to reenable lazy context propagation

The disabling was to produce a build that could help track down whether
this flag is causing a possibly related bug in transitions but we don't
intend to disable it just fix forward once we figure out what the
problem is
2024-11-01 15:17:51 -07:00
Josh Story
16409d0560 temporarily disable lazy context propagation (#31403)
disables lazy context propagation in oss to help determine if it is
causing bugs in startTransition. Will reenable after cutting a canary
release with this flag disabled
2024-11-01 15:16:26 -07:00
Sebastian Markbåge
b7e2157922 [Flight] Handle errors during JSON stringify of console values (#31391)
When we serialize debug info we should never error even though we don't
currently support everything being serialized. Since it's non-essential
dev information.

We already handle errors in the replacer but not when errors happen in
the JSON algorithm itself - such as cyclic errors.

We should ideally support cyclic objects but regardless we should
gracefully handle the errors.
2024-10-31 16:47:51 -04:00
Timothy Yung
ea3ac58669 Fix Ref Lifecycles in Hidden Subtrees (#31379)
## Summary

We're seeing certain situations in React Native development where ref
callbacks in `<Activity mode="hidden">` are sometimes invoked exactly
once with `null` without ever being called with a "current" value.

This violates the contract for refs because refs are expected to always
attach before detach (and to always eventually detach after attach).
This is *particularly* bad for refs that return cleanup functions,
because refs that return cleanup functions expect to never be invoked
with `null`. This bug causes such refs to be invoked with `null`
(because since `safelyAttachRef` was never called, `safelyDetachRef`
thinks the ref does not return a cleanup function and invokes it with
`null`).

This fix makes use of `offscreenSubtreeWasHidden` in
`commitDeletionEffectsOnFiber`, similar to how
ec52a5698e
did this for `commitDeletionEffectsOnFiber`.

## How did you test this change?

We were able to isolate the repro steps to isolate the React Native
experimental changes. However, the repro steps depend on Fast Refresh.

```
function callbackRef(current) {
  // Called once with `current` as null, upon triggering Fast Refresh.
}

<Activity mode="hidden">
  <View ref={callbackRef} />;
</Activity>
```

Ideally, we would have a unit test that verifies this behavior without
Fast Refresh. (We have evidence that this bug occurs without Fast
Refresh in real product implementations. However, we have not
successfully deduced the root cause, yet.)

This PR currently includes a unit test that reproduces the Fast Refresh
scenario, which is also demonstrated in this CodeSandbox:
https://codesandbox.io/p/sandbox/hungry-darkness-33wxy7

Verified unit tests pass:

```
$ yarn
$ yarn test
# Run with `-r=www-classic` for `enableScopeAPI` tests.
$ yarn test -r=www-classic
```

Verified on the internal React Native development branch that the bug no
longer repros.

---------

Co-authored-by: Rick Hanlon <rickhanlonii@fb.com>
2024-10-31 13:24:45 -07:00
lauren
603e6108f3 [compiler] Update react deps to experimental instead of beta (#31385)
Some tests rely on experimental APIs so let's just use
react@experimental instead of beta
2024-10-29 21:51:41 -04:00
lauren
4abe4b5821 [compiler] Check if local identifier is a hook when resolving globals (#31384)
When resolving import specifiers from the react namespace (`import
{imported as local} from 'react'`), we were previously only checking if
the `imported` identifier was a hook if we didn't already have its
definition in the global registry. We also need to check if `local` is a
hook in the case of aliasing since there may be hook-like APIs in react
that don't start with `use` (eg they are experimental or unstable).

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31384).
* #31385
* __->__ #31384
* #31383
2024-10-29 21:36:48 -04:00
lauren
3928cb00db [compiler] Ref validation repro for ImportSpecifier with renamed local (#31383)
This was originally reported in
https://github.com/reactwg/react-compiler/discussions/27.

Adding a failing repro to capture this case.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31383).
* #31385
* #31384
* __->__ #31383
2024-10-29 21:36:36 -04:00
Sebastian Markbåge
0bc3074873 Capture the source and not just the stack on first seen error (#31367)
Otherwise we can't capture the owner stack at the right location when
there's a rethrow.
2024-10-28 13:59:38 -07:00
Sathya Gunasekaran
02c0e824e4 [compiler][ez] Remove unused param (#31376) 2024-10-28 15:08:27 +00:00
Sathya Gunasekaran
aded0ef831 [compiler] Handle member expr as computed property (#31344)
This PR loosens the restriction on the types of computed properties we
can handle.

Previously, we would disallow anything that is not an identifier because
non-identifiers could be mutating. But member expressions are not
mutating so we can treat them similar to identifiers.
2024-10-28 13:10:01 +00:00
Mike Vitousek
fe04dbcbc4 [compiler] Fix to ref access check to ban ref?.current
ghstack-source-id: ea417a468e
Pull Request resolved: https://github.com/facebook/react/pull/31360
2024-10-25 16:51:36 -07:00
Jack Pope
cae764ce81 Revert "[Re-land] Make prerendering always non-blocking (#31268)" (#31355)
This reverts commit 6c4bbc7832.

It looked like the bug we found on the original land was related to
broken product code. But through landing #31268 we found additional bugs
internally. Since disabling the feature flag does not fix the bugs, we
have to revert again to unblock the sync. We can continue to debug with
our internal build.
2024-10-25 09:17:07 -07:00
Pascal Birchler
d19ba8ecdd [react-compiler-runtime] Support React 17 peer dependency (#31336)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

The recent blog post and
[documentation](https://react.dev/learn/react-compiler#using-react-compiler-with-react-17-or-18)
say that `react-compiler-runtime` supports React 17, yet it currently
requires React 18 or 19 as a peer dependency, making it unusable for
installing on a project still using React 17.

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

Manually installing the package on a React 17 codebase.

---------

Co-authored-by: lauren <poteto@users.noreply.github.com>
2024-10-24 14:08:57 -04:00
Sebastian Markbåge
1631855f43 [Flight] encodeURI filenames parsed from stack traces (#31340)
When parsing stacks from third parties they may include invalid url
characters. So we need to encode them. Since these are expected to be
urls though we use just encodeURI instead of encodeURIComponent.
2024-10-23 16:29:20 -07:00
lauren
28668d39be [playground] Upgrade to Next 15 stable (#31333) 2024-10-23 12:13:22 -04:00
Sebastian Markbåge
b3e0a11e8f [Flight] Allow <anonymous> stack frames to be serialized if opt-in (#31329)
Normally we filter out stack frames with missing `filename` because they
can be noisy and not ignore listed. However, it's up to the
filterStackFrame function to determine whether to do it. This lets us
match `<anonymous>` stack frames in V8 parsing (they don't have line
numbers).
2024-10-23 08:38:33 -07:00
Hendrik Liebau
2dc5bebd46 Fix error handling in resolveClientReference (#31332)
When a React Server Consumer Manifest does not include an entry for a
client reference ID, we must not try to look up the export name (or
`'*'`) for the client reference. Otherwise this will fail with
`TypeError: Cannot read properties of undefined (reading '...')` instead
of the custom error we intended to throw.
2024-10-23 08:36:40 -07:00
Henry Q. Dineen
b4cbdc5a7c remove terser from react-compiler-runtime build (#31326)
## Summary

This fixes a minor nit I have about the `react-compiler-runtime` package
in that the published code is minified. I assume most consumers will
minify their own bundles so there's no real advantage to minifying it as
part of the build.

For my purposes it makes it more difficult to read the code, use
`patch-package` (if needed), or diff two versions without referencing
the source code on github or mapping it back to original source using
the source maps.

## How did you test this change?

I ran the build locally and looked at the result but did not run the
code. It's a lot more readable except for the commonjs
compatibility-related stuff that Rollup inserts.
2024-10-22 19:49:10 -04:00
Marin Atanasov
9daabc0bf9 react-hooks/rules-of-hooks: Add support for do/while loops (#28714)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

Currently, `react-hooks/rules-of-hooks` does not support `do/while`
loops - I've also reported this in
https://github.com/facebook/react/issues/28713.

This PR takes a stab at adding support for `do/while` by following the
same logic we already have for detecting `while` loops.

After this PR, any hooks called inside a `do/while` loop will be
considered invalid.

We're also adding some unit tests to confirm that the behavior is
working as expected.

Fixes #28713.

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

I've added unit tests that cover the case and verified that they pass by
running:

```
yarn test packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js --watch
```

I've also verified that the rest of the tests continue to pass by
running:

```
yarn test
```

and

```
yarn test --prod
```
2024-10-22 13:07:10 -07:00
Simen Bekkhus
ae90522bc6 chore: remove unsued deps from React Compiler Babel plugin (#31315)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Since the Babel plugin is bundled into a single file (except for
`@babel/types`
45804af18d/compiler/packages/babel-plugin-react-compiler/rollup.config.js (L18))
we can move these deps to `devDependencies`.

Main motivation is e.g. not installing ancient version of
`pretty-format` (asked in https://github.com/facebook/react/issues/29062
without getting a reason, but if consumers can just skip the deps
entirely that's even better).

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

I tested by installing the plugin into an empty project, deleting
everything in `node_modules` _except_ for `babel-plugin-react-compiler`
and doing `require('babel-plugin-react-compiler')`. It still worked
fine, so it should work in other cases as well 😀

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2024-10-22 13:51:58 -04:00
Sam Zhou
45804af18d [flow] Eliminate usage of more than 1-arg React.AbstractComponent in React codebase (#31314)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

In order to adopt react 19's ref-as-prop model, Flow needs to eliminate
all the places where they are treated differently.
`React.AbstractComponent` is the worst example of this, and we need to
eliminate it.

This PR eliminates them from the react repo, and only keeps the one that
has 1 argument of props.

## How did you test this change?

yarn flow
2024-10-21 16:17:41 -07:00
Sebastian Markbåge
69d4b800a6 [Flight] Support Async Modules in Server References (#31313)
This is required to support for example top level await in a "use
server" module or dependency of a "use server".
2024-10-21 14:52:10 -04:00
Sebastian Markbåge
65a56d0e99 Fix timing issue with fake promise resolving sync (#31304) 2024-10-20 02:35:15 -04:00
Sebastian Markbåge
f11bd3439c Fix types (#31303) 2024-10-20 02:23:31 -04:00
Sebastian Markbåge
251b666ded [Flight] Handle bound arguments for loaded server references (#31302)
Follow up to #31300.

I forgot to pass the bound arguments to the loaded function.
2024-10-20 02:12:06 -04:00
Sebastian Markbåge
c1e1358b2d [Flight] Align turbopack option name with webpack name (#31301)
This was renamed in #31300.
2024-10-20 00:02:48 -04:00
Sebastian Markbåge
d49123f73f Expose prerender() for SSG in stable (#31298)
When we added `renderToReadableStream` we added the `allReady` helper to
make it easier to do SSG rendering but it's kind of awkward to wire up
that way. Since we're also discouraging `renderToString` in React 19 the
cliff is kind of awkward. ([As noted by
Docusaurus.](https://github.com/facebook/react/pull/24752#issuecomment-2178309299))

The idea of the `react-dom/static` `prerender` API was that this would
be the replacement for SSG rendering. Awkwardly this entry point
actually already exists in stable but it has only `undefined` exports.

Since then we've also added other useful heuristics into the `prerender`
branch that makes this really the favored and easiest to use API for the
prerender (SSG/ISR) use case.

`prerender` is also used for Partial Prerendering but that part is still
experimental.

However, we can expose only the `prerender` API on `react-dom/static`
without it returning the `postponeState`. Instead the stream is on
`prelude`. The naming is a bit awkward if you don't consider resuming
but it's the same thing.

It's really just `renderToReadable` stream with automatic `allReady` and
better heuristics for prerendering.
2024-10-19 22:33:28 -04:00
Sebastian Markbåge
22b2b1a05a [Flight] Add serverModuleMap option for mapping ServerReferences (#31300)
Stacked on #31299.

We already have an option for resolving Client References to other
Client References when consuming an RSC payload on the server.

This lets you resolve Server References on the consuming side when the
environment where you're consuming the RSC payload also has access to
those Server References. Basically they becomes like Client References
for this consumer but for another consumer they wouldn't be.
2024-10-19 21:10:25 -04:00
Sebastian Markbåge
39a7730b13 Rename SSRManifest to ServerConsumerManifest (#31299)
This config is more generally applicable to all server-side Flight
Clients and not just SSR.
2024-10-19 20:45:20 -04:00
lauren
1839e1437f [ez] Update compiler issue template (#31297)
Add a field to specify which version of React Compiler is being used.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31297).
* __->__ #31297
* #31296
2024-10-18 15:02:26 -04:00
lauren
54c0edc019 [ci] Don't use branch name for concurrency (#31296)
I happened to notice some jobs on main get canceled if another PR landed
before the prior commit on main had finished running CI. This is not
great for difftrain because the commit artifacts job relies on the CI
jobs on main finishing before it triggers. This would lead to commits
being skipped on DiffTrain which is not great for provenance since we
want it to be a 1:1 sync.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31296).
* #31297
* __->__ #31296
2024-10-18 15:02:14 -04:00
lauren
9deb36748d [ci] Publish compiler weekly prereleases (#31294)
Adds a new weekly job for the compiler
2024-10-18 14:43:43 -04:00
Joseph Savona
cdde15efe1 [compiler] InlineJSXTransform transforms jsx inside function expressions (#31282)
InlineJSXTransform wasn't traversing into function expressions or object
methods, so any JSX inside such functions wouldn't have gotten inlined.
This PR updates to traverse nested functions to transform all JSX within
a hook or component.

Note that this still doesn't transform JSX outside of components or
hooks, ie in standalone render helpers.
2024-10-18 11:27:48 -07:00
lauren
915be0ef78 [playground] Upgrade various packages (#31293)
Just some housekeeping
2024-10-18 14:25:36 -04:00
lauren
61383303d3 [playground] Remove unnecessary fs package (#31292)
Seems like this was accidentally added.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31292).
* #31293
* __->__ #31292
* #31291
2024-10-18 14:09:30 -04:00
lauren
ee6ca23b24 [playground] Upgrade to Next 15 (#31291)
This was previously blocked because the playground was a part of the
compiler's yarn workspace and there was some funky hoisting going on.
Now that we are decoupled we can upgrade to Next 15, which hopefully
should improve build times.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31291).
* #31293
* #31292
* __->__ #31291
2024-10-18 14:09:07 -04:00
lauren
d57217544a [fixture] Update compiler to use latest package (#31289)
Pins the compiler to the latest version in our fixture app.
2024-10-18 13:32:26 -04:00
lauren
35b63ca90d [ci:compiler] Only add latest tag to non-experimental (#31288)
It turns out npm sets the latest tag by default so simply removing it
didn't change the previous behavior.

The `latest` tag is typically used for stable release versions, and
other tags for unstable versions such as prereleases. Since the compiler
is still in prerelease, let's set the latest tag only for
non-experimental releases to help signal which version is the safest to
try out.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31288).
* #31289
* __->__ #31288
2024-10-18 13:24:30 -04:00
Sebastian Markbåge
b8ae38f88b Audit try/finally around console patching (#31286)
Otherwise if something errors they can be left patched.

[Review without
whitespace](https://github.com/facebook/react/pull/31286/files?w=1)
2024-10-18 12:05:20 -04:00
lauren
1ce58ddd67 [ci] Don't auto push to latest tag (#31284)
By default let's stop pushing to the latest tag now that we have a
non-experimental release.
2024-10-18 00:20:14 -04:00
lauren
25cac220d6 [ci] Allow passing various params to compiler publish script (#31283)
Allow passing in a few more inputs when manually publishing.
2024-10-17 18:12:58 -04:00
lauren
9c60cbe3d1 [compiler] Clean up publish script (#31278)
Few small tweaks to make it easier to run adhoc publishes
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31278).
* #31283
* __->__ #31278
2024-10-17 18:02:41 -04:00
Sathya Gunasekaran
c91b3b090a JSX Outlining (#30956)
Currently, the react compiler can not compile within callbacks which can
potentially cause over rendering. Consider this example:
```jsx
function Component(countries, onDelete) {
  const name = useFoo();
  return countries.map(() => {
    return (
      <Foo>
        <Bar name={name}/>
        <Baz onclick={onDelete} />
      </Foo>
    );
  });
}
```

In this case, there's no memoization of the nested jsx elements. But
instead if we were to manually refactor the nested jsx into separate
component like this:
```jsx
function Component(countries, onDelete) {
  const name = useFoo();
  return countries.map(() => {
    return <Temp name={name} onDelete={onDelete} />;
  });
}

function Temp({ name, onDelete }) {
  return (
    <Foo>
      <Bar name={name} />
      <Baz onclick={onDelete} />
    </Foo>
  );
}

```

The compiler can now optimise both these components:
```jsx
function Component(countries, onDelete) {
  const $ = _c(4);
  const name = useFoo();
  let t0;
  if ($[0] !== name || $[1] !== onDelete || $[2] !== countries) {
    t0 = countries.map(() => <Temp name={name} onDelete={onDelete} />);
    $[0] = name;
    $[1] = onDelete;
    $[2] = countries;
    $[3] = t0;
  } else {
    t0 = $[3];
  }
  return t0;
}

function Temp(t0) {
  const $ = _c(7);
  const { name, onDelete } = t0;
  let t1;
  if ($[0] !== name) {
    t1 = <Bar name={name} />;
    $[0] = name;
    $[1] = t1;
  } else {
    t1 = $[1];
  }
  let t2;
  if ($[2] !== onDelete) {
    t2 = <Baz onclick={onDelete} />;
    $[2] = onDelete;
    $[3] = t2;
  } else {
    t2 = $[3];
  }
  let t3;
  if ($[4] !== t1 || $[5] !== t2) {
    t3 = (
      <Foo>
        {t1}
        {t2}
      </Foo>
    );
    $[4] = t1;
    $[5] = t2;
    $[6] = t3;
  } else {
    t3 = $[6];
  }
  return t3;
}
```

Now, when `countries` is updated by adding one single value, only the
newly added value is re-rendered and not the entire list. Rather than
having to do this manually, this PR teaches the react compiler to do
this transformation.

This PR adds a new pass (`OutlineJsx`) to capture nested jsx statements
and outline them in a separate component. This newly outlined component
can then by memoized by the compiler, giving us more fine grained
rendering.
2024-10-17 18:15:32 +01:00
Ruslan Lesiutin
bf7e210cb5 tests[react-devtools]: added tests for Compiler integration (#31241)
Adds tests for Compiler integration.

This includes:
- Tests against Compiler from source.
- Versioned (18.2 - <19) tests against Compiler from npm.

For tests against React 18.2, I had to download `react-compiler-runtime`
from npm and put it to `react/compiler-runtime.js`.
2024-10-17 09:02:41 +01:00
lauren
3ed64f8232 [ez] Update references to 'forget' in react-compiler-runtime (#31277)
Updates the runtime to reference React Compiler instead of Forget.
2024-10-16 18:44:50 -04:00
Timothy Yung
a3d9ea05bf Delete __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED from React Native Renderer (#31276)
## Summary

The React Native Renderer exports a
`__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED` property with a
single method that has no remaining call sites:
`computeComponentStackForErrorReporting`

This PR cleans up this unused export.

## How did you test this change?

```
$ yarn
$ yarn flow fabric
$ yarn test
```
2024-10-16 11:19:01 -07:00
Edmond Chui
77b637d612 Add Bridge types for Fusebox (#31274)
New types used by Fusebox
https://github.com/facebookexperimental/rn-chrome-devtools-frontend/pull/117
2024-10-16 16:45:24 +01:00
Sebastian Markbåge
be94b10826 [Flight] Enable sync stack traces for errors and console replay (#31270)
This was gated behind `enableOwnerStacks` since they share some code
paths but it's really part of `enableServerComponentLogs`.

This just includes the server-side regular stack on Error/replayed logs
but doesn't use console.createTask and doesn't include owner stacks.
2024-10-16 10:57:08 -04:00
Jack Pope
6c4bbc7832 [Re-land] Make prerendering always non-blocking (#31268)
Follows https://github.com/facebook/react/pull/31238

___

This is a partial re-land of
https://github.com/facebook/react/pull/31056. We saw breakages surface
after the original land and had to revert. Now that they've been fixed,
let's try this again. This time we'll split up the commits to give us
more control of testing and rollout internally.

Original PR: https://github.com/facebook/react/pull/31056
Original Commit:
4c71025d8d
Revert PR: https://github.com/facebook/react/pull/31080

Commit description:

> When a synchronous update suspends, and we prerender the siblings, the
prerendering should be non-blocking so that we can immediately restart
once the data arrives.
>
> This happens automatically when there's a Suspense boundary, because
we immediately commit the boundary and then proceed to a Retry render,
which are always concurrent. When there's not a Suspense boundary, there
is no Retry, so we need to take care to switch from the synchronous work
loop to the concurrent one, to enable time slicing.

Co-authored-by: Andrew Clark <git@andrewclark.io>
2024-10-15 16:47:02 -04:00
Sebastian "Sebbie" Silbermann
8382581446 [ESLint] Add test for rejected useId in async Components (#31208) 2024-10-15 17:17:41 +02:00
Ruslan Lesiutin
c7c68ef842 React DevTools 6.0.0 -> 6.0.1 (#31263)
Changes in this release:

* Fix React Compiler badging ([poteto](https://github.com/poteto) in
[#31196](https://github.com/facebook/react/pull/31196))
* fix[react-devtools]: fixed timeline profiler tests
([hoxyq](https://github.com/hoxyq) in
[#31261](https://github.com/facebook/react/pull/31261))
* fix[react-devtools]: record timeline data only when supported
([hoxyq](https://github.com/hoxyq) in
[#31154](https://github.com/facebook/react/pull/31154))
* refactor[react-devtools]: flatten reload and profile config
([hoxyq](https://github.com/hoxyq) in
[#31132](https://github.com/facebook/react/pull/31132))
* fix[react-devtools]: remove all listeners when Agent is shutdown
([hoxyq](https://github.com/hoxyq) in
[#31151](https://github.com/facebook/react/pull/31151))
* fix[react-devtools]: removed redundant startProfiling call
([hoxyq](https://github.com/hoxyq) in
[#31131](https://github.com/facebook/react/pull/31131))
* refactor[react-devtools/fiber/renderer]: optimize durations resolution
([hoxyq](https://github.com/hoxyq) in
[#31118](https://github.com/facebook/react/pull/31118))
* fix[react-devtools]: update profiling status before receiving response
from backend ([hoxyq](https://github.com/hoxyq) in
[#31117](https://github.com/facebook/react/pull/31117))
* fix[react-devtools]: wrap key string in preformatted text html element
([hoxyq](https://github.com/hoxyq) in
[#31153](https://github.com/facebook/react/pull/31153))
* chore[react-devtools]: drop legacy context tests
([hoxyq](https://github.com/hoxyq) in
[#31059](https://github.com/facebook/react/pull/31059))
* chore[react-devtools]: add legacy mode error message to the ignore
list for tests ([hoxyq](https://github.com/hoxyq) in
[#31060](https://github.com/facebook/react/pull/31060))
* fix[react-devtools]: request hook initialization inside http server
response ([hoxyq](https://github.com/hoxyq) in
[#31102](https://github.com/facebook/react/pull/31102))
* [Flight] Serialize Server Components Props in DEV
([sebmarkbage](https://github.com/sebmarkbage) in
[#31105](https://github.com/facebook/react/pull/31105))
* Add: reload to profile for Fusebox
([EdmondChuiHW](https://github.com/EdmondChuiHW) in
[#31021](https://github.com/facebook/react/pull/31021))
* refactor: allow custom impl of backend realod-to-profile support check
([EdmondChuiHW](https://github.com/EdmondChuiHW) in
[#31048](https://github.com/facebook/react/pull/31048))
* fix: use public instance in Fiber renderer and expose it from
getInspectorDataForViewAtPoint ([hoxyq](https://github.com/hoxyq) in
[#31068](https://github.com/facebook/react/pull/31068))
2024-10-15 14:15:26 +01:00
lauren
9806a4b0d4 [DevTools] Fix React Compiler badging (#31196)
In #31140 we switched over the uMC polyfill to use memo instead of state
since memo would FastRefresh properly. However this busted devtools'
badging of compiled components; this PR fixes it.

TODO: tests
Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>

---------

Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>
2024-10-15 12:53:45 +01:00
Ruslan Lesiutin
ec2bf02245 fix[react-devtools]: fixed timeline profiler tests (#31261)
Fixes tests against React 18 after
https://github.com/facebook/react/pull/31154:
- Set `supportsTimeline` to true for `Store`.
- Execute `store.profilerStore.startProfiling` after `legacyRender`
import, because this is where `react-dom` is imported and renderer is
registered. We don't yet propagate `isProfiling` flag to newly
registered renderers, when profiling already started see:

d5bba18b5d/packages/react-devtools-shared/src/hook.js (L203-L204)
2024-10-15 12:46:05 +01:00
Jack Pope
13411e4589 [Re-land] Make prerendering always non-blocking: Add missing feature flag checks (#31238)
This is a partial re-land of
https://github.com/facebook/react/pull/31056. We saw breakages surface
after the original land and had to revert. Now that they've been fixed,
let's try this again. This time we'll split up the commits to give us
more control of testing and rollout internally.

Original PR: https://github.com/facebook/react/pull/31056
Original Commit:
2a9fb445d9
Revert PR: https://github.com/facebook/react/pull/31080

Commit description:
```
Neglected to wrap some places in the enableSiblingPrerendering flag.
```

Co-authored-by: Andrew Clark <git@andrewclark.io>
2024-10-14 14:12:23 -04:00
lauren
6cf8518505 [ci] Specify limited concurrency for PR jobs (#31240)
There was a concurrency setting we hadn't enabled on jobs that are
primarily triggered for PRs. This meant that every update to the PR
would trigger new CI jobs without canceling any ones already in flight,
which can greatly slow down CI due to the number of jobs that need to
run.

This PR adds concurrency [based on the workflow name and PR number or
head
ref.](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31240).
* __->__ #31240
* #31239
2024-10-14 12:07:24 -04:00
lauren
cbcc1d2027 [ci] Consistent cache names (#31239)
Makes cache names more descriptive and consistent for CI, so it's easier
to tell which cache is used for what purpose.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31239).
* #31240
* __->__ #31239
2024-10-14 12:07:08 -04:00
lauren
b60286b834 [compiler] Use consistent version hash for npm (#31177)
Modifies our release script to use the same version hash (the hashed
`compiler` directory) for all compiler packages to keep them consistent.
2024-10-14 11:20:47 -04:00
Jan Kassens
75dd053b5e [string-refs] make disableStringRefs a dynamic www flag (#31175) 2024-10-14 10:13:40 -04:00
Sebastian Markbåge
cd22717c27 [Flight] Also don't cut off type and key (#31209) 2024-10-13 18:57:50 +02:00
Mike Vitousek
147374d71a [compiler] Kill markReactiveIdentifier and friends
Summary:
With the previous PR we no longer need to mark identifiers as reactive in contexts where we don't have places. We already deleted most uses of markReactiveId; the last case was to track identifiers through loadlocals etc -- but we already use a disjoint alias map that accounts for loadlocals when setting reactivity.

ghstack-source-id: 69ce0a78b0
Pull Request resolved: https://github.com/facebook/react/pull/31178
2024-10-11 17:26:57 -07:00
Mike Vitousek
6cf5bd9013 [compiler] Allow refs to be lazily initialized during render
Summary:
The official guidance for useRef notes an exception to the rule that refs cannot be accessed during render: to avoid recreating the ref's contents, you can test that the ref is uninitialized and then initialize it using an if statement:

```
if (ref.current == null) {
  ref.current = SomeExpensiveOperation()
}
```

The compiler didn't recognize this exception, however, leading to code that obeyed all the official guidance for refs being rejected by the compiler. This PR fixes that, by extending the ref validation machinery with an awareness of guard operations that allow lazy initialization. We now understand `== null` and similar operations, when applied to a ref and consumed by an if terminal, as marking the consequent of the if as a block in which the ref can be safely written to. In order to do so we need to create a notion of ref ids, which link different usages of the same ref via both the ref and the ref value.

ghstack-source-id: d2729274f351e1eb0268f28f629fa4c2568ebc4d
Pull Request resolved: https://github.com/facebook/react/pull/31188
2024-10-11 16:14:33 -07:00
dependabot[bot]
9c525ea44a Bump micromatch from 4.0.5 to 4.0.8 in /compiler (#31186)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5
to 4.0.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/micromatch/releases">micromatch's
releases</a>.</em></p>
<blockquote>
<h2>4.0.8</h2>
<p>Ultimate release that fixes both CVE-2024-4067 and CVE-2024-4068. We
consider the issues low-priority, so even if you see automated scanners
saying otherwise, don't be scared.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md">micromatch's
changelog</a>.</em></p>
<blockquote>
<h2>[4.0.8] - 2024-08-22</h2>
<ul>
<li>backported CVE-2024-4067 fix (from v4.0.6) over to 4.x branch</li>
</ul>
<h2>[4.0.7] - 2024-05-22</h2>
<ul>
<li>this is basically v4.0.5, with some README updates</li>
<li><strong>it is vulnerable to CVE-2024-4067</strong></li>
<li>Updated braces to v3.0.3 to avoid CVE-2024-4068</li>
<li>does NOT break API compatibility</li>
</ul>
<h2>[4.0.6] - 2024-05-21</h2>
<ul>
<li>Added <code>hasBraces</code> to check if a pattern contains
braces.</li>
<li>Fixes CVE-2024-4067</li>
<li><strong>BREAKS API COMPATIBILITY</strong></li>
<li>Should be labeled as a major release, but it's not.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8bd704ec0d"><code>8bd704e</code></a>
4.0.8</li>
<li><a
href="a0e68416a4"><code>a0e6841</code></a>
run verb to generate README documentation</li>
<li><a
href="4ec288484f"><code>4ec2884</code></a>
Merge branch 'v4' into hauserkristof-feature/v4.0.8</li>
<li><a
href="03aa805217"><code>03aa805</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/micromatch/issues/266">#266</a>
from hauserkristof/feature/v4.0.8</li>
<li><a
href="814f5f70ef"><code>814f5f7</code></a>
lint</li>
<li><a
href="67fcce6a10"><code>67fcce6</code></a>
fix: CHANGELOG about braces &amp; CVE-2024-4068, v4.0.5</li>
<li><a
href="113f2e3fa7"><code>113f2e3</code></a>
fix: CVE numbers in CHANGELOG</li>
<li><a
href="d9dbd9a266"><code>d9dbd9a</code></a>
feat: updated CHANGELOG</li>
<li><a
href="2ab13157f4"><code>2ab1315</code></a>
fix: use actions/setup-node@v4</li>
<li><a
href="1406ea38f3"><code>1406ea3</code></a>
feat: rework test to work on macos with node 10,12 and 14</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=micromatch&package-manager=npm_and_yarn&previous-version=4.0.5&new-version=4.0.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-11 16:29:34 -04:00
dependabot[bot]
2011074ab8 Bump json5 from 2.2.1 to 2.2.3 in /compiler (#31185)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/json5/json5/releases">json5's
releases</a>.</em></p>
<blockquote>
<h2>v2.2.3</h2>
<ul>
<li>Fix: json5@2.2.3 is now the 'latest' release according to npm
instead of v1.0.2. (<a
href="https://redirect.github.com/json5/json5/issues/299">#299</a>)</li>
</ul>
<h2>v2.2.2</h2>
<ul>
<li>Fix: Properties with the name <code>__proto__</code> are added to
objects and arrays.
(<a href="https://redirect.github.com/json5/json5/issues/199">#199</a>)
This also fixes a prototype pollution vulnerability reported by
Jonathan Gregson! (<a
href="https://redirect.github.com/json5/json5/issues/295">#295</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/json5/json5/blob/main/CHANGELOG.md">json5's
changelog</a>.</em></p>
<blockquote>
<h3>v2.2.3 [<a
href="https://github.com/json5/json5/tree/v2.2.3">code</a>, <a
href="https://github.com/json5/json5/compare/v2.2.2...v2.2.3">diff</a>]</h3>
<ul>
<li>Fix: json5@2.2.3 is now the 'latest' release according to npm
instead of
v1.0.2. (<a
href="https://redirect.github.com/json5/json5/issues/299">#299</a>)</li>
</ul>
<h3>v2.2.2 [<a
href="https://github.com/json5/json5/tree/v2.2.2">code</a>, <a
href="https://github.com/json5/json5/compare/v2.2.1...v2.2.2">diff</a>]</h3>
<ul>
<li>Fix: Properties with the name <code>__proto__</code> are added to
objects and arrays.
(<a href="https://redirect.github.com/json5/json5/issues/199">#199</a>)
This also fixes a prototype pollution vulnerability reported by
Jonathan Gregson! (<a
href="https://redirect.github.com/json5/json5/issues/295">#295</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c3a7524277"><code>c3a7524</code></a>
2.2.3</li>
<li><a
href="94fd06d82e"><code>94fd06d</code></a>
docs: update CHANGELOG for v2.2.3</li>
<li><a
href="3b8cebf0c4"><code>3b8cebf</code></a>
docs(security): use GitHub security advisories</li>
<li><a
href="f0fd9e194d"><code>f0fd9e1</code></a>
docs: publish a security policy</li>
<li><a
href="6a91a05fff"><code>6a91a05</code></a>
docs(template): bug -&gt; bug report</li>
<li><a
href="14f8cb186e"><code>14f8cb1</code></a>
2.2.2</li>
<li><a
href="10cc7ca916"><code>10cc7ca</code></a>
docs: update CHANGELOG for v2.2.2</li>
<li><a
href="7774c10979"><code>7774c10</code></a>
fix: add <strong>proto</strong> to objects and arrays</li>
<li><a
href="edde30abd8"><code>edde30a</code></a>
Readme: slight tweak to intro</li>
<li><a
href="97286f8bd5"><code>97286f8</code></a>
Improve example in readme</li>
<li>Additional commits viewable in <a
href="https://github.com/json5/json5/compare/v2.2.1...v2.2.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=json5&package-manager=npm_and_yarn&previous-version=2.2.1&new-version=2.2.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-11 16:29:18 -04:00
Sebastian "Sebbie" Silbermann
09111202d6 eslint-plugin-react-hooks: Release 5.0.0 (#31176)
Co-authored-by: lauren <poteto@users.noreply.github.com>
2024-10-11 14:51:59 +02:00
Jan Kassens
5636fad840 [string-refs] log string ref from prod (#31161)
If passed as a feature flag, this calls the configured function when a
string ref is used even from prod code to find the last usages.
2024-10-10 18:12:47 -04:00
Mike Vitousek
7b7fac073d [compiler] Represent phis with places rather than identifiers
Summary:
The fact that phis are identifiers rather than places is unfortunate in a few cases. In some later analyses, we might wish to know whether a phi is reactive, but we don't have an easy way to do that currently.

Most of the changes here is just replacing phi.id with phi.place.identifier and such. Interesting bits are EnterSSA (several functions now take places rather than identifiers, and InferReactivePlaces now needs to mark places as reactive explicitly.

ghstack-source-id: 5f4fb396cd86b421008c37832a5735ac40f8806e
Pull Request resolved: https://github.com/facebook/react/pull/31171
2024-10-10 12:41:03 -07:00
Sebastian Markbåge
70fb136391 Fix limit without owner stacks (#31179)
Move out of gate.
2024-10-10 14:52:33 -04:00
mofeiZ
fbfe37ee40 [compiler] Test fixture: non-reactive phi creates 'dangling ref' scope (#31103) 2024-10-10 12:23:44 -04:00
lauren
77f438931f [compiler] Consume compiled lib in react 19 app (#31167)
it works
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31167).
* __->__ #31167
* #31166
* #31165
2024-10-10 11:40:06 -04:00
lauren
0a1fdeee9e [compiler] Consume compiled lib in react 18 app (#31166)
`yarn dev` doesn't work quite correctly because of an outdated
assumption in vite-plugin-react, I have a [PR
open](https://github.com/vitejs/vite-plugin-react/pull/374) to address.

However `yarn build` and `yarn preview` does work as expected.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31166).
* #31167
* __->__ #31166
* #31165
2024-10-10 11:39:54 -04:00
lauren
eb0e265cd9 [compiler] Compile lib (#31165)
Add and compile a simple hook with rollup and babel.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31165).
* #31167
* #31166
* __->__ #31165
2024-10-10 11:39:42 -04:00
lauren
2ef407937b [compiler] Scaffold fixture apps (#31164)
Scaffold empty apps to consume the fixture lib.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31164).
* #31167
* #31166
* #31165
* __->__ #31164
* #31148
* #31168
2024-10-10 11:33:02 -04:00
Sebastian "Sebbie" Silbermann
16ac71a650 Fix prepare-prerelease script (#31159) 2024-10-10 17:24:51 +02:00
lauren
b781c9f564 [compiler] Scaffold fixture library (#31148)
Scaffolds an empty library to test backwards compatibility with the
compiler enabled.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31148).
* #31167
* #31166
* #31165
* #31164
* __->__ #31148
* #31168
2024-10-10 10:53:58 -04:00
lauren
0f1127b552 [prettier] Ignore compiler/target (#31168)
Add missing directory to prettierignore.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31168).
* #31167
* #31166
* #31165
* #31164
* #31148
* __->__ #31168
2024-10-10 10:53:27 -04:00
Josh Story
38af456a49 [Flight] don't emit chunks for rejected thenables after abort (#31169)
When aborting we emit chunks for each pending task. However there was a
bug where a thenable could also reject before we could flush and we end
up with an extra chunk throwing off the pendingChunks bookeeping. When a
task is retried we skip it if is is not in PENDING status because we
understand it was completed some other way. We need to replciate this
for the reject pathway on serialized thenables since aborting if
effectively completing all pending tasks and not something we need to
continue to do once the thenable rejects later.
2024-10-10 06:47:32 -07:00
Sebastian Markbåge
566b0b0f14 [Flight] Don't limit objects that are children of special types (#31160)
We can't make a special getter to mark the boundary of deep
serialization (which can be used for lazy loading in the future) when
the parent object is a special object that we parse with
getOutlinedModel. Such as Map/Set and JSX.

This marks the objects that are direct children of those as not possible
to limit.

I don't love this solution since ideally it would maybe be more local to
the serialization of a specific object.

It also means that very deep trees of only Map/Set never get cut off.
Maybe we should instead override the `get()` and enumeration methods on
these instead somehow.

It's important to have it be a getter though because that's the
mechanism that lets us lazy-load more depth in the future.
2024-10-10 07:36:37 -04:00
Mike Vitousek
131ae818a1 [compiler][ez] Include phi identifier in AssertValidMutableRanges
Summary:
Looks like we accidentally skipped validating this identifier.

ghstack-source-id: 05964331a8
Pull Request resolved: https://github.com/facebook/react/pull/31170
2024-10-09 22:15:41 -07:00
Josh Story
f5b8d9378b [Flight] Serialize top-level Date (#31163)
renderModelDesctructive can sometimes be called direclty on Date values.
When this happens we don't first call toJSON on the Date value so we
need to explicitly handle the case where where the rendered value is a
Date instance as well. This change updates renderModelDesctructive to
account for sometimes receiving Date instances directly.
2024-10-09 20:29:48 -07:00
Jan Kassens
de43d560a8 [cleanup] remove flag enableAddPropertiesFastPath (#31062)
The experiment was tested internally and rolled out, replacing the flag
with `true`.
2024-10-09 16:46:35 -04:00
Ruslan Lesiutin
d5bba18b5d fix[react-devtools]: record timeline data only when supported (#31154)
Stacked on https://github.com/facebook/react/pull/31132. See last
commit.

There are 2 issues:
1. We've been recording timeline events, even if Timeline Profiler was
not supported by the Host. We've been doing this for React Native, for
example, which would significantly regress perf of recording a profiling
session, but we were not even using this data.
2. Currently, we are generating component stack for every state update
event. This is extremely expensive, and we should not be doing this.

We can't currently fix the second one, because we would still need to
generate all these stacks, and this would still take quite a lot of
time. As of right now, we can't generate a component stack lazily
without relying on the fact that reference to the Fiber is not stale.
With `enableOwnerStacks` we could populate component stacks in some
collection, which would be cached at the Backend, and then returned only
once Frontend asks for it. This approach also eliminates the need for
keeping a reference to a Fiber.
2024-10-09 15:27:04 +01:00
Ruslan Lesiutin
bfe91fbecf refactor[react-devtools]: flatten reload and profile config (#31132)
Stacked on https://github.com/facebook/react/pull/31131. See last
commit.

This is a clean-up and a pre-requisite for next changes:
1. `ReloadAndProfileConfig` is now split into boolean value and settings
object. This is mainly because I will add one more setting soon, and
also because settings might be persisted for a longer time than the flag
which signals if the Backend was reloaded for profiling. Ideally, this
settings should probably be moved to the global Hook object, same as we
did for console patching.
2. Host is now responsible for reseting the cached values, Backend will
execute provided `onReloadAndProfileFlagsReset` callback.
2024-10-09 13:57:02 +01:00
Ruslan Lesiutin
1d8d12005f fix[react-devtools]: remove all listeners when Agent is shutdown (#31151)
Based on https://github.com/facebook/react/pull/31049, credits to
@EdmondChuiHW.

What is happening here:
1. Once Agent is destroyed, unsubscribe own listeners and bridge
listeners.
2. [Browser extension only] Once Agent is destroyed, unsubscribe
listeners from BackendManager.
3. [Browser extension only] I've discovered that `backendManager.js`
content script can get injected multiple times by the browser. When
Frontend is initializing, it will create Store first, and then execute a
content script for bootstraping backend manager. If Frontend was
destroyed somewhere between these 2 steps, Backend won't be notified,
because it is not initialized yet, so it will not unsubscribe listeners
correctly. We might end up duplicating listeners, and the next time
Frontend is launched, it will report an issues "Cannot add / remove node
...", because same operations are emitted twice.

To reproduce 3 you can do the following:
1. Click reload-to-profile
2. Right after when both app and Chrome DevTools panel are reloaded,
close Chrome DevTools.
3. Open Chrome DevTools again, open Profiler panel and observe "Cannot
add / remove node ..." error in the UI.
2024-10-09 13:34:01 +01:00
Ruslan Lesiutin
4a86ec5a66 fix[react-devtools]: removed redundant startProfiling call (#31131)
Stacked on https://github.com/facebook/react/pull/31118. See last
commit.

We don't need to call `startProfiling()` here, because we delegate this
to the Renderer itself:

830e823cd2/packages/react-devtools-shared/src/backend/fiber/renderer.js (L5227-L5232)

Since this is de-facto the constructor of Renderer, this will be called
earlier.

Validated via testing the reload-to-profile for Chrome browser
extension.
2024-10-09 13:32:04 +01:00
Ruslan Lesiutin
389a2deebc refactor[react-devtools/fiber/renderer]: optimize durations resolution (#31118)
Stacked on https://github.com/facebook/react/pull/31117. 

No need for sending long float numbers and to have resolution less than
a microsecond, we end up formatting it on a Frontend side:

6c7b41da3d/packages/react-devtools-shared/src/devtools/views/Profiler/utils.js (L359-L360)
2024-10-09 13:26:16 +01:00
Ruslan Lesiutin
dbf80c8d7a fix[react-devtools]: update profiling status before receiving response from backend (#31117)
We can't wait for a response from Backend, because it might take some
time to actually finish profiling.

We should keep a flag on the frontend side, so user can quickly see the
feedback in the UI.
2024-10-09 13:23:23 +01:00
Ruslan Lesiutin
bf0c054649 fix[react-devtools]: wrap key string in preformatted text html element (#31153)
Fixes https://github.com/facebook/react/issues/28984.
2024-10-09 09:54:34 +01:00
lauren
ed966dac4a [compiler] Fix busted postinstall script (#31147) 2024-10-07 20:37:41 -04:00
lauren
f74f6cd945 [rcr] Publish react-compiler-runtime to npm (#31146)
Updates our publishing scripts to also publish react-compiler-runtime.
2024-10-07 18:50:07 -04:00
lauren
23cd3aca28 [rcr] Remove runtimeModule compiler option (#31145)
Now that the compiler always injects `react-compiler-runtime`, this
option is unnecessary.
2024-10-07 18:07:12 -04:00
lauren
3fd3364107 [rcr] Update default runtimeModule to react-compiler-runtime (#31144)
Updates the compiler to always import from `react-compiler-runtime` by
default. The runtime then decides whether to use the official or
userspace implementation of useMemoCache.
2024-10-07 17:59:33 -04:00
lauren
8dd4cda380 [rcr] Add target flag to compiler (#31143) 2024-10-07 17:50:53 -04:00
lauren
68d5288359 [snap] Add react-compiler-runtime as a dependency (#31142)
We need `react-compiler-runtime` to use the same version of React as
snap
2024-10-07 16:38:06 -04:00
lauren
0e43aa7f7a [snap] Remove unnecessary React.c override (#31141) 2024-10-07 16:31:59 -04:00
lauren
d2367f17d9 [rcr] Reexport React.__COMPILER_RUNTIME.c or fallback to polyfill (#31140)
This PR updates the standalone `react-compiler-runtime` package to
either re-export `React.__COMPILER_RUNTIME.c` or to use a userspace
polyfill.
2024-10-07 16:31:20 -04:00
lauren
b78a7f2f35 [rcr] Re-export useMemoCache in top level React namespace (#31139)
In order to support using the compiler on versions of React prior to 19,
we need the ability to statically import `c` (aka useMemoCache) or
fallback to a polyfill supplied by `react-compiler-runtime` (note: this
is a separate npm package, not to be confused with
`react/compiler-runtime`, which is currently a part of react).

To do this we first need to re-export `useMemoCache` under the top level
React namespace again, which is additive and thus non-breaking. Doing so
allows `react-compiler-runtime` to statically either re-export
`React.__COMPILER_RUNTIME.c` or supply a polyfill, without the need for
a dynamic import which is finicky to support due to returning a promise.

In later PRs I will remove `react/compiler-runtime` and update the
compiler to emit imports to `react-compiler-runtime` instead.
2024-10-07 16:25:04 -04:00
mofeiZ
68d59d43d5 [compiler][ez] Fix reanimated custom type defs for imports (#31137)
When we added support for Reanimated, we didn't distinguish between true
globals (i.e. identifiers with no static resolutions), module types, and
imports #29188. For the past 3-4 months, Reanimated imports were not
being matched to the correct hook / function shape we match globals and
module imports against two different registries.

This PR fixes our support for Reanimated library functions imported
under `react-native-reanimated`. See test fixtures for details
2024-10-07 13:09:39 -04:00
lauren
91c42a14c7 [rcr][ez] Clean up unused $read from rcr (#31136) 2024-10-07 12:39:14 -04:00
mofeiZ
1460d67c5b [compiler][hir] Only hoist always-accessed PropertyLoads from function decls (#31066)
Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at
bottom):
* __->__ #31066
* #31032

Prior to this PR, we consider all of a nested function's accessed paths
as 'hoistable' (to the basic block in which the function was defined).
Now, we traverse nested functions and find all paths hoistable to their
*entry block*.

Note that this only replaces the *hoisting* part of function
declarations, not dependencies. This realistically only affects optional
chains within functions, which always get truncated to its inner
non-optional path (see
[todo-infer-function-uncond-optionals-hoisted.tsx](576f3c0aa8/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.tsx))

See newly added test fixtures for details

Update: Note that toggling `enableTreatFunctionDepsAsConditional` makes
a non-trivial impact on granularity of inferred deps (i.e. we find that
function declarations uniquely identify some paths as hoistable).
Snapshot comparison of internal code shows ~2.5% of files get worse
dependencies ([internal
link](https://www.internalfb.com/phabricator/paste/view/P1625792186))
2024-10-03 14:41:32 -04:00
mofeiZ
edacbde73f [compiler][hir-rewrite] Check mutability of base identifier when hoisting (#31032)
Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at
bottom):
* #31066
* __->__ #31032

Prior to this PR, we check whether the property load source (e.g. the
evaluation of `<base>` in `<base>.property`) is mutable + scoped to
determine whether the property load itself is eligible for hoisting.
This changes to check the base identifier of the load.
- This is needed for the next PR #31066. We want to evaluate whether the
base identifier is mutable within the context of the *outermost
function*. This is because all LoadLocals and PropertyLoads within a
nested function declaration have mutable-ranges within the context of
the function, but the base identifier is a context variable.
- A side effect is that we no longer infer loads from props / other
function arguments as mutable in edge cases (e.g. props escaping out of
try-blocks or being assigned to context variables)
2024-10-03 13:55:59 -04:00
Mofei Zhang
0751fac747 [compiler] Optional chaining for dependencies (HIR rewrite)
Adds HIR version of `PropagateScopeDeps` to handle optional chaining.

Internally, this improves memoization on ~4% of compiled files (internal links: [1](https://www.internalfb.com/intern/paste/P1610406497/))

Summarizing the changes in this PR.
1. `CollectOptionalChainDependencies` recursively traverses optional blocks down to the base. From the base, we build up a set of `baseIdentifier.propertyA?.propertyB` mappings.
The tricky bit here is that optional blocks sometimes reference other optional blocks that are *not* part of the same chain e.g. a(c?.d)?.d. See code + comments in `traverseOptionalBlock` for how we avoid concatenating unrelated blocks.

2. Adding optional chains into non-null object calculation.
(Note that marking `a?.b` as 'non-null' means that `a?.b.c` is safe to evaluate, *not* `(a?.b).c`. Happy to rename this / reword comments accordingly if there's a better term)
This pass is split into two stages. (1) collecting non-null objects by block and (2) propagating non-null objects across blocks. The only significant change here was to (2). We add an extra reduce step `X=Reduce(Union(X, Intersect(X_neighbors)))` to merge optional and non-optional nodes (e.g. nonNulls=`{a, a?.b}` reduces to `{a, a.b}`)

3. Adding optional chains into dependency calculation.
This was the trickiest. We need to take the "maximal" property chain as a dependency. Prior to this PR, we avoided taking subpaths e.g. `a.b` of `a.b.c` as dependencies by only visiting non-PropertyLoad/LoadLocal instructions. This effectively only recorded the property-path at site-of-use.

    Unfortunately, this *quite* doesn't work for optional chains for a few reasons:
    - We would need to skip relevant `StoreLocal`/`Branch terminal` instructions (but only those within optional blocks that have been successfully read).
    - Given an optional chain, either (1) only a subpath or (2) the entire path can be represented as a PropertyLoad. We cannot directly add the last hoistable optional-block as a dependency as MethodCalls are an edge case e.g. given a?.b.c(), we should depend on `a?.b`, not `a?.b.c`
      This means that we add its dependency at either the innermost unhoistable optional-block or when encountering it within its phi-join.

4. Handle optional chains in DeriveMinimalDependenciesHIR.
This was also a bit tricky to formulate. Ideally, we would avoid a 2^3 case join (cond | uncond cfg, optional | not optional load, access | dependency). This PR attempts to simplify by building two trees
    1. First add each hoistable path into a tree containing `Optional | NonOptional` nodes.
    2. Then add each dependency into another tree containing `Optional | NonOptional`, `Access | Dependency` nodes, truncating the dependency at the earliest non-hoistable node (i.e. non-matching pair when walking the hoistable tree)

ghstack-source-id: a2170f2628
Pull Request resolved: https://github.com/facebook/react/pull/31037
2024-10-02 13:30:55 -04:00
Timothy Yung
459fd418cf Define HostInstance type for React Native (#31101)
## Summary

Creates a new `HostInstance` type for React Native, to more accurately
capture the intent most developers have when using the `NativeMethods`
type or `React.ElementRef<HostComponent<T>>`.

Since `React.ElementRef<HostComponent<T>>` is typed as
`React.AbstractComponent<T, NativeMethods>`, that means
`React.ElementRef<HostComponent<T>>` is equivalent to `NativeMethods`
which is equivalent to `HostInstance`.


## How did you test this change?

```
$ yarn
$ yarn flow fabric
```
2024-10-01 17:25:59 -07:00
Sebastian Markbåge
99c056abb0 [Flight] Allow aborting encodeReply (#31106)
Allow aborting encoding arguments to a Server Action if a Promise
doesn't resolve. That way at least part of the arguments can be used on
the receiving side. This leaves it unresolved in the stream rather than
encoding an error.

This should error on the receiving side when the stream closes but it
doesn't right now in the Edge/Browser versions because closing happens
immediately before we've had a chance to call `.then()` so the Chunks
are still in pending state. This is an existing bug also in
FlightClient.
2024-10-01 14:28:51 -04:00
Jack Pope
d8c90fa48d Disable infinite render loop detection (#31088)
We're seeing issues with this feature internally including bugs with
sibling prerendering and errors that are difficult for developers to
action on. We'll turn off the feature for the time being until we can
improve the stability and ergonomics.

This PR does two things:
- Turn off `enableInfiniteLoopDetection` everywhere while leaving it as
a variant on www so we can do further experimentation.
- Revert https://github.com/facebook/react/pull/31061 which was a
temporary change for debugging. This brings the feature back to
baseline.
2024-10-01 11:00:57 -04:00
Ruslan Lesiutin
6e612587ec chore[react-devtools]: drop legacy context tests (#31059)
We've dropped the support for detecting changes in legacy Contexts in
https://github.com/facebook/react/pull/30896.
2024-10-01 14:26:12 +01:00
Ruslan Lesiutin
9ea5ffa9cb chore[react-devtools]: add legacy mode error message to the ignore list for tests (#31060)
Without this, the console gets spammy whenever we run React DevTools
tests against React 18.x, where this deprecation message was added.
2024-10-01 14:04:05 +01:00
Ruslan Lesiutin
40357fe630 fix[react-devtools]: request hook initialization inside http server response (#31102)
Fixes https://github.com/facebook/react/issues/31100.

There are 2 things:
1. In https://github.com/facebook/react/pull/30987, we've introduced a
breaking change: importing `react-devtools-core` is no longer enough for
installing React DevTools global Hook. You need to call `initialize`, in
which you may provide initial settings. I am not adding settings here,
because it is not implemented, and there are no plans for supporting
this.
2. Calling `installHook` is not necessary inside `standalone.js`,
because this script is running inside Electron wrapper (which is just a
UI, not the app that we are debugging). We will loose the ability to use
React DevTools on this React application, but I guess thats fine.
2024-10-01 14:03:48 +01:00
Sebastian Markbåge
654e387d7e [Flight] Serialize Server Components Props in DEV (#31105)
This allows us to show props in React DevTools when inspecting a Server
Component.

I currently drastically limit the object depth that's serialized since
this is very implicit and you can have heavy objects on the server.

We previously was using the general outlineModel to outline
ReactComponentInfo but we weren't consistently using it everywhere which
could cause some bugs with the parsing when it got deduped on the
client. It also lead to the weird feature detect of `isReactComponent`.
It also meant that this serialization was using the plain serialization
instead of `renderConsoleValue` which means we couldn't safely serialize
arbitrary debug info that isn't serializable there.

So the main change here is to call `outlineComponentInfo` and have that
always write every "Server Component" instance as outlined and in a way
that lets its props be serialized using `renderConsoleValue`.

<img width="1150" alt="Screenshot 2024-10-01 at 1 25 05 AM"
src="https://github.com/user-attachments/assets/f6e7811d-51a3-46b9-bbe0-1b8276849ed4">
2024-10-01 01:39:20 -04:00
Sebastian Markbåge
326832a56d [Flight] Serialize Error Values (#31104)
The idea is that the RSC protocol is a superset of Structured Clone.
#25687 One exception that we left out was serializing Error objects as
values. We serialize "throws" or "rejections" as Error (regardless of
their type) but not Error values.

This fixes that by serializing `Error` objects. We don't include digest
in this case since we don't call `onError` and it's not really expected
that you'd log it on the server with some way to look it up.

In general this is not super useful outside throws. Especially since we
hide their values in prod. However, there is one case where it is quite
useful. When you replay console logs in DEV you might often log an Error
object within the scope of a Server Component. E.g. the default RSC
error handling just console.error and error object.

Before this would just be an empty object due to our lax console log
serialization:
<img width="1355" alt="Screenshot 2024-09-30 at 2 24 03 PM"
src="https://github.com/user-attachments/assets/694b3fd3-f95f-4863-9321-bcea3f5c5db4">
After:
<img width="1348" alt="Screenshot 2024-09-30 at 2 36 48 PM"
src="https://github.com/user-attachments/assets/834b129d-220d-43a2-a2f4-2eb06921747d">

TODO for a follow up: Flight Reply direction. This direction doesn't
actually serialize thrown errors because they always reject the
serialization.
2024-09-30 15:45:13 -04:00
Mofei Zhang
c67e241c16 [compiler] Renames and no-op refactor for next PR
Rename for clarity:
- `CollectHoistablePropertyLoads:Tree` -> `CollectHoistablePropertyLoads:PropertyPathRegistry`
    - `getPropertyLoadNode` -> `getOrCreateProperty`
    - `getOrCreateRoot` -> `getOrCreateIdentifier`
- `PropertyLoadNode` -> `PropertyPathNode`

Refactor to CFG joining logic for `CollectHoistablePropertyLoads`. We now write to the same set of inferredNonNullObjects when traversing from entry and exit blocks. This is more correct, as non-nulls inferred from a forward traversal should be included when computing the backward traversal (and vice versa). This fix is needed by an edge case in #31036

Added invariant into fixed-point iteration to terminate (instead of infinite looping).

ghstack-source-id: 1e8eb2d566b649ede93de9a9c13dad09b96416a5
Pull Request resolved: https://github.com/facebook/react/pull/31036
2024-09-30 12:35:16 -04:00
Mofei Zhang
2cbea245cc [compiler][fixtures] Patch error-handling edge case in snap evaluator
Fix edge case in which we incorrectly returned a cached exception instead of trying to rerender with new props.
ghstack-source-id: 843fb85df4a2ae7a88f296104fb16b5f9a34c76e
Pull Request resolved: https://github.com/facebook/react/pull/31082
2024-09-30 12:35:16 -04:00
Mofei Zhang
5d12e9e10b [compiler] repro for dep merging edge case (non-hir)
Found when writing #31037, summary copied from comments:

This is an extreme edge case and not code we'd expect any reasonable developer to write. In most cases e.g. `(a?.b != null ? a.b : DEFAULT)`, we do want to take a dependency on `a?.b`.

I found this trying to come up with edge cases that break the current dependency + CFG merging logic. I think it makes sense to error on the side of correctness. After all, we still take `a` as a dependency if users write `a != null ? a.b : DEFAULT`, and the same fix (understanding the `<hoistable> != null` test expression) works for both. Can be convinced otherwise though!

ghstack-source-id: cc06afda59f7681e228495f5e35a596c20f875f5
Pull Request resolved: https://github.com/facebook/react/pull/31035
2024-09-30 12:35:16 -04:00
Mofei Zhang
58a3ca3b47 [compiler][hir-rewrite] Cleanup Identifier -> IdentifierId
Since removing ExitSSA, Identifier and IdentifierId should mean the same thing

ghstack-source-id: 076cacbe8360e716b0555088043502823f9ee72e
Pull Request resolved: https://github.com/facebook/react/pull/31034
2024-09-30 12:35:16 -04:00
Mofei Zhang
8c89fa7643 [compiler][hir-rewrite] Infer non-null props, destructure source
Followup from #30894.
This adds a new flagged mode `enablePropagateScopeDepsInHIR: "enabled_with_optimizations"`, under which we infer more hoistable loads:
- it's always safe to evaluate loads from `props` (i.e. first parameter of a `component`)
- destructuring sources are safe to evaluate loads from (e.g. given `{x} = obj`, we infer that it's safe to evaluate obj.y)
- computed load sources are safe to evaluate loads from (e.g. given `arr[0]`, we can infer that it's safe to evaluate arr.length)

ghstack-source-id: 32f3bb72e9f85922825579bd785d636f4ccf724d
Pull Request resolved: https://github.com/facebook/react/pull/31033
2024-09-30 12:35:16 -04:00
Mofei Zhang
1a779207a7 [compiler][test fixtures] Add enablePropagateDepsInHIR to forked tests
Annotates fixtures added in #31030 with `@enablePropagateDepsInHIR` to fork behavior (and commit snapshot differences)
ghstack-source-id: e423e8c42db62f1bb87562b770761be09fc8ffc6
Pull Request resolved: https://github.com/facebook/react/pull/31031
2024-09-30 12:35:15 -04:00
Mofei Zhang
943e45e910 [compiler][test fixtures] Fork more fixtures for hir-rewrite
Followup from #30894 , not sure how these got missed. Note that this PR just copies the fixtures without adding `@enablePropagateDepsInHIR`. #31032 follows and actually enables the HIR-version of propagateScopeDeps to run. I split this out into two PRs to make snapshot differences easier to review, but also happy to merge

Fixtures found from locally setting snap test runner to default to `enablePropagateDepsInHIR: 'enabled_baseline'` and forking fixtures files with different output.

ghstack-source-id: 7d7cf41aa923d83ad49f89079171b0411923ce6b
Pull Request resolved: https://github.com/facebook/react/pull/31030
2024-09-30 12:35:15 -04:00
Ruslan Lesiutin
2d16326d9a fix[scripts/devtools/publish-release]: parse version list instead of handling 404 (#31087)
Discovered yesterday while was publishing a new release.

NPM `10.x.x` changed the text for 404 errors, so this check was failing.
Instead of handling 404 as a signal, I think its better to just parse
the whole list of versions and check if the new one is already there.
2024-09-30 17:07:54 +01:00
Lauren Tan
db240980a3 [playground] Decouple playground from compiler
Currently the playground is setup as a linked workspace for the
compiler which complicates our yarn workspace setup and means that snap
can sometimes pull in a different version of react than was otherwise
specified.

There's no real reason to have these workspaces combined so let's split
them up.

ghstack-source-id: 56ab064b2f
Pull Request resolved: https://github.com/facebook/react/pull/31081
2024-09-27 15:25:07 -04:00
Lauren Tan
3edc000d77 [compiler] Fix broken tests
ghstack-source-id: 000a37ae1f819eef676dcd52410d5231cd2d50fe
Pull Request resolved: https://github.com/facebook/react/pull/31078
2024-09-26 17:44:28 -04:00
Lauren Tan
d34da5cdb9 [ci] Fix incorrect sha / commit messages in manual Meta builds
ghstack-source-id: 0790b32d29
Pull Request resolved: https://github.com/facebook/react/pull/31083
2024-09-26 17:43:07 -04:00
Josh Story
67fee58b1f [Fizz] Start initial work immediately (#31079)
In a recent update we make Flight start working immediately rather than
waitin for a new task. This commit updates fizz to have similar
mechanics. We start the render in the currently running task but we do
so in a microtask to avoid reentrancy. This aligns Fizz with Flight.

ref: https://github.com/facebook/react/pull/30961
2024-09-26 13:51:45 -07:00
Ricky
76aee6f39d Revert "Make prerendering always non-blocking" (#31080)
Reverts facebook/react#31056
2024-09-26 16:48:57 -04:00
Jack Pope
60b1420f18 Turn on lazy context propagation for RN and RTR (#31076)
Following https://github.com/facebook/react/pull/30935 let's turn this
on across the board so we can clean up experiments in RN.
2024-09-26 12:55:46 -04:00
Edmond Chui
204a551eae Add: reload to profile for Fusebox (#31021)
## Summary

Add reload to profile for Fusebox 

Stacked on #31048. See
6be1977112

## How did you test this change?

Test E2E in [D63233256](https://www.internalfb.com/diff/D63233256)
2024-09-26 16:39:51 +01:00
Ruslan Lesiutin
b091ef7e0b fix: update release scripts for react devtools (#31069)
This has been broken since the migration to GitHub actions.

Previously, we've been using `buildId` as an identifier from CircleCI.
I've decided to use a commit hash as an identifier, because I don't know
if there is a better option, and
`scripts/release/download_build_artifacts.js` allows us to download them
for a specific commit.
2024-09-26 15:42:59 +01:00
Lauren Tan
b90e440231 [dependabot] Ignore all fixture directories
Seems like we can specify a wildcard dependency name to ignore all
dependencies from being updated. As I understand it dependabot will
still run monthly but no PRs will be generated.

ghstack-source-id: 64b76bd532663cdc4db10ba6299e791b5908d5b1
Pull Request resolved: https://github.com/facebook/react/pull/31074
2024-09-26 10:34:35 -04:00
Lauren Tan
0e9ccde185 [dependabot] Remove stale directories from config
ghstack-source-id: 570399bc77529bf9fb005149cfd20ba59405b2bc
Pull Request resolved: https://github.com/facebook/react/pull/31073
2024-09-26 10:34:35 -04:00
Lauren Tan
b7f74f39d8 [ez] Update deprecated action
ghstack-source-id: 991b314fd610bcbca68df52149866b2c6d8e6799
Pull Request resolved: https://github.com/facebook/react/pull/31047
2024-09-26 10:20:10 -04:00
Lauren Tan
c900ee0cce [ez] Make commit sha clickable for fbsource
ghstack-source-id: 1307b9a83a8613c08f11be5397ab45719df00992
Pull Request resolved: https://github.com/facebook/react/pull/31046
2024-09-26 10:20:10 -04:00
Lauren Tan
64be7b4dcc [ci] Fix missing values during manual workflow dispatch
It seems like the github.event.workflow_run payload is only populated
for non manual runs of the workflow, so this would crash the manual
dispatch

Test plan: https://github.com/facebook/react/actions/runs/11017512571
completes

ghstack-source-id: fce02b17f85ac4762de0561764785882e767c872
Pull Request resolved: https://github.com/facebook/react/pull/31045
2024-09-26 10:20:10 -04:00
dependabot[bot]
db320ea4d9 Bump rollup from 3.20.0 to 3.29.5 (#31072)
Bumps [rollup](https://github.com/rollup/rollup) from 3.20.0 to 3.29.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rollup/rollup/releases">rollup's
releases</a>.</em></p>
<blockquote>
<h2>v3.29.4</h2>
<h2>3.29.4</h2>
<p><em>2023-09-28</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Fix static analysis when an exported function uses callbacks (<a
href="https://redirect.github.com/rollup/rollup/issues/5158">#5158</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5158">#5158</a>:
Deoptimize all parameters when losing track of a function (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>v3.29.3</h2>
<h2>3.29.3</h2>
<p><em>2023-09-24</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Fix a bug where code was wrongly tree-shaken after mutating function
parameters (<a
href="https://redirect.github.com/rollup/rollup/issues/5153">#5153</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5145">#5145</a>:
docs: improve the docs repl appearance in the light mode (<a
href="https://github.com/TrickyPi"><code>@​TrickyPi</code></a>)</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5148">#5148</a>:
chore(deps): update dependency
<code>@​vue/eslint-config-typescript</code> to v12 (<a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot])</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5149">#5149</a>:
chore(deps): lock file maintenance minor/patch updates (<a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot])</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5153">#5153</a>:
Fully deoptimize first level path when deoptimizing nested parameter
paths (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>v3.29.2</h2>
<h2>3.29.2</h2>
<p><em>2023-09-15</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Export <code>TreeshakingPreset</code> type (<a
href="https://redirect.github.com/rollup/rollup/issues/5131">#5131</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5131">#5131</a>:
fix: exports <code>TreeshakingPreset</code> (<a
href="https://github.com/moltar"><code>@​moltar</code></a>)</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5134">#5134</a>:
docs: steps to enable symlinks on windows (<a
href="https://github.com/thebanjomatic"><code>@​thebanjomatic</code></a>)</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5137">#5137</a>:
chore(deps): lock file maintenance minor/patch updates (<a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot])</li>
</ul>
<h2>v3.29.1</h2>
<h2>3.29.1</h2>
<p><em>2023-09-10</em></p>
<h3>Bug Fixes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rollup/rollup/blob/master/CHANGELOG.md">rollup's
changelog</a>.</em></p>
<blockquote>
<h1>rollup changelog</h1>
<h2>4.22.4</h2>
<p><em>2024-09-21</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Fix a vulnerability in generated code that affects IIFE, UMD and CJS
bundles when run in a browser context (<a
href="https://redirect.github.com/rollup/rollup/issues/5671">#5671</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5670">#5670</a>:
refactor: Use object.prototype to check for reserved properties (<a
href="https://github.com/YuHyeonWook"><code>@​YuHyeonWook</code></a>)</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5671">#5671</a>:
Fix DOM Clobbering CVE (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>4.22.3</h2>
<p><em>2024-09-21</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Ensure that mutations in modules without side effects are observed
while properly handling transitive dependencies (<a
href="https://redirect.github.com/rollup/rollup/issues/5669">#5669</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5669">#5669</a>:
Ensure impure dependencies of pure modules are added (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>4.22.2</h2>
<p><em>2024-09-20</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Revert fix for side effect free modules until other issues are
investigated (<a
href="https://redirect.github.com/rollup/rollup/issues/5667">#5667</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5667">#5667</a>:
Partially revert <a
href="https://redirect.github.com/rollup/rollup/issues/5658">#5658</a>
and re-apply <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a>
(<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>4.22.1</h2>
<p><em>2024-09-20</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Revert <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a>
&quot;stable chunk hashes&quot; while issues are being investigated</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5663">#5663</a>:
chore(deps): update dependency inquirer to v11 (<a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot], <a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="dfd233d3a9"><code>dfd233d</code></a>
3.29.5</li>
<li><a
href="2ef77c00ec"><code>2ef77c0</code></a>
Fix DOM Clobbering CVE</li>
<li><a
href="a6448b99f7"><code>a6448b9</code></a>
3.29.4</li>
<li><a
href="4e92d60fa9"><code>4e92d60</code></a>
Deoptimize all parameters when losing track of a function (<a
href="https://redirect.github.com/rollup/rollup/issues/5158">#5158</a>)</li>
<li><a
href="801ffd1a9b"><code>801ffd1</code></a>
3.29.3</li>
<li><a
href="353e462828"><code>353e462</code></a>
Fully deoptimize first level path when deoptimizing nested parameter
paths (#...</li>
<li><a
href="a1a89e77fb"><code>a1a89e7</code></a>
chore(deps): update dependency
<code>@​vue/eslint-config-typescript</code> to v12 (<a
href="https://redirect.github.com/rollup/rollup/issues/5148">#5148</a>)</li>
<li><a
href="cc14f7070a"><code>cc14f70</code></a>
chore(deps): lock file maintenance minor/patch updates (<a
href="https://redirect.github.com/rollup/rollup/issues/5149">#5149</a>)</li>
<li><a
href="1e8355b2b6"><code>1e8355b</code></a>
docs: improve the docs repl appearance in the light mode (<a
href="https://redirect.github.com/rollup/rollup/issues/5145">#5145</a>)</li>
<li><a
href="5950fc8a6b"><code>5950fc8</code></a>
Adapt branches in REPL workflow</li>
<li>Additional commits viewable in <a
href="https://github.com/rollup/rollup/compare/v3.20.0...v3.29.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rollup&package-manager=npm_and_yarn&previous-version=3.20.0&new-version=3.29.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 09:56:33 -04:00
dependabot[bot]
f7aff7f15c Bump express from 4.18.2 to 4.21.0 (#30980)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to
4.21.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/releases">express's
releases</a>.</em></p>
<blockquote>
<h2>4.21.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Deprecate <code>&quot;back&quot;</code> magic string in redirects by
<a href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5935">expressjs/express#5935</a></li>
<li>finalhandler@1.3.1 by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5954">expressjs/express#5954</a></li>
<li>fix(deps): serve-static@1.16.2 by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5951">expressjs/express#5951</a></li>
<li>Upgraded dependency qs to 6.13.0 to match qs in body-parser by <a
href="https://github.com/agadzinski93"><code>@​agadzinski93</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5946">expressjs/express#5946</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/agadzinski93"><code>@​agadzinski93</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5946">expressjs/express#5946</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.20.0...4.21.0">https://github.com/expressjs/express/compare/4.20.0...4.21.0</a></p>
<h2>4.20.0</h2>
<h2>What's Changed</h2>
<h3>Important</h3>
<ul>
<li>IMPORTANT: The default <code>depth</code> level for parsing
URL-encoded data is now <code>32</code> (previously was
<code>Infinity</code>)</li>
<li>Remove link renderization in html while using
<code>res.redirect</code></li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>4.19.2 Staging by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5561">expressjs/express#5561</a></li>
<li>remove duplicate location test for data uri by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5562">expressjs/express#5562</a></li>
<li>feat: document beta releases expectations by <a
href="https://github.com/marco-ippolito"><code>@​marco-ippolito</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5565">expressjs/express#5565</a></li>
<li>Cut down on duplicated CI runs by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5564">expressjs/express#5564</a></li>
<li>Add a Threat Model by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5526">expressjs/express#5526</a></li>
<li>Assign captain of encodeurl by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5579">expressjs/express#5579</a></li>
<li>Nominate jonchurch as repo captain for <code>http-errors</code>,
<code>expressjs.com</code>, <code>morgan</code>, <code>cors</code>,
<code>body-parser</code> by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5587">expressjs/express#5587</a></li>
<li>docs: update Security.md by <a
href="https://github.com/inigomarquinez"><code>@​inigomarquinez</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5590">expressjs/express#5590</a></li>
<li>docs: update triage nomination policy by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5600">expressjs/express#5600</a></li>
<li>Add CodeQL (SAST) by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5433">expressjs/express#5433</a></li>
<li>docs: add UlisesGascon as triage initiative captain by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5605">expressjs/express#5605</a></li>
<li>deps: encodeurl@~2.0.0 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5569">expressjs/express#5569</a></li>
<li>skip QUERY method test by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5628">expressjs/express#5628</a></li>
<li>ignore ETAG query test on 21 and 22, reuse skip util by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5639">expressjs/express#5639</a></li>
<li>add support Node.js@22 in the CI by <a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5627">expressjs/express#5627</a></li>
<li>doc: add table of contents, tc/triager lists to readme by <a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5619">expressjs/express#5619</a></li>
<li>List and sort all projects, add captains by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5653">expressjs/express#5653</a></li>
<li>docs: add <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
as captain for cookie-parser by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5666">expressjs/express#5666</a></li>
<li> bring back query tests for node 21 by <a
href="https://github.com/ctcpip"><code>@​ctcpip</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5690">expressjs/express#5690</a></li>
<li>[v4] Deprecate <code>res.clearCookie</code> accepting
<code>options.maxAge</code> and <code>options.expires</code> by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5672">expressjs/express#5672</a></li>
<li>skip QUERY tests for Node 21 only, still not supported by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5695">expressjs/express#5695</a></li>
<li>📝 update people, add ctcpip to TC by <a
href="https://github.com/ctcpip"><code>@​ctcpip</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5683">expressjs/express#5683</a></li>
<li>remove minor version pinning from ci by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5722">expressjs/express#5722</a></li>
<li>Fix link variable use in attribution section of CODE OF CONDUCT by
<a href="https://github.com/IamLizu"><code>@​IamLizu</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5762">expressjs/express#5762</a></li>
<li>Replace Appveyor windows testing with GHA by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5599">expressjs/express#5599</a></li>
<li>Add OSSF Scorecard badge by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5436">expressjs/express#5436</a></li>
<li>update scorecard link by <a
href="https://github.com/bjohansebas"><code>@​bjohansebas</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5814">expressjs/express#5814</a></li>
<li>Nominate <a
href="https://github.com/IamLizu"><code>@​IamLizu</code></a> to the
triage team by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5836">expressjs/express#5836</a></li>
<li>deps: path-to-regexp@0.1.8 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5603">expressjs/express#5603</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/blob/4.21.0/History.md">express's
changelog</a>.</em></p>
<blockquote>
<h1>4.21.0 / 2024-09-11</h1>
<ul>
<li>Deprecate <code>res.location(&quot;back&quot;)</code> and
<code>res.redirect(&quot;back&quot;)</code> magic string</li>
<li>deps: serve-static@1.16.2
<ul>
<li>includes send@0.19.0</li>
</ul>
</li>
<li>deps: finalhandler@1.3.1</li>
<li>deps: qs@6.13.0</li>
</ul>
<h1>4.20.0 / 2024-09-10</h1>
<ul>
<li>deps: serve-static@0.16.0
<ul>
<li>Remove link renderization in html while redirecting</li>
</ul>
</li>
<li>deps: send@0.19.0
<ul>
<li>Remove link renderization in html while redirecting</li>
</ul>
</li>
<li>deps: body-parser@0.6.0
<ul>
<li>add <code>depth</code> option to customize the depth level in the
parser</li>
<li>IMPORTANT: The default <code>depth</code> level for parsing
URL-encoded data is now <code>32</code> (previously was
<code>Infinity</code>)</li>
</ul>
</li>
<li>Remove link renderization in html while using
<code>res.redirect</code></li>
<li>deps: path-to-regexp@0.1.10
<ul>
<li>Adds support for named matching groups in the routes using a
regex</li>
<li>Adds backtracking protection to parameters without regexes
defined</li>
</ul>
</li>
<li>deps: encodeurl@~2.0.0
<ul>
<li>Removes encoding of <code>\</code>, <code>|</code>, and
<code>^</code> to align better with URL spec</li>
</ul>
</li>
<li>Deprecate passing <code>options.maxAge</code> and
<code>options.expires</code> to <code>res.clearCookie</code>
<ul>
<li>Will be ignored in v5, clearCookie will set a cookie with an expires
in the past to instruct clients to delete the cookie</li>
</ul>
</li>
</ul>
<h1>4.19.2 / 2024-03-25</h1>
<ul>
<li>Improved fix for open redirect allow list bypass</li>
</ul>
<h1>4.19.1 / 2024-03-20</h1>
<ul>
<li>Allow passing non-strings to res.location with new encoding handling
checks</li>
</ul>
<h1>4.19.0 / 2024-03-20</h1>
<ul>
<li>Prevent open redirect allow list bypass due to encodeurl</li>
<li>deps: cookie@0.6.0</li>
</ul>
<h1>4.18.3 / 2024-02-29</h1>
<ul>
<li>Fix routing requests without method</li>
<li>deps: body-parser@1.20.2
<ul>
<li>Fix strict json error message on Node.js 19+</li>
<li>deps: content-type@~1.0.5</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7e562c6d8d"><code>7e562c6</code></a>
4.21.0</li>
<li><a
href="1bcde96bc8"><code>1bcde96</code></a>
fix(deps): qs@6.13.0 (<a
href="https://redirect.github.com/expressjs/express/issues/5946">#5946</a>)</li>
<li><a
href="7d36477568"><code>7d36477</code></a>
fix(deps): serve-static@1.16.2 (<a
href="https://redirect.github.com/expressjs/express/issues/5951">#5951</a>)</li>
<li><a
href="40d2d8f2c8"><code>40d2d8f</code></a>
fix(deps): finalhandler@1.3.1</li>
<li><a
href="77ada906db"><code>77ada90</code></a>
Deprecate <code>&quot;back&quot;</code> magic string in redirects (<a
href="https://redirect.github.com/expressjs/express/issues/5935">#5935</a>)</li>
<li><a
href="21df421ebc"><code>21df421</code></a>
4.20.0</li>
<li><a
href="4c9ddc1c47"><code>4c9ddc1</code></a>
feat: upgrade to serve-static@0.16.0</li>
<li><a
href="9ebe5d500d"><code>9ebe5d5</code></a>
feat: upgrade to send@0.19.0 (<a
href="https://redirect.github.com/expressjs/express/issues/5928">#5928</a>)</li>
<li><a
href="ec4a01b6b8"><code>ec4a01b</code></a>
feat: upgrade to body-parser@1.20.3 (<a
href="https://redirect.github.com/expressjs/express/issues/5926">#5926</a>)</li>
<li><a
href="54271f69b5"><code>54271f6</code></a>
fix: don't render redirect values in anchor href</li>
<li>Additional commits viewable in <a
href="https://github.com/expressjs/express/compare/4.18.2...4.21.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~wesleytodd">wesleytodd</a>, a new releaser
for express since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=express&package-manager=npm_and_yarn&previous-version=4.18.2&new-version=4.21.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 09:56:24 -04:00
dependabot[bot]
f04b8fc88e Bump ws from 6.2.2 to 6.2.3 in /scripts/release (#30463)
Bumps [ws](https://github.com/websockets/ws) from 6.2.2 to 6.2.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/websockets/ws/releases">ws's
releases</a>.</em></p>
<blockquote>
<h2>6.2.3</h2>
<h1>Bug fixes</h1>
<ul>
<li>Backported e55e5106 to the 6.x release line (eeb76d31).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d87f3b6d3a"><code>d87f3b6</code></a>
[dist] 6.2.3</li>
<li><a
href="eeb76d313e"><code>eeb76d3</code></a>
[security] Fix crash when the Upgrade header cannot be read (<a
href="https://redirect.github.com/websockets/ws/issues/2231">#2231</a>)</li>
<li>See full diff in <a
href="https://github.com/websockets/ws/compare/6.2.2...6.2.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ws&package-manager=npm_and_yarn&previous-version=6.2.2&new-version=6.2.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 09:56:10 -04:00
dependabot[bot]
d0772d5866 Bump axios from 1.7.1 to 1.7.4 in /compiler (#30694)
Bumps [axios](https://github.com/axios/axios) from 1.7.1 to 1.7.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>Release v1.7.4</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>sec:</strong> CVE-2024-39338 (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6543">#6543</a>)
(<a
href="6b6b605eaf">6b6b605</a>)</li>
<li><strong>sec:</strong> disregard protocol-relative URL to remediate
SSRF (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="07a661a2a6">07a661a</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/levpachmanov"
title="+47/-11 ([#6543](https://github.com/axios/axios/issues/6543)
)">Lev Pachmanov</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/hainenber"
title="+49/-4 ([#6539](https://github.com/axios/axios/issues/6539) )">Đỗ
Trọng Hải</a></li>
</ul>
<h2>Release v1.7.3</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>adapter:</strong> fix progress event emitting; (<a
href="https://redirect.github.com/axios/axios/issues/6518">#6518</a>)
(<a
href="e3c76fc9bd">e3c76fc</a>)</li>
<li><strong>fetch:</strong> fix withCredentials request config (<a
href="https://redirect.github.com/axios/axios/issues/6505">#6505</a>)
(<a
href="85d4d0ea0a">85d4d0e</a>)</li>
<li><strong>xhr:</strong> return original config on errors from XHR
adapter (<a
href="https://redirect.github.com/axios/axios/issues/6515">#6515</a>)
(<a
href="8966ee7ea6">8966ee7</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+211/-159
([#6518](https://github.com/axios/axios/issues/6518)
[#6519](https://github.com/axios/axios/issues/6519) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/ValeraS"
title="+3/-3 ([#6515](https://github.com/axios/axios/issues/6515)
)">Valerii Sidorenko</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/prianyu"
title="+2/-2 ([#6505](https://github.com/axios/axios/issues/6505)
)">prianYu</a></li>
</ul>
<h2>Release v1.7.2</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>fetch:</strong> enhance fetch API detection; (<a
href="https://redirect.github.com/axios/axios/issues/6413">#6413</a>)
(<a
href="4f79aef81b">4f79aef</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+3/-3
([#6413](https://github.com/axios/axios/issues/6413) )">Dmitriy
Mozgovoy</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/axios/axios/compare/v1.7.3...v1.7.4">1.7.4</a>
(2024-08-13)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>sec:</strong> CVE-2024-39338 (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6543">#6543</a>)
(<a
href="6b6b605eaf">6b6b605</a>)</li>
<li><strong>sec:</strong> disregard protocol-relative URL to remediate
SSRF (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="07a661a2a6">07a661a</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/levpachmanov"
title="+47/-11 ([#6543](https://github.com/axios/axios/issues/6543)
)">Lev Pachmanov</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/hainenber"
title="+49/-4 ([#6539](https://github.com/axios/axios/issues/6539) )">Đỗ
Trọng Hải</a></li>
</ul>
<h2><a
href="https://github.com/axios/axios/compare/v1.7.2...v1.7.3">1.7.3</a>
(2024-08-01)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>adapter:</strong> fix progress event emitting; (<a
href="https://redirect.github.com/axios/axios/issues/6518">#6518</a>)
(<a
href="e3c76fc9bd">e3c76fc</a>)</li>
<li><strong>fetch:</strong> fix withCredentials request config (<a
href="https://redirect.github.com/axios/axios/issues/6505">#6505</a>)
(<a
href="85d4d0ea0a">85d4d0e</a>)</li>
<li><strong>xhr:</strong> return original config on errors from XHR
adapter (<a
href="https://redirect.github.com/axios/axios/issues/6515">#6515</a>)
(<a
href="8966ee7ea6">8966ee7</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+211/-159
([#6518](https://github.com/axios/axios/issues/6518)
[#6519](https://github.com/axios/axios/issues/6519) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/ValeraS"
title="+3/-3 ([#6515](https://github.com/axios/axios/issues/6515)
)">Valerii Sidorenko</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/prianyu"
title="+2/-2 ([#6505](https://github.com/axios/axios/issues/6505)
)">prianYu</a></li>
</ul>
<h2><a
href="https://github.com/axios/axios/compare/v1.7.1...v1.7.2">1.7.2</a>
(2024-05-21)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>fetch:</strong> enhance fetch API detection; (<a
href="https://redirect.github.com/axios/axios/issues/6413">#6413</a>)
(<a
href="4f79aef81b">4f79aef</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+3/-3
([#6413](https://github.com/axios/axios/issues/6413) )">Dmitriy
Mozgovoy</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="abd24a7367"><code>abd24a7</code></a>
chore(release): v1.7.4 (<a
href="https://redirect.github.com/axios/axios/issues/6544">#6544</a>)</li>
<li><a
href="6b6b605eaf"><code>6b6b605</code></a>
fix(sec): CVE-2024-39338 (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6543">#6543</a>)</li>
<li><a
href="07a661a2a6"><code>07a661a</code></a>
fix(sec): disregard protocol-relative URL to remediate SSRF (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)</li>
<li><a
href="c6cce43cd9"><code>c6cce43</code></a>
chore(release): v1.7.3 (<a
href="https://redirect.github.com/axios/axios/issues/6521">#6521</a>)</li>
<li><a
href="e3c76fc9bd"><code>e3c76fc</code></a>
fix(adapter): fix progress event emitting; (<a
href="https://redirect.github.com/axios/axios/issues/6518">#6518</a>)</li>
<li><a
href="85d4d0ea0a"><code>85d4d0e</code></a>
fix(fetch): fix withCredentials request config (<a
href="https://redirect.github.com/axios/axios/issues/6505">#6505</a>)</li>
<li><a
href="92cd8ed943"><code>92cd8ed</code></a>
chore(github): update ISSUE_TEMPLATE.md (<a
href="https://redirect.github.com/axios/axios/issues/6519">#6519</a>)</li>
<li><a
href="8966ee7ea6"><code>8966ee7</code></a>
fix(xhr): return original config on errors from XHR adapter (<a
href="https://redirect.github.com/axios/axios/issues/6515">#6515</a>)</li>
<li><a
href="0e4f9fa290"><code>0e4f9fa</code></a>
chore(release): v1.7.2 (<a
href="https://redirect.github.com/axios/axios/issues/6414">#6414</a>)</li>
<li><a
href="4f79aef81b"><code>4f79aef</code></a>
fix(fetch): enhance fetch API detection; (<a
href="https://redirect.github.com/axios/axios/issues/6413">#6413</a>)</li>
<li>See full diff in <a
href="https://github.com/axios/axios/compare/v1.7.1...v1.7.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.7.1&new-version=1.7.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 09:51:12 -04:00
dependabot[bot]
9927ab238b Bump rollup from 4.13.2 to 4.22.4 in /compiler (#31039)
Bumps [rollup](https://github.com/rollup/rollup) from 4.13.2 to 4.22.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rollup/rollup/releases">rollup's
releases</a>.</em></p>
<blockquote>
<h2>v4.22.4</h2>
<h2>4.22.4</h2>
<p><em>2024-09-21</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Fix a vulnerability in generated code that affects IIFE, UMD and CJS
bundles when run in a browser context (<a
href="https://redirect.github.com/rollup/rollup/issues/5671">#5671</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5670">#5670</a>:
refactor: Use object.prototype to check for reserved properties (<a
href="https://github.com/YuHyeonWook"><code>@​YuHyeonWook</code></a>)</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5671">#5671</a>:
Fix DOM Clobbering CVE (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>v4.22.3</h2>
<h2>4.22.3</h2>
<p><em>2024-09-21</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Ensure that mutations in modules without side effects are observed
while properly handling transitive dependencies (<a
href="https://redirect.github.com/rollup/rollup/issues/5669">#5669</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5669">#5669</a>:
Ensure impure dependencies of pure modules are added (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>v4.22.2</h2>
<h2>4.22.2</h2>
<p><em>2024-09-20</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Revert fix for side effect free modules until other issues are
investigated (<a
href="https://redirect.github.com/rollup/rollup/issues/5667">#5667</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5667">#5667</a>:
Partially revert <a
href="https://redirect.github.com/rollup/rollup/issues/5658">#5658</a>
and re-apply <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a>
(<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>v4.22.1</h2>
<h2>4.22.1</h2>
<p><em>2024-09-20</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Revert <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a>
&quot;stable chunk hashes&quot; while issues are being investigated</li>
</ul>
<h3>Pull Requests</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rollup/rollup/blob/master/CHANGELOG.md">rollup's
changelog</a>.</em></p>
<blockquote>
<h2>4.22.4</h2>
<p><em>2024-09-21</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Fix a vulnerability in generated code that affects IIFE, UMD and CJS
bundles when run in a browser context (<a
href="https://redirect.github.com/rollup/rollup/issues/5671">#5671</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5670">#5670</a>:
refactor: Use object.prototype to check for reserved properties (<a
href="https://github.com/YuHyeonWook"><code>@​YuHyeonWook</code></a>)</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5671">#5671</a>:
Fix DOM Clobbering CVE (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>4.22.3</h2>
<p><em>2024-09-21</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Ensure that mutations in modules without side effects are observed
while properly handling transitive dependencies (<a
href="https://redirect.github.com/rollup/rollup/issues/5669">#5669</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5669">#5669</a>:
Ensure impure dependencies of pure modules are added (<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>4.22.2</h2>
<p><em>2024-09-20</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Revert fix for side effect free modules until other issues are
investigated (<a
href="https://redirect.github.com/rollup/rollup/issues/5667">#5667</a>)</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5667">#5667</a>:
Partially revert <a
href="https://redirect.github.com/rollup/rollup/issues/5658">#5658</a>
and re-apply <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a>
(<a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
</ul>
<h2>4.22.1</h2>
<p><em>2024-09-20</em></p>
<h3>Bug Fixes</h3>
<ul>
<li>Revert <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a>
&quot;stable chunk hashes&quot; while issues are being investigated</li>
</ul>
<h3>Pull Requests</h3>
<ul>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5663">#5663</a>:
chore(deps): update dependency inquirer to v11 (<a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot], <a
href="https://github.com/lukastaegert"><code>@​lukastaegert</code></a>)</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5664">#5664</a>:
chore(deps): lock file maintenance minor/patch updates (<a
href="https://github.com/renovate"><code>@​renovate</code></a>[bot])</li>
<li><a
href="https://redirect.github.com/rollup/rollup/pull/5665">#5665</a>:
fix: type in CI file (<a
href="https://github.com/YuHyeonWook"><code>@​YuHyeonWook</code></a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="79c0aba353"><code>79c0aba</code></a>
4.22.4</li>
<li><a
href="e2552c9e95"><code>e2552c9</code></a>
Fix DOM Clobbering CVE (<a
href="https://redirect.github.com/rollup/rollup/issues/5671">#5671</a>)</li>
<li><a
href="10ab90ea61"><code>10ab90e</code></a>
refactor: Use object.prototype to check for reserved properties (<a
href="https://redirect.github.com/rollup/rollup/issues/5670">#5670</a>)</li>
<li><a
href="e1cba8e84a"><code>e1cba8e</code></a>
4.22.3</li>
<li><a
href="59cec3e867"><code>59cec3e</code></a>
Ensure impure dependencies of pure modules are added (<a
href="https://redirect.github.com/rollup/rollup/issues/5669">#5669</a>)</li>
<li><a
href="b86ffd776c"><code>b86ffd7</code></a>
4.22.2</li>
<li><a
href="d5ff63de9e"><code>d5ff63d</code></a>
Partially revert <a
href="https://redirect.github.com/rollup/rollup/issues/5658">#5658</a>
and re-apply <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a>
(<a
href="https://redirect.github.com/rollup/rollup/issues/5667">#5667</a>)</li>
<li><a
href="0a821d9318"><code>0a821d9</code></a>
Create SECURITY.md</li>
<li><a
href="76e962daca"><code>76e962d</code></a>
4.22.1</li>
<li><a
href="68c23da882"><code>68c23da</code></a>
Partially revert <a
href="https://redirect.github.com/rollup/rollup/issues/5644">#5644</a></li>
<li>Additional commits viewable in <a
href="https://github.com/rollup/rollup/compare/v4.13.2...v4.22.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rollup&package-manager=npm_and_yarn&previous-version=4.13.2&new-version=4.22.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 09:45:02 -04:00
Edmond Chui
f8024b0686 refactor: allow custom impl of backend realod-to-profile support check (#31048)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

In preparation to support reload-to-profile in Fusebox (#31021), we need
a way to check capability of different backends, e.g. web vs React
Native.

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

* Default, e.g. existing web impl = no-op
* Custom impl: is called
2024-09-26 12:39:28 +01:00
Ruslan Lesiutin
d66fa02a30 fix: use public instance in Fiber renderer and expose it from getInspectorDataForViewAtPoint (#31068)
React DevTools no longer operates with just Fibers, it now builds its
own Shadow Tree, which represents the tree on the Host (Fabric on
Native, DOM on Web).

We have to keep track of public instances for a select-to-inspect
feature. We've recently changed this logic in
https://github.com/facebook/react/pull/30831, and looks like we've been
incorrectly getting a public instance for Fabric case.

Not only this, turns out that all `getInspectorData...` APIs are
returning Fibers, and not public instances. I have to expose it, so that
React DevTools can correctly identify the element, which was selected.

Changes for React Native are in
[D63421463](https://www.internalfb.com/diff/D63421463)
2024-09-26 10:17:16 +01:00
Sebastian "Sebbie" Silbermann
778e1ed2e5 [Fiber] Fix missing render times when we cancel a pending commit (#31065) 2024-09-26 00:20:03 +02:00
Andrew Clark
0f1856c49f Make prerendering always non-blocking (#31056)
When a synchronous update suspends, and we prerender the siblings, the
prerendering should be non-blocking so that we can immediately restart
once the data arrives.

This happens automatically when there's a Suspense boundary, because we
immediately commit the boundary and then proceed to a Retry render,
which are always concurrent. When there's not a Suspense boundary, there
is no Retry, so we need to take care to switch from the synchronous work
loop to the concurrent one, to enable time slicing.
2024-09-25 16:31:44 -04:00
Andrew Clark
3c7667a694 Unify perform{Sync,Concurrent}WorkOnRoot implementation (#31029)
Over time the behavior of these two paths has converged to be
essentially the same. So this merges them back into one function. This
should save some code size and also make it harder for the behavior to
accidentally diverge. (For the same reason, rolling out this change
might expose some areas where we had already accidentally diverged.)
2024-09-25 14:33:11 -04:00
Jack Pope
f9ebd85a19 Increase nested update limit to 100 (#31061)
We're seeing the limit hit in some tests after enabling sibling
prerendering. Let's bump the limit so we can run more tests and gather
more signal on the changes. When we understand the scope of the problem
we can determine whether we need to change how the updates are counted
in prerenders and/or fix specific areas of product code.
2024-09-25 11:50:41 -04:00
Ruslan Lesiutin
d2e9b9b4dc React DevTools 5.3.1 -> 6.0.0 (#31058)
Full list of changes:
* refactor: data source for errors and warnings tracking is now in Store
([hoxyq](https://github.com/hoxyq) in
[#31010](https://github.com/facebook/react/pull/31010))
* fix: consider alternate as a key for componentLogsEntry when
inspecting raw fiber instance ([hoxyq](https://github.com/hoxyq) in
[#31009](https://github.com/facebook/react/pull/31009))
* Fix: profiling crashes #30661 #28838
([EdmondChuiHW](https://github.com/EdmondChuiHW) in
[#31024](https://github.com/facebook/react/pull/31024))
* chore: remove using local storage for persisting console settings on
the frontend ([hoxyq](https://github.com/hoxyq) in
[#31002](https://github.com/facebook/react/pull/31002))
* feat: display message if user ended up opening hook script
([hoxyq](https://github.com/hoxyq) in
[#31000](https://github.com/facebook/react/pull/31000))
* feat: expose installHook with settings argument from
react-devtools-core/backend ([hoxyq](https://github.com/hoxyq) in
[#30987](https://github.com/facebook/react/pull/30987))
* chore: remove settings manager from react-devtools-core
([hoxyq](https://github.com/hoxyq) in
[#30986](https://github.com/facebook/react/pull/30986))
* feat[react-devtools/extension]: use chrome.storage to persist settings
across sessions ([hoxyq](https://github.com/hoxyq) in
[#30636](https://github.com/facebook/react/pull/30636))
* refactor[react-devtools]: propagate settings from global hook object
to frontend ([hoxyq](https://github.com/hoxyq) in
[#30610](https://github.com/facebook/react/pull/30610))
* chore[react-devtools]: extract some utils into separate modules to
unify implementations ([hoxyq](https://github.com/hoxyq) in
[#30597](https://github.com/facebook/react/pull/30597))
* refactor[react-devtools]: move console patching to global hook
([hoxyq](https://github.com/hoxyq) in
[#30596](https://github.com/facebook/react/pull/30596))
* refactor[react-devtools]: remove browserTheme from
ConsolePatchSettings ([hoxyq](https://github.com/hoxyq) in
[#30566](https://github.com/facebook/react/pull/30566))
* feat[react-devtools]: add settings to global hook object
([hoxyq](https://github.com/hoxyq) in
[#30564](https://github.com/facebook/react/pull/30564))
* fix: add Error prefix to Error objects names
([hoxyq](https://github.com/hoxyq) in
[#30969](https://github.com/facebook/react/pull/30969))
* Add enableComponentPerformanceTrack Flag
([sebmarkbage](https://github.com/sebmarkbage) in
[#30960](https://github.com/facebook/react/pull/30960))
* fix[rdt/fiber/renderer.js]: getCurrentFiber can be injected as null
([hoxyq](https://github.com/hoxyq) in
[#30968](https://github.com/facebook/react/pull/30968))
* disable `enableSiblingPrerendering` in experimental channel
([gnoff](https://github.com/gnoff) in
[#30952](https://github.com/facebook/react/pull/30952))
* refactor[react-devtools]: initialize renderer interface early
([hoxyq](https://github.com/hoxyq) in
[#30946](https://github.com/facebook/react/pull/30946))
* Start prerendering Suspense retries immediately
([acdlite](https://github.com/acdlite) in
[#30934](https://github.com/facebook/react/pull/30934))
* refactor[Agent/Store]: Store to send messages only after Agent is
initialized ([hoxyq](https://github.com/hoxyq) in
[#30945](https://github.com/facebook/react/pull/30945))
* refactor[RendererInterface]: expose onErrorOrWarning and
getComponentStack ([hoxyq](https://github.com/hoxyq) in
[#30931](https://github.com/facebook/react/pull/30931))
* Implement getComponentStack and onErrorOrWarning for replayed Flight
logs ([sebmarkbage](https://github.com/sebmarkbage) in
[#30930](https://github.com/facebook/react/pull/30930))
* Use Unicode Atom Symbol instead of Atom Emoji
([sebmarkbage](https://github.com/sebmarkbage) in
[#30832](https://github.com/facebook/react/pull/30832))
* Improve Layering Between Console and Renderer
([sebmarkbage](https://github.com/sebmarkbage) in
[#30925](https://github.com/facebook/react/pull/30925))
* Add Map for Server Component Logs
([sebmarkbage](https://github.com/sebmarkbage) in
[#30905](https://github.com/facebook/react/pull/30905))
* Delete fiberToFiberInstanceMap
([sebmarkbage](https://github.com/sebmarkbage) in
[#30900](https://github.com/facebook/react/pull/30900))
* Add Flight Renderer ([sebmarkbage](https://github.com/sebmarkbage) in
[#30906](https://github.com/facebook/react/pull/30906))
* Refactor Error / Warning Count Tracking
([sebmarkbage](https://github.com/sebmarkbage) in
[#30899](https://github.com/facebook/react/pull/30899))
* [flow] Upgrade Flow to 0.245.2
([SamChou19815](https://github.com/SamChou19815) in
[#30919](https://github.com/facebook/react/pull/30919))
* Separate RDT Fusebox into single-panel entry points
([huntie](https://github.com/huntie) in
[#30708](https://github.com/facebook/react/pull/30708))
* Build Updater List from the Commit instead of Map
([sebmarkbage](https://github.com/sebmarkbage) in
[#30897](https://github.com/facebook/react/pull/30897))
* Simplify Context Change Tracking in Profiler
([sebmarkbage](https://github.com/sebmarkbage) in
[#30896](https://github.com/facebook/react/pull/30896))
* Remove use of .alternate in root and recordProfilingDurations
([sebmarkbage](https://github.com/sebmarkbage) in
[#30895](https://github.com/facebook/react/pull/30895))
* Handle reordered contexts in Profiler
([sebmarkbage](https://github.com/sebmarkbage) in
[#30887](https://github.com/facebook/react/pull/30887))
* Refactor Forcing Fallback / Error of Suspense / Error Boundaries
([sebmarkbage](https://github.com/sebmarkbage) in
[#30870](https://github.com/facebook/react/pull/30870))
* Avoid getFiberIDUnsafe in debug() Helper
([sebmarkbage](https://github.com/sebmarkbage) in
[#30878](https://github.com/facebook/react/pull/30878))
* Include some Filtered Fiber Instances
([sebmarkbage](https://github.com/sebmarkbage) in
[#30865](https://github.com/facebook/react/pull/30865))
* Track root instances in a root Map
([sebmarkbage](https://github.com/sebmarkbage) in
[#30875](https://github.com/facebook/react/pull/30875))
* Track all public HostInstances in a Map
([sebmarkbage](https://github.com/sebmarkbage) in
[#30831](https://github.com/facebook/react/pull/30831))
* Support VirtualInstances in findAllCurrentHostInstances
([sebmarkbage](https://github.com/sebmarkbage) in
[#30853](https://github.com/facebook/react/pull/30853))
* Add Filtering of Environment Names
([sebmarkbage](https://github.com/sebmarkbage) in
[#30850](https://github.com/facebook/react/pull/30850))
* Support secondary environment name when it changes
([sebmarkbage](https://github.com/sebmarkbage) in
[#30842](https://github.com/facebook/react/pull/30842))
* Increase max payload for websocket in standalone app
([runeb](https://github.com/runeb) in
[#30848](https://github.com/facebook/react/pull/30848))
* Filter Server Components
([sebmarkbage](https://github.com/sebmarkbage) in
[#30839](https://github.com/facebook/react/pull/30839))
* Track virtual instances on the tracked path for selections
([sebmarkbage](https://github.com/sebmarkbage) in
[#30802](https://github.com/facebook/react/pull/30802))
* Remove displayName from inspected data
([sebmarkbage](https://github.com/sebmarkbage) in
[#30841](https://github.com/facebook/react/pull/30841))
* chore[react-devtools/hook]: remove unused native values
([hoxyq](https://github.com/hoxyq) in
[#30827](https://github.com/facebook/react/pull/30827))
* chore[react-devtools/extensions]: remove unused storage permission
([hoxyq](https://github.com/hoxyq) in
[#30826](https://github.com/facebook/react/pull/30826))
* fix[react-devtools/extensions]: fixed tabs API calls and displaying
restricted access popup ([hoxyq](https://github.com/hoxyq) in
[#30825](https://github.com/facebook/react/pull/30825))
* feat[react-devtools]: support Manifest v3 for Firefox extension
([hoxyq](https://github.com/hoxyq) in
[#30824](https://github.com/facebook/react/pull/30824))
* Reconcile Fibers Against Previous Children Instances
([sebmarkbage](https://github.com/sebmarkbage) in
[#30822](https://github.com/facebook/react/pull/30822))
* Remove findCurrentFiberUsingSlowPathByFiberInstance
([sebmarkbage](https://github.com/sebmarkbage) in
[#30818](https://github.com/facebook/react/pull/30818))
* Track Tree Base Duration of Virtual Instances
([sebmarkbage](https://github.com/sebmarkbage) in
[#30817](https://github.com/facebook/react/pull/30817))
* Use Owner Stacks to Implement View Source of a Server Component
([sebmarkbage](https://github.com/sebmarkbage) in
[#30798](https://github.com/facebook/react/pull/30798))
* Make function inspection instant
([sebmarkbage](https://github.com/sebmarkbage) in
[#30786](https://github.com/facebook/react/pull/30786))
* Make Functions Clickable to Jump to Definition
([sebmarkbage](https://github.com/sebmarkbage) in
[#30769](https://github.com/facebook/react/pull/30769))
* Support REACT_LEGACY_ELEMENT_TYPE for formatting JSX
([sebmarkbage](https://github.com/sebmarkbage) in
[#30779](https://github.com/facebook/react/pull/30779))
* Find owners from the parent path that matches the Fiber or
ReactComponentInfo ([sebmarkbage](https://github.com/sebmarkbage) in
[#30717](https://github.com/facebook/react/pull/30717))
* [Flight/DevTools] Pass the Server Component's "key" as Part of the
ReactComponentInfo ([sebmarkbage](https://github.com/sebmarkbage) in
[#30703](https://github.com/facebook/react/pull/30703))
* Hide props section if it is null
([sebmarkbage](https://github.com/sebmarkbage) in
[#30696](https://github.com/facebook/react/pull/30696))
* Support Server Components in Tree
([sebmarkbage](https://github.com/sebmarkbage) in
[#30684](https://github.com/facebook/react/pull/30684))
* fix[react-devtools/InspectedElement]: fixed border stylings when some
of the panels are not rendered ([hoxyq](https://github.com/hoxyq) in
[#30676](https://github.com/facebook/react/pull/30676))
* Compute new reordered child set from the instance tree
([sebmarkbage](https://github.com/sebmarkbage) in
[#30668](https://github.com/facebook/react/pull/30668))
* Unmount instance by walking the instance tree instead of the fiber
tree ([sebmarkbage](https://github.com/sebmarkbage) in
[#30665](https://github.com/facebook/react/pull/30665))
* Further Refactoring of Unmounts
([sebmarkbage](https://github.com/sebmarkbage) in
[#30658](https://github.com/facebook/react/pull/30658))
* Remove lodash.throttle ([sebmarkbage](https://github.com/sebmarkbage)
in [#30657](https://github.com/facebook/react/pull/30657))
* Unmount by walking previous nodes no longer in the new tree
([sebmarkbage](https://github.com/sebmarkbage) in
[#30644](https://github.com/facebook/react/pull/30644))
* Build up DevTools Instance Shadow Tree
([sebmarkbage](https://github.com/sebmarkbage) in
[#30625](https://github.com/facebook/react/pull/30625))
* chore[packages/react-devtools]: remove unused index.js
([hoxyq](https://github.com/hoxyq) in
[#30579](https://github.com/facebook/react/pull/30579))
* Track DOM nodes to Fiber map for HostHoistable Resources
([sebmarkbage](https://github.com/sebmarkbage) in
[#30590](https://github.com/facebook/react/pull/30590))
* Rename mountFiberRecursively/updateFiberRecursively
([sebmarkbage](https://github.com/sebmarkbage) in
[#30586](https://github.com/facebook/react/pull/30586))
* Allow Highlighting/Inspect HostSingletons/Hoistables and Resources
([sebmarkbage](https://github.com/sebmarkbage) in
[#30584](https://github.com/facebook/react/pull/30584))
* chore[react-devtools]: add global for native and use it to fork
backend implementation ([hoxyq](https://github.com/hoxyq) in
[#30533](https://github.com/facebook/react/pull/30533))
* Enable pointEvents while scrolling
([sebmarkbage](https://github.com/sebmarkbage) in
[#30560](https://github.com/facebook/react/pull/30560))
* Make Element Inspection Feel Snappy
([sebmarkbage](https://github.com/sebmarkbage) in
[#30555](https://github.com/facebook/react/pull/30555))
* Track the parent DevToolsInstance while mounting a tree
([sebmarkbage](https://github.com/sebmarkbage) in
[#30542](https://github.com/facebook/react/pull/30542))
* Add DevToolsInstance to Store Stateful Information
([sebmarkbage](https://github.com/sebmarkbage) in
[#30517](https://github.com/facebook/react/pull/30517))
* Implement "best renderer" by taking the inner most matched node
([sebmarkbage](https://github.com/sebmarkbage) in
[#30494](https://github.com/facebook/react/pull/30494))
* Rename NativeElement to HostInstance in the Bridge
([sebmarkbage](https://github.com/sebmarkbage) in
[#30491](https://github.com/facebook/react/pull/30491))
* Rename Fiber to Element in the Bridge Protocol and RendererInterface
([sebmarkbage](https://github.com/sebmarkbage) in
[#30490](https://github.com/facebook/react/pull/30490))
* Stop filtering owner stacks
([sebmarkbage](https://github.com/sebmarkbage) in
[#30438](https://github.com/facebook/react/pull/30438))
* [Fiber] Call life-cycles with a react-stack-bottom-frame stack frame
([sebmarkbage](https://github.com/sebmarkbage) in
[#30429](https://github.com/facebook/react/pull/30429))
* [Flight] Prefix owner stacks added to the console.log with the current
stack ([sebmarkbage](https://github.com/sebmarkbage) in
[#30427](https://github.com/facebook/react/pull/30427))
* [BE] switch to hermes parser for prettier
([kassens](https://github.com/kassens) in
[#30421](https://github.com/facebook/react/pull/30421))
* Implement Owner Stacks ([sebmarkbage](https://github.com/sebmarkbage)
in [#30417](https://github.com/facebook/react/pull/30417))
* [BE] upgrade prettier to 3.3.3 ([kassens](https://github.com/kassens)
in [#30420](https://github.com/facebook/react/pull/30420))
* [ci] Add yarn_test_build job to gh actions
* [Fizz] Refactor Component Stack Nodes
([sebmarkbage](https://github.com/sebmarkbage) in
[#30298](https://github.com/facebook/react/pull/30298))
* Print component stacks as error objects to get source mapping
([sebmarkbage](https://github.com/sebmarkbage) in
[#30289](https://github.com/facebook/react/pull/30289))
* Upgrade flow to 0.235.0 ([kassens](https://github.com/kassens) in
[#30118](https://github.com/facebook/react/pull/30118))
* fix: path handling in react devtools
([Jack-Works](https://github.com/Jack-Works) in
[#29199](https://github.com/facebook/react/pull/29199))
2024-09-25 14:38:34 +01:00
Ruslan Lesiutin
a15bbe1475 refactor: data source for errors and warnings tracking is now in Store (#31010)
Stacked on https://github.com/facebook/react/pull/31009.

1. Instead of keeping `showInlineWarningsAndErrors` in `Settings`
context (which was removed in
https://github.com/facebook/react/pull/30610), `Store` will now have a
boolean flag, which controls if the UI should be displaying information
about errors and warnings.
2. The errors and warnings counters in the Tree view are now counting
only unique errors. This makes more sense, because it is part of the
Elements Tree view, so ideally it should be showing number of components
with errors and number of components of warnings. Consider this example:
2.1. Warning for element `A` was emitted once and warning for element
`B` was emitted twice.
2.2. With previous implementation, we would show `3 ⚠️`, because in
total there were 3 warnings in total. If user tries to iterate through
these, it will only take 2 steps to do the full cycle, because there are
only 2 elements with warnings (with one having same warning, which was
emitted twice).
2.3 With current implementation, we would show `2 ⚠️`. Inspecting the
element with doubled warning will still show the warning counter (2)
before the warning message.

With these changes, the feature correctly works.
https://fburl.com/a7fw92m4
2024-09-24 19:51:21 +01:00
Ruslan Lesiutin
fc4a33eaa9 fix: consider alternate as a key for componentLogsEntry when inspecting raw fiber instance (#31009)
Related - https://github.com/facebook/react/pull/30899.

Looks like this was missed. We actually do this when we record errors
and warnings before sending them via Bridge:

e4953922a9/packages/react-devtools-shared/src/backend/fiber/renderer.js (L2169-L2173)

So, what is happening in the end, errors or warnings are displayed in
the Tree, but when user clicks on the component, nothing is shown,
because `fiberToComponentLogsMap` has only `alternate` as a key.
2024-09-24 17:49:19 +01:00
Hendrik Liebau
04bd67a490 Resolve references to deduped owner objects (#30549)
This is a follow-up from #30528 to not only handle props (the critical
change), but also the owner ~and stack~ of a referenced element.

~Handling stacks here is rather academic because the Flight Server
currently does not deduplicate owner stacks. And if they are really
identical, we should probably just dedupe the whole element.~ EDIT:
Removed from the PR.

Handling owner objects on the other hand is an actual requirement as
reported in https://github.com/vercel/next.js/issues/69545. This problem
only affects the stable release channel, as the absence of owner stacks
allows for the specific kind of shared owner deduping as demonstrated in
the unit test.
2024-09-24 02:34:53 -04:00
Lauren Tan
4708fb92c2 Fix runtime_commit_artifacts workflow
I messed up the yml syntax and also realized that our script doesn't
currently handle renames or deletes, so I fixed that

ghstack-source-id: 7d481a951a
Pull Request resolved: https://github.com/facebook/react/pull/31028
2024-09-23 17:39:53 -04:00
Lauren Tan
5b19dc0f06 Allow forcing a build in artifacts workflow dispatch
Sometimes it is useful to bypass the revision check when we need to make
changes to the runtime_commit_artifacts script. The `force` input can be
passed via the GitHub UI for manual runs of the workflow.

ghstack-source-id: cf9e32c01a
Pull Request resolved: https://github.com/facebook/react/pull/31027
2024-09-23 15:38:05 -04:00
Lauren Tan
79bcf6eb23 Fix missing trailing / in commit artifacts workflow
The trailing / was being omitted, so instead of moving the cjs
directory itself, it would move only its contents instead. This broke
some internal path assumptions.

Additionally, updates the step to create the react-dom directory prior
to moving.

ghstack-source-id: b6eedb0c88cd3aa3a786a3d3d280ede5ee81a063
Pull Request resolved: https://github.com/facebook/react/pull/31026
2024-09-23 14:58:19 -04:00
Sebastian Markbåge
4e9540e3c2 [Fiber] Log the Render/Commit phases and the gaps in between (#31016)
A slight behavior change here too is that I now mark the start of the
commit phase before the BeforeMutationEffect phase. This affects
`<Profiler>` too.

The named sequences are as follows:

Render -> Suspended or Throttled -> Commit -> Waiting for Paint ->
Remaining Effects

The Suspended phase is only logged if we delay the Commit due to CSS /
images.

The Throttled phase is only logged if we delay the commit due to the
Suspense throttling timer.

<img width="1246" alt="Screenshot 2024-09-20 at 9 14 23 PM"
src="https://github.com/user-attachments/assets/8d01f444-bb85-472b-9b42-6157d92c81b4">

I don't yet log render phases that don't complete. I think I also need
to special case renders that or don't commit after being suspended.
2024-09-23 14:09:48 -04:00
Edmond Chui
5d19e1c8d1 Fix: profiling crashes #30661 #28838 (#31024)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Profiling fails sometimes because `onProfilingStatus` is called
repeatedly on some occasions, e.g. multiple calls to
`getProfilingStatus`.

Subsequent calls should be a no-op if the profiling status hasn't
changed.

Reported via #30661 #28838.

> [!TIP]
> Hide whitespace changes on this PR

<img width="328" alt="screenshot showing the UI controls for hiding
whitespace changes on GitHub"
src="https://github.com/user-attachments/assets/036385cf-2610-4e69-a717-17c05d7ef047">


## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

Tested as part of Fusebox implementation of reload-to-profile.

https://github.com/facebook/react/pull/31021?#discussion_r1770589753
2024-09-23 13:25:44 +01:00
Sebastian Markbåge
d4688dfaaf [Fiber] Track Event Time, startTransition Time and setState Time (#31008)
This tracks the current window.event.timeStamp the first time we
setState or call startTransition. For either the blocking track or
transition track. We can use this to show how long we were blocked by
other events or overhead from when the user interacted until we got
called into React.

Then we track the time we start awaiting a Promise returned from
startTransition. We can use this track how long we waited on an Action
to complete before setState was called.

Then finally we track when setState was called so we can track how long
we were blocked by other word before we could actually start rendering.
For a Transition this might be blocked by Blocking React render work.

We only log these once a subsequent render actually happened. If no
render was actually scheduled, then we don't log these. E.g. if an
isomorphic Action doesn't call startTransition there's no render so we
don't log it.

We only log the first event/update/transition even if multiple are
batched into it later. If multiple Actions are entangled they're all
treated as one until an update happens. If no update happens and all
entangled actions finish, we clear the transition so that the next time
a new sequence starts we can log it.

We also clamp these (start the track later) if they were scheduled
within a render/commit. Since we share a single track we don't want to
create overlapping tracks.

The purpose of this is not to show every event/action that happens but
to show a prelude to how long we were blocked before a render started.
So you can follow the first event to commit.

<img width="674" alt="Screenshot 2024-09-20 at 1 59 58 AM"
src="https://github.com/user-attachments/assets/151ba9e8-6b3c-4fa1-9f8d-e3602745eeb7">

I still need to add the rendering/suspended phases to the timeline which
why this screenshot has a gap.

<img width="993" alt="Screenshot 2024-09-20 at 12 50 27 AM"
src="https://github.com/user-attachments/assets/155b6675-b78a-4a22-a32b-212c15051074">

In this case it's a Form Action which started a render into the form
which then suspended on the action. The action then caused a refresh,
which interrupts with its own update that's blocked before rendering.
Suspended roots like this is interesting because we could in theory
start working on a different root in the meantime which makes this
timeline less linear.
2024-09-20 14:27:12 -04:00
Josh Story
ae75d5a3f5 [Fizz] Include componentStack at the root when aborting (#31011)
When aborting we currently don't produce a componentStack when aborting
the shell. This is likely just an oversight and this change updates this
behavior to be consistent with what we do when there is a boundary
2024-09-20 10:00:02 -07:00
Jack Pope
e4953922a9 Update react-native/react-dom build directory (#31006)
Commit artifact actions are breaking after
https://github.com/facebook/react/pull/30711

See:
https://github.com/facebook/react/actions/runs/10930658977/job/30344033974

> mv: cannot stat 'build/facebook-react-native/react/dom/': No such file
or directory

After build, the new artifacts are in `/react-dom/cjs`, not
`/react/dom/`
```
$> yarn build
$> ls build/facebook-react-native/react/
# ... no dom
$> ls build/facebook-react-native/react-dom/cjs
```
2024-09-19 18:04:06 -04:00
Ricky
babde5d182 [lint] Add no-optional-chaining (#31003)
## Overview

Adds a lint rule to prevent optional chaining to catch issues like
https://github.com/facebook/react/pull/30982 until we support optional
chaining without a bundle impact.
2024-09-19 13:42:49 -04:00
Ruslan Lesiutin
e740d4b14b chore: remove using local storage for persisting console settings on the frontend (#31002)
After https://github.com/facebook/react/pull/30636 and
https://github.com/facebook/react/pull/30986 we no longer store settings
on the Frontend side via `localStorage`.

This PR removes all occurrences of it from
`react-devtools-core/standalone` and `react-devtools-inline`.
2024-09-19 15:47:25 +01:00
Ruslan Lesiutin
c21ce4a396 feat: display message if user ended up opening hook script (#31000)
In https://github.com/facebook/react/pull/30596 we've moved console
patching to the global hook. Generally speaking, the patching happens
even before React is loaded on the page.

If browser DevTools were opened after when `console.error` or
`console.warn` were called, the source script will be `hook.js`, because
of the patching.

![devtools-opened-after-the-message](https://github.com/user-attachments/assets/3d3dbc16-96b8-4234-b061-57b21b60cf2e)

This is because ignore listing is not applied retroactively by Chrome
DevTools.
If you had it open before console calls, Hook script would be correctly
filtered out from the stack:

![devtools-opened-before-the-message](https://github.com/user-attachments/assets/3e99cb22-97b0-4b49-9a76-f7bc948e6452)

I had hopes that the fix for
https://issues.chromium.org/issues/345248263 will also apply ignore
listing retroactively, but looks like we need to open a separate feature
request for the Chrome DevTools team.

With these changes, if user attempts to open `hook.js` script, they are
going to see this message:
![Screenshot 2024-09-19 at 11 30
59](https://github.com/user-attachments/assets/5850b74c-329f-4fbe-a3dd-33f9ac717ee9)
2024-09-19 15:44:34 +01:00
Jack Pope
632f88df11 [compiler] Allow ReactElement symbol to be configured when inlining jsx (#30996)
Based on https://github.com/facebook/react/pull/30995 ([rendered
diff](https://github.com/jackpope/react/compare/inline-jsx-2...jackpope:react:inline-jsx-3?expand=1))

____

Some apps still use `react.element` symbols. Not only do we want to test
there but we also want to be able to upgrade those sites to
`react.transitional.element` without blocking on the compiler (we can
change the symbol feature flag and compiler config at the same time).

The compiler runtime uses `react.transitional.element`, so the snap
fixture will fail if we change the default here. However I confirmed
that commenting out the fixture entrypoint and running snap with
`react.element` will update the fixture symbols as expected.
2024-09-19 10:34:24 -04:00
Jack Pope
d5e955d3c0 [compiler] Pass through unmodified props spread when inlining jsx (#30995)
If JSX receives a props spread without additional attributes (besides
`ref` and `key`), we can pass the spread object as a property directly
to avoid the extra object copy.

```
<Test {...propsToSpread} />
// {props: propsToSpread}
<Test {...propsToSpread} a="z" />
// {props: {...propsToSpread, a: "z"}}
```
2024-09-19 10:07:29 -04:00
Ruslan Lesiutin
a86afe8e56 feat: expose installHook with settings argument from react-devtools-core/backend (#30987)
Stacked on https://github.com/facebook/react/pull/30986. 

Previously, we would call `installHook` at a top level of the JavaScript
module. Because of this, having `require` statement for
`react-devtools-core` package was enough to initialize the React
DevTools global hook on the `window`.

Now, the Hook can actually receive an argument - initial user settings
for console patching. We expose this as a function `initialize`, which
can be used by third parties (including React Native) to provide the
persisted settings.

The README was also updated to reflect the changes.
2024-09-19 13:55:08 +01:00
Timothy Yung
e72127a4ec Build react-dom in builds/facebook-fbsource (#30711)
## Summary

Builds `react-dom` for React Native so that it also populates the
`builds/facebook-fbsource` branch.

**NOTE:** For Meta employees, D61354219 is the internal integration.

## How did you test this change?

```
$ yarn build
…
$ ls build/facebook-react-native/react-dom/cjs
ReactDOM-dev.js       ReactDOM-prod.js      ReactDOM-profiling.js
```
2024-09-18 14:44:55 -07:00
mofeiZ
09d8283599 [ez] Rewrite optional chaining and nullish coalescing syntax (#30982)
Rewrite `containerInfo?.ownerDocument?.defaultView ?? window` to instead
use a ternary.

This changes the compilation output (see [bundle changes from
#30951](d65fb06955)).
```js
// compilation of containerInfo?.ownerDocument?.defaultView ?? window
var $jscomp$optchain$tmpm1756096108$1, $jscomp$nullish$tmp0;
containerInfo =
  null !=
  ($jscomp$nullish$tmp0 =
    null == containerInfo
      ? void 0
      : null ==
          ($jscomp$optchain$tmpm1756096108$1 = containerInfo.ownerDocument)
        ? void 0
        : $jscomp$optchain$tmpm1756096108$1.defaultView)
    ? $jscomp$nullish$tmp0
    : window;

// compilation of ternary expression
containerInfo =
  null != containerInfo &&
  null != containerInfo.ownerDocument &&
  null != containerInfo.ownerDocument.defaultView
    ? containerInfo.ownerDocument.defaultView
    : window;
```

This also reduces the number of no-op bundle syncs for Meta. Note that
Closure compiler's `jscomp$optchain$tmp<HASH>` identifiers change when
we rebuild (likely due to version number changes). See
[workflow](https://github.com/facebook/react/actions/runs/10891164281/job/30221518374)
for a PR that was synced despite making no changes to the runtime.
2024-09-18 14:39:04 -04:00
Ruslan Lesiutin
f2c57a31e9 chore: remove settings manager from react-devtools-core (#30986)
Stacked on https://github.com/facebook/react/pull/30636. See [this
commit](20cec76c44).

This has been only used for React Native and will be replaced by another
approach (initialization via `installHook` call) in the next PR.
2024-09-18 18:30:32 +01:00
Ruslan Lesiutin
f37c7bc653 feat[react-devtools/extension]: use chrome.storage to persist settings across sessions (#30636)
Stacked on https://github.com/facebook/react/pull/30610 and whats under
it. See [last
commit](248ddba186).

Now, we are using
[`chrome.storage`](https://developer.chrome.com/docs/extensions/reference/api/storage)
to persist settings for the browser extension across different sessions.
Once settings are updated from the UI, the `Store` will emit
`settingsUpdated` event, and we are going to persist them via
`chrome.storage.local.set` in `main/index.js`.

When hook is being injected, we are going to pass a `Promise`, which is
going to be resolved after the settings are read from the storage via
`chrome.storage.local.get` in `hookSettingsInjector.js`.
2024-09-18 18:26:39 +01:00
Ruslan Lesiutin
e33acfd67f refactor[react-devtools]: propagate settings from global hook object to frontend (#30610)
Stacked on https://github.com/facebook/react/pull/30597 and whats under
it. See [this
commit](59b4efa723).

With this change, the initial values for console patching settings are
propagated from hook (which is the source of truth now, because of
https://github.com/facebook/react/pull/30596) to the UI. Instead of
reading from `localStorage` the frontend is now requesting it from the
hook. This happens when settings modal is rendered, and wrapped in a
transition. Also, this is happening even if settings modal is not opened
yet, so we have enough time to fetch this data without displaying loader
or similar UI.
2024-09-18 18:19:01 +01:00
Ruslan Lesiutin
fce4606657 chore[react-devtools]: extract some utils into separate modules to unify implementations (#30597)
Stacked on https://github.com/facebook/react/pull/30596. See [this
commit](4ba5e784bb).

Moving `formatWithStyles` and `formatConsoleArguments` to its own
modules, so that we can finally have a single implementation for these
and stop inlining them in RDT global hook object.
2024-09-18 18:16:20 +01:00
Ruslan Lesiutin
3cac0875dc refactor[react-devtools]: move console patching to global hook (#30596)
Stacked on https://github.com/facebook/react/pull/30566 and whats under
it. See [this
commit](374fd737e4).

It is mostly copying code from one place to another and updating tests.
With these changes, for every console method that we patch, there is
going to be a single applied patch:
- For `error`, `warn`, and `trace` we are patching when hook is
installed. This guarantees that component stacks are going to be
appended even if browser DevTools are not opened. We pay some price for
it, though: if user has browser DevTools closed and if at this point
some warning or error is emitted (logged), the next time user opens
browser DevTools, they are going to see `hook.js` as the source frame.
Unfortunately, ignore listing from source maps is not applied
retroactively, and I don't know if its a bug or just a design
limitations. Once browser DevTools are opened, source maps will be
loaded and ignore listing will be applied for all emitted logs in the
future.
- For `log`, `info`, `group`, `groupCollapsed` we are only patching when
React notifies React DevTools about running in StrictMode. We unpatch
the methods right after it.
2024-09-18 18:12:18 +01:00
Ruslan Lesiutin
b521ef8a2a refactor[react-devtools]: remove browserTheme from ConsolePatchSettings (#30566)
Stacked on https://github.com/facebook/react/pull/30564.

We are no longer using browser theme in our console patching, this was
removed in unification of console patching for strict mode, we started
using ansi escape symbols and forking based on browser theme is no
longer required - https://github.com/facebook/react/pull/29869

The real browser theme initialization for frontend is happening at the
other place and is not affected:

40be968257/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js (L117-L120)
2024-09-18 18:02:13 +01:00
Ruslan Lesiutin
5e83d9ab3b feat[react-devtools]: add settings to global hook object (#30564)
Right now we are patching console 2 times: when hook is installed
(before page is loaded) and when backend is connected. Because of this,
even if user had `appendComponentStack` setting enabled, all emitted
error and warning logs are not going to have component stacks appended.
They also won't have component stacks appended retroactively when user
opens browser DevTools (this is when frontend is initialized and
connects to backend).

This behavior adds potential race conditions with LogBox in React
Native, and also unpredictable to the user, because in order to get
component stacks logged you have to open browser DevTools, but by the
time you do it, error or warning log was already emitted.

To solve this, we are going to only patch console in the hook object,
because it is guaranteed to load even before React. Settings are going
to be synchronized with the hook via Bridge, and React DevTools Backend
Host (React Native or browser extension shell) will be responsible for
persisting these settings across the session, this is going to be
implemented in a separate PR.
2024-09-18 17:37:00 +01:00
Jack Pope
5dcb009760 [compiler] Add JSX inlining optimization (#30867)
This adds an `InlineJsxTransform` optimization pass, toggled by the
`enableInlineJsxTransform` flag. When enabled, JSX will be transformed
into React Element object literals, preventing runtime overhead during
element creation.

TODO:
- [ ] Add conditionals to make transform PROD-only
- [ ] Make the React element symbol configurable so this works with
runtimes that support `react.element` or `react.transitional.element`
- [ ] Look into additional optimization to pass props spread through
directly if none of the properties are mutated
2024-09-18 11:51:36 -04:00
Sebastian Markbåge
8dfbd16fce [Fiber] Color Performance Track Entries by Self Time (#30984)
Stacked on #30983.

This colors each component entry by its self time from light to dark
depending on how long it took. If it took longer than a cut off we color
it red (the error color).

<img width="435" alt="Screenshot 2024-09-16 at 11 48 15 PM"
src="https://github.com/user-attachments/assets/5d0bda83-6205-40e9-bec1-b81db2d48b2d">
2024-09-17 16:36:10 -04:00
Sebastian Markbåge
e1c20902c3 [Fiber] Log Component Effects to Performance Track (#30983)
Stacked on #30981. Same as #30967 but for effects.

This logs a tree of components using `performance.measure()`.

In addition to the previous render phase this logs one tree for each
commit phase:

- Mutation Phase
- Layout Effect
- Passive Unmounts
- Passive Mounts

I currently skip the Before Mutation phase since the snapshots are so
unusual it's not worth creating trees for those.

The mechanism is that I reuse the timings we track for
`enableProfilerCommitHooks`. I track first and last effect timestamp
within each component subtree. Then on the way up do we log the entry.
This means that we don't include overhead to find our way down to a
component and that we don't need to add any additional overhead by
reading timestamps.

To ensure that the entries get ordered correctly we need to ensure that
the start time of each parent is slightly before the inner one.
2024-09-17 16:14:57 -04:00
Sebastian Markbåge
15da917451 Don't read currentTransition back from internals (#30991)
This code is weird. It reads back the transition that it just set from
the shared internals. It's almost like it expects it to be a getter or
something.

This avoids that and makes it consistent with what ReactFiberHooks
already does.
2024-09-17 15:25:00 -04:00
Sebastian Markbåge
4549be0f84 [Fiber] Optimize enableProfilerCommitHooks by Collecting Elapsed Effect Duration in Module Scope (#30981)
Stacked on #30979.

The problem with the previous approach is that it recursively walked the
tree up to propagate the resulting time from recording a layout effect.

Instead, we keep a running count of the effect duration on the module
scope. Then we reset it when entering a nested Profiler and then we add
its elapsed count when we exit the Profiler.

This also fixes a bug where we weren't previously including unmount
times for some detached trees since they couldn't bubble up to find the
profiler.
2024-09-17 15:12:16 -04:00
Mike Vitousek
7b56a54298 [compiler][playground] create playground API in pipeline, and allow spaces in pass names
Summary:
1. Minor refactor to provide a stable API for calling the compiler from the playground
2. Allows spaces in pass names without breaking the appearance of the playground by replacing spaces with &nbsp; in pass tabs

ghstack-source-id: 12a43ad86c16c0e21f3e6b4086d531cdefd893eb
Pull Request resolved: https://github.com/facebook/react/pull/30988
2024-09-17 11:05:59 -07:00
mofeiZ
a99d8e8d97 [compiler][eslint] Report bailout diagnostics with correct column # (#30977)
Compiler bailout diagnostics should now highlight only the first line of
the source location span.

(Resubmission of #30423 which was reverted due to invalid column
number.)
2024-09-16 15:56:24 -04:00
Sebastian Markbåge
8152e5cd27 Remove execution context check from shouldProfile (#30971)
I don't know why this is here since all these callsites are within the
CommitWork/CommitEffects helpers.

This should help with inlining.
2024-09-16 15:00:17 -04:00
Mike Vitousek
d7167c3505 [compiler] Implement support for hoisted and recursive functions
Summary:
Introduces a new binding kind for functions that allows them to be hoisted. Also has the result of causing all nested function declarations to be outputted as function declarations, not as let bindings.

ghstack-source-id: fa40d4909fb3d30c23691e36510ebb3c3cc41053
Pull Request resolved: https://github.com/facebook/react/pull/30922
2024-09-16 11:12:58 -07:00
Mike Vitousek
e78c9362c0 [compiler] Allow all hooks to take callbacks which access refs, but ban hooks from taking direct ref value arguments
Summary:
This brings the behavior of ref mutation within hook callbacks into alignment with the behavior of global mutations--that is, we allow all hooks to take callbacks that may mutate a ref. This is potentially unsafe if the hook eagerly calls its callback, but the alternative is excessively limiting (and inconsistent with other enforcement).

This also bans *directly* passing a ref.current value to a hook, which was previously allowed.

ghstack-source-id: e66ce7123e
Pull Request resolved: https://github.com/facebook/react/pull/30917
2024-09-16 10:53:34 -07:00
Mike Vitousek
1e68a0a3ae [compiler] Improve handling of refs
Summary:
This change expands our handling of refs to build an understanding of nested refs within objects and functions that may return refs. It builds a special-purpose type system within the ref analysis that gives a very lightweight structural type to objects and array expressions (merging the types of all their members), and then propagating those types throughout the analysis (e.g., if `ref` has type `Ref`, then `{ x: ref }` and `[ref]` have type `Structural(value=Ref)` and `{x: ref}.anything` and `[ref][anything]` have type `Ref`).

This allows us to support structures that contain refs, and functions that operate over them, being created and passed around during rendering without at runtime accessing a ref value.

The analysis here uses a fixpoint to allow types to be fully propagated through the system, and we defend against diverging by widening the type of a variable if it could grow infinitely: so, in something like
```
let x = ref;
while (condition) {
  x = [x]
}
```
we end up giving `x` the type `Structural(value=Ref)`.

ghstack-source-id: afb0b0cb01
Pull Request resolved: https://github.com/facebook/react/pull/30902
2024-09-16 10:53:32 -07:00
Mike Vitousek
c8a7cab13f [compiler] Fix issue where second argument of all functions was considered to be a ref
ghstack-source-id: 1817f3b816
Pull Request resolved: https://github.com/facebook/react/pull/30912
2024-09-16 10:53:29 -07:00
Pieter De Baets
26855e4680 [react-native] Fix misleading crash when view config is not found (#30970)
## Summary

When a view config can not be found, it currently errors with
`TypeError: Cannot read property 'bubblingEventTypes' of null`. Instead
invariant at the correct location and prevent further processing of the
null viewConfig to improve the error logged.

## How did you test this change?

Build and run RN playground app referencing an invalid native view
through `requireNativeComponent`.
2024-09-16 17:51:00 +01:00
Ruslan Lesiutin
9f4e4611ea fix: add Error prefix to Error objects names (#30969)
This fixes printing Error objects in Chrome DevTools.

I've observed that Chrome DevTools is not source mapping and linkifying
URLs, when was running this on larger apps. Chrome DevTools talks to V8
via Chrome DevTools protocol, every object has a corresponding
[`RemoteObject`](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject).

When Chrome DevTools sees that Error object is printed in the console,
it will try to prettify it. `description` field of the corresponding
`RemoteObject` for the `Error` JavaScript object is a combination of
`Error` `name`, `message`, `stack` fields. This is not just a raw
`stack` field, so our prefix for this field just doesn't work. [V8 is
actually filtering out first line of the `stack` field, it only keeps
the stack frames as a string, and then this gets prefixed by `name` and
`message` fields, if they are
available](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a?fbclid=IwZXh0bgNhZW0CMTEAAR1tMm5YC4jqowObad1qXFT98X4RO76CMkCGNSxZ8rVsg6k2RrdvkVFL0i4_aem_e2fRrqotKdkYIeWlJnk0RA).
As an illustration, this:
```
const fakeError = new Error('');
fakeError.name = 'Stack';
fakeError.stack = 'Error Stack:' + stack;
```

will be formatted by `V8` as this `RemoteObject`:
```
{
  ...
  description: 'Stack: ...',
  ...
}
```

Notice that there is no `Error` prefix, that was previously added.
Because of this, [Chrome DevTools won't even try to symbolicate the
stack](ee4729d2cc/front_end/panels/console/ErrorStackParser.ts (L33-L35)),
because it doesn't have such prefix.
2024-09-16 17:43:40 +01:00
Sebastian Markbåge
f2df5694f2 [Fiber] Log Component Renders to Custom Performance Track (#30967)
Stacked on #30960 and #30966. Behind the enableComponentPerformanceTrack
flag.

This is the first step of performance logging. This logs the start and
end time of a component render in the passive effect phase. We use the
data we're already tracking on components when the Profiler component or
DevTools is active in the Profiling or Dev builds. By backdating this
after committing we avoid adding more overhead in the hot path. By only
logging things that actually committed, we avoid the costly unwinding of
an interrupted render which was hard to maintain in earlier versions.

We already have the start time but we don't have the end time. That's
because `actualStartTime + actualDuration` isn't enough since
`actualDuration` counts the actual CPU time excluding yields and
suspending in the render.

Instead, we infer the end time to be the start time of the next sibling
or the complete time of the whole root if there are no more siblings. We
need to pass this down the passive effect tree. This will mean that any
overhead and yields are attributed to this component's span. In a follow
up, we'll need to start logging these yields to make it clear that this
is not part of the component's self-time.

In follow ups, I'll do the same for commit phases. We'll also need to
log more information about the phases in the top track. We'll also need
to filter out more components from the trees that we don't need to
highlight like the internal Offscreen components. It also needs polish
on colors etc.

Currently, I place the components into separate tracks depending on
which lane currently committed. That way you can see what was blocking
Transitions or Suspense etc. One problem that I've hit with the new
performance.measure extensions is that these tracks show up in the order
they're used which is not the order of priority that we use. Even when
you add fake markers they have to actually be within the performance run
since otherwise the calls are noops so it's not enough to do that once.

However, I think this visualization is actually not good because these
trees end up so large that you can't see any other lanes once you expand
one. Therefore, I think in a follow up I'll actually instead switch to a
model where Components is a single track regardless of lane since we
don't currently have overlap anyway. Then the description about what is
actually rendering can be separate lanes.

<img width="1512" alt="Screenshot 2024-09-15 at 10 55 55 PM"
src="https://github.com/user-attachments/assets/5ca3fa74-97ce-40c7-97f7-80c1dd7d6470">

<img width="1512" alt="Screenshot 2024-09-15 at 10 56 27 PM"
src="https://github.com/user-attachments/assets/557ad65b-4190-465f-843c-0bc6cbb9326d">
2024-09-16 11:45:50 -04:00
Sebastian Markbåge
ee1a403a30 [Fiber] Move Profiler onPostCommit processing of passive effect durations to plain passive effect (#30966)
We used to queue a separate third passive phase to invoke onPostCommit
but this is unnecessary. We can just treat it as a plain passive effect.
This means it is interleaved with other passive effects but we only need
to know the duration of the things below us which is already done at
this point.

I also extracted the user space call to onPostCommit into
ReactCommitEffects. Same as onCommit. It's now covered by
runWithFiberInDEV and catches.
2024-09-16 11:10:05 -04:00
Sebastian Markbåge
0eab377a96 Add enableComponentPerformanceTrack Flag (#30960)
This flag will be used to gate a new timeline profiler that's integrate
with the Performance Tab and the new performance.measure extensions in
Chrome.

It replaces the existing DevTools feature so this disables
enableSchedulingProfiler when it is enabled since they can interplay in
weird ways potentially.

This means that experimental React now disable scheduling profiler and
enables this new approach.
2024-09-16 11:09:40 -04:00
Ruslan Lesiutin
8cf64620c7 fix[rdt/fiber/renderer.js]: getCurrentFiber can be injected as null (#30968)
In production artifacts for `18.x.x` `getCurrentFiber` can actually be
injected as `null`. Updated `getComponentStack` and `onErrorOrWarning`
implementations to support this.

![Screenshot 2024-09-16 at 10 52
00](https://github.com/user-attachments/assets/a0c773aa-ebbf-4fd5-95c4-cac3cc0c203f)
2024-09-16 14:47:57 +01:00
Josh Story
fc5ef50da8 [Flight] Start initial work immediately (#30961)
In a past update we made render and prerender have different work
scheduling behavior because these methods are meant to be used in
differeent environments with different performance tradeoffs in mind.
For instance to prioritize streaming we want to allow as much IO to
complete before triggering a round of work because we want to flush as
few intermediate UI states. With Prerendering there will never be any
intermediate UI states so we can more aggressively render tasks as they
complete.

One thing we've found is that even during render we should ideally kick
off work immediately. This update normalizes the intitial work for
render and prerender to start in a microtask. Choosing microtask over
sync is somewhat arbitrary but there really isn't a reason to make them
different between render/prerender so for now we'll unify them and keep
it as a microtask for now.

This change also updates pinging behavior. If the request is still in
the initial task that spawned it then pings will schedule on the
microtask queue. This allows immediately available async APIs to resolve
right away. The concern with doing this for normal pings is that it
might crowd out IO events but since this is the initial task there would
be IO to already be scheduled.
2024-09-14 09:26:01 -07:00
春希与子晴
b75cc078c5 Fix nodeName to UPPERCASE in insertStylesheetIntoRoot (#28255)
## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

<img width="518" alt="image"
src="https://github.com/facebook/react/assets/18693190/6d12df76-7dae-403b-b486-4940992abe8d">

The condition `node.nodeName === 'link'` is always `false`, because
`node.nodeName` is Uppercase in specification. And the condition
`node.nodeName === 'LINK'` is unnecessary, because Fizz hoists tags when
it's `media` attribute is `"not all"`, whether it is a `link` or a
`style` (line 36):


18cbcbf783/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js (L30-L44)


18cbcbf783/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js (L30-L44)
2024-09-14 08:18:27 -07:00
Sebastian Markbåge
3d95c43b89 [Fiber] Profiler - Use two separate functions instead of branch by flag (#30957)
Nit: I don't trust flags in hot code. While it can take somewhat longer
to compile two functions and JIT them. After that they don't need to
check branches. Also makes it clearer the purpose.
2024-09-13 21:51:52 -04:00
Josh Story
6774caa379 [Flight] properly track pendingChunks when changing environment names (#30958)
When the environment name changes for a chunk we issue a new debug chunk
which updates the environment name. This chunk was not beign included in
the pendingChunks count so the count was off when flushing
2024-09-13 15:55:42 -07:00
Hendrik Liebau
5deb78223a [Flight] Respect async flag in client manifest (#30959)
In #26624, the ability to mark a client reference module as `async` in
the React client manifest was removed because it was not utilized by
Webpack, neither in `ReactFlightWebpackPlugin` nor in Next.js. However,
some bundlers and frameworks are sophisticated enough to properly handle
and identify async ESM modules (e.g., client component modules with
top-level `await`), most notably Turbopack in Next.js. Therefore, we
need to consider the `async` flag in the client manifest when resolving
the client reference metadata on the server. The SSR manifest cannot
override this flag, meaning that if a module is async, it must remain
async in all client environments.

x-ref: https://github.com/vercel/next.js/pull/70022
2024-09-13 16:33:05 -04:00
ling1726
d9c4920e8b fix: restore selection should consider the window of the container (#30951)
## Summary


Fixes #30864 

Before this PR the active elemen was always taken from the global
`window`. This is incorrect if the renderer is in one window rendering
into a container element in another window. The changes in this PR adds
another code branch to use a `defaultView` of the container element if
it exists so that `restoreSelection` after a commit will actually
restore to the correct window.

## How did you test this change?

I patched these changes to the repro repo in the linked issue #39864
https://github.com/ling1726/react-child-window-focus-repro/blob/master/patches/react-dom%2B18.3.1.patch.

I followed the same repro steps in the linked issue and and could not
repro the reported problem. Attaching screen recordings below:

Before
![focus
repro](https://github.com/user-attachments/assets/81c4b4f9-08b5-4356-8251-49b909771f3f)

After

![after](https://github.com/user-attachments/assets/84883032-5558-4650-9b9a-bd4d5fd9cb13)
2024-09-13 13:29:40 -07:00
Jan Kassens
d3d4d3a46b Call cleanup of insertion effects when hidden (#30954)
Insertion effects do not unmount when a subtree is removed while
offscreen.

Current behavior for an insertion effect is if the component goes

- *visible -> removed:* calls insertion effect cleanup
- *visible -> offscreen -> removed:* insertion effect cleanup is never
called

This makes it so we always call insertion effect cleanup when removing
the component.

Likely also fixes https://github.com/facebook/react/issues/26670

---------

Co-authored-by: Rick Hanlon <rickhanlonii@fb.com>
2024-09-13 16:18:14 -04:00
Mike Vitousek
633a0fe536 [compiler] Factor out function effects from reference effects
Summary:
This PR performs a major refactor of InferReferenceEffects to separate out the work on marking places with Effects from inferring FunctionEffects. The behavior should be identical after this change (see [internal sync](https://www.internalfb.com/intern/everpaste/?handle=GN74VxscnUaztTYDAL8q0CRWBIxibsIXAAAB)) but the FunctionEffect logic should be easier to work with.

These analyses are unfortunately still deeply linked--the FunctionEffect analysis needs to reason about the "current" value kind for each point in the program, while the InferReferenceEffects algorithm performs global updates on the state of the program (e.g. freezing). In the future, it might be possible to make these entirely separate passes if we store the ValueKind directly on places.

For the most part, the logic of reference effects and function effects can be cleanly separated: for each instruction and terminal, we visit its places and infer their effects, and then we visit its places and infer any function effects that they cause. The biggest wrinkle here is that when a transitive function freeze operation occurs, it has to happen *after* inferring the function effects on the place, because otherwise we may convert a value from Context to Frozen, which will cause the ContextualMutation function effect to be converted to a ReactMutation effect too early. This can be observed in a case like this:

```
export default component C() {
  foo(() => {
    const p = {};
    return () => {
      p['a'] = 1
    };
  });
}
```
Here when the outer function returns the inner function, it freezes the inner function which transitively freezes `p`. But before that freeze happens, we need to replay the ContextualMutation on the inner function to determine that the value is mutable in the outer context. If we froze `p` first, we would instead convert the ContextualMutation to a ReactMutation and error.

To handle this, InferReferenceEffects now delays the exection of the freezeValue action until after it's called the helper functions that generate function effects. So the order of operations on a given place is now

set effect --> generate function effects --> transitively freeze dependencies, if applicable

ghstack-source-id: 21cb50c140
Pull Request resolved: https://github.com/facebook/react/pull/30920
2024-09-13 12:38:17 -07:00
Sebastian Markbåge
94e4acaa14 [Fiber] Set profiler values to doubles (#30942)
At some point this trick was added to initialize the value first to NaN
and then replace them with zeros and negative ones.

This is to address the issue noted in
https://github.com/facebook/react/issues/14365 where these hidden
classes can be initialized to SMIs at first and then deopt when we
realize they're actually doubles.

However, this fix has been long broken and has deopted the profiling
build for years because closure compiler optimizes out the first write.

I'm not sure because I haven't A/B-tested this in the JIT yet but I
think we can use negative zero and -1.1 as the initial values instead
since they're not simple integers. Negative zero `===` zero (but not
Object.is) so is the same as far as our code is concerned. The negative
value is just `< 0` comparisons.
2024-09-13 12:27:00 -04:00
Mofei Zhang
206df66e70 [compiler][rewrite] PropagateScopeDeps hir rewrite
Resubmission of #30079 -- core logic unchanged, but needed to rebase past #30573

### Quick background
#### Temporaries

The compiler currently treats temporaries and named variables (e.g. `x`) differently in this pass.
- named variables may be reassigned (in fact, since we're running after LeaveSSA, a single named identifier's IdentifierId may map to multiple `Identifier` instances -- each with its own scope and mutable range)
- temporaries are replaced with their represented expressions during codegen. This is correct (mostly correct, see #29878) as we're careful to always lower the correct evaluation semantics. However, since we rewrite reactive scopes entirely (to if/else blocks), we need to track temporaries that a scope produces in `ReactiveScope.declarations` and later promote them to named variables.
In the same example, $4, $5, and $6 need to be promoted: $2 ->`t0`,  $5 ->`t1`, and $6 ->`t2`.
```js
[1] $2 = LoadGlobal(global) foo
[2] $3 = LoadLocal bar$1
scope 0:
  [3] $4 = Call $2(<unknown> $3)
scope 1:
  [4] $5 = Object {  }
scope 2:
  [5] $6 = Object { a: $4, b: $5 }
[6] $8 = StoreLocal Const x$7 = $6
```

#### Dependencies
`ReactiveScope.dependencies` records the set of (read-only) values that a reactive scope is dependent on. This is currently limited to just variables (named variables from source and promoted temporaries) and property-loads.
All dependencies we record need to be hoistable -- i.e. reordered to just before the ReactiveScope begins. Not all PropertyLoads are hoistable.

In this example, we should not evaluate `obj.a.b` without before creating x and checking `objIsNull`.
```js
// reduce-reactive-deps/no-uncond.js
function useFoo({ obj, objIsNull }) {
  const x = [];
  if (isFalse(objIsNull)) {
    x.push(obj.a.b);
  }
  return x;
}
```

While other memoization strategies with different constraints exist, the current compiler requires that `ReactiveScope.dependencies` be re-orderable to the beginning of the reactive scope. But.. `PropertyLoad`s from null values will throw `TypeError`. This means that evaluating hoisted dependencies should throw if and only if the source program throws. (It is also a bug if source throws and compiler output does not throw. See https://github.com/facebook/react-forget/pull/2709)

---
### Rough high level overview
1. Pass 1
Walk over instructions to gather every temporary used outside of its defining scope (same as ReactiveFunction version). These determine the sidemaps we produce, as temporaries used outside of their declaring scopes get promoted to named variables later (and are not considered hoistable rvals).
2. Pass 2 (collectTemporariesSidemap)
Walk over instructions to generate a sidemap of temporary identifier -> named variable and property path (e.g. `$3 -> {obj: props, path: ["a", "b"]}`)
2. Pass 2 (collectHoistablePropertyLoads)
  a. Build a sidemap of block -> accessed variables and properties (e.g. `bb0 -> [ {obj: props, path: ["a", "b"]} ]`)
  b. Propagate "non-nullness" i.e. variables and properties for which we can safely evaluate `PropertyLoad`.
  A basic block can unconditionally read from identifier X if any of the following applies:
    - the block itself reads from identifier X
    - all predecessors of the block read from identifier X
    - all successors of the block read from identifier X
4. Pass 3: (collectDependencies)
Walks over instructions again to record dependencies and declarations, using the previously produced sidemaps. We do not record any control-flow here
5. Merge every scope's recorded dependencies with the set of hoistable PropertyLoads

Tested by syncing internally and (1) checking compilation output differences ([internal link](https://www.internalfb.com/intern/everpaste/?handle=GPCfUBt_HCoy_S4EAJDVFJyJJMR0bsIXAAAB)), running internally e2e tests ([internal link](https://fburl.com/sandcastle/cs5mlkxq))

---
### Followups:
1. Rewrite function expression deps
This change produces much more optimal output as the compiler now uses the function CFG to understand which variables / paths are assumed to be non-null. However, it may exacerbate [this function-expr hoisting bug](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.tsx). A short term fix here is to simply call some form of `collectNonNullObjects` on every function expression to find hoistable variable / paths. In the longer term, we should refactor out `FunctionExpression.deps`.

2. Enable optional paths
(a) don't count optional load temporaries as dependencies (e.g. `collectOptionalLoadRValues(...)`).
(b) record optional paths in both collectHoistablePropertyLoads and dependency collection

ghstack-source-id: 2507f6ea751dce09ad1dccd353ae6fc7cf411582
Pull Request resolved: https://github.com/facebook/react/pull/30894
2024-09-12 17:40:57 -04:00
Mofei Zhang
5ac4034e14 [compiler] Fork fixtures for enablePropagateDepsInHIR
- flip `enablePropagateDepsInHIR` to off by default
- fork fixtures which produce compilation differences in #30894 to separate directory `propagate-scope-deps-hir-fork`, to be cleaned up when we remove this flag

ghstack-source-id: 7d5b8dc29788a65c272c846af9877b09fbf2cd60
Pull Request resolved: https://github.com/facebook/react/pull/30949
2024-09-12 17:40:57 -04:00
Mofei Zhang
f6dcce5199 [compiler][ez] Add entrypoints to ssa fixtures
Adds evaluator support for a few compiler test fixtures

ghstack-source-id: 202654992a9876cea59885b54a338c908e369ddb
Pull Request resolved: https://github.com/facebook/react/pull/30948
2024-09-12 17:40:57 -04:00
Sebastian Markbåge
dff50825c6 [Flight] Track owner/stack where the Flight Client reads as the root (#30933)
This means that the owner of a Component rendered on the remote server
becomes the Component on this server.

Ideally we'd support this for the Client side too. In particular Fiber
but currently ReactComponentInfo's owner is typed as only supporting
other ReactComponentInfo and it's a bigger lift to support that.
2024-09-12 17:19:34 -04:00
Sebastian Markbåge
89b445709d Enable lazy context propagation (#30935)
Last I heard this was great so not sure there are any more blockers to
just include it in 19?
2024-09-12 13:01:56 -04:00
Josh Story
94e652d505 disable enableSiblingPrerendering in experimental channel (#30952)
Disables `enableSiblingPrerendering` in the experimental builds until
the feature is tested at Meta first.
2024-09-12 09:33:20 -07:00
Sebastian Markbåge
473522093d [Fizz] Add resumeAndPrerender to Static Rendering (#30950)
This is only in the same experimental exports as `resume`. Useful with
Postpone/Halt.

We already have `prerender()` to create a partial tree with postponed
state. We also have `resume()` to dynamically resume such a tree.

This lets you do a new prerender by resuming an already existing
postponed state. Basically creating a chain of preludes. The next
prelude would include the scripts to patch up the document.

This mostly just works since both prerender and resume are already
implemented using the same code so we just enable both at the root. I'm
sure we'll find some edge cases since this wasn't considered when it was
first written but so far I've only found an unrelated existing bug with
`keyPath` fixed here.
2024-09-12 10:51:01 -04:00
Ruslan Lesiutin
bb6b86ed59 refactor[react-devtools]: initialize renderer interface early (#30946)
The current state is that `rendererInterface`, which contains all the
backend logic, like generating component stack or attaching errors to
fibers, or traversing the Fiber tree, ..., is only mounted after the
Frontend is created.

For browser extension, this means that we don't patch console or track
errors and warnings before Chrome DevTools is opened.

With these changes, `rendererInterface` is created right after
`renderer` is injected from React via global hook object (e. g.
`__REACT_DEVTOOLS_GLOBAL_HOOK__.inject(...)`.

Because of the current implementation, in case of multiple Reacts on the
page, all of them will patch the console independently. This will be
fixed in one of the next PRs, where I am moving console patching to the
global Hook.

This change of course makes `hook.js` script bigger, but I think it is a
reasonable trade-off for better DevX. We later can add more heuristics
to optimize the performance (if necessary) of `rendererInterface` for
cases when Frontend was connected late and Backend is attempting to
flush out too many recorded operations.

This essentially reverts https://github.com/facebook/react/pull/26563.
2024-09-12 13:59:29 +01:00
Andrew Clark
d6cb4e7713 Start prerendering Suspense retries immediately (#30934)
When a component suspends and is replaced by a fallback, we should start
prerendering the fallback immediately, even before any new data is
received. During the retry, we can enter prerender mode directly if
we're sure that no new data was received since we last attempted to
render the boundary.

To do this, when completing the fallback, we leave behind a pending
retry lane on the Suspense boundary. Previously we only did this once a
promise resolved, but by assigning a lane during the complete phase, we
will know that there's speculative work to be done.

Then, upon committing the fallback, we mark the retry lane as suspended
— but only if nothing was pinged or updated in the meantime. That allows
us to immediately enter prerender mode (i.e. render without skipping any
siblings) when performing the retry.
2024-09-11 11:41:54 -04:00
Sebastian Markbåge
1bb056363c [Fizz] Use RequestInstance constructor for resuming (#30947)
We added enough fields to need a constructor instead of inline object in
V8.

We didn't update the resumeRequest path though so it wasn't using the
constructor and had a different hidden class.
2024-09-11 11:35:01 -04:00
Ruslan Lesiutin
344bc8128b refactor[Agent/Store]: Store to send messages only after Agent is initialized (#30945)
Both for browser extension, and for React Native (as part of
`react-devtools-core`) `Store` is initialized before the Backend (and
`Agent` as a part of it):

bac33d1f82/packages/react-devtools-extensions/src/main/index.js (L111-L113)

Any messages that we send from `Store`'s constructor are ignored,
because there is nothing on the other end yet. With these changes,
`Agent` will send `backendInitialized` message to `Store`, after which
`getBackendVersion` and other events will be sent.

Note that `isBackendStorageAPISupported` and `isSynchronousXHRSupported`
are still sent from `Agent`'s constructor, because we don't explicitly
ask for it from `Store`, but these are used.

This the pre-requisite for fetching settings and unsupported renderers
reliably from the Frontend.
2024-09-11 15:10:13 +01:00
Sebastian Markbåge
bac33d1f82 [Flight] Unwrap lazy before reading the value (#30938)
This is important if the lazy is at the root of the chunk. I don't have
a unit test for it but @gnoff has a repro.

It also shouldn't unwrap the last value since that's the one we're
referencing.

This was already done correctly by @unstubbable in waitForReference so
this just aligns with that.
2024-09-10 19:42:19 -04:00
Sebastian Markbåge
a5a7f1063e [Fiber] Don't call performance.now() twice in a row (#30936) 2024-09-10 16:46:35 -04:00
Andrew Clark
66cf2cfc8a Prerender during same pass if blocked anyway (#30879)
If something suspends in the shell — i.e. we won't replace the suspended
content with a fallback — we might as well prerender the siblings during
the current render pass, instead of spawning a separate prerender pass.

This is implemented by setting the "is prerendering" flag to true
whenever we suspend in the shell. But only if we haven't already skipped
over some siblings, because if so, then we need to schedule a separate
prerender pass regardless.
2024-09-10 13:23:32 -04:00
Joe Savona
d724ba96ff [compiler] Validate type configs for hooks/non-hooks
Alternative to #30868. The goal is to ensure that the types coming out of moduleTypeProvider are valid wrt to hook typing. If something is named like a hook, then it must be typed as a hook (or don't type it).

ghstack-source-id: 3e8b5a0a7010d0c484bbb417fb258e76bf4e32bc
Pull Request resolved: https://github.com/facebook/react/pull/30888
2024-09-10 09:25:28 -07:00
Ruslan Lesiutin
3dfd5d9efb refactor[RendererInterface]: expose onErrorOrWarning and getComponentStack (#30931)
Make `onErrorOrWarning` and `getComponentStack` part of
`rendererInterface`. By doing this, they will be available from the
global hook `rendererInterfaces` Map. This makes them available to be
used by Hook, which soon will be the only one who is doing console
patching.

This is also a pre-requisite for removing `registerRenderer`:

d160aa0fbb/packages/react-devtools-shared/src/backend/console.js (L113-L121)
2024-09-10 15:59:40 +01:00
Sebastian Markbåge
63cefa2957 [DevTools] Implement getComponentStack and onErrorOrWarning for replayed Flight logs (#30930)
This adds owner stacks to replayed Server Component logs in environments
that don't support native console.createTask.

<img width="521" alt="Screenshot 2024-09-09 at 8 55 21 PM"
src="https://github.com/user-attachments/assets/261cfaee-ea65-4044-abf0-c41abf358fea">

It also tracks the logs in the global componentInfoToComponentLogsMap
which lets us associate those logs with Server Components when they
later commit into the fiber tree.

<img width="1280" alt="Screenshot 2024-09-09 at 9 31 16 PM"
src="https://github.com/user-attachments/assets/436312a6-f9f4-4add-8129-0fb9b9eb18ee">

I tried to create unit tests for this since it's now wired up
end-to-end. Unfortunately, the complicated testing set up for Flight
requires a complex set of resetting modules which are incompatible with
the complicated test setup in getVersionedRenderImplementation for
DevTools tests.
2024-09-10 10:37:29 -04:00
Sebastian Markbåge
d160aa0fbb [DevTools] Use Unicode Atom Symbol instead of Atom Emoji (#30832)
This reverts #19603.

Before:
<img width="724" alt="Screenshot 2024-08-28 at 12 07 29 AM"
src="https://github.com/user-attachments/assets/0613088f-c013-4f1c-92c3-fbdae8c1f109">

After:
<img width="771" alt="Screenshot 2024-08-28 at 12 08 13 AM"
src="https://github.com/user-attachments/assets/eef21bee-d11f-4f0a-9147-053a163f720f">

Consensus seems to be that while the purple on is a bit clearer and
easier to read. The purple is not on brand so it doesn't look like
React. It looks ugly. It's distracting (too eye catching). Taking away
attention from other tabs in an unfair way.

It also gets worse with more tabs added. We plan on both adding another
tab and panes inside other tabs (elements/sources) soon. Each needs to
be marked somehow as part of React but spelling it out is too long.
Putting inside a second tab means two clicks and takes away real-estate
from our extension and doesn't solve the problem with extension panes in
other tabs. We also plan on adding multiple different tracks to the
Performance tab which also needs a name other than just React and
spelling out React as a prefix is too long. The Emoji is too
distracting. So it seems best to uniformly apply the symbol - albeit it
might just look like a dot to many.

Dark mode looks close to on brand:

<img width="1089" alt="Screenshot 2024-08-28 at 12 32 50 AM"
src="https://github.com/user-attachments/assets/7175a540-4241-4c26-9e4d-4d367873af57">
2024-09-10 00:09:42 -04:00
Sebastian Markbåge
6066b8e8e6 [Flight] Reset currentOwnerInDEV (#30929)
Missed this bit.
2024-09-09 22:07:35 -04:00
Sebastian Markbåge
49825c0ffc [Flight] Add react-stack-bottom-frame to console replaying (#30926)
Any time we're creating a stack trace we should have a
react-stack-bottom-frame so we know what to filter out.

This is the same thing we already do for createFakeJSXCallStackInDEV but
we should do that when replaying logs too.
2024-09-09 19:52:05 -04:00
Sebastian Markbåge
0dbacf2041 [DevTools] Improve Layering Between Console and Renderer (#30925)
The console instrumentation should not know about things like Fibers.
Only the renderer bindings should know about that stuff. We can improve
the layering by just moving all that stuff behind a `getComponentStack`
helper that gets injected by the renderer.

This sets us up for the Flight renderer #30906 to have its own
implementation of this function.
2024-09-09 15:33:30 -04:00
Sebastian Markbåge
fa3cf509a9 [DevTools] Add Map for Server Component Logs (#30905)
Stacked on #30899.

This adds another map to store Server Components logs. When they're
replayed with an owner we can associate them with a DevToolsInstance.
The replaying should happen before they can mount in Fiber so they'll
always have all logs when they mount. There can be more than one
Instance associated with any particular ReactComponentInfo. It can also
be unmounted and restored later.

One thing that's interesting about these is that when a Server Component
tree refreshes a new set of ReactComponentInfo will update through the
tree and the VirtualInstances will update with new instances. This means
that the old errors/warnings are no longer associated with the
VirtualInstance. I.e. it's not continually appended like updates do for
Fiber backed instances. On the client we dedupe errors/warnings for the
life time of the page. On the server that doesn't work well because it
would mean that when you refresh the page, you miss out on warnings so
we dedupe them per request instead. If we just appended on refresh it
would keep adding them.

If ever add a deduping mechanism that spans longer than a request, we
might need to do more of a merge when these updates.

Nothing actually adds logs to this map yet. That will need an
integration with Flight in a follow up.
2024-09-09 15:12:28 -04:00
Sebastian Markbåge
f4b3a1fea2 [DevTools] Delete fiberToFiberInstanceMap (#30900)
Stacked on #30899.

After the rest of the stack this is now unused so we can save time and
memory avoiding to maintain it. 🎉
2024-09-09 15:12:13 -04:00
Sebastian Markbåge
2283d7204c [Flight] Inject Client Into DevTools (#30910)
Stacked on #30906.

Injects the Flight Client into the DevTools hook if it `supportsFlight`.
This only injects in DEV. We could inject it in prod too but so far the
only feature this exposes is only available in DEV anyway. I also only
call `injectIntoDevTools` in the browser builds since we don't really
support DevTools on the server anyway.

The main purpose of this for now is so that DevTools can track the
Server Component owner of replayed logs. This lets us add owner stacks
where `console.createTask` is not natively supported (like Firefox). It
also lets us associate the log with the Server Component in the
Component tree #30905.
2024-09-09 15:11:57 -04:00
Sebastian Markbåge
3cac8cd5a9 [DevTools] Add Flight Renderer (#30906)
This represents a virtual renderer that connects to the Flight Client.
It's virtual in the sense that the actual rendering has already happened
on the server. The Flight Client parses the result. Most of the result
then end up in objects that render into another renderer and that's how
we see most Server Components in DevTools. As part of the client's tree.

However, some things are side-effects that don't really connect to any
particular client renderer. For example preloads() and logs. For those
we need to treat the Flight Client as if it was its own renderer just
like a Fiber renderer or even legacy renderer. We really could support
Fizz and Flight Server as DevTools targets too for example to connect it
to the backend but there's just not much demand for that.

This will initially only be used to track the owners of replayed console
logs but could be expanded to more. For example to send controls to
start profiling on the server. It could also be expanded to build an RSC
payload inspector that is automatically connected.
2024-09-09 15:11:34 -04:00
Sebastian Markbåge
e07235b980 [DevTools] Refactor Error / Warning Count Tracking (#30899)
We can simplify this tracking by not having a separate pending set of
logs and the logs tracked per instance and instead we just track the
logs per Fiber. This avoids the need to move it back into the pending
set after unmounts in case a Fiber is reparented.

The main motivation for this is to unify with an upcoming tracking of
logs for Server Components. For those it doesn't make sense to move them
into a per instance set and because the same Server Component - and its
logs - may appear more than once. So no particular instance should steal
it.

The second part of this change is that instead of looking up the
instance from fiber, which requires the fiberToFiberInstanceMap, we
instead look up if a component has any new logs when we traverse it in
the commit phase. After all for a component to have had a log it must
have updated. This is a similar technique to #30897. This technique also
works for Server Components without having to maintain a one to many
relationship from ComponentInfo to VirtualInstance. So it unifies them.

Normally this look up would be fast since the `fiberToComponentsLogs`
set would be empty and so doesn't add any significant weight to the
commit phase. If there's a ton of logs on many different components then
it's not great since it would slow down the commit phase but that's not
what we expect to see so in typical usage, this is better.

There is an unfortunate consequence though which is that
`console.warn/error` in passive effects (i.e. `useEffect`) wouldn't be
picked up because currently we traverse the logs in
`handleCommitFiberRoot` which is too early. If we moved that to
`handlePostCommitFiberRoot` this wouldn't be a problem. In the meantime,
I just detect this and do a brute force flush by walking all mounted
instances if there's a `console.warn/error` inside a passive effect.

If we ever add "owners" to event handlers that are triggered outside the
render/commit phases (like `<div onClick={...}>`) and we want to
associate error/warnings in those, we'd need a different technique to
ensure those get flushed in time.
2024-09-09 15:10:04 -04:00
Sam Zhou
e210d08180 [flow] Upgrade Flow to 0.245.2 (#30919)
## Summary

This PR bumps Flow all the way to the latest 0.245.2. 

Most of the suppressions comes from Flow v0.239.0's change to include
undefined in the return of `Array.pop`.

I also enabled `react.custom_jsx_typing=true` and added custom jsx
typing to match the old behavior that `React.createElement` is
effectively any typed. This is necessary since various builtin
components like `React.Fragment` is actually symbol in the React repo
instead of `React.AbstractComponent<...>`. It can be made more accurate
by customizing the `React$CustomJSXFactory` type, but I will leave it to
the React team to decide.

## How did you test this change?

`yarn flow` for all the renderers
2024-09-09 08:41:44 -07:00
Alex Hunt
984ea11d14 [DevTools] Separate RDT Fusebox into single-panel entry points (#30708)
## Summary

Separate function entry points for `react-devtools-fusebox` into
`initializeComponents` and `initializeProfiler`. The motivation behind
this change is to separate these tabs into top-level Chrome DevTools
panels (aligned with web) in React Native.

## How did you test this change?

- Build `react-devtools-fusebox` and load into local
[rn-chrome-devtools-frontend](https://github.com/facebookexperimental/rn-chrome-devtools-frontend)
project with updated call sites.

<img width="1933" alt="image"
src="https://github.com/user-attachments/assets/202d32a1-b8da-4936-b0e1-04875a30f256">

<img width="1949" alt="image"
src="https://github.com/user-attachments/assets/39dbe154-989c-4f76-b103-aa19f07a3b9c">

 Tabs can be separately initialised in individual Chrome DevTools
panels
2024-09-09 08:12:49 -04:00
Mike Vitousek
a8fc4b1ef8 [compiler][playground] Fix displayed naming of outlined functions
ghstack-source-id: 20c8e9eeba1620b061e41119dfb1a0e15c36645e
Pull Request resolved: https://github.com/facebook/react/pull/30907
2024-09-07 17:50:19 -07:00
Sebastian Markbåge
99cba2b041 [DevTools] Build Updater List from the Commit instead of Map (#30897)
Stacked on #30896.

The problem with the `getUpdatersList` function is that it iterates over
Fibers and then looks up each of those Fibers in the
fiberToFiberInstanceMap which we ideally could get rid of.

However, every time an updater comes into play for a commit it must mean
that something below the updater itself updated and so the updater will
also be cloned which means we'll pass it on the way down when traversing
the tree in the commit.

When we do this traversal, we can just look if the Fiber is in the
updater set and if so add it to the updater list as we go.
2024-09-06 21:59:09 -04:00
Sebastian Markbåge
6292398241 [DevTools] Simplify Context Change Tracking in Profiler (#30896)
When Context change tracking was added to support modern Context it
relied on the "memoizedValue" to read the current value. This only works
in React 18+ when it was added to support Lazy Context Propagation.
However, the backend stored the old value the same way it used to work
for legacy Context in a global map. This was unnecessary since we *also*
have the old value on the previous Fiber.

This removes all the costly tracking of previous values for every Fiber
that uses Contexts slowing down profiling. Instead, we just compare the
Contexts from

The downside is that this no longer supports detecting changes due to
legacy Context because it doesn't have a similar "previous" value.
However, legacy Context has long been deprecated and is completely
removed in 19. So I don't think it's worth supporting since you have to
be on an old version *and* actually use legacy Context *and* trying to
profile something that updates it. Which btw, updating legacy contexts
only worked at all from 16 something when we made updates work. So it
was unusual even in the slight gap where you could and before you had
migrated to modern Context introduced in 16.3.
2024-09-06 21:58:20 -04:00
Sebastian Markbåge
baf47462d6 [DevTools] Remove use of .alternate in root and recordProfilingDurations (#30895)
Ideally we shouldn't use the `.alternate` to access previous state
because ideally Fibers shouldn't have alternates.

The only case it's ok to use it is when it is used to identity the
stateful part of a component's identity. In a non-alternate Fiber model
there would instead be another object that represents instance but in
the current model it's modeled by the pair.

It's not ok is to get the previous state of the tree since that would
not live on the stateful part.

We don't generally need this though because we have the previous state
on instance.data before updating it, or passed from above.
2024-09-06 21:46:09 -04:00
Sebastian Markbåge
d76a5651f4 [DevTools] Handle reordered contexts in Profiler (#30887)
While looking at the Context tracking implementation for other reasons I
noticed this bug.

Originally it wasn't allowed to have conditional `useContext(context)`
(although we did because it's technically possible). With `use(context)`
it is officially allowed to be conditional as long as it is within a
Hook/Component and not within a try/catch.

This means that this loop comparing previous and next contexts need to
consider that the Context objects might not line up and so it's possibly
comparing apples to oranges. We already bailed if one was longer than
the other.

If the order of contexts changes later in the component that means
something else must have already changed earlier so the reason for the
rerender isn't the context so we can just return false in that case.
2024-09-06 21:45:52 -04:00
Joe Savona
727b361528 Fix useMemoCache with setState in render
Fixes the bug that @alexmckenley and @mofeiZ found where setState-in-render can reset useMemoCache and cause an infinite loop. The bug was that renderWithHooksAgain() was not resetting hook state when rerendering (so useMemo values were preserved) but was resetting the updateQueue. This meant that the entire memo cache was cleared on a setState-in-render.

The fix here is to call a new helper function to clear the update queue. It nulls out other properties, but for memoCache it just sets the index back to zero.

ghstack-source-id: fc0947ce219334117075df6a4e33b39975af2bc4
Pull Request resolved: https://github.com/facebook/react/pull/30889
2024-09-06 14:32:18 -07:00
Mofei Zhang
bd788b4180 [compiler] Add enablePropagateDepsInHIR flag
Adding new feature flag in preparation for #30894

ghstack-source-id: 59278028cf178d6b2c28732ded751e9c838183d2
Pull Request resolved: https://github.com/facebook/react/pull/30893
2024-09-06 13:02:08 -04:00
Mofei Zhang
7b98a168fd [compiler][cleanup] Delete now-unused reactive scope fork
Followup to #30891

ghstack-source-id: 6b42055b5d28da39d99a235bcd86a82eb7c270f4
Pull Request resolved: https://github.com/facebook/react/pull/30892
2024-09-06 13:02:08 -04:00
Mofei Zhang
43264a61d0 [compiler][cleanup] Remove unused enableReactiveScopesInHIR flag
Reactive scopes in HIR has been stable for over 3 months now and is the future direction of react compiler, removing this flag to reduce implementation forks.

ghstack-source-id: 65cdf63cf7
Pull Request resolved: https://github.com/facebook/react/pull/30891
2024-09-06 13:02:08 -04:00
Sebastian Markbåge
a03254bc60 [Fiber] Move runWithFiberInDEV from CommitWork to CommitEffects (#30882)
Stacked on #30881.

Move `runWithFiberInDEV` from the recursive part of the commit phase and
instead wrap each call into user space. These should really map 1:1 with
where we're using `try/catch` since that's where we're calling into user
space.

The goal of this is to avoid the extra stack frames added by
`enableOwnerStacks` in the recursive parts to avoid stack overflow. This
way we only have a couple of extra at the end of the stack instead of a
couple of extra at every depth of the tree.
2024-09-05 20:54:39 -04:00
Sebastian Markbåge
fe03c56d1e [Fiber] Extract Functions that Call User Space and Host Configs in Commit to Separate Modules (#30881)
This is mostly just moves and same code extracted into utility
functions.

This is to help clarify what needs to be wrapped in try/catch and
runWithFiberInDEV. I'll do the runWithFiberInDEV changes in a follow up.

This leaves ReactCommitWork mostly to do matching on the tag and the
recursive loops.
2024-09-05 20:53:27 -04:00
Sebastian Markbåge
a06cd9e1d1 [DevTools] Refactor Forcing Fallback / Error of Suspense / Error Boundaries (#30870)
First, this basically reverts
1f3892ef8c
to use a Map/Set to track what is forced to suspend/error again instead
of flags on the Instance. The difference is that now the key in the
Fiber itself instead of the ID. Critically this avoids the
fiberToFiberInstance map to look up whether or not a Fiber should be
forced to suspend when asked by the renderer.

This also allows us to force suspend/error on filtered instances. It's a
bit unclear what should happen when you try to Suspend or Error a child
but its parent boundary is filtered. It was also inconsistent between
Suspense and Error due to how they were implemented.

I think conceptually you're trying to simulate what would happen if that
Component errored or suspended so it would be misleading if we triggered
a different boundary than would happen in real life. So I think we
should trigger the nearest unfiltered Fiber, not the nearest Instance.
The consequence of this however is that if this instance was filtered,
there's no way to undo it without refreshing or removing the filter.
This is an edge case though since it's unusual you'd filter these in the
first place.

It used to be that Suspense walked the store in the frontend and Error
walked the Fibers in the backend. They also did this somewhat eagerly.
This simplifies and unifies the model by passing the id of what you
clicked in the frontend and then we walk the Fiber tree from there in
the backend to lazily find the boundary. However I also eagerly walk the
tree at first to find whether we have any Suspense or Error boundary
parents at all so we can hide the buttons if not.

This also implements it to work with VirtualInstances using #30865. I
find the nearest Fiber Instance downwards filtered or otherwise. Then
from its parent we find the nearest Error or Suspense boundary. That's
because VirtualInstance will always have their inner Fiber as an
Instance but they might not have their parent since it might be
filtered. Which would potentially cause us to skip over a filtered
parent Suspense boundary.
2024-09-05 15:48:17 -04:00
mofeiZ
d72e477814 [compiler runtime] repro: infinite render with useMemoCache + render phase updates (#30849)
Repro for an infinite render bug we found when testing internally. See
equivalent codesandbox repro
[here](https://codesandbox.io/p/sandbox/epic-euclid-mr7lm3).

When render phase updates cause a re-render, useMemoCache arrays for the
fiber are
[cleared](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L819)
and [recreated on every
retry](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js#L1223)
while hook state is preserved.

This pattern (queuing re-renders on the current fiber during render) is
perfectly valid. I believe this is a bug as React compiler currently
replaces `useMemo`s with `useMemoCache` calls and inlined instructions,
taking care to preserve existing memoization dependencies. This should
be the identity transform, but runtime implementation differences mean
that uncompiled code behaves as expected (no infinite render) while
compiled code fails to render.
2024-09-05 13:30:34 -04:00
Sebastian Markbåge
4c58fce777 [DevTools] Avoid getFiberIDUnsafe in debug() Helper (#30878)
Avoids looking up id from fiber and instead pass the instance to the
debug() helper.
2024-09-04 20:25:35 -04:00
Sebastian Markbåge
01ae2ddaa9 [DevTools] Include some Filtered Fiber Instances (#30865)
When we filter Fiber Instances where have no way to recover our position
in the Fiber tree. The extreme form of this is if you filter out all the
Fibers and keep only Server Components.

This affects operations that are performed against fibers such as
collecting Host Instances for highlighting or emulating
suspending/erroring.

Conceptually we don't need to add this into the DevToolsInstance tree
because we only need to get to some Fibers from a VirtualInstance. A
Virtual Instance can contain more than one conceptual child Fiber. It
would be easier if we didn't include them in the tree on one hand
because we could just traverse the tree and assume it looks like the one
on the frontend. But it's also tricky to manage the lifetime. So I went
with a special FilteredFiberInstance node in the tree.

Currently I only add it if its parent would've been a VirtualInstance
since we don't need it in any other cases. If the parent was another
FiberInstance it already has a Fiber.

There might be need for always tracking all Instances whether they're
filtered or not or just moving filtering to the frontend but for now I'm
keeping the general architecture as is.
2024-09-04 19:35:28 -04:00
Joe Savona
f820f5a8b6 [compiler] Type inference for tagged template literals
At Meta we have a pattern of using tagged template literals for features that are compiled away:

```
// Relay:
graphql`...graphql text...`
```

In many cases these tags produce a primitive value, and we can get even more optimal output if we can tell the compiler about these types. The new moduleTypeProvider gives us the ability to declare such types, this PR extends the compiler to use this type information for TaggedTemplateExpression values.

ghstack-source-id: 3cd6511b7f
Pull Request resolved: https://github.com/facebook/react/pull/30869
2024-09-04 13:28:33 -07:00
Sebastian Markbåge
0123d7c19f [DevTools] Track root instances in a root Map (#30875)
The FiberRoot is a stateful node that can be tracked this way.

This is another step that will let us remove the
`fiberToFiberInstanceMap`.
2024-09-04 16:15:53 -04:00
Andrew Clark
e10e868182 Schedule prerender after something suspends (#30800)
Adds the concept of a "prerender". These special renders are spawned
whenever something suspends (and we're not already prerendering).

The purpose is to move speculative rendering work into a separate phase
that does not block the UI from updating. For example, during a
transition, if something suspends, we should not speculatively prerender
siblings that will be replaced by a fallback in the UI until *after* the
fallback has been shown to the user.
2024-09-04 13:55:29 -04:00
Andrew Clark
8b4c54c00f Temporarily disable suspending during work loop (#30762)
### Based on

- #30761 
- #30759 

---

`use` has an optimization where in some cases it can suspend the work
loop during the render phase until the data has resolved, rather than
unwind the stack and lose context. However, the current implementation
is not compatible with sibling prerendering. So I've temporarily
disabled it until the sibling prerendering has been refactored. We will
add it back in a later step.
2024-09-04 12:38:36 -04:00
Sebastian Markbåge
d1afcb43fd [DevTools] Track all public HostInstances in a Map (#30831)
This lets us get from a HostInstance to the nearest DevToolsInstance
without relying on `findFiberByHostInstance` and
`fiberToDevToolsInstanceMap`. We already did the equivalent of this for
Resources in HostHoistables.

One issue before was that we'd ideally get away from the
`fiberToDevToolsInstanceMap` map in general since we should ideally not
treat Fibers as stateful but they could be replaced by something else
stateful in principle.

This PR also addresses Virtual Instances. Now you can select a DOM node
and have it select a Virtual Instance if that's the nearest parent since
the parent doesn't have to be a Fiber anymore.

However, the other reason for this change is that I'd like to get rid of
the need for the `findFiberByHostInstance` from being injected. A
renderer should not need to store a reference back from its instance to
a Fiber. Without the Synthetic Event system this wouldn't be needed by
the renderer so we should be able to remove it. We also don't really
need it since we have all the information by just walking the commit to
collect the nodes if we just maintain our own Map.

There's one subtle nuance that the different renderers do. Typically a
HostInstance is the same thing as a PublicInstance in React but
technically in Fabric they're not the same. So we need to translate
between PublicInstance and HostInstance. I just hardcoded the Fabric
implementation of this since it's the only known one that does this but
could feature detect other ones too if necessary. On one hand it's more
resilient to refactors to not rely on injected helpers and on hand it
doesn't follow changes to things like this.

For the conflict resolution I added in #30494 I had to make that
specific to DOM so we can move the DOM traversal to the backend instead
of the injected helper.
2024-09-03 17:28:05 -04:00
Sebastian Markbåge
8d68da3f73 [Fiber] Stash ThenableState on the Dependencies Object for Use By DevTools (#30866)
This lets us track what a Component might suspend on from DevTools. We
could already collect this by replaying a component's Hooks but that
would be expensive to collect from a whole tree.

The thenables themselves might contain useful information but mainly
we'd want access to the `_debugInfo` on the thenables which might
contain additional information from the server.


19bd26beb6/packages/shared/ReactTypes.js (L114)

In a follow up we should really do something similar in Flight to
transfer `use()` on the debugInfo of that Server Component.
2024-09-03 16:04:24 -04:00
Sebastian Markbåge
e0a07e9738 [DevTools] Support VirtualInstances in findAllCurrentHostInstances (#30853)
This lets us highlight Server Components.

However, there is a problem with this because if the actual nearest
Fiber is filtered, there's no FiberInstance and so we might skip past it
and maybe never find a child while walking the whole tree. This is very
common in the case where you have just Server Components and Host
Components which are filtered by default.

Note how the DOM nodes that are just plain host instances without client
component wrappers are not highlighted here:

<img width="1102" alt="Screenshot 2024-08-30 at 4 33 55 PM"
src="https://github.com/user-attachments/assets/c9a7b91e-5faf-4c60-99a8-1195539ff8b5">

Fixing that needs a separate refactor though and related to several
other features that already have a similar issue without
VirtualInstances like Suspense/Error Boundaries (triggering
suspense/error on a filtered Suspense/ErrorBoundary doesn't work
correctly). So this first PR just adds the feature for the common case
where there's at least some Fibers.
2024-09-03 12:29:59 -04:00
Sebastian Markbåge
04ec50efa9 [DevTools] Add Filtering of Environment Names (#30850)
Stacked on #30842.

This adds a filter to be able to exclude Components from a certain
environment. Default to Client or Server.

The available options are computed into a dropdown based on the names
that are currently used on the page (or an option that were previously
used). In addition to the hardcoded "Client". Meaning that if you have
Server Components on the page you see "Server" or "Client" as possible
options but it can be anything if there are multiple RSC environments on
the page.

"Client" in this case means Function and Class Components in Fiber -
excluding built-ins.

If a Server Component has two environments (primary and secondary) then
both have to be filtered to exclude it.

We don't show the option at all if there are no Server Components used
in the page to avoid confusing existing users that are just using Client
Components and wouldn't know the difference between Server vs Client.

<img width="815" alt="Screenshot 2024-08-30 at 12 56 42 AM"
src="https://github.com/user-attachments/assets/e06b225a-e85d-4cdc-8707-d4630fede19e">
2024-09-03 12:29:15 -04:00
930 changed files with 41567 additions and 19877 deletions

View File

@@ -8,11 +8,9 @@ indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
[COMMIT_EDITMSG]
max_line_length = 0

View File

@@ -23,6 +23,7 @@ module.exports = {
'babel',
'ft-flow',
'jest',
'es',
'no-for-of-loops',
'no-function-declare-after-return',
'react',
@@ -47,7 +48,7 @@ module.exports = {
'ft-flow/no-unused-expressions': ERROR,
// 'ft-flow/no-weak-types': WARNING,
// 'ft-flow/require-valid-file-annotation': ERROR,
'es/no-optional-chaining': ERROR,
'no-cond-assign': OFF,
'no-constant-condition': OFF,
'no-control-regex': OFF,
@@ -435,6 +436,7 @@ module.exports = {
'packages/react-dom/src/test-utils/*.js',
],
rules: {
'es/no-optional-chaining': OFF,
'react-internal/no-production-logging': OFF,
'react-internal/warning-args': OFF,
'react-internal/safe-string-coercion': [
@@ -567,6 +569,7 @@ module.exports = {
React$Node: 'readonly',
React$Portal: 'readonly',
React$Ref: 'readonly',
React$RefSetter: 'readonly',
ReadableStreamController: 'readonly',
ReadableStreamReader: 'readonly',
RequestInfo: 'readonly',

View File

@@ -55,3 +55,10 @@ body:
Please provide your React version in the app where this issue occurred.
validations:
required: true
- type: input
attributes:
label: What version of React Compiler are you using?
description: |
Please provide the exact React Compiler version in the app where this issue occurred.
validations:
required: true

107
.github/dependabot.yml vendored
View File

@@ -1,107 +1,10 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/fixtures/art"
directories:
- "/fixtures/*"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/attribute-behavior"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/concurrent"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/devtools"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/dom"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/eslint"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/expiration"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fiber-debugger"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fiber-triangle"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fizz"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fizz-ssr-browser"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/flight"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/flight-browser"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/flight-esm"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/legacy-jsx-runtimes"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/nesting"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/packaging"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/scheduler"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/ssr"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/ssr-2"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/stacks"
schedule:
interval: "weekly"
interval: "monthly"
open-pull-requests-limit: 0
ignore:
- dependency-name: "*"

View File

@@ -8,6 +8,10 @@ on:
- compiler/**
- .github/workflows/compiler-playground.yml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
@@ -15,7 +19,7 @@ env:
defaults:
run:
working-directory: compiler
working-directory: compiler/apps/playground
jobs:
playground:
@@ -27,13 +31,22 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: compiler/yarn.lock
cache-dependency-path: compiler/**/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- name: yarn install compiler
run: yarn install --frozen-lockfile
working-directory: compiler
- name: yarn install playground
run: yarn install --frozen-lockfile
- run: npx playwright install --with-deps chromium
- run: yarn workspace playground test
- run: yarn test
- name: Archive test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results

View File

@@ -13,6 +13,9 @@ on:
dist_tag:
required: true
type: string
version_name:
required: true
type: string
secrets:
NPM_TOKEN:
required: true
@@ -44,9 +47,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --tags ${{ inputs.dist_tag }}
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag ${{ inputs.dist_tag }}

View File

@@ -5,6 +5,15 @@ on:
inputs:
prerelease_commit_sha:
required: false
release_channel:
required: true
type: string
dist_tag:
required: true
type: string
version_name:
required: true
type: string
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -15,7 +24,8 @@ jobs:
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
with:
commit_sha: ${{ inputs.prerelease_commit_sha || github.sha }}
release_channel: experimental
dist_tag: experimental
release_channel: ${{ inputs.release_channel }}
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -16,5 +16,6 @@ jobs:
commit_sha: ${{ github.sha }}
release_channel: experimental
dist_tag: experimental
version_name: '0.0.0'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -0,0 +1,21 @@
name: (Compiler) Publish Prereleases Weekly
on:
schedule:
# At 10 minutes past 9:00 on Mon
- cron: 10 9 * * 1
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
publish_prerelease_beta:
name: Publish to beta channel
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
with:
commit_sha: ${{ github.sha }}
release_channel: beta
dist_tag: beta
version_name: '19.0.0'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -15,6 +15,10 @@ on:
- compiler/Cargo.*
- compiler/*.toml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings

View File

@@ -8,6 +8,10 @@ on:
- compiler/**
- .github/workflows/compiler_typescript.yml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
@@ -43,7 +47,7 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn workspace babel-plugin-react-compiler lint
@@ -63,7 +67,7 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn workspace babel-plugin-react-compiler jest
@@ -87,6 +91,6 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn workspace ${{ matrix.workspace_name }} test

View File

@@ -30,7 +30,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
@@ -62,7 +64,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -103,6 +107,7 @@ jobs:
- "16.8" # hooks
- "17.0"
- "18.0"
- "18.2" # compiler polyfill
continue-on-error: true
steps:
- uses: actions/checkout@v4
@@ -116,7 +121,7 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4
@@ -150,7 +155,7 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4

View File

@@ -3,10 +3,18 @@ name: (Runtime) Build and Test
on:
push:
branches: [main]
tags:
# To get CI for backport releases.
# This will duplicate CI for releases from main which is acceptable
- "v*"
pull_request:
paths-ignore:
- compiler/**
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
@@ -48,7 +56,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: node ./scripts/tasks/flow-ci ${{ matrix.flow_inline_config_shortname }}
@@ -68,7 +78,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: |
yarn generate-inline-fizz-runtime
@@ -90,7 +102,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn flags
@@ -139,7 +153,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
@@ -168,7 +184,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn build --index=${{ matrix.worker_id }} --total=20 --r=${{ matrix.release_channel }} --ci
env:
@@ -238,7 +256,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -266,7 +286,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -309,7 +331,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -340,7 +364,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -368,7 +394,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: v2-yarn_cache_fixtures_dom-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: fixtures_dom-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
working-directory: fixtures/dom
@@ -408,7 +436,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: v2-yarn_cache_fixtures_flight-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: fixtures_flight-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -464,7 +494,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -510,7 +542,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
@@ -543,6 +577,8 @@ jobs:
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
@@ -581,7 +617,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build for PR
uses: actions/download-artifact@v4

View File

@@ -11,6 +11,11 @@ on:
commit_sha:
required: false
type: string
force:
description: 'Force a commit to the builds/... branches'
required: true
default: false
type: boolean
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -69,7 +74,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
name: yarn install (react)
- run: yarn install --frozen-lockfile
@@ -77,7 +84,7 @@ jobs:
working-directory: scripts/release
- name: Download artifacts for base revision
run: |
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha }}
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
- name: Display structure of build
run: ls -R build
- name: Strip @license from eslint plugin and react-refresh
@@ -118,13 +125,14 @@ jobs:
run: |
BASE_FOLDER='compiled-rn/facebook-fbsource/xplat/js'
mkdir -p ${BASE_FOLDER}/react-native-github/Libraries/Renderer/
mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-is,react-test-renderer}/
mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-dom,react-is,react-test-renderer}/
# Move React Native renderer
mv build/react-native/implementations/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/
mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react/
mv build/facebook-react-native/react-dom/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-dom/
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
@@ -139,9 +147,9 @@ jobs:
ls -R ./compiled-rn
- name: Add REVISION files
run: |
echo ${{ github.event.workflow_run.head_sha }} >> ./compiled/facebook-www/REVISION
echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION
cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
echo ${{ github.event.workflow_run.head_sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
echo ${{ github.sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
- name: "Get current version string"
id: get_current_version
run: |
@@ -165,7 +173,7 @@ jobs:
commit_www_artifacts:
needs: download_artifacts
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }}
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -195,6 +203,7 @@ jobs:
grep -rl "$CURRENT_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_MODERN/$LAST_VERSION_MODERN/g"
grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "Modern version reverted"
- name: Check for changes
if: inputs.force != true
id: check_should_commit
run: |
echo "Full git status"
@@ -212,7 +221,7 @@ jobs:
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Re-apply version changes
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != ''
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '')
env:
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
@@ -229,26 +238,26 @@ jobs:
grep -rl "$LAST_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_MODERN/$CURRENT_VERSION_MODERN/g"
grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "Classic version re-applied"
- name: Will commit these changes
if: steps.check_should_commit.outputs.should_commit == 'true'
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
run: |
echo ":"
git status -u
- name: Commit changes to branch
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message }}
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
DiffTrain build for [${{ github.event.workflow_run.head_sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }})
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
branch: builds/facebook-www
commit_user_name: ${{ github.event.workflow_run.triggering_actor.login }}
commit_user_email: ${{ github.event.workflow_run.triggering_actor.email || format('{0}@users.noreply.github.com', github.event.workflow_run.triggering_actor.login) }}
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
create_branch: true
commit_fbsource_artifacts:
needs: download_artifacts
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }}
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -271,6 +280,7 @@ jobs:
grep -rl "$CURRENT_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$CURRENT_VERSION/$LAST_VERSION/g"
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "Version reverted"
- name: Check for changes
if: inputs.force != 'true'
id: check_should_commit
run: |
echo "Full git status"
@@ -289,7 +299,7 @@ jobs:
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Re-apply version changes
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != ''
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '')
env:
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
@@ -299,12 +309,12 @@ jobs:
grep -rl "$LAST_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$LAST_VERSION/$CURRENT_VERSION/g"
grep -rl "$LAST_VERSION" ./compiled-rn || echo "Version re-applied"
- name: Add files for signing
if: steps.check_should_commit.outputs.should_commit == 'true'
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
run: |
echo ":"
git add .
- name: Signing files
if: steps.check_should_commit.outputs.should_commit == 'true'
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
uses: actions/github-script@v7
with:
script: |
@@ -358,8 +368,9 @@ jobs:
console.log('Signing files in directory:', directory);
try {
const result = execSync(`git status --porcelain ${directory}`, {encoding: 'utf8'});
console.log(result);
// Parse the git status output to get file paths
// Parse the git status output to get file paths!
const files = result.split('\n').filter(file => file.endsWith('.js'));
if (files.length === 0) {
@@ -368,7 +379,14 @@ jobs:
);
} else {
files.forEach(line => {
const file = line.slice(3).trim();
let file = null;
if (line.startsWith('D ')) {
return;
} else if (line.startsWith('R ')) {
file = line.slice(line.indexOf('->') + 3);
} else {
file = line.slice(3).trim();
}
if (file) {
console.log(' Signing file:', file);
const originalContents = fs.readFileSync(file, 'utf8');
@@ -387,19 +405,19 @@ jobs:
console.error('Error signing files:', e);
}
- name: Will commit these changes
if: steps.check_should_commit.outputs.should_commit == 'true'
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
run: |
git add .
git status
- name: Commit changes to branch
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message }}
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
DiffTrain build for commit https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }}.
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
branch: builds/facebook-fbsource
commit_user_name: ${{ github.event.workflow_run.triggering_actor.login }}
commit_user_email: ${{ github.event.workflow_run.triggering_actor.email || format('{0}@users.noreply.github.com', github.event.workflow_run.triggering_actor.login) }}
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
create_branch: true

View File

@@ -40,7 +40,9 @@ jobs:
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release

View File

@@ -5,6 +5,10 @@ on:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
@@ -25,7 +29,9 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn prettier-check
@@ -43,7 +49,9 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: node ./scripts/tasks/eslint
@@ -61,7 +69,9 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/check_license.sh
@@ -79,6 +89,8 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/test_print_warnings.sh

View File

@@ -24,6 +24,7 @@ compiler/**/.next
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-kitchensink.js
compiler/crates
compiler/target
compiler/apps/playground/public
compiler/**/LICENSE

View File

@@ -7,18 +7,18 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 19.0.0
// 19.0.4
//
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 19.0.0-canary-a1c2d3e4
// 19.0.4-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.0.0';
const ReactVersion = '19.0.4';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -30,7 +30,7 @@ const canaryChannelLabel = 'rc';
// If the canaryChannelLabel is "rc", the build pipeline will use this to build
// an RC version of the packages.
const rcNumber = 0;
const rcNumber = 1;
const stablePackages = {
'eslint-plugin-react-hooks': '5.1.0',

View File

@@ -33,7 +33,7 @@ test('editor should compile successfully', async ({page}) => {
path: 'test-results/01-show-js-before.png',
});
const userInput =
(await page.locator('.monaco-editor').nth(2).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
expect(concat(userInput)).toMatchSnapshot('user-input.txt');
// Reset button works
@@ -44,6 +44,6 @@ test('editor should compile successfully', async ({page}) => {
path: 'test-results/02-show-js-after.png',
});
const defaultInput =
(await page.locator('.monaco-editor').nth(2).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
expect(concat(defaultInput)).toMatchSnapshot('default-input.txt');
});

View File

@@ -7,7 +7,11 @@
import '../styles/globals.css';
export default function RootLayout({children}: {children: React.ReactNode}) {
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
'use no memo';
return (
<html lang="en">

View File

@@ -11,7 +11,7 @@ import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
export default function Hoot() {
export default function Page(): JSX.Element {
return (
<StoreProvider>
<SnackbarProvider

View File

@@ -1,21 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = function (api) {
api.cache(true);
return {
presets: ['next/babel'],
plugins: [
[
'babel-plugin-react-compiler',
{
runtimeModule: 'react-compiler-runtime',
},
],
],
};
};

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {parse as babelParse, ParserPlugin} from '@babel/parser';
import {parse as babelParse} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import traverse, {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
@@ -14,11 +14,9 @@ import {
CompilerErrorDetail,
Effect,
ErrorSeverity,
parseConfigPragma,
printHIR,
printReactiveFunction,
run,
parseConfigPragmaForTests,
ValueKind,
runPlayground,
type Hook,
} from 'babel-plugin-react-compiler/src';
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
@@ -45,7 +43,7 @@ import {
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
function parseInput(input: string, language: 'flow' | 'typescript') {
function parseInput(input: string, language: 'flow' | 'typescript'): any {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
@@ -183,9 +181,9 @@ function getFunctionIdentifier(
}
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, PrintedCompilerPipelineValue[]>();
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const upsert = (result: PrintedCompilerPipelineValue) => {
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
@@ -210,21 +208,17 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const config = parseConfigPragma(pragma);
const config = parseConfigPragmaForTests(pragma);
for (const fn of parseFunctions(source, language)) {
const id = withIdentifier(getFunctionIdentifier(fn));
for (const result of run(
for (const result of runPlayground(
fn,
{
...config,
customHooks: new Map([...COMMON_HOOKS]),
},
getReactFunctionType(id),
'_c',
null,
null,
null,
)) {
const fnName = id.name;
switch (result.kind) {
@@ -235,7 +229,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
name: result.name,
value: {
type: 'FunctionDeclaration',
id,
id: withIdentifier(result.value.id),
async: result.value.async,
generator: result.value.generator,
body: result.value.body,
@@ -279,13 +273,17 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
}
}
} catch (err) {
// error might be an invariant violation or other runtime error
// (i.e. object shape that is not CompilerError)
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.details.push(...err.details);
} else {
// Handle unexpected failures by logging (to get a stack trace)
// and reporting
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
console.error(err);
error.details.push(
new CompilerErrorDetail({
@@ -303,7 +301,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
return [{kind: 'ok', results}, language];
}
export default function Editor() {
export default function Editor(): JSX.Element {
const store = useStore();
const deferredStore = useDeferredValue(store);
const dispatchStore = useStoreDispatch();

View File

@@ -15,18 +15,17 @@ import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
// TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
// @ts-ignore
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
loader.config({monaco});
type Props = {
errors: CompilerErrorDetail[];
errors: Array<CompilerErrorDetail>;
language: 'flow' | 'typescript';
};
export default function Input({errors, language}: Props) {
export default function Input({errors, language}: Props): JSX.Element {
const [monaco, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
const dispatchStore = useStoreDispatch();
@@ -38,18 +37,19 @@ export default function Input({errors, language}: Props) {
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({monaco, model, details: errors});
// N.B. that `tabSize` is a model property, not an editor property.
// So, the tab size has to be set per model.
/**
* N.B. that `tabSize` is a model property, not an editor property.
* So, the tab size has to be set per model.
*/
model.updateOptions({tabSize: 2});
}, [monaco, errors]);
const flowDiagnosticDisable = [
7028 /* unused label */, 6133 /* var declared but not read */,
];
useEffect(() => {
// Ignore "can only be used in TypeScript files." errors, since
// we want to support syntax highlighting for Flow (*.js) files
// and Flow is not a built-in language.
/**
* Ignore "can only be used in TypeScript files." errors, since
* we want to support syntax highlighting for Flow (*.js) files
* and Flow is not a built-in language.
*/
if (!monaco) return;
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
diagnosticCodesToIgnore: [
@@ -64,7 +64,9 @@ export default function Input({errors, language}: Props) {
8011,
8012,
8013,
...(language === 'flow' ? flowDiagnosticDisable : []),
...(language === 'flow'
? [7028 /* unused label */, 6133 /* var declared but not read */]
: []),
],
noSemanticValidation: true,
// Monaco can't validate Flow component syntax
@@ -72,7 +74,7 @@ export default function Input({errors, language}: Props) {
});
}, [monaco, language]);
const handleChange = (value: string | undefined) => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
dispatchStore({
@@ -83,7 +85,10 @@ export default function Input({errors, language}: Props) {
});
};
const handleMount = (_: editor.IStandaloneCodeEditor, monaco: Monaco) => {
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
setMonaco(monaco);
const tscOptions = {
@@ -111,10 +116,12 @@ export default function Input({errors, language}: Props) {
monaco.languages.typescript.javascriptDefaults.addExtraLib(...reactLib);
monaco.languages.typescript.typescriptDefaults.addExtraLib(...reactLib);
// Remeasure the font in case the custom font is loaded only after
// Monaco Editor is mounted.
// N.B. that this applies also to the output editor as it seems
// Monaco Editor instances share the same font config.
/**
* Remeasure the font in case the custom font is loaded only after
* Monaco Editor is mounted.
* N.B. that this applies also to the output editor as it seems
* Monaco Editor instances share the same font config.
*/
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
@@ -125,14 +132,18 @@ export default function Input({errors, language}: Props) {
<Resizable
minWidth={650}
enable={{right: true}}
// Restrict MonacoEditor's height, since the config autoLayout:true
// will grow the editor to fit within parent element
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
className="!h-[calc(100vh_-_3.5rem)]">
<MonacoEditor
path={'index.js'}
// .js and .jsx files are specified to be TS so that Monaco can actually
// check their syntax using its TS language service. They are still JS files
// due to their extensions, so TS language features don't work.
/**
* .js and .jsx files are specified to be TS so that Monaco can actually
* check their syntax using its TS language service. They are still JS files
* due to their extensions, so TS language features don't work.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}

View File

@@ -17,7 +17,7 @@ import {type CompilerError} from 'babel-plugin-react-compiler/src';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {memo, useEffect, useState} from 'react';
import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
@@ -42,10 +42,10 @@ export type PrintedCompilerPipelineValue =
| {kind: 'debug'; name: string; fnName: string | null; value: string};
export type CompilerOutput =
| {kind: 'ok'; results: Map<string, PrintedCompilerPipelineValue[]>}
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
| {
kind: 'err';
results: Map<string, PrintedCompilerPipelineValue[]>;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
error: CompilerError;
};
@@ -54,7 +54,10 @@ type Props = {
compilerOutput: CompilerOutput;
};
async function tabify(source: string, compilerOutput: CompilerOutput) {
async function tabify(
source: string,
compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
@@ -112,8 +115,10 @@ async function tabify(source: string, compilerOutput: CompilerOutput) {
}
// Ensure that JS and the JS source map come first
if (topLevelFnDecls.length > 0) {
// Make a synthetic Program so we can have a single AST with all the top level
// FunctionDeclarations
/**
* Make a synthetic Program so we can have a single AST with all the top level
* FunctionDeclarations
*/
const ast = t.program(topLevelFnDecls);
const {code, sourceMapUrl} = await codegen(ast, source);
reorderedTabs.set(
@@ -175,7 +180,7 @@ function getSourceMapUrl(code: string, map: string): string | null {
)}`;
}
function Output({store, compilerOutput}: Props) {
function Output({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
@@ -236,11 +241,13 @@ function TextTabContent({
output: string;
diff: string | null;
showInfoPanel: boolean;
}) {
}): JSX.Element {
const [diffMode, setDiffMode] = useState(false);
return (
// Restrict MonacoEditor's height, since the config autoLayout:true
// will grow the editor to fit within parent element
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
<div className="w-full h-monaco_small sm:h-monaco">
{showInfoPanel ? (
<div className="flex items-center gap-1 bg-amber-50 p-2">

View File

@@ -7,8 +7,10 @@
import dynamic from 'next/dynamic';
// monaco-editor is currently not compatible with ssr
// https://github.com/vercel/next.js/issues/31692
/**
* monaco-editor is currently not compatible with ssr
* https://github.com/vercel/next.js/issues/31692
*/
const Editor = dynamic(() => import('./EditorImpl'), {
ssr: false,
});

View File

@@ -16,26 +16,26 @@ import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStoreDispatch} from './StoreContext';
export default function Header() {
export default function Header(): JSX.Element {
const [showCheck, setShowCheck] = useState(false);
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
const handleReset = () => {
const handleReset: () => void = () => {
if (confirm('Are you sure you want to reset the playground?')) {
/*
Close open snackbars if any. This is necessary because when displaying
outputs (Preview or not), we only close previous snackbars if we received
new messages, which is needed in order to display "Bad URL" or success
messages when loading Playground for the first time. Otherwise, messages
such as "Bad URL" will be closed by the outputs calling `closeSnackbar`.
*/
/**
* Close open snackbars if any. This is necessary because when displaying
* outputs (Preview or not), we only close previous snackbars if we received
* new messages, which is needed in order to display "Bad URL" or success
* messages when loading Playground for the first time. Otherwise, messages
* such as "Bad URL" will be closed by the outputs calling `closeSnackbar`.
*/
closeSnackbar();
dispatchStore({type: 'setStore', payload: {store: defaultStore}});
}
};
const handleShare = () => {
const handleShare: () => void = () => {
navigator.clipboard.writeText(location.href).then(() => {
enqueueSnackbar('URL copied to clipboard');
setShowCheck(true);

View File

@@ -7,7 +7,7 @@
// https://github.com/reactjs/reactjs.org/blob/main/beta/src/components/Logo.tsx
export default function Logo(props: JSX.IntrinsicElements['svg']) {
export default function Logo(props: JSX.IntrinsicElements['svg']): JSX.Element {
return (
<svg
viewBox="0 0 410 369"

View File

@@ -29,7 +29,7 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
/**
* Make Store and dispatch function available to all sub-components in children.
*/
export function StoreProvider({children}: {children: ReactNode}) {
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
const [store, dispatch] = useReducer(storeReducer, emptyStore);
return (

View File

@@ -69,6 +69,9 @@ function TabbedWindowItem({
setTabsOpen(nextState);
}, [tabsOpen, name, setTabsOpen]);
// Replace spaces with non-breaking spaces
const displayName = name.replace(/ /g, '\u00A0');
return (
<div key={name} className="flex flex-row">
{isShow ? (
@@ -80,7 +83,7 @@ function TabbedWindowItem({
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
- {name}
- {displayName}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
@@ -94,7 +97,7 @@ function TabbedWindowItem({
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{name}
{displayName}
</button>
</div>
)}

View File

@@ -23,10 +23,13 @@ import React from 'react';
* Instead, it throws an error when `useContext` is not called within a
* Provider with a value.
*/
export default function createContext<T>() {
export default function createContext<T>(): {
useContext: () => NonNullable<T>;
Provider: React.Provider<T | null>;
} {
const context = React.createContext<T | null>(null);
function useContext() {
function useContext(): NonNullable<T> {
const c = React.useContext(context);
if (!c)
throw new Error('useContext must be within a Provider with a value');

View File

@@ -46,9 +46,9 @@ function mapReactCompilerDiagnosticToMonacoMarker(
type ReactCompilerMarkerConfig = {
monaco: Monaco;
model: editor.ITextModel;
details: CompilerErrorDetail[];
details: Array<CompilerErrorDetail>;
};
let decorations: string[] = [];
let decorations: Array<string> = [];
export function renderReactCompilerMarkers({
monaco,
model,

View File

@@ -28,7 +28,7 @@ export function decodeStore(hash: string): Store {
/**
* Serialize, encode, and save @param store to localStorage and update URL.
*/
export function saveStore(store: Store) {
export function saveStore(store: Store): void {
const hash = encodeStore(store);
localStorage.setItem('playgroundStore', hash);
history.replaceState({}, '', `#${hash}`);
@@ -56,8 +56,10 @@ export function initStoreFromUrlOrLocalStorage(): Store {
const encodedSourceFromLocal = localStorage.getItem('playgroundStore');
const encodedSource = encodedSourceFromUrl || encodedSourceFromLocal;
// No data in the URL and no data in the localStorage to fallback to.
// Initialize with the default store.
/**
* No data in the URL and no data in the localStorage to fallback to.
* Initialize with the default store.
*/
if (!encodedSource) return defaultStore;
const raw = decodeStore(encodedSource);

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

@@ -9,6 +9,9 @@ const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const path = require('path');
const nextConfig = {
experimental: {
reactCompiler: true,
},
reactStrictMode: true,
webpack: (config, options) => {
// Load *.d.ts files as strings using https://webpack.js.org/guides/asset-modules/#source-assets.

View File

@@ -3,58 +3,60 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "NODE_ENV=development next dev",
"build": "next build && node ./scripts/downloadFonts.js",
"vercel-build": "yarn workspaces run build",
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build\" \"yarn workspace react-compiler-runtime run build\"",
"build": "yarn build:compiler && next build",
"postbuild": "node ./scripts/downloadFonts.js",
"postinstall": "./scripts/link-compiler.sh",
"vercel-build": "yarn build",
"start": "next start",
"lint": "next lint",
"test": "playwright test"
},
"dependencies": {
"@babel/core": "^7.19.1",
"@babel/generator": "^7.19.1",
"@babel/parser": "^7.19.1",
"@babel/plugin-syntax-typescript": "^7.18.6",
"@babel/core": "^7.18.9",
"@babel/generator": "^7.18.9",
"@babel/parser": "^7.18.9",
"@babel/plugin-syntax-typescript": "^7.18.9",
"@babel/plugin-transform-block-scoping": "^7.18.9",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@babel/traverse": "^7.19.1",
"@babel/types": "^7.19.0",
"@babel/plugin-transform-modules-commonjs": "^7.18.9",
"@babel/preset-react": "^7.18.9",
"@babel/preset-typescript": "^7.18.9",
"@babel/traverse": "^7.18.9",
"@babel/types": "7.18.9",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.42.1",
"@use-gesture/react": "^10.2.22",
"fs": "^0.0.1-security",
"hermes-eslint": "^0.14.0",
"hermes-parser": "^0.22.0",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
"invariant": "^2.2.4",
"lz-string": "^1.5.0",
"monaco-editor": "^0.34.1",
"next": "^13.5.6",
"monaco-editor": "^0.52.0",
"next": "^15.0.1",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "18.2.0",
"react-compiler-runtime": "*",
"react-dom": "18.2.0"
"react": "19.0.0-rc-77b637d6-20241016",
"react-dom": "19.0.0-rc-77b637d6-20241016"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "^13.5.6",
"hermes-parser": "^0.22.0",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4"
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",
"./**/@babel/types": "7.7.4"
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
}

View File

@@ -30,8 +30,7 @@ export default defineConfig({
// Run your local dev server before starting the tests:
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
webServer: {
command:
'yarn workspace babel-plugin-react-compiler build && yarn workspace react-compiler-runtime build && yarn dev',
command: 'yarn dev',
url: baseURL,
timeout: 300 * 1000,
reuseExistingServer: !process.env.CI,

View File

@@ -0,0 +1,15 @@
#!/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
HERE=$(pwd)
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

View File

@@ -31,6 +31,7 @@
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
"node_modules",
"../../../**"
]
}

File diff suppressed because it is too large Load Diff

View File

View File

@@ -2,12 +2,7 @@
"private": true,
"workspaces": {
"packages": [
"packages/*",
"apps/*"
],
"nohoist": [
"**/next",
"**/next/**"
"packages/*"
]
},
"repository": {
@@ -20,11 +15,10 @@
"start": "yarn workspace playground run start",
"next": "yarn workspace playground run dev",
"build": "yarn workspaces run build",
"dev": "concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && yarn workspace playground run dev\"",
"dev": "echo 'DEPRECATED: use `cd apps/playground && yarn dev` instead!' && sleep 5 && cd apps/playground && yarn dev",
"test": "yarn workspaces run test",
"snap": "yarn workspace babel-plugin-react-compiler run snap",
"snap:build": "yarn workspace snap run build",
"postinstall": "perl -p -i -e 's/react\\.element/react.transitional.element/' packages/snap/node_modules/fbt/lib/FbtReactUtil.js && perl -p -i -e 's/didWarnAboutUsingAct = false;/didWarnAboutUsingAct = true;/' packages/babel-plugin-react-compiler/node_modules/react-dom/cjs/react-dom-test-utils.development.js",
"npm:publish": "node scripts/release/publish"
},
"dependencies": {
@@ -39,11 +33,12 @@
"@tsconfig/strictest": "^2.0.5",
"concurrently": "^7.4.0",
"folder-hash": "^4.0.4",
"object-assign": "^4.1.1",
"ora": "5.4.1",
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.23.0",
"prompt-promise": "^1.0.3",
"rollup": "^4.13.2",
"rollup": "^4.22.4",
"rollup-plugin-banner2": "^1.2.3",
"rollup-plugin-prettier": "^4.1.1",
"typescript": "^5.4.3",

View File

@@ -9,7 +9,7 @@
],
"scripts": {
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
"test": "yarn snap:ci",
"test": "./scripts/link-react-compiler-runtime.sh && yarn snap:ci",
"jest": "yarn build && ts-node node_modules/.bin/jest",
"snap": "node ../snap/dist/main.js",
"snap:build": "yarn workspace snap run build",
@@ -18,16 +18,11 @@
"lint": "yarn eslint src"
},
"dependencies": {
"@babel/generator": "7.2.0",
"@babel/types": "^7.19.0",
"chalk": "4",
"invariant": "^2.2.4",
"pretty-format": "^24",
"zod": "^3.22.4",
"zod-validation-error": "^2.1.0"
"@babel/types": "^7.19.0"
},
"devDependencies": {
"@babel/core": "^7.2.0",
"@babel/generator": "7.2.0",
"@babel/parser": "^7.2.0",
"@babel/plugin-syntax-typescript": "^7.18.6",
"@babel/plugin-transform-block-scoping": "^7.18.9",
@@ -41,20 +36,25 @@
"@types/invariant": "^2.2.35",
"@types/jest": "^29.0.3",
"@types/node": "^18.7.18",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"babel-jest": "^29.0.3",
"babel-plugin-fbt": "^1.0.0",
"babel-plugin-fbt-runtime": "^1.0.0",
"eslint": "8.27.0",
"chalk": "4",
"eslint": "^8.57.1",
"glob": "^7.1.6",
"invariant": "^2.2.4",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"react": "19.0.0-beta-b498834eab-20240506",
"react-dom": "19.0.0-beta-b498834eab-20240506",
"pretty-format": "^24",
"react": "0.0.0-experimental-0bc30748-20241028",
"react-dom": "0.0.0-experimental-0bc30748-20241028",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2"
"ts-node": "^10.9.2",
"zod": "^3.22.4",
"zod-validation-error": "^2.1.0"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",

View File

@@ -0,0 +1,10 @@
#!/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
yarn --silent workspace react-compiler-runtime link
yarn --silent workspace babel-plugin-react-compiler link react-compiler-runtime

View File

@@ -14,6 +14,7 @@ import {
parseEnvironmentConfig,
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error';
const PanicThresholdOptionsSchema = z.enum([
/*
@@ -86,17 +87,6 @@ export type PluginOptions = {
*/
compilationMode: CompilationMode;
/*
* If enabled, Forget will import `useMemoCache` from the given module
* instead of `react/compiler-runtime`.
*
* ```
* // If set to "react-compiler-runtime"
* import {c as useMemoCache} from 'react-compiler-runtime';
* ```
*/
runtimeModule?: string | null | undefined;
/**
* By default React Compiler will skip compilation of code that suppresses the default
* React ESLint rules, since this is a strong indication that the code may be breaking React rules
@@ -121,8 +111,19 @@ export type PluginOptions = {
* Set this flag (on by default) to automatically check for this library and activate the support.
*/
enableReanimatedCheck: boolean;
/**
* The minimum major version of React that the compiler should emit code for. If the target is 19
* or higher, the compiler emits direct imports of React runtime APIs needed by the compiler. On
* versions prior to 19, an extra runtime package react-compiler-runtime is necessary to provide
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
};
const CompilerReactTargetSchema = z.enum(['17', '18', '19']);
export type CompilerReactTarget = z.infer<typeof CompilerReactTargetSchema>;
const CompilationModeSchema = z.enum([
/*
* Compiles functions annotated with "use forget" or component/hook-like functions.
@@ -202,7 +203,6 @@ export const defaultOptions: PluginOptions = {
logger: null,
gating: null,
noEmit: false,
runtimeModule: null,
eslintSuppressionRules: null,
flowSuppressions: true,
ignoreUseNoForget: false,
@@ -210,6 +210,7 @@ export const defaultOptions: PluginOptions = {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
target: '19',
} as const;
export function parsePluginOptions(obj: unknown): PluginOptions {
@@ -222,25 +223,49 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
// normalize string configs to be case insensitive
value = value.toLowerCase();
}
if (key === 'environment') {
const environmentResult = parseEnvironmentConfig(value);
if (environmentResult.isErr()) {
CompilerError.throwInvalidConfig({
reason:
'Error in validating environment config. This is an advanced setting and not meant to be used directly',
description: environmentResult.unwrapErr().toString(),
suggestions: null,
loc: null,
});
if (isCompilerFlag(key)) {
switch (key) {
case 'environment': {
const environmentResult = parseEnvironmentConfig(value);
if (environmentResult.isErr()) {
CompilerError.throwInvalidConfig({
reason:
'Error in validating environment config. This is an advanced setting and not meant to be used directly',
description: environmentResult.unwrapErr().toString(),
suggestions: null,
loc: null,
});
}
parsedOptions[key] = environmentResult.unwrap();
break;
}
case 'target': {
parsedOptions[key] = parseTargetConfig(value);
break;
}
default: {
parsedOptions[key] = value;
}
}
parsedOptions[key] = environmentResult.unwrap();
} else if (isCompilerFlag(key)) {
parsedOptions[key] = value;
}
}
return {...defaultOptions, ...parsedOptions};
}
export function parseTargetConfig(value: unknown): CompilerReactTarget {
const parsed = CompilerReactTargetSchema.safeParse(value);
if (parsed.success) {
return parsed.data;
} else {
CompilerError.throwInvalidConfig({
reason: 'Not a valid target',
description: `${fromZodError(parsed.error)}`,
suggestions: null,
loc: null,
});
}
}
function isCompilerFlag(s: string): s is keyof PluginOptions {
return hasOwnProperty(defaultOptions, s);
}

View File

@@ -41,27 +41,22 @@ import {
constantPropagation,
deadCodeElimination,
pruneMaybeThrows,
inlineJsxTransform,
} from '../Optimization';
import {instructionReordering} from '../Optimization/InstructionReordering';
import {
CodegenFunction,
alignObjectMethodScopes,
alignReactiveScopesToBlockScopes,
assertScopeInstructionsWithinScopes,
assertWellFormedBreakTargets,
buildReactiveBlocks,
buildReactiveFunction,
codegenFunction,
extractScopeDeclarationsFromDestructuring,
flattenReactiveLoops,
flattenScopesWithHooksOrUse,
inferReactiveScopeVariables,
memoizeFbtAndMacroOperandsInSameScope,
mergeOverlappingReactiveScopes,
mergeReactiveScopesThatInvalidateTogether,
promoteUsedTemporaries,
propagateEarlyReturns,
propagateScopeDependencies,
pruneHoistedContexts,
pruneNonEscapingScopes,
pruneNonReactiveDependencies,
@@ -106,6 +101,8 @@ import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -281,6 +278,10 @@ function* runWithEnvironment(
value: hir,
});
if (env.config.enableJsxOutlining) {
outlineJSX(hir);
}
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
@@ -300,53 +301,66 @@ function* runWithEnvironment(
value: hir,
});
if (env.config.enableReactiveScopesInHIR) {
pruneUnusedLabelsHIR(hir);
pruneUnusedLabelsHIR(hir);
yield log({
kind: 'hir',
name: 'PruneUnusedLabelsHIR',
value: hir,
});
alignReactiveScopesToBlockScopesHIR(hir);
yield log({
kind: 'hir',
name: 'AlignReactiveScopesToBlockScopesHIR',
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
yield log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
value: hir,
});
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
value: hir,
});
assertValidBlockNesting(hir);
flattenReactiveLoopsHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenReactiveLoopsHIR',
value: hir,
});
flattenScopesWithHooksOrUseHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenScopesWithHooksOrUseHIR',
value: hir,
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
yield log({
kind: 'hir',
name: 'PropagateScopeDependenciesHIR',
value: hir,
});
if (env.config.inlineJsxTransform) {
inlineJsxTransform(hir, env.config.inlineJsxTransform);
yield log({
kind: 'hir',
name: 'PruneUnusedLabelsHIR',
name: 'inlineJsxTransform',
value: hir,
});
alignReactiveScopesToBlockScopesHIR(hir);
yield log({
kind: 'hir',
name: 'AlignReactiveScopesToBlockScopesHIR',
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
yield log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
value: hir,
});
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
value: hir,
});
assertValidBlockNesting(hir);
flattenReactiveLoopsHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenReactiveLoopsHIR',
value: hir,
});
flattenScopesWithHooksOrUseHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenScopesWithHooksOrUseHIR',
value: hir,
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
}
const reactiveFunction = buildReactiveFunction(hir);
@@ -364,53 +378,8 @@ function* runWithEnvironment(
name: 'PruneUnusedLabels',
value: reactiveFunction,
});
if (!env.config.enableReactiveScopesInHIR) {
alignReactiveScopesToBlockScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'AlignReactiveScopesToBlockScopes',
value: reactiveFunction,
});
mergeOverlappingReactiveScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'MergeOverlappingReactiveScopes',
value: reactiveFunction,
});
buildReactiveBlocks(reactiveFunction);
yield log({
kind: 'reactive',
name: 'BuildReactiveBlocks',
value: reactiveFunction,
});
flattenReactiveLoops(reactiveFunction);
yield log({
kind: 'reactive',
name: 'FlattenReactiveLoops',
value: reactiveFunction,
});
flattenScopesWithHooksOrUse(reactiveFunction);
yield log({
kind: 'reactive',
name: 'FlattenScopesWithHooks',
value: reactiveFunction,
});
}
assertScopeInstructionsWithinScopes(reactiveFunction);
propagateScopeDependencies(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PropagateScopeDependencies',
value: reactiveFunction,
});
pruneNonEscapingScopes(reactiveFunction);
yield log({
kind: 'reactive',
@@ -588,3 +557,14 @@ export function log(value: CompilerPipelineValue): CompilerPipelineValue {
}
return value;
}
export function* runPlayground(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
): Generator<CompilerPipelineValue, CodegenFunction> {
const ast = yield* run(func, config, fnType, '_c', null, null, null);
return ast;
}

View File

@@ -199,7 +199,7 @@ function insertNewOutlinedFunctionNode(
program: NodePath<t.Program>,
originalFn: BabelFn,
compiledFn: CodegenFunction,
): NodePath<t.Function> {
): BabelFn {
switch (originalFn.type) {
case 'FunctionDeclaration': {
return originalFn.insertAfter(
@@ -298,7 +298,6 @@ export function compileProgram(
return;
}
const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c');
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
/*
* Record lint errors and critical errors as depending on Forget's config,
@@ -492,18 +491,11 @@ export function compileProgram(
fn.skip();
ALREADY_COMPILED.add(fn.node);
if (outlined.type !== null) {
CompilerError.throwTodo({
reason: `Implement support for outlining React functions (components/hooks)`,
loc: outlined.fn.loc,
queue.push({
kind: 'outlined',
fn,
fnType: outlined.type,
});
/*
* Above should be as simple as the following, but needs testing:
* queue.push({
* kind: "outlined",
* fn,
* fnType: outlined.type,
* });
*/
}
}
compiledFns.push({
@@ -605,7 +597,7 @@ export function compileProgram(
if (needsMemoCacheFunctionImport) {
updateMemoCacheFunctionImport(
program,
moduleName,
getReactCompilerRuntimeModule(pass.opts),
useMemoCacheIdentifier.name,
);
}
@@ -638,8 +630,12 @@ function shouldSkipCompilation(
}
}
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
if (hasMemoCacheFunctionImport(program, moduleName)) {
if (
hasMemoCacheFunctionImport(
program,
getReactCompilerRuntimeModule(pass.opts),
)
) {
return true;
}
return false;
@@ -1126,3 +1122,31 @@ function checkFunctionReferencedBeforeDeclarationAtTopLevel(
return errors.details.length > 0 ? errors : null;
}
type ReactCompilerRuntimeModule =
| 'react/compiler-runtime' // from react namespace
| 'react-compiler-runtime'; // npm package
function getReactCompilerRuntimeModule(
opts: PluginOptions,
): ReactCompilerRuntimeModule {
let moduleName: ReactCompilerRuntimeModule | null = null;
switch (opts.target) {
case '17':
case '18': {
moduleName = 'react-compiler-runtime';
break;
}
case '19': {
moduleName = 'react/compiler-runtime';
break;
}
default:
CompilerError.invariant(moduleName != null, {
reason: 'Expected target to already be validated',
description: null,
loc: null,
suggestions: null,
});
}
return moduleName;
}

View File

@@ -29,9 +29,9 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
const assignments: Set<IdentifierId> = new Set();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
validate(identifiers, phi.id);
validate(identifiers, phi.place.identifier);
for (const [, operand] of phi.operands) {
validate(identifiers, operand);
validate(identifiers, operand.identifier);
}
}
for (const instr of block.instructions) {

View File

@@ -20,8 +20,9 @@ import {
export function assertValidMutableRanges(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
visitIdentifier(phi.place.identifier);
for (const [, operand] of phi.operands) {
visitIdentifier(operand);
visitIdentifier(operand.identifier);
}
}
for (const instr of block.instructions) {

View File

@@ -420,7 +420,19 @@ function lowerStatement(
// Already hoisted
continue;
}
if (!binding.path.isVariableDeclarator()) {
let kind:
| InstructionKind.Let
| InstructionKind.HoistedConst
| InstructionKind.HoistedLet
| InstructionKind.HoistedFunction;
if (binding.kind === 'const' || binding.kind === 'var') {
kind = InstructionKind.HoistedConst;
} else if (binding.kind === 'let') {
kind = InstructionKind.HoistedLet;
} else if (binding.path.isFunctionDeclaration()) {
kind = InstructionKind.HoistedFunction;
} else if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: 'Unsupported declaration type for hoisting',
@@ -429,11 +441,7 @@ function lowerStatement(
loc: id.parentPath.node.loc ?? GeneratedSource,
});
continue;
} else if (
binding.kind !== 'const' &&
binding.kind !== 'var' &&
binding.kind !== 'let'
) {
} else {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: 'Handle non-const declarations for hoisting',
@@ -443,6 +451,7 @@ function lowerStatement(
});
continue;
}
const identifier = builder.resolveIdentifier(id);
CompilerError.invariant(identifier.kind === 'Identifier', {
reason:
@@ -456,13 +465,6 @@ function lowerStatement(
reactive: false,
loc: id.node.loc ?? GeneratedSource,
};
const kind =
// Avoid double errors on var declarations, which we do not plan to support anyways
binding.kind === 'const' || binding.kind === 'var'
? InstructionKind.HoistedConst
: binding.kind === 'let'
? InstructionKind.HoistedLet
: assertExhaustive(binding.kind, 'Unexpected binding kind');
lowerValueToTemporary(builder, {
kind: 'DeclareContext',
lvalue: {
@@ -999,7 +1001,7 @@ function lowerStatement(
lowerAssignment(
builder,
stmt.node.loc ?? GeneratedSource,
InstructionKind.Let,
InstructionKind.Function,
id,
fn,
'Assignment',
@@ -1416,7 +1418,7 @@ function lowerObjectPropertyKey(
name: key.node.value,
};
} else if (property.node.computed && key.isExpression()) {
if (!key.isIdentifier()) {
if (!key.isIdentifier() && !key.isMemberExpression()) {
/*
* NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
* see fixture

View File

@@ -14,6 +14,7 @@ import {
ScopeId,
} from './HIR';
import {
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
@@ -176,20 +177,7 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void {
* Step 5:
* Fix scope and identifier ranges to account for renumbered instructions
*/
for (const [, block] of fn.body.blocks) {
const terminal = block.terminal;
if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
/*
* Scope ranges should always align to start at the 'scope' terminal
* and end at the first instruction of the fallthrough block
*/
const fallthroughBlock = fn.body.blocks.get(terminal.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
terminal.scope.range.start = terminal.id;
terminal.scope.range.end = firstId;
}
}
fixScopeAndIdentifierRanges(fn.body);
}
type TerminalRewriteInfo =

View File

@@ -0,0 +1,627 @@
import {CompilerError} from '../CompilerError';
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
import {
Set_equal,
Set_filter,
Set_intersect,
Set_union,
getOrInsertDefault,
} from '../Utils/utils';
import {
BasicBlock,
BlockId,
DependencyPathEntry,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
InstructionValue,
ReactiveScopeDependency,
ScopeId,
} from './HIR';
const DEBUG_PRINT = false;
/**
* Helper function for `PropagateScopeDependencies`. Uses control flow graph
* analysis to determine which `Identifier`s can be assumed to be non-null
* objects, on a per-block basis.
*
* Here is an example:
* ```js
* function useFoo(x, y, z) {
* // NOT safe to hoist PropertyLoads here
* if (...) {
* // safe to hoist loads from x
* read(x.a);
* return;
* }
* // safe to hoist loads from y, z
* read(y.b);
* if (...) {
* // safe to hoist loads from y, z
* read(z.a);
* } else {
* // safe to hoist loads from y, z
* read(z.b);
* }
* // safe to hoist loads from y, z
* return;
* }
* ```
*
* Note that we currently do NOT account for mutable / declaration range when
* doing the CFG-based traversal, producing results that are technically
* incorrect but filtered by PropagateScopeDeps (which only takes dependencies
* on constructed value -- i.e. a scope's dependencies must have mutable ranges
* ending earlier than the scope start).
*
* Take this example, this function will infer x.foo.bar as non-nullable for
* bb0, via the intersection of bb1 & bb2 which in turn comes from bb3. This is
* technically incorrect bb0 is before / during x's mutable range.
* ```
* bb0:
* const x = ...;
* if cond then bb1 else bb2
* bb1:
* ...
* goto bb3
* bb2:
* ...
* goto bb3:
* bb3:
* x.foo.bar
* ```
*
* @param fn
* @param temporaries sidemap of identifier -> baseObject.a.b paths. Does not
* contain optional chains.
* @param hoistableFromOptionals sidemap of optionalBlock -> baseObject?.a
* optional paths for which it's safe to evaluate non-optional loads (see
* CollectOptionalChainDependencies).
* @returns
*/
export function collectHoistablePropertyLoads(
fn: HIRFunction,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
): ReadonlyMap<BlockId, BlockInfo> {
const registry = new PropertyPathRegistry();
/**
* Due to current limitations of mutable range inference, there are edge cases in
* which we infer known-immutable values (e.g. props or hook params) to have a
* mutable range and scope.
* (see `destructure-array-declaration-to-context-var` fixture)
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
* is being rewritten to HIR).
*/
const knownImmutableIdentifiers = new Set<IdentifierId>();
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
for (const p of fn.params) {
if (p.kind === 'Identifier') {
knownImmutableIdentifiers.add(p.identifier.id);
}
}
}
return collectHoistablePropertyLoadsImpl(fn, {
temporaries,
knownImmutableIdentifiers,
hoistableFromOptionals,
registry,
nestedFnImmutableContext: null,
});
}
type CollectHoistablePropertyLoadsContext = {
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>;
registry: PropertyPathRegistry;
/**
* (For nested / inner function declarations)
* Context variables (i.e. captured from an outer scope) that are immutable.
* Note that this technically could be merged into `knownImmutableIdentifiers`,
* but are currently kept separate for readability.
*/
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
};
function collectHoistablePropertyLoadsImpl(
fn: HIRFunction,
context: CollectHoistablePropertyLoadsContext,
): ReadonlyMap<BlockId, BlockInfo> {
const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
const actuallyEvaluatedTemporaries = new Map(
[...context.temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
);
const nodes = collectNonNullsInBlocks(fn, {
...context,
temporaries: actuallyEvaluatedTemporaries,
});
propagateNonNull(fn, nodes, context.registry);
if (DEBUG_PRINT) {
console.log('(printing hoistable nodes in blocks)');
for (const [blockId, node] of nodes) {
console.log(
`bb${blockId}: ${[...node.assumedNonNullObjects].map(n => printDependency(n.fullPath)).join(' ')}`,
);
}
}
return nodes;
}
export function keyByScopeId<T>(
fn: HIRFunction,
source: ReadonlyMap<BlockId, T>,
): ReadonlyMap<ScopeId, T> {
const keyedByScopeId = new Map<ScopeId, T>();
for (const [_, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
keyedByScopeId.set(
block.terminal.scope.id,
source.get(block.terminal.block)!,
);
}
}
return keyedByScopeId;
}
export type BlockInfo = {
block: BasicBlock;
assumedNonNullObjects: ReadonlySet<PropertyPathNode>;
};
/**
* PropertyLoadRegistry data structure to dedupe property loads (e.g. a.b.c)
* and make computing sets intersections simpler.
*/
type RootNode = {
properties: Map<string, PropertyPathNode>;
optionalProperties: Map<string, PropertyPathNode>;
parent: null;
// Recorded to make later computations simpler
fullPath: ReactiveScopeDependency;
hasOptional: boolean;
root: IdentifierId;
};
type PropertyPathNode =
| {
properties: Map<string, PropertyPathNode>;
optionalProperties: Map<string, PropertyPathNode>;
parent: PropertyPathNode;
fullPath: ReactiveScopeDependency;
hasOptional: boolean;
}
| RootNode;
class PropertyPathRegistry {
roots: Map<IdentifierId, RootNode> = new Map();
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
/**
* Reads from a statically scoped variable are always safe in JS,
* with the exception of TDZ (not addressed by this pass).
*/
let rootNode = this.roots.get(identifier.id);
if (rootNode === undefined) {
rootNode = {
root: identifier.id,
properties: new Map(),
optionalProperties: new Map(),
fullPath: {
identifier,
path: [],
},
hasOptional: false,
parent: null,
};
this.roots.set(identifier.id, rootNode);
}
return rootNode;
}
static getOrCreatePropertyEntry(
parent: PropertyPathNode,
entry: DependencyPathEntry,
): PropertyPathNode {
const map = entry.optional ? parent.optionalProperties : parent.properties;
let child = map.get(entry.property);
if (child == null) {
child = {
properties: new Map(),
optionalProperties: new Map(),
parent: parent,
fullPath: {
identifier: parent.fullPath.identifier,
path: parent.fullPath.path.concat(entry),
},
hasOptional: parent.hasOptional || entry.optional,
};
map.set(entry.property, child);
}
return child;
}
getOrCreateProperty(n: ReactiveScopeDependency): PropertyPathNode {
/**
* We add ReactiveScopeDependencies according to instruction ordering,
* 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);
if (n.path.length === 0) {
return currNode;
}
for (let i = 0; i < n.path.length - 1; i++) {
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
n.path[i],
);
}
return PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
n.path.at(-1)!,
);
}
}
function getMaybeNonNullInInstruction(
instr: InstructionValue,
context: CollectHoistablePropertyLoadsContext,
): PropertyPathNode | null {
let path = null;
if (instr.kind === 'PropertyLoad') {
path = context.temporaries.get(instr.object.identifier.id) ?? {
identifier: instr.object.identifier,
path: [],
};
} else if (instr.kind === 'Destructure') {
path = context.temporaries.get(instr.value.identifier.id) ?? null;
} else if (instr.kind === 'ComputedLoad') {
path = context.temporaries.get(instr.object.identifier.id) ?? null;
}
return path != null ? context.registry.getOrCreateProperty(path) : null;
}
function isImmutableAtInstr(
identifier: Identifier,
instr: InstructionId,
context: CollectHoistablePropertyLoadsContext,
): boolean {
if (context.nestedFnImmutableContext != null) {
/**
* Comparing instructions ids across inner-outer function bodies is not valid, as they are numbered
*/
return context.nestedFnImmutableContext.has(identifier.id);
} else {
/**
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
* are not valid with respect to current instruction id numbering.
* We use attached reactive scope ranges as a proxy for mutable range, but this
* is an overestimate as (1) scope ranges merge and align to form valid program
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
* non-mutable identifiers.
*
* See comment in exported function for why we track known immutable identifiers.
*/
const mutableAtInstr =
identifier.mutableRange.end > identifier.mutableRange.start + 1 &&
identifier.scope != null &&
inRange(
{
id: instr,
},
identifier.scope.range,
);
return (
!mutableAtInstr || context.knownImmutableIdentifiers.has(identifier.id)
);
}
}
function collectNonNullsInBlocks(
fn: HIRFunction,
context: CollectHoistablePropertyLoadsContext,
): ReadonlyMap<BlockId, BlockInfo> {
/**
* Known non-null objects such as functional component props can be safely
* read from any block.
*/
const knownNonNullIdentifiers = new Set<PropertyPathNode>();
if (
fn.fnType === 'Component' &&
fn.params.length > 0 &&
fn.params[0].kind === 'Identifier'
) {
const identifier = fn.params[0].identifier;
knownNonNullIdentifiers.add(
context.registry.getOrCreateIdentifier(identifier),
);
}
const nodes = new Map<BlockId, BlockInfo>();
for (const [_, block] of fn.body.blocks) {
const assumedNonNullObjects = new Set<PropertyPathNode>(
knownNonNullIdentifiers,
);
const maybeOptionalChain = context.hoistableFromOptionals.get(block.id);
if (maybeOptionalChain != null) {
assumedNonNullObjects.add(
context.registry.getOrCreateProperty(maybeOptionalChain),
);
}
for (const instr of block.instructions) {
const maybeNonNull = getMaybeNonNullInInstruction(instr.value, context);
if (
maybeNonNull != null &&
isImmutableAtInstr(maybeNonNull.fullPath.identifier, instr.id, context)
) {
assumedNonNullObjects.add(maybeNonNull);
}
if (
(instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod') &&
!fn.env.config.enableTreatFunctionDepsAsConditional
) {
const innerFn = instr.value.loweredFunc;
const innerHoistableMap = collectHoistablePropertyLoadsImpl(
innerFn.func,
{
...context,
nestedFnImmutableContext:
context.nestedFnImmutableContext ??
new Set(
innerFn.func.context
.filter(place =>
isImmutableAtInstr(place.identifier, instr.id, context),
)
.map(place => place.identifier.id),
),
},
);
const innerHoistables = assertNonNull(
innerHoistableMap.get(innerFn.func.body.entry),
);
for (const entry of innerHoistables.assumedNonNullObjects) {
assumedNonNullObjects.add(entry);
}
}
}
nodes.set(block.id, {
block,
assumedNonNullObjects,
});
}
return nodes;
}
function propagateNonNull(
fn: HIRFunction,
nodes: ReadonlyMap<BlockId, BlockInfo>,
registry: PropertyPathRegistry,
): void {
const blockSuccessors = new Map<BlockId, Set<BlockId>>();
const terminalPreds = new Set<BlockId>();
for (const [blockId, block] of fn.body.blocks) {
for (const pred of block.preds) {
getOrInsertDefault(blockSuccessors, pred, new Set()).add(blockId);
}
if (block.terminal.kind === 'throw' || block.terminal.kind === 'return') {
terminalPreds.add(blockId);
}
}
/**
* In the context of a control flow graph, the identifiers that a block
* can assume are non-null can be calculated from the following:
* X = Union(Intersect(X_neighbors), X)
*/
function recursivelyPropagateNonNull(
nodeId: BlockId,
direction: 'forward' | 'backward',
traversalState: Map<BlockId, 'active' | 'done'>,
): boolean {
/**
* Avoid re-visiting computed or currently active nodes, which can
* occur when the control flow graph has backedges.
*/
if (traversalState.has(nodeId)) {
return false;
}
traversalState.set(nodeId, 'active');
const node = nodes.get(nodeId);
if (node == null) {
CompilerError.invariant(false, {
reason: `Bad node ${nodeId}, kind: ${direction}`,
loc: GeneratedSource,
});
}
const neighbors = Array.from(
direction === 'backward'
? (blockSuccessors.get(nodeId) ?? [])
: node.block.preds,
);
let changed = false;
for (const pred of neighbors) {
if (!traversalState.has(pred)) {
const neighborChanged = recursivelyPropagateNonNull(
pred,
direction,
traversalState,
);
changed ||= neighborChanged;
}
}
/**
* Note that a predecessor / successor can only be active (status != 'done')
* if it is a self-loop or other transitive cycle. Active neighbors can be
* filtered out (i.e. not included in the intersection)
* Example: self loop.
* X = Union(Intersect(X, ...X_other_neighbors), X)
*
* Example: transitive cycle through node Y, for some Y that is a
* predecessor / successor of X.
* X = Union(
* Intersect(
* Union(Intersect(X, ...Y_other_neighbors), Y),
* ...X_neighbors
* ),
* X
* )
*
* Non-active neighbors with no recorded results can occur due to backedges.
* it's not safe to assume they can be filtered out (e.g. not included in
* the intersection)
*/
const neighborAccesses = Set_intersect(
Array.from(neighbors)
.filter(n => traversalState.get(n) === 'done')
.map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects),
);
const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects;
const mergedObjects = Set_union(prevObjects, neighborAccesses);
reduceMaybeOptionalChains(mergedObjects, registry);
assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects;
traversalState.set(nodeId, 'done');
/**
* Note that it's not sufficient to compare set sizes since
* reduceMaybeOptionalChains may replace optional-chain loads with
* unconditional loads. This could in turn change `assumedNonNullObjects` of
* downstream blocks and backedges.
*/
changed ||= !Set_equal(prevObjects, mergedObjects);
return changed;
}
const traversalState = new Map<BlockId, 'done' | 'active'>();
const reversedBlocks = [...fn.body.blocks];
reversedBlocks.reverse();
let changed;
let i = 0;
do {
CompilerError.invariant(i++ < 100, {
reason:
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
loc: GeneratedSource,
});
changed = false;
for (const [blockId] of fn.body.blocks) {
const forwardChanged = recursivelyPropagateNonNull(
blockId,
'forward',
traversalState,
);
changed ||= forwardChanged;
}
traversalState.clear();
for (const [blockId] of reversedBlocks) {
const backwardChanged = recursivelyPropagateNonNull(
blockId,
'backward',
traversalState,
);
changed ||= backwardChanged;
}
traversalState.clear();
} while (changed);
}
export function assertNonNull<T extends NonNullable<U>, U>(
value: T | null | undefined,
source?: string,
): T {
CompilerError.invariant(value != null, {
reason: 'Unexpected null',
description: source != null ? `(from ${source})` : null,
loc: GeneratedSource,
});
return value;
}
/**
* Any two optional chains with different operations . vs ?. but the same set of
* property strings paths de-duplicates.
*
* Intuitively: given <base>?.b, we know <base> to be either hoistable or not.
* If unconditional reads from <base> are hoistable, we can replace all
* <base>?.PROPERTY_STRING subpaths with <base>.PROPERTY_STRING
*/
function reduceMaybeOptionalChains(
nodes: Set<PropertyPathNode>,
registry: PropertyPathRegistry,
): void {
let optionalChainNodes = Set_filter(nodes, n => n.hasOptional);
if (optionalChainNodes.size === 0) {
return;
}
let changed: boolean;
do {
changed = false;
for (const original of optionalChainNodes) {
let {identifier, path: origPath} = original.fullPath;
let currNode: PropertyPathNode =
registry.getOrCreateIdentifier(identifier);
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
const nextEntry: DependencyPathEntry =
entry.optional && nodes.has(currNode)
? {property: entry.property, optional: false}
: entry;
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
nextEntry,
);
}
if (currNode !== original) {
changed = true;
optionalChainNodes.delete(original);
optionalChainNodes.add(currNode);
nodes.delete(original);
nodes.add(currNode);
}
}
} while (changed);
}
function collectFunctionExpressionFakeLoads(
fn: HIRFunction,
): Set<IdentifierId> {
const sources = new Map<IdentifierId, IdentifierId>();
const functionExpressionReferences = new Set<IdentifierId>();
for (const [_, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
if (
value.kind === 'FunctionExpression' ||
value.kind === 'ObjectMethod'
) {
for (const reference of value.loweredFunc.dependencies) {
let curr: IdentifierId | undefined = reference.identifier.id;
while (curr != null) {
functionExpressionReferences.add(curr);
curr = sources.get(curr);
}
}
} else if (value.kind === 'PropertyLoad') {
sources.set(lvalue.identifier.id, value.object.identifier.id);
}
}
}
return functionExpressionReferences;
}

View File

@@ -0,0 +1,402 @@
import {CompilerError} from '..';
import {assertNonNull} from './CollectHoistablePropertyLoads';
import {
BlockId,
BasicBlock,
IdentifierId,
ReactiveScopeDependency,
BranchTerminal,
TInstruction,
PropertyLoad,
StoreLocal,
GotoVariant,
TBasicBlock,
OptionalTerminal,
HIRFunction,
DependencyPathEntry,
Instruction,
Terminal,
} from './HIR';
import {printIdentifier} from './PrintHIR';
export function collectOptionalChainSidemap(
fn: HIRFunction,
): OptionalChainSidemap {
const context: OptionalTraversalContext = {
currFn: fn,
blocks: fn.body.blocks,
seenOptionals: new Set(),
processedInstrsInOptional: new Set(),
temporariesReadInOptional: new Map(),
hoistableObjects: new Map(),
};
traverseFunction(fn, context);
return {
temporariesReadInOptional: context.temporariesReadInOptional,
processedInstrsInOptional: context.processedInstrsInOptional,
hoistableObjects: context.hoistableObjects,
};
}
export type OptionalChainSidemap = {
/**
* Stores the correct property mapping (e.g. `a?.b` instead of `a.b`) for
* dependency calculation. Note that we currently do not store anything on
* outer phi nodes.
*/
temporariesReadInOptional: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
/**
* Records instructions (PropertyLoads, StoreLocals, and test terminals)
* processed in this pass. When extracting dependencies in
* PropagateScopeDependencies, these instructions are skipped.
*
* E.g. given a?.b
* ```
* bb0
* $0 = LoadLocal 'a'
* test $0 then=bb1 <- Avoid adding dependencies from these instructions, as
* bb1 the sidemap produced by readOptionalBlock already maps
* $1 = PropertyLoad $0.'b' <- $1 and $2 back to a?.b. Instead, we want to add a?.b
* StoreLocal $2 = $1 <- as a dependency when $1 or $2 are later used in either
* - an unhoistable expression within an outer optional
* block e.g. MethodCall
* - a phi node (if the entire optional value is hoistable)
* ```
*
* Note that mapping blockIds to their evaluated dependency path does not
* work, since values produced by inner optional chains may be referenced in
* outer ones
* ```
* a?.b.c()
* ->
* bb0
* $0 = LoadLocal 'a'
* test $0 then=bb1
* bb1
* $1 = PropertyLoad $0.'b'
* StoreLocal $2 = $1
* goto bb2
* bb2
* test $2 then=bb3
* bb3:
* $3 = PropertyLoad $2.'c'
* StoreLocal $4 = $3
* goto bb4
* bb4
* test $4 then=bb5
* bb5:
* $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4!
* ```
*
* Also note that InstructionIds are not unique across inner functions.
*/
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>;
/**
* Records optional chains for which we can safely evaluate non-optional
* PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at
* the optional terminal in bb1.
* ```js
* bb1:
* ...
* Optional optional=false test=bb2 fallth=...
* bb2:
* Optional optional=true test=bb3 fallth=...
* ...
* ```
*/
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
};
type OptionalTraversalContext = {
currFn: HIRFunction;
blocks: ReadonlyMap<BlockId, BasicBlock>;
// Track optional blocks to avoid outer calls into nested optionals
seenOptionals: Set<BlockId>;
processedInstrsInOptional: Set<Instruction | Terminal>;
temporariesReadInOptional: Map<IdentifierId, ReactiveScopeDependency>;
hoistableObjects: Map<BlockId, ReactiveScopeDependency>;
};
function traverseFunction(
fn: HIRFunction,
context: OptionalTraversalContext,
): void {
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
traverseFunction(instr.value.loweredFunc.func, {
...context,
currFn: instr.value.loweredFunc.func,
blocks: instr.value.loweredFunc.func.body.blocks,
});
}
}
if (
block.terminal.kind === 'optional' &&
!context.seenOptionals.has(block.id)
) {
traverseOptionalBlock(
block as TBasicBlock<OptionalTerminal>,
context,
null,
);
}
}
}
/**
* Match the consequent and alternate blocks of an optional.
* @returns propertyload computed by the consequent block, or null if the
* consequent block is not a simple PropertyLoad.
*/
function matchOptionalTestBlock(
terminal: BranchTerminal,
blocks: ReadonlyMap<BlockId, BasicBlock>,
): {
consequentId: IdentifierId;
property: string;
propertyId: IdentifierId;
storeLocalInstr: Instruction;
consequentGoto: BlockId;
} | null {
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
if (
consequentBlock.instructions.length === 2 &&
consequentBlock.instructions[0].value.kind === 'PropertyLoad' &&
consequentBlock.instructions[1].value.kind === 'StoreLocal'
) {
const propertyLoad: TInstruction<PropertyLoad> = consequentBlock
.instructions[0] as TInstruction<PropertyLoad>;
const storeLocal: StoreLocal = consequentBlock.instructions[1].value;
const storeLocalInstr = consequentBlock.instructions[1];
CompilerError.invariant(
propertyLoad.value.object.identifier.id === terminal.test.identifier.id,
{
reason:
'[OptionalChainDeps] Inconsistent optional chaining property load',
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
loc: propertyLoad.loc,
},
);
CompilerError.invariant(
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected storeLocal',
loc: propertyLoad.loc,
},
);
if (
consequentBlock.terminal.kind !== 'goto' ||
consequentBlock.terminal.variant !== GotoVariant.Break
) {
return null;
}
const alternate = assertNonNull(blocks.get(terminal.alternate));
CompilerError.invariant(
alternate.instructions.length === 2 &&
alternate.instructions[0].value.kind === 'Primitive' &&
alternate.instructions[1].value.kind === 'StoreLocal',
{
reason: 'Unexpected alternate structure',
loc: terminal.loc,
},
);
return {
consequentId: storeLocal.lvalue.place.identifier.id,
property: propertyLoad.value.property,
propertyId: propertyLoad.lvalue.identifier.id,
storeLocalInstr,
consequentGoto: consequentBlock.terminal.block,
};
}
return null;
}
/**
* Traverse into the optional block and all transitively referenced blocks to
* collect sidemaps of optional chain dependencies.
*
* @returns the IdentifierId representing the optional block if the block and
* all transitively referenced optional blocks precisely represent a chain of
* property loads. If any part of the optional chain is not hoistable, returns
* null.
*/
function traverseOptionalBlock(
optional: TBasicBlock<OptionalTerminal>,
context: OptionalTraversalContext,
outerAlternate: BlockId | null,
): IdentifierId | null {
context.seenOptionals.add(optional.id);
const maybeTest = context.blocks.get(optional.terminal.test)!;
let test: BranchTerminal;
let baseObject: ReactiveScopeDependency;
if (maybeTest.terminal.kind === 'branch') {
CompilerError.invariant(optional.terminal.optional, {
reason: '[OptionalChainDeps] Expect base case to be always optional',
loc: optional.terminal.loc,
});
/**
* Optional base expressions are currently within value blocks which cannot
* be interrupted by scope boundaries. As such, the only dependencies we can
* hoist out of optional chains are property load chains with no intervening
* instructions.
*
* Ideally, we would be able to flatten base instructions out of optional
* blocks, but this would require changes to HIR.
*
* For now, only match base expressions that are straightforward
* PropertyLoad chains
*/
if (
maybeTest.instructions.length === 0 ||
maybeTest.instructions[0].value.kind !== 'LoadLocal'
) {
return null;
}
const path: Array<DependencyPathEntry> = [];
for (let i = 1; i < maybeTest.instructions.length; i++) {
const instrVal = maybeTest.instructions[i].value;
const prevInstr = maybeTest.instructions[i - 1];
if (
instrVal.kind === 'PropertyLoad' &&
instrVal.object.identifier.id === prevInstr.lvalue.identifier.id
) {
path.push({property: instrVal.property, optional: false});
} else {
return null;
}
}
CompilerError.invariant(
maybeTest.terminal.test.identifier.id ===
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected test expression',
loc: maybeTest.terminal.loc,
},
);
baseObject = {
identifier: maybeTest.instructions[0].value.place.identifier,
path,
};
test = maybeTest.terminal;
} else if (maybeTest.terminal.kind === 'optional') {
/**
* This is either
* - <inner_optional>?.property (optional=true)
* - <inner_optional>.property (optional=false)
* - <inner_optional> <other operation>
* - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
*/
const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
if (testBlock!.terminal.kind !== 'branch') {
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
loc: maybeTest.terminal.loc,
});
}
/**
* Recurse into inner optional blocks to collect inner optional-chain
* expressions, regardless of whether we can match the outer one to a
* PropertyLoad.
*/
const innerOptional = traverseOptionalBlock(
maybeTest as TBasicBlock<OptionalTerminal>,
context,
testBlock.terminal.alternate,
);
if (innerOptional == null) {
return null;
}
/**
* Check that the inner optional is part of the same optional-chain as the
* outer one. This is not guaranteed, e.g. given a(c?.d)?.d
* ```
* bb0:
* Optional test=bb1
* bb1:
* $0 = LoadLocal a <-- part 1 of the outer optional-chaining base
* Optional test=bb2 fallth=bb5 <-- start of optional chain for c?.d
* bb2:
* ... (optional chain for c?.d)
* ...
* bb5:
* $1 = phi(c.d, undefined) <-- part 2 (continuation) of the outer optional-base
* $2 = Call $0($1)
* Branch $2 ...
* ```
*/
if (testBlock.terminal.test.identifier.id !== innerOptional) {
return null;
}
if (!optional.terminal.optional) {
/**
* If this is an non-optional load participating in an optional chain
* (e.g. loading the `c` property in `a?.b.c`), record that PropertyLoads
* from the inner optional value are hoistable.
*/
context.hoistableObjects.set(
optional.id,
assertNonNull(context.temporariesReadInOptional.get(innerOptional)),
);
}
baseObject = assertNonNull(
context.temporariesReadInOptional.get(innerOptional),
);
test = testBlock.terminal;
} else {
return null;
}
if (test.alternate === outerAlternate) {
CompilerError.invariant(optional.instructions.length === 0, {
reason:
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
loc: optional.terminal.loc,
});
}
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
if (!matchConsequentResult) {
// Optional chain consequent is not hoistable e.g. a?.[computed()]
return null;
}
CompilerError.invariant(
matchConsequentResult.consequentGoto === optional.terminal.fallthrough,
{
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
loc: optional.terminal.loc,
},
);
const load = {
identifier: baseObject.identifier,
path: [
...baseObject.path,
{
property: matchConsequentResult.property,
optional: optional.terminal.optional,
},
],
};
context.processedInstrsInOptional.add(matchConsequentResult.storeLocalInstr);
context.processedInstrsInOptional.add(test);
context.temporariesReadInOptional.set(
matchConsequentResult.consequentId,
load,
);
context.temporariesReadInOptional.set(matchConsequentResult.propertyId, load);
return matchConsequentResult.consequentId;
}

View File

@@ -0,0 +1,361 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {
DependencyPathEntry,
GeneratedSource,
Identifier,
ReactiveScopeDependency,
} from '../HIR';
import {printIdentifier} from '../HIR/PrintHIR';
import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies';
/**
* Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR
* for detailed explanation.
*/
export class ReactiveScopeDependencyTreeHIR {
/**
* Paths from which we can hoist PropertyLoads. If an `identifier`,
* `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();
/**
* @param hoistableObjects a set of paths from which we can safely evaluate
* PropertyLoads. Note that we expect these to not contain duplicates (e.g.
* both `a?.b` and `a.b`) only because CollectHoistablePropertyLoads merges
* duplicates when traversing the CFG.
*/
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
for (const {path, identifier} of hoistableObjects) {
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
this.#hoistableObjects,
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
);
for (let i = 0; i < path.length; i++) {
const prevAccessType = currNode.properties.get(
path[i].property,
)?.accessType;
const accessType =
i + 1 < path.length && path[i + 1].optional ? 'Optional' : 'NonNull';
CompilerError.invariant(
prevAccessType == null || prevAccessType === accessType,
{
reason: 'Conflicting access types',
loc: GeneratedSource,
},
);
let nextNode = currNode.properties.get(path[i].property);
if (nextNode == null) {
nextNode = {
properties: new Map(),
accessType,
};
currNode.properties.set(path[i].property, nextNode);
}
currNode = nextNode;
}
}
}
static #getOrCreateRoot<T extends string>(
identifier: Identifier,
roots: Map<Identifier, TreeNode<T>>,
defaultAccessType: T,
): TreeNode<T> {
// roots can always be accessed unconditionally in JS
let rootNode = roots.get(identifier);
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
accessType: defaultAccessType,
};
roots.set(identifier, rootNode);
}
return rootNode;
}
/**
* Join a dependency with `#hoistableObjects` to record the hoistable
* dependency. This effectively truncates @param dep to its maximal
* safe-to-evaluate subpath
*/
addDependency(dep: ReactiveScopePropertyDependency): void {
const {identifier, path} = dep;
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
this.#deps,
PropertyAccessType.UnconditionalAccess,
);
/**
* hoistableCursor is null if depCursor is not an object we can hoist
* property reads from otherwise, it represents the same node in the
* hoistable / cfg-informed tree
*/
let hoistableCursor: HoistableNode | undefined =
this.#hoistableObjects.get(identifier);
// All properties read 'on the way' to a dependency are marked as 'access'
for (const entry of path) {
let nextHoistableCursor: HoistableNode | undefined;
let nextDepCursor: DependencyNode;
if (entry.optional) {
/**
* No need to check the access type since we can match both optional or non-optionals
* in the hoistable
* e.g. a?.b<rest> is hoistable if a.b<rest> is hoistable
*/
if (hoistableCursor != null) {
nextHoistableCursor = hoistableCursor?.properties.get(entry.property);
}
let accessType;
if (
hoistableCursor != null &&
hoistableCursor.accessType === 'NonNull'
) {
/**
* For an optional chain dep `a?.b`: if the hoistable tree only
* contains `a`, we can keep either `a?.b` or 'a.b' as a dependency.
* (note that we currently do the latter for perf)
*/
accessType = PropertyAccessType.UnconditionalAccess;
} else {
/**
* Given that it's safe to evaluate `depCursor` and optional load
* never throws, it's also safe to evaluate `depCursor?.entry`
*/
accessType = PropertyAccessType.OptionalAccess;
}
nextDepCursor = makeOrMergeProperty(
depCursor,
entry.property,
accessType,
);
} else if (
hoistableCursor != null &&
hoistableCursor.accessType === 'NonNull'
) {
nextHoistableCursor = hoistableCursor.properties.get(entry.property);
nextDepCursor = makeOrMergeProperty(
depCursor,
entry.property,
PropertyAccessType.UnconditionalAccess,
);
} else {
/**
* Break to truncate the dependency on its first non-optional entry that PropertyLoads are not hoistable from
*/
break;
}
depCursor = nextDepCursor;
hoistableCursor = nextHoistableCursor;
}
// mark the final node as a dependency
depCursor.accessType = merge(
depCursor.accessType,
PropertyAccessType.OptionalDependency,
);
}
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#deps.entries()) {
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
}
return results;
}
/*
* Prints dependency tree to string for debugging.
* @param includeAccesses
* @returns string representation of DependencyTree
*/
printDeps(includeAccesses: boolean): string {
let res: Array<Array<string>> = [];
for (const [rootId, rootNode] of this.#deps.entries()) {
const rootResults = printSubtree(rootNode, includeAccesses).map(
result => `${printIdentifier(rootId)}.${result}`,
);
res.push(rootResults);
}
return res.flat().join('\n');
}
static debug<T extends string>(roots: Map<Identifier, TreeNode<T>>): string {
const buf: Array<string> = [`tree() [`];
for (const [rootId, rootNode] of roots) {
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
this.#debugImpl(buf, rootNode, 1);
}
buf.push(']');
return buf.length > 2 ? buf.join('\n') : buf.join('');
}
static #debugImpl<T extends string>(
buf: Array<string>,
node: TreeNode<T>,
depth: number = 0,
): void {
for (const [property, childNode] of node.properties) {
buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
this.#debugImpl(buf, childNode, depth + 1);
}
}
}
/*
* Enum representing the access type of single property on a parent object.
* We distinguish on two independent axes:
* Optional / Unconditional:
* - whether this property is an optional load (within an optional chain)
* Access / Dependency:
* - Access: this property is read on the path of a dependency. We do not
* need to track change variables for accessed properties. Tracking accesses
* helps Forget do more granular dependency tracking.
* - Dependency: this property is read as a dependency and we must track changes
* to it for correctness.
* ```javascript
* // props.a is a dependency here and must be tracked
* deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
* // props.a is just an access here and does not need to be tracked
* deps: {props.a.b} ---> minimalDeps: {props.a.b}
* ```
*/
enum PropertyAccessType {
OptionalAccess = 'OptionalAccess',
UnconditionalAccess = 'UnconditionalAccess',
OptionalDependency = 'OptionalDependency',
UnconditionalDependency = 'UnconditionalDependency',
}
function isOptional(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalAccess ||
access === PropertyAccessType.OptionalDependency
);
}
function isDependency(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalDependency ||
access === PropertyAccessType.UnconditionalDependency
);
}
function merge(
access1: PropertyAccessType,
access2: PropertyAccessType,
): PropertyAccessType {
const resultIsUnconditional = !(isOptional(access1) && isOptional(access2));
const resultIsDependency = isDependency(access1) || isDependency(access2);
/*
* Straightforward merge.
* This can be represented as bitwise OR, but is written out for readability
*
* Observe that `UnconditionalAccess | ConditionalDependency` produces an
* unconditionally accessed conditional dependency. We currently use these
* as we use unconditional dependencies. (i.e. to codegen change variables)
*/
if (resultIsUnconditional) {
if (resultIsDependency) {
return PropertyAccessType.UnconditionalDependency;
} else {
return PropertyAccessType.UnconditionalAccess;
}
} else {
// result is optional
if (resultIsDependency) {
return PropertyAccessType.OptionalDependency;
} else {
return PropertyAccessType.OptionalAccess;
}
}
}
type TreeNode<T extends string> = {
properties: Map<string, TreeNode<T>>;
accessType: T;
};
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,
rootIdentifier: Identifier,
path: Array<DependencyPathEntry>,
results: Set<ReactiveScopeDependency>,
): void {
if (isDependency(node.accessType)) {
results.add({identifier: rootIdentifier, path});
} else {
for (const [childName, childNode] of node.properties) {
collectMinimalDependenciesInSubtree(
childNode,
rootIdentifier,
[
...path,
{
property: childName,
optional: isOptional(childNode.accessType),
},
],
results,
);
}
}
}
function printSubtree(
node: DependencyNode,
includeAccesses: boolean,
): Array<string> {
const results: Array<string> = [];
for (const [propertyName, propertyNode] of node.properties) {
if (includeAccesses || isDependency(propertyNode.accessType)) {
results.push(`${propertyName} (${propertyNode.accessType})`);
}
const propertyResults = printSubtree(propertyNode, includeAccesses);
results.push(...propertyResults.map(result => `${propertyName}.${result}`));
}
return results;
}
function makeOrMergeProperty(
node: DependencyNode,
property: string,
accessType: PropertyAccessType,
): DependencyNode {
let child = node.properties.get(property);
if (child == null) {
child = {
properties: new Map(),
accessType,
};
node.properties.set(property, child);
} else {
child.accessType = merge(child.accessType, accessType);
}
return child;
}

View File

@@ -16,7 +16,7 @@ import {
DEFAULT_SHAPES,
Global,
GlobalRegistry,
installReAnimatedTypes,
getReanimatedModuleType,
installTypeConfig,
} from './Globals';
import {
@@ -33,6 +33,7 @@ import {
Type,
ValidatedIdentifier,
ValueKind,
getHookKindForType,
makeBlockId,
makeIdentifierId,
makeIdentifierName,
@@ -49,6 +50,14 @@ import {
import {Scope as BabelScope} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
z.literal('react.element'),
z.literal('react.transitional.element'),
]),
globalDevVar: z.string(),
});
export const ExternalFunctionSchema = z.object({
// Source for the imported module that exports the `importSpecifierName` functions
source: z.string(),
@@ -60,8 +69,8 @@ export const ExternalFunctionSchema = z.object({
export const InstrumentationSchema = z
.object({
fn: ExternalFunctionSchema,
gating: ExternalFunctionSchema.nullish(),
globalGating: z.string().nullish(),
gating: ExternalFunctionSchema.nullable(),
globalGating: z.string().nullable(),
})
.refine(
opts => opts.gating != null || opts.globalGating != null,
@@ -138,7 +147,7 @@ export type Hook = z.infer<typeof HookSchema>;
*/
const EnvironmentConfigSchema = z.object({
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
customHooks: z.map(z.string(), HookSchema).default(new Map()),
/**
* A function that, given the name of a module, can optionally return a description
@@ -222,15 +231,14 @@ const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
enableReactiveScopesInHIR: z.boolean().default(true),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on
* just `props`. With this flag enabled, we'll infer that full path as
* the dependency.
* Enables inlining ReactElement object literals in place of JSX
* An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime
* Currently a prod-only optimization, requiring Fast JSX dependencies
*
* The symbol configuration is set for backwards compatability with pre-React 19 transforms
*/
enableOptionalDependencies: z.boolean().default(true),
inlineJsxTransform: ReactElementSymbolSchema.nullable().default(null),
/*
* Enable validation of hooks to partially check that the component honors the rules of hooks.
@@ -321,9 +329,9 @@ const EnvironmentConfigSchema = z.object({
* }
* }
*/
enableEmitFreeze: ExternalFunctionSchema.nullish(),
enableEmitFreeze: ExternalFunctionSchema.nullable().default(null),
enableEmitHookGuards: ExternalFunctionSchema.nullish(),
enableEmitHookGuards: ExternalFunctionSchema.nullable().default(null),
/**
* Enable instruction reordering. See InstructionReordering.ts for the details
@@ -337,6 +345,54 @@ const EnvironmentConfigSchema = z.object({
*/
enableFunctionOutlining: z.boolean().default(true),
/**
* If enabled, this will outline nested JSX into a separate component.
*
* This will enable the compiler to memoize the separate component, giving us
* the same behavior as compiling _within_ the callback.
*
* ```
* function Component(countries, onDelete) {
* const name = useFoo();
* return countries.map(() => {
* return (
* <Foo>
* <Bar>{name}</Bar>
* <Button onclick={onDelete}>delete</Button>
* </Foo>
* );
* });
* }
* ```
*
* will be transpiled to:
*
* ```
* function Component(countries, onDelete) {
* const name = useFoo();
* return countries.map(() => {
* return (
* <Temp name={name} onDelete={onDelete} />
* );
* });
* }
*
* function Temp({name, onDelete}) {
* return (
* <Foo>
* <Bar>{name}</Bar>
* <Button onclick={onDelete}>delete</Button>
* </Foo>
* );
* }
*
* Both, `Component` and `Temp` will then be memoized by the compiler.
*
* With this change, when `countries` is updated by adding one single value,
* only the newly added value is re-rendered and not the entire list.
*/
enableJsxOutlining: z.boolean().default(false),
/*
* Enables instrumentation codegen. This emits a dev-mode only call to an
* instrumentation function, for components and hooks that Forget compiles.
@@ -359,7 +415,7 @@ const EnvironmentConfigSchema = z.object({
* }
*
*/
enableEmitInstrumentForget: InstrumentationSchema.nullish(),
enableEmitInstrumentForget: InstrumentationSchema.nullable().default(null),
// Enable validation of mutable ranges
assertValidMutableRanges: z.boolean().default(false),
@@ -398,8 +454,6 @@ const EnvironmentConfigSchema = z.object({
*/
throwUnknownException__testonly: z.boolean().default(false),
enableSharedRuntime__testonly: z.boolean().default(false),
/**
* Enables deps of a function epxression to be treated as conditional. This
* makes sure we don't load a dep when it's a property (to check if it has
@@ -437,7 +491,8 @@ const EnvironmentConfigSchema = z.object({
* computed one. This detects cases where rules of react violations may cause the
* compiled code to behave differently than the original.
*/
enableChangeDetectionForDebugging: ExternalFunctionSchema.nullish(),
enableChangeDetectionForDebugging:
ExternalFunctionSchema.nullable().default(null),
/**
* The react native re-animated library uses custom Babel transforms that
@@ -477,7 +532,7 @@ const EnvironmentConfigSchema = z.object({
*
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false),
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
@@ -499,12 +554,57 @@ const EnvironmentConfigSchema = z.object({
* const {foo, bar} = useCompiledContext(MyContext, (c) => [c.foo, c.bar]);
* ```
*/
lowerContextAccess: ExternalFunctionSchema.nullish(),
lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
export function parseConfigPragma(pragma: string): EnvironmentConfig {
/**
* For test fixtures and playground only.
*
* Pragmas are straightforward to parse for boolean options (`:true` and
* `:false`). These are 'enabled' config values for non-boolean configs (i.e.
* what is used when parsing `:true`).
*/
const testComplexConfigDefaults: PartialEnvironmentConfig = {
validateNoCapitalizedCalls: [],
enableChangeDetectionForDebugging: {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
},
enableEmitFreeze: {
source: 'react-compiler-runtime',
importSpecifierName: 'makeReadOnly',
},
enableEmitInstrumentForget: {
fn: {
source: 'react-compiler-runtime',
importSpecifierName: 'useRenderCounter',
},
gating: {
source: 'react-compiler-runtime',
importSpecifierName: 'shouldInstrument',
},
globalGating: '__DEV__',
},
enableEmitHookGuards: {
source: 'react-compiler-runtime',
importSpecifierName: '$dispatcherGuard',
},
inlineJsxTransform: {
elementSymbol: 'react.transitional.element',
globalDevVar: 'DEV',
},
lowerContextAccess: {
source: 'react-compiler-runtime',
importSpecifierName: 'useContext_withSelector',
},
};
/**
* For snap test fixtures and playground only.
*/
export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
const maybeConfig: any = {};
// Get the defaults to programmatically check for boolean properties
const defaultConfig = EnvironmentConfigSchema.parse({});
@@ -514,21 +614,12 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
continue;
}
const keyVal = token.slice(1);
let [key, val]: any = keyVal.split(':');
let [key, val = undefined] = keyVal.split(':');
const isSet = val === undefined || val === 'true';
if (key === 'validateNoCapitalizedCalls') {
maybeConfig[key] = [];
continue;
}
if (
key === 'enableChangeDetectionForDebugging' &&
(val === undefined || val === 'true')
) {
maybeConfig[key] = {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
};
if (isSet && key in testComplexConfigDefaults) {
maybeConfig[key] =
testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
continue;
}
@@ -543,7 +634,6 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
props.push({type: 'name', name: elt});
}
}
console.log([valSplit[0], props.map(x => x.name ?? '*').join('.')]);
maybeConfig[key] = [[valSplit[0], props]];
}
continue;
@@ -554,11 +644,10 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
continue;
}
if (val === undefined || val === 'true') {
val = true;
maybeConfig[key] = true;
} else {
val = false;
maybeConfig[key] = false;
}
maybeConfig[key] = val;
}
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
@@ -671,7 +760,8 @@ export class Environment {
}
if (config.enableCustomTypeDefinitionForReanimated) {
installReAnimatedTypes(this.#globals, this.#shapes);
const reanimatedModuleType = getReanimatedModuleType(this.#shapes);
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
}
this.#contextIdentifiers = contextIdentifiers;
@@ -717,11 +807,11 @@ export class Environment {
}
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
if (this.config.moduleTypeProvider == null) {
return null;
}
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
if (this.config.moduleTypeProvider == null) {
return null;
}
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
@@ -737,6 +827,8 @@ export class Environment {
this.#globals,
this.#shapes,
moduleConfig,
moduleName,
loc,
);
} else {
moduleType = null;
@@ -784,7 +876,9 @@ export class Environment {
*/
return (
this.#globals.get(binding.imported) ??
(isHookName(binding.imported) ? this.#getCustomHookType() : null)
(isHookName(binding.imported) || isHookName(binding.name)
? this.#getCustomHookType()
: null)
);
} else {
const moduleType = this.#resolveModuleType(binding.module, loc);
@@ -794,6 +888,21 @@ export class Environment {
binding.imported,
);
if (importedType != null) {
/*
* Check that hook-like export names are hook types, and non-hook names are non-hook types.
* The user-assigned alias isn't decidable by the type provider, so we ignore that for the check.
* Thus we allow `import {fooNonHook as useFoo} from ...` because the name and type both say
* that it's not a hook.
*/
const expectHook = isHookName(binding.imported);
const isHook = getHookKindForType(this, importedType) != null;
if (expectHook !== isHook) {
CompilerError.throwInvalidConfig({
reason: `Invalid type configuration for module`,
description: `Expected type for \`import {${binding.imported}} from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the exported name`,
loc,
});
}
return importedType;
}
}
@@ -822,13 +931,30 @@ export class Environment {
} else {
const moduleType = this.#resolveModuleType(binding.module, loc);
if (moduleType !== null) {
let importedType: Type | null = null;
if (binding.kind === 'ImportDefault') {
const defaultType = this.getPropertyType(moduleType, 'default');
if (defaultType !== null) {
return defaultType;
importedType = defaultType;
}
} else {
return moduleType;
importedType = moduleType;
}
if (importedType !== null) {
/*
* Check that the hook-like modules are defined as types, and non hook-like modules are not typed as hooks.
* So `import Foo from 'useFoo'` is expected to be a hook based on the module name
*/
const expectHook = isHookName(binding.module);
const isHook = getHookKindForType(this, importedType) != null;
if (expectHook !== isHook) {
CompilerError.throwInvalidConfig({
reason: `Invalid type configuration for module`,
description: `Expected type for \`import ... from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the module name`,
loc,
});
}
return importedType;
}
}
return isHookName(binding.name) ? this.#getCustomHookType() : null;
@@ -906,6 +1032,8 @@ export class Environment {
}
}
const REANIMATED_MODULE_NAME = 'react-native-reanimated';
// From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2
export function isHookName(name: string): boolean {
return /^use[A-Z0-9]/.test(name);

View File

@@ -25,9 +25,11 @@ import {
addHook,
addObject,
} from './ObjectShape';
import {BuiltInType, PolyType} from './Types';
import {BuiltInType, ObjectType, PolyType} from './Types';
import {TypeConfig} from './TypeSchema';
import {assertExhaustive} from '../Utils/utils';
import {isHookName} from './Environment';
import {CompilerError, SourceLocation} from '..';
/*
* This file exports types and defaults for JavaScript global objects.
@@ -362,6 +364,17 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Mutable,
}),
],
[
'useImperativeHandle',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
hookKind: 'useImperativeHandle',
returnValueKind: ValueKind.Frozen,
}),
],
[
'useMemo',
addHook(DEFAULT_SHAPES, {
@@ -535,6 +548,8 @@ export function installTypeConfig(
globals: GlobalRegistry,
shapes: ShapeRegistry,
typeConfig: TypeConfig,
moduleName: string,
loc: SourceLocation,
): Global {
switch (typeConfig.kind) {
case 'type': {
@@ -567,7 +582,13 @@ export function installTypeConfig(
positionalParams: typeConfig.positionalParams,
restParam: typeConfig.restParam,
calleeEffect: typeConfig.calleeEffect,
returnType: installTypeConfig(globals, shapes, typeConfig.returnType),
returnType: installTypeConfig(
globals,
shapes,
typeConfig.returnType,
moduleName,
loc,
),
returnValueKind: typeConfig.returnValueKind,
noAlias: typeConfig.noAlias === true,
mutableOnlyIfOperandsAreMutable:
@@ -580,7 +601,13 @@ export function installTypeConfig(
positionalParams: typeConfig.positionalParams ?? [],
restParam: typeConfig.restParam ?? Effect.Freeze,
calleeEffect: Effect.Read,
returnType: installTypeConfig(globals, shapes, typeConfig.returnType),
returnType: installTypeConfig(
globals,
shapes,
typeConfig.returnType,
moduleName,
loc,
),
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
});
@@ -589,10 +616,31 @@ export function installTypeConfig(
return addObject(
shapes,
null,
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => [
key,
installTypeConfig(globals, shapes, value),
]),
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => {
const type = installTypeConfig(
globals,
shapes,
value,
moduleName,
loc,
);
const expectHook = isHookName(key);
let isHook = false;
if (type.kind === 'Function' && type.shapeId !== null) {
const functionType = shapes.get(type.shapeId);
if (functionType?.functionType?.hookKind !== null) {
isHook = true;
}
}
if (expectHook !== isHook) {
CompilerError.throwInvalidConfig({
reason: `Invalid type configuration for module`,
description: `Expected type for object property '${key}' from module '${moduleName}' ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the property name`,
loc,
});
}
return [key, type];
}),
);
}
default: {
@@ -604,10 +652,7 @@ export function installTypeConfig(
}
}
export function installReAnimatedTypes(
globals: GlobalRegistry,
registry: ShapeRegistry,
): void {
export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
// hooks that freeze args and return frozen value
const frozenHooks = [
'useFrameCallback',
@@ -617,8 +662,9 @@ export function installReAnimatedTypes(
'useAnimatedReaction',
'useWorkletCallback',
];
const reanimatedType: Array<[string, BuiltInType]> = [];
for (const hook of frozenHooks) {
globals.set(
reanimatedType.push([
hook,
addHook(registry, {
positionalParams: [],
@@ -629,7 +675,7 @@ export function installReAnimatedTypes(
calleeEffect: Effect.Read,
hookKind: 'Custom',
}),
);
]);
}
/**
@@ -638,7 +684,7 @@ export function installReAnimatedTypes(
*/
const mutableHooks = ['useSharedValue', 'useDerivedValue'];
for (const hook of mutableHooks) {
globals.set(
reanimatedType.push([
hook,
addHook(registry, {
positionalParams: [],
@@ -649,7 +695,7 @@ export function installReAnimatedTypes(
calleeEffect: Effect.Read,
hookKind: 'Custom',
}),
);
]);
}
// functions that return mutable value
@@ -663,7 +709,7 @@ export function installReAnimatedTypes(
'executeOnUIRuntimeSync',
];
for (const fn of funcs) {
globals.set(
reanimatedType.push([
fn,
addFunction(registry, [], {
positionalParams: [],
@@ -673,6 +719,8 @@ export function installReAnimatedTypes(
returnValueKind: ValueKind.Mutable,
noAlias: true,
}),
);
]);
}
return addObject(registry, null, reanimatedType);
}

View File

@@ -367,6 +367,7 @@ export type BasicBlock = {
preds: Set<BlockId>;
phis: Set<Phi>;
};
export type TBasicBlock<T extends Terminal> = BasicBlock & {terminal: T};
/*
* Terminal nodes generally represent statements that affect control flow, such as
@@ -746,6 +747,9 @@ export enum InstructionKind {
// hoisted const declarations
HoistedLet = 'HoistedLet',
HoistedFunction = 'HoistedFunction',
Function = 'Function',
}
function _staticInvariantInstructionValueHasLocation(
@@ -757,8 +761,8 @@ function _staticInvariantInstructionValueHasLocation(
export type Phi = {
kind: 'Phi';
id: Identifier;
operands: Map<BlockId, Identifier>;
place: Place;
operands: Map<BlockId, Place>;
};
/**
@@ -865,18 +869,13 @@ export type InstructionValue =
kind:
| InstructionKind.Let
| InstructionKind.HoistedConst
| InstructionKind.HoistedLet;
| InstructionKind.HoistedLet
| InstructionKind.HoistedFunction;
place: Place;
};
loc: SourceLocation;
}
| {
kind: 'StoreLocal';
lvalue: LValue;
value: Place;
type: t.FlowType | t.TSType | null;
loc: SourceLocation;
}
| StoreLocal
| {
kind: 'StoreContext';
lvalue: {
@@ -921,15 +920,7 @@ export type InstructionValue =
type: Type;
loc: SourceLocation;
}
| {
kind: 'JsxExpression';
tag: Place | BuiltinTag;
props: Array<JsxAttribute>;
children: Array<Place> | null; // null === no children
loc: SourceLocation;
openingLoc: SourceLocation;
closingLoc: SourceLocation;
}
| JsxExpression
| {
kind: 'ObjectExpression';
properties: Array<ObjectProperty | SpreadPattern>;
@@ -1075,6 +1066,16 @@ export type InstructionValue =
loc: SourceLocation;
};
export type JsxExpression = {
kind: 'JsxExpression';
tag: Place | BuiltinTag;
props: Array<JsxAttribute>;
children: Array<Place> | null; // null === no children
loc: SourceLocation;
openingLoc: SourceLocation;
closingLoc: SourceLocation;
};
export type JsxAttribute =
| {kind: 'JsxSpreadAttribute'; argument: Place}
| {kind: 'JsxAttribute'; name: string; place: Place};
@@ -1119,6 +1120,13 @@ export type Primitive = {
export type JSXText = {kind: 'JSXText'; value: string; loc: SourceLocation};
export type StoreLocal = {
kind: 'StoreLocal';
lvalue: LValue;
value: Place;
type: t.FlowType | t.TSType | null;
loc: SourceLocation;
};
export type PropertyLoad = {
kind: 'PropertyLoad';
object: Place;
@@ -1235,6 +1243,17 @@ export function makeTemporaryIdentifier(
};
}
export function forkTemporaryIdentifier(
id: IdentifierId,
source: Identifier,
): Identifier {
return {
...source,
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
id,
};
}
/**
* Creates a valid identifier name. This should *not* be used for synthesizing
* identifier names: only call this method for identifier names that appear in the
@@ -1492,6 +1511,8 @@ export type ReactiveScopeDeclaration = {
scope: ReactiveScope; // the scope in which the variable was originally declared
};
export type DependencyPathEntry = {property: string; optional: boolean};
export type DependencyPath = Array<DependencyPathEntry>;
export type ReactiveScopeDependency = {
identifier: Identifier;
path: DependencyPath;
@@ -1506,7 +1527,21 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
)
);
}
export type DependencyPath = Array<{property: string; optional: boolean}>;
export function getPlaceScope(
id: InstructionId,
place: Place,
): ReactiveScope | null {
const scope = place.identifier.scope;
if (scope !== null && isScopeActive(scope, id)) {
return scope;
}
return null;
}
function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
return id >= scope.range.start && id < scope.range.end;
}
/*
* Simulated opaque type for BlockIds to prevent using normal numbers as block ids

View File

@@ -912,3 +912,23 @@ export function clonePlaceToTemporary(env: Environment, place: Place): Place {
temp.reactive = place.reactive;
return temp;
}
/**
* Fix scope and identifier ranges to account for renumbered instructions
*/
export function fixScopeAndIdentifierRanges(func: HIR): void {
for (const [, block] of func.blocks) {
const terminal = block.terminal;
if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
/*
* Scope ranges should always align to start at the 'scope' terminal
* and end at the first instruction of the fallthrough block
*/
const fallthroughBlock = func.blocks.get(terminal.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
terminal.scope.range.start = terminal.id;
terminal.scope.range.end = firstId;
}
}
}

View File

@@ -84,20 +84,14 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
id: predecessor.terminal.id,
lvalue: {
kind: 'Identifier',
identifier: phi.id,
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
},
value: {
kind: 'LoadLocal',
place: {
kind: 'Identifier',
identifier: operand,
effect: Effect.Read,
reactive: false,
loc: GeneratedSource,
},
place: {...operand},
loc: GeneratedSource,
},
loc: GeneratedSource,

View File

@@ -5,7 +5,7 @@ import {
ReactiveScope,
makeInstructionId,
} from '.';
import {getPlaceScope} from '../ReactiveScopes/BuildReactiveBlocks';
import {getPlaceScope} from '../HIR/HIR';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
import {getOrInsertDefault} from '../Utils/utils';

View File

@@ -127,6 +127,7 @@ export type HookKind =
| 'useMemo'
| 'useCallback'
| 'useTransition'
| 'useImperativeHandle'
| 'Custom';
/*

View File

@@ -163,13 +163,13 @@ export function printInstruction(instr: ReactiveInstruction): string {
export function printPhi(phi: Phi): string {
const items = [];
items.push(printIdentifier(phi.id));
items.push(printMutableRange(phi.id));
items.push(printType(phi.id.type));
items.push(printPlace(phi.place));
items.push(printMutableRange(phi.place.identifier));
items.push(printType(phi.place.identifier.type));
items.push(': phi(');
const phis = [];
for (const [blockId, id] of phi.operands) {
phis.push(`bb${blockId}: ${printIdentifier(id)}`);
for (const [blockId, place] of phi.operands) {
phis.push(`bb${blockId}: ${printPlace(place)}`);
}
items.push(phis.join(', '));
@@ -765,6 +765,12 @@ export function printLValue(lval: LValue): string {
case InstructionKind.HoistedLet: {
return `HoistedLet ${lvalue}$`;
}
case InstructionKind.Function: {
return `Function ${lvalue}$`;
}
case InstructionKind.HoistedFunction: {
return `HoistedFunction ${lvalue}$`;
}
default: {
assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``);
}

View File

@@ -0,0 +1,695 @@
import {
ScopeId,
HIRFunction,
Place,
Instruction,
ReactiveScopeDependency,
Identifier,
ReactiveScope,
isObjectMethodType,
isRefValueType,
isUseRefType,
makeInstructionId,
InstructionId,
InstructionKind,
GeneratedSource,
DeclarationId,
areEqualPaths,
IdentifierId,
Terminal,
} from './HIR';
import {
collectHoistablePropertyLoads,
keyByScopeId,
} from './CollectHoistablePropertyLoads';
import {
ScopeBlockTraversal,
eachInstructionOperand,
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
} from './visitors';
import {Stack, empty} from '../Utils/Stack';
import {CompilerError} from '../CompilerError';
import {Iterable_some} from '../Utils/utils';
import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
const usedOutsideDeclaringScope =
findTemporariesUsedOutsideDeclaringScope(fn);
const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope);
const {
temporariesReadInOptional,
processedInstrsInOptional,
hoistableObjects,
} = collectOptionalChainSidemap(fn);
const hoistablePropertyLoads = keyByScopeId(
fn,
collectHoistablePropertyLoads(fn, temporaries, hoistableObjects),
);
const scopeDeps = collectDependencies(
fn,
usedOutsideDeclaringScope,
new Map([...temporaries, ...temporariesReadInOptional]),
processedInstrsInOptional,
);
/**
* Derive the minimal set of hoistable dependencies for each scope.
*/
for (const [scope, deps] of scopeDeps) {
if (deps.length === 0) {
continue;
}
/**
* Step 1: Find hoistable accesses, given the basic block in which the scope
* begins.
*/
const hoistables = hoistablePropertyLoads.get(scope.id);
CompilerError.invariant(hoistables != null, {
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
loc: GeneratedSource,
});
/**
* Step 2: Calculate hoistable dependencies.
*/
const tree = new ReactiveScopeDependencyTreeHIR(
[...hoistables.assumedNonNullObjects].map(o => o.fullPath),
);
for (const dep of deps) {
tree.addDependency({...dep});
}
/**
* Step 3: Reduce dependencies to a minimal set.
*/
const candidates = tree.deriveMinimalDependencies();
for (const candidateDep of candidates) {
if (
!Iterable_some(
scope.dependencies,
existingDep =>
existingDep.identifier.declarationId ===
candidateDep.identifier.declarationId &&
areEqualPaths(existingDep.path, candidateDep.path),
)
)
scope.dependencies.add(candidateDep);
}
}
}
function findTemporariesUsedOutsideDeclaringScope(
fn: HIRFunction,
): ReadonlySet<DeclarationId> {
/*
* tracks all relevant LoadLocal and PropertyLoad lvalues
* and the scope where they are defined
*/
const declarations = new Map<DeclarationId, ScopeId>();
const prunedScopes = new Set<ScopeId>();
const scopeTraversal = new ScopeBlockTraversal();
const usedOutsideDeclaringScope = new Set<DeclarationId>();
function handlePlace(place: Place): void {
const declaringScope = declarations.get(place.identifier.declarationId);
if (
declaringScope != null &&
!scopeTraversal.isScopeActive(declaringScope) &&
!prunedScopes.has(declaringScope)
) {
// Declaring scope is not active === used outside declaring scope
usedOutsideDeclaringScope.add(place.identifier.declarationId);
}
}
function handleInstruction(instr: Instruction): void {
const scope = scopeTraversal.currentScope;
if (scope == null || prunedScopes.has(scope)) {
return;
}
switch (instr.value.kind) {
case 'LoadLocal':
case 'LoadContext':
case 'PropertyLoad': {
declarations.set(instr.lvalue.identifier.declarationId, scope);
break;
}
default: {
break;
}
}
}
for (const [blockId, block] of fn.body.blocks) {
scopeTraversal.recordScopes(block);
const scopeStartInfo = scopeTraversal.blockInfos.get(blockId);
if (scopeStartInfo?.kind === 'begin' && scopeStartInfo.pruned) {
prunedScopes.add(scopeStartInfo.scope.id);
}
for (const instr of block.instructions) {
for (const place of eachInstructionOperand(instr)) {
handlePlace(place);
}
handleInstruction(instr);
}
for (const place of eachTerminalOperand(block.terminal)) {
handlePlace(place);
}
}
return usedOutsideDeclaringScope;
}
/**
* @returns mapping of LoadLocal and PropertyLoad to the source of the load.
* ```js
* // source
* foo(a.b);
*
* // HIR: a potential sidemap is {0: a, 1: a.b, 2: foo}
* $0 = LoadLocal 'a'
* $1 = PropertyLoad $0, 'b'
* $2 = LoadLocal 'foo'
* $3 = CallExpression $2($1)
* ```
* @param usedOutsideDeclaringScope is used to check the correctness of
* reordering LoadLocal / PropertyLoad calls. We only track a LoadLocal /
* PropertyLoad in the returned temporaries map if reordering the read (from the
* time-of-load to time-of-use) is valid.
*
* If a LoadLocal or PropertyLoad instruction is within the reactive scope range
* (a proxy for mutable range) of the load source, later instructions may
* reassign / mutate the source value. Since it's incorrect to reorder these
* load instructions to after their scope ranges, we also do not store them in
* identifier sidemaps.
*
* Take this example (from fixture
* `evaluation-order-mutate-call-after-dependency-load`)
* ```js
* // source
* function useFoo(arg) {
* const arr = [1, 2, 3, ...arg];
* return [
* arr.length,
* arr.push(0)
* ];
* }
*
* // IR pseudocode
* scope @0 {
* $0 = arr = ArrayExpression [1, 2, 3, ...arg]
* $1 = arr.length
* $2 = arr.push(0)
* }
* scope @1 {
* $3 = ArrayExpression [$1, $2]
* }
* ```
* Here, it's invalid for scope@1 to take `arr.length` as a dependency instead
* of $1, as the evaluation of `arr.length` changes between instructions $1 and
* $3. We do not track $1 -> arr.length in this case.
*/
export function collectTemporariesSidemap(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
const temporaries = new Map();
collectTemporariesSidemapImpl(
fn,
usedOutsideDeclaringScope,
temporaries,
false,
);
return temporaries;
}
/**
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
* function and all nested functions.
*
* Note that IdentifierIds are currently unique, so we can use a single
* Map<IdentifierId, ...> across all nested functions.
*/
function collectTemporariesSidemapImpl(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
isInnerFn: boolean,
): void {
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
const usedOutside = usedOutsideDeclaringScope.has(
lvalue.identifier.declarationId,
);
if (value.kind === 'PropertyLoad' && !usedOutside) {
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
/**
* All dependencies of a inner / nested function must have a base
* identifier from the outermost component / hook. This is because the
* compiler cannot break an inner function into multiple granular
* scopes.
*/
const property = getProperty(
value.object,
value.property,
false,
temporaries,
);
temporaries.set(lvalue.identifier.id, property);
}
} else if (
value.kind === 'LoadLocal' &&
lvalue.identifier.name == null &&
value.place.identifier.name !== null &&
!usedOutside
) {
if (
!isInnerFn ||
fn.context.some(
context => context.identifier.id === value.place.identifier.id,
)
) {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
path: [],
});
}
} else if (
value.kind === 'FunctionExpression' ||
value.kind === 'ObjectMethod'
) {
collectTemporariesSidemapImpl(
value.loweredFunc.func,
usedOutsideDeclaringScope,
temporaries,
true,
);
}
}
}
}
function getProperty(
object: Place,
propertyName: string,
optional: boolean,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
): ReactiveScopeDependency {
/*
* (1) Get the base object either from the temporary sidemap (e.g. a LoadLocal)
* or a deep copy of an existing property dependency.
* Example 1:
* $0 = LoadLocal x
* $1 = PropertyLoad $0.y
* getProperty($0, ...) -> resolvedObject = x, resolvedDependency = null
*
* Example 2:
* $0 = LoadLocal x
* $1 = PropertyLoad $0.y
* $2 = PropertyLoad $1.z
* getProperty($1, ...) -> resolvedObject = null, resolvedDependency = x.y
*
* Example 3:
* $0 = Call(...)
* $1 = PropertyLoad $0.y
* getProperty($0, ...) -> resolvedObject = null, resolvedDependency = null
*/
const resolvedDependency = temporaries.get(object.identifier.id);
/**
* (2) Push the last PropertyLoad
* TODO(mofeiZ): understand optional chaining
*/
let property: ReactiveScopeDependency;
if (resolvedDependency == null) {
property = {
identifier: object.identifier,
path: [{property: propertyName, optional}],
};
} else {
property = {
identifier: resolvedDependency.identifier,
path: [...resolvedDependency.path, {property: propertyName, optional}],
};
}
return property;
}
type Decl = {
id: InstructionId;
scope: Stack<ReactiveScope>;
};
class Context {
#declarations: Map<DeclarationId, Decl> = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
#scopes: Stack<ReactiveScope> = empty();
// Reactive dependencies used in the current reactive scope.
#dependencies: Stack<Array<ReactiveScopeDependency>> = empty();
deps: Map<ReactiveScope, Array<ReactiveScopeDependency>> = new Map();
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
/**
* Tracks the traversal state. See Context.declare for explanation of why this
* is needed.
*/
inInnerFn: boolean = false;
constructor(
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
) {
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
this.#temporaries = temporaries;
}
enterScope(scope: ReactiveScope): void {
// Set context for new scope
this.#dependencies = this.#dependencies.push([]);
this.#scopes = this.#scopes.push(scope);
}
exitScope(scope: ReactiveScope, pruned: boolean): void {
// Save dependencies we collected from the exiting scope
const scopedDependencies = this.#dependencies.value;
CompilerError.invariant(scopedDependencies != null, {
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
loc: scope.loc,
});
// Restore context of previous scope
this.#scopes = this.#scopes.pop();
this.#dependencies = this.#dependencies.pop();
/*
* Collect dependencies we recorded for the exiting scope and propagate
* them upward using the same rules as normal dependency collection.
* Child scopes may have dependencies on values created within the outer
* scope, which necessarily cannot be dependencies of the outer scope.
*/
for (const dep of scopedDependencies) {
if (this.#checkValidDependency(dep)) {
this.#dependencies.value?.push(dep);
}
}
if (!pruned) {
this.deps.set(scope, scopedDependencies);
}
}
isUsedOutsideDeclaringScope(place: Place): boolean {
return this.#temporariesUsedOutsideScope.has(
place.identifier.declarationId,
);
}
/*
* Records where a value was declared, and optionally, the scope where the
* value originated from. This is later used to determine if a dependency
* should be added to a scope; if the current scope we are visiting is the
* same scope where the value originates, it can't be a dependency on itself.
*
* Note that we do not track declarations or reassignments within inner
* functions for the following reasons:
* - inner functions cannot be split by scope boundaries and are guaranteed
* to consume their own declarations
* - reassignments within inner functions are tracked as context variables,
* which already have extended mutable ranges to account for reassignments
* - *most importantly* it's currently simply incorrect to compare inner
* function instruction ids (tracked by `decl`) with outer ones (as stored
* by root identifier mutable ranges).
*/
declare(identifier: Identifier, decl: Decl): void {
if (this.inInnerFn) return;
if (!this.#declarations.has(identifier.declarationId)) {
this.#declarations.set(identifier.declarationId, decl);
}
this.#reassignments.set(identifier, decl);
}
// Checks if identifier is a valid dependency in the current scope
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
// ref.current access is not a valid dep
if (
isUseRefType(maybeDependency.identifier) &&
maybeDependency.path.at(0)?.property === 'current'
) {
return false;
}
// ref value is not a valid dep
if (isRefValueType(maybeDependency.identifier)) {
return false;
}
/*
* object methods are not deps because they will be codegen'ed back in to
* the object literal.
*/
if (isObjectMethodType(maybeDependency.identifier)) {
return false;
}
const identifier = maybeDependency.identifier;
/*
* If this operand is used in a scope, has a dynamic value, and was defined
* before this scope, then its a dependency of the scope.
*/
const currentDeclaration =
this.#reassignments.get(identifier) ??
this.#declarations.get(identifier.declarationId);
const currentScope = this.currentScope.value;
return (
currentScope != null &&
currentDeclaration !== undefined &&
currentDeclaration.id < currentScope.range.start
);
}
#isScopeActive(scope: ReactiveScope): boolean {
if (this.#scopes === null) {
return false;
}
return this.#scopes.find(state => state === scope);
}
get currentScope(): Stack<ReactiveScope> {
return this.#scopes;
}
visitOperand(place: Place): void {
/*
* if this operand is a temporary created for a property load, try to resolve it to
* the expanded Place. Fall back to using the operand as-is.
*/
this.visitDependency(
this.#temporaries.get(place.identifier.id) ?? {
identifier: place.identifier,
path: [],
},
);
}
visitProperty(object: Place, property: string, optional: boolean): void {
const nextDependency = getProperty(
object,
property,
optional,
this.#temporaries,
);
this.visitDependency(nextDependency);
}
visitDependency(maybeDependency: ReactiveScopeDependency): void {
/*
* Any value used after its originally defining scope has concluded must be added as an
* output of its defining scope. Regardless of whether its a const or not,
* some later code needs access to the value. If the current
* scope we are visiting is the same scope where the value originates, it can't be a dependency
* on itself.
*/
/*
* if originalDeclaration is undefined here, then this is not a local var
* (all decls e.g. `let x;` should be initialized in BuildHIR)
*/
const originalDeclaration = this.#declarations.get(
maybeDependency.identifier.declarationId,
);
if (
originalDeclaration !== undefined &&
originalDeclaration.scope.value !== null
) {
originalDeclaration.scope.each(scope => {
if (
!this.#isScopeActive(scope) &&
!Iterable_some(
scope.declarations.values(),
decl =>
decl.identifier.declarationId ===
maybeDependency.identifier.declarationId,
)
) {
scope.declarations.set(maybeDependency.identifier.id, {
identifier: maybeDependency.identifier,
scope: originalDeclaration.scope.value!,
});
}
});
}
if (this.#checkValidDependency(maybeDependency)) {
this.#dependencies.value!.push(maybeDependency);
}
}
/*
* Record a variable that is declared in some other scope and that is being reassigned in the
* current one as a {@link ReactiveScope.reassignments}
*/
visitReassignment(place: Place): void {
const currentScope = this.currentScope.value;
if (
currentScope != null &&
!Iterable_some(
currentScope.reassignments,
identifier =>
identifier.declarationId === place.identifier.declarationId,
) &&
this.#checkValidDependency({identifier: place.identifier, path: []})
) {
currentScope.reassignments.add(place.identifier);
}
}
}
function handleInstruction(instr: Instruction, context: Context): void {
const {id, value, lvalue} = instr;
if (value.kind === 'LoadLocal') {
if (
value.place.identifier.name === null ||
lvalue.identifier.name !== null ||
context.isUsedOutsideDeclaringScope(lvalue)
) {
context.visitOperand(value.place);
}
} else if (value.kind === 'PropertyLoad') {
if (context.isUsedOutsideDeclaringScope(lvalue)) {
context.visitProperty(value.object, value.property, false);
}
} else if (value.kind === 'StoreLocal') {
context.visitOperand(value.value);
if (value.lvalue.kind === InstructionKind.Reassign) {
context.visitReassignment(value.lvalue.place);
}
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
} else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
/*
* Some variables may be declared and never initialized. We need
* to retain (and hoist) these declarations if they are included
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
* as scope declarations.
*/
/*
* We add context variable declarations here, not at `StoreContext`, since
* context Store / Loads are modeled as reads and mutates to the underlying
* variable reference (instead of through intermediate / inlined temporaries)
*/
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
} else if (value.kind === 'Destructure') {
context.visitOperand(value.value);
for (const place of eachPatternOperand(value.lvalue.pattern)) {
if (value.lvalue.kind === InstructionKind.Reassign) {
context.visitReassignment(place);
}
context.declare(place.identifier, {
id,
scope: context.currentScope,
});
}
} else {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
}
context.declare(lvalue.identifier, {
id,
scope: context.currentScope,
});
}
function collectDependencies(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new Context(usedOutsideDeclaringScope, temporaries);
for (const param of fn.params) {
if (param.kind === 'Identifier') {
context.declare(param.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
} else {
context.declare(param.place.identifier, {
id: makeInstructionId(0),
scope: empty(),
});
}
}
const scopeTraversal = new ScopeBlockTraversal();
for (const [blockId, block] of fn.body.blocks) {
scopeTraversal.recordScopes(block);
const scopeBlockInfo = scopeTraversal.blockInfos.get(blockId);
if (scopeBlockInfo?.kind === 'begin') {
context.enterScope(scopeBlockInfo.scope);
} else if (scopeBlockInfo?.kind === 'end') {
context.exitScope(scopeBlockInfo.scope, scopeBlockInfo?.pruned);
}
// Record referenced optional chains in phis
for (const phi of block.phis) {
for (const operand of phi.operands) {
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
if (maybeOptionalChain) {
context.visitDependency(maybeOptionalChain);
}
}
}
for (const instr of block.instructions) {
if (!processedInstrsInOptional.has(instr)) {
handleInstruction(instr, context);
}
}
if (!processedInstrsInOptional.has(block.terminal)) {
for (const place of eachTerminalOperand(block.terminal)) {
context.visitOperand(place);
}
}
}
return context.deps;
}

View File

@@ -17,7 +17,7 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR';
export {computeDominatorTree, computePostDominatorTree} from './Dominator';
export {
Environment,
parseConfigPragma,
parseConfigPragmaForTests,
validateEnvironmentConfig,
type EnvironmentConfig,
type ExternalFunction,

View File

@@ -6,7 +6,9 @@
*/
import {assertExhaustive} from '../Utils/utils';
import {CompilerError} from '..';
import {
BasicBlock,
BlockId,
Instruction,
InstructionValue,
@@ -14,7 +16,9 @@ import {
Pattern,
Place,
ReactiveInstruction,
ReactiveScope,
ReactiveValue,
ScopeId,
SpreadPattern,
Terminal,
} from './HIR';
@@ -1149,3 +1153,80 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
}
}
}
/**
* Helper class for traversing scope blocks in HIR-form.
*/
export class ScopeBlockTraversal {
// Live stack of active scopes
#activeScopes: Array<ScopeId> = [];
blockInfos: Map<
BlockId,
| {
kind: 'end';
scope: ReactiveScope;
pruned: boolean;
}
| {
kind: 'begin';
scope: ReactiveScope;
pruned: boolean;
fallthrough: BlockId;
}
> = new Map();
recordScopes(block: BasicBlock): void {
const blockInfo = this.blockInfos.get(block.id);
if (blockInfo?.kind === 'begin') {
this.#activeScopes.push(blockInfo.scope.id);
} else if (blockInfo?.kind === 'end') {
const top = this.#activeScopes.at(-1);
CompilerError.invariant(blockInfo.scope.id === top, {
reason:
'Expected traversed block fallthrough to match top-most active scope',
loc: block.instructions[0]?.loc ?? block.terminal.id,
});
this.#activeScopes.pop();
}
if (
block.terminal.kind === 'scope' ||
block.terminal.kind === 'pruned-scope'
) {
CompilerError.invariant(
!this.blockInfos.has(block.terminal.block) &&
!this.blockInfos.has(block.terminal.fallthrough),
{
reason: 'Expected unique scope blocks and fallthroughs',
loc: block.terminal.loc,
},
);
this.blockInfos.set(block.terminal.block, {
kind: 'begin',
scope: block.terminal.scope,
pruned: block.terminal.kind === 'pruned-scope',
fallthrough: block.terminal.fallthrough,
});
this.blockInfos.set(block.terminal.fallthrough, {
kind: 'end',
scope: block.terminal.scope,
pruned: block.terminal.kind === 'pruned-scope',
});
}
}
/**
* @returns if the given scope is currently 'active', i.e. if the scope start
* block but not the scope fallthrough has been recorded.
*/
isScopeActive(scopeId: ScopeId): boolean {
return this.#activeScopes.indexOf(scopeId) !== -1;
}
/**
* The current, innermost active scope.
*/
get currentScope(): ScopeId | null {
return this.#activeScopes.at(-1) ?? null;
}
}

View File

@@ -15,11 +15,11 @@ export function inferAliasForPhis(
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.id.mutableRange.end >
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (isPhiMutatedAfterCreation) {
for (const [, operand] of phi.operands) {
aliases.union([phi.id, operand]);
aliases.union([phi.place.identifier, operand.identifier]);
}
}
}

View File

@@ -0,0 +1,335 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity, ValueKind} from '..';
import {
AbstractValue,
BasicBlock,
Effect,
Environment,
FunctionEffect,
Instruction,
InstructionValue,
Place,
ValueReason,
getHookKind,
isRefOrRefValue,
} from '../HIR';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
interface State {
kind(place: Place): AbstractValue;
values(place: Place): Array<InstructionValue>;
isDefined(place: Place): boolean;
}
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
const value = state.kind(place);
CompilerError.invariant(value != null, {
reason: 'Expected operand to have a kind',
loc: null,
});
switch (place.effect) {
case Effect.Store:
case Effect.Mutate: {
if (isRefOrRefValue(place.identifier)) {
break;
} else if (value.kind === ValueKind.Context) {
return {
kind: 'ContextMutation',
loc: place.loc,
effect: place.effect,
places: value.context.size === 0 ? new Set([place]) : value.context,
};
} else if (
value.kind !== ValueKind.Mutable &&
// We ignore mutations of primitives since this is not a React-specific problem
value.kind !== ValueKind.Primitive
) {
let reason = getWriteErrorReason(value);
return {
kind:
value.reason.size === 1 && value.reason.has(ValueReason.Global)
? 'GlobalMutation'
: 'ReactMutation',
error: {
reason,
description:
place.identifier.name !== null &&
place.identifier.name.kind === 'named'
? `Found mutation of \`${place.identifier.name.value}\``
: null,
loc: place.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
};
}
break;
}
}
return null;
}
function inheritFunctionEffects(
state: State,
place: Place,
): Array<FunctionEffect> {
const effects = inferFunctionInstrEffects(state, place);
return effects
.flatMap(effect => {
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
return [effect];
} else {
const effects: Array<FunctionEffect | null> = [];
CompilerError.invariant(effect.kind === 'ContextMutation', {
reason: 'Expected ContextMutation',
loc: null,
});
/**
* Contextual effects need to be replayed against the current inference
* state, which may know more about the value to which the effect applied.
* The main cases are:
* 1. The mutated context value is _still_ a context value in the current scope,
* so we have to continue propagating the original context mutation.
* 2. The mutated context value is a mutable value in the current scope,
* so the context mutation was fine and we can skip propagating the effect.
* 3. The mutated context value is an immutable value in the current scope,
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
// Case 1, still a context value so propagate the original effect
effects.push(effect);
} else {
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
}
}
return effects;
}
})
.filter((effect): effect is FunctionEffect => effect != null);
}
function inferFunctionInstrEffects(
state: State,
place: Place,
): Array<FunctionEffect> {
const effects: Array<FunctionEffect> = [];
const instrs = state.values(place);
CompilerError.invariant(instrs != null, {
reason: 'Expected operand to have instructions',
loc: null,
});
for (const instr of instrs) {
if (
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
instr.loweredFunc.func.effects != null
) {
effects.push(...instr.loweredFunc.func.effects);
}
}
return effects;
}
function operandEffects(
state: State,
place: Place,
filterRenderSafe: boolean,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
const effect = inferOperandEffect(state, place);
effect && functionEffects.push(effect);
functionEffects.push(...inheritFunctionEffects(state, place));
if (filterRenderSafe) {
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
} else {
return functionEffects;
}
}
export function inferInstructionFunctionEffects(
env: Environment,
state: State,
instr: Instruction,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
switch (instr.value.kind) {
case 'JsxExpression': {
if (instr.value.tag.kind === 'Identifier') {
functionEffects.push(...operandEffects(state, instr.value.tag, false));
}
instr.value.children?.forEach(child =>
functionEffects.push(...operandEffects(state, child, false)),
);
for (const attr of instr.value.props) {
if (attr.kind === 'JsxSpreadAttribute') {
functionEffects.push(...operandEffects(state, attr.argument, false));
} else {
functionEffects.push(...operandEffects(state, attr.place, true));
}
}
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
/**
* If this function references other functions, propagate the referenced function's
* effects to this function.
*
* ```
* let f = () => global = true;
* let g = () => f();
* g();
* ```
*
* In this example, because `g` references `f`, we propagate the GlobalMutation from
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
* function effect context and report an error. But if instead we do:
*
* ```
* let f = () => global = true;
* let g = () => f();
* useEffect(() => g(), [g])
* ```
*
* Now `g`'s effects will be discarded since they're in a useEffect.
*/
for (const operand of eachInstructionOperand(instr)) {
instr.value.loweredFunc.func.effects ??= [];
instr.value.loweredFunc.func.effects.push(
...inferFunctionInstrEffects(state, operand),
);
}
break;
}
case 'MethodCall':
case 'CallExpression': {
let callee;
if (instr.value.kind === 'MethodCall') {
callee = instr.value.property;
functionEffects.push(
...operandEffects(state, instr.value.receiver, false),
);
} else {
callee = instr.value.callee;
}
functionEffects.push(...operandEffects(state, callee, false));
let isHook = getHookKind(env, callee.identifier) != null;
for (const arg of instr.value.args) {
const place = arg.kind === 'Identifier' ? arg : arg.place;
/*
* Join the effects of the argument with the effects of the enclosing function,
* unless the we're detecting a global mutation inside a useEffect hook
*/
functionEffects.push(...operandEffects(state, place, isHook));
}
break;
}
case 'StartMemoize':
case 'FinishMemoize':
case 'LoadLocal':
case 'StoreLocal': {
break;
}
case 'StoreGlobal': {
functionEffects.push({
kind: 'GlobalMutation',
error: {
reason:
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
loc: instr.loc,
suggestions: null,
severity: ErrorSeverity.InvalidReact,
},
});
break;
}
default: {
for (const operand of eachInstructionOperand(instr)) {
functionEffects.push(...operandEffects(state, operand, false));
}
}
}
return functionEffects;
}
export function inferTerminalFunctionEffects(
state: State,
block: BasicBlock,
): Array<FunctionEffect> {
const functionEffects: Array<FunctionEffect> = [];
for (const operand of eachTerminalOperand(block.terminal)) {
functionEffects.push(...operandEffects(state, operand, true));
}
return functionEffects;
}
export function raiseFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): void {
functionEffects.forEach(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
CompilerError.throw(eff.error);
}
case 'ContextMutation': {
CompilerError.throw({
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
});
}
default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``,
);
}
});
}
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
return effect.kind === 'GlobalMutation';
}
function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Mutating a value returned from 'useContext()', which should not be mutated`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Mutating a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
} else {
return 'This mutates a variable that React considers immutable';
}
}

View File

@@ -116,19 +116,23 @@ export function inferMutableLifetimes(
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.id.mutableRange.end >
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (
inferMutableRangeForStores &&
isPhiMutatedAfterCreation &&
phi.id.mutableRange.start === 0
phi.place.identifier.mutableRange.start === 0
) {
for (const [, operand] of phi.operands) {
if (phi.id.mutableRange.start === 0) {
phi.id.mutableRange.start = operand.mutableRange.start;
if (phi.place.identifier.mutableRange.start === 0) {
phi.place.identifier.mutableRange.start =
operand.identifier.mutableRange.start;
} else {
phi.id.mutableRange.start = makeInstructionId(
Math.min(phi.id.mutableRange.start, operand.mutableRange.start),
phi.place.identifier.mutableRange.start = makeInstructionId(
Math.min(
phi.place.identifier.mutableRange.start,
operand.identifier.mutableRange.start,
),
);
}
}

View File

@@ -157,28 +157,27 @@ export function inferReactivePlaces(fn: HIRFunction): void {
}
do {
const identifierMapping = new Map<Identifier, Identifier>();
for (const [, block] of fn.body.blocks) {
let hasReactiveControl = isReactiveControlledBlock(block.id);
for (const phi of block.phis) {
if (reactiveIdentifiers.isReactiveIdentifier(phi.id)) {
if (reactiveIdentifiers.isReactive(phi.place)) {
// Already marked reactive on a previous pass
continue;
}
let isPhiReactive = false;
for (const [, operand] of phi.operands) {
if (reactiveIdentifiers.isReactiveIdentifier(operand)) {
if (reactiveIdentifiers.isReactive(operand)) {
isPhiReactive = true;
break;
}
}
if (isPhiReactive) {
reactiveIdentifiers.markReactiveIdentifier(phi.id);
reactiveIdentifiers.markReactive(phi.place);
} else {
for (const [pred] of phi.operands) {
if (isReactiveControlledBlock(pred)) {
reactiveIdentifiers.markReactiveIdentifier(phi.id);
reactiveIdentifiers.markReactive(phi.place);
break;
}
}
@@ -233,10 +232,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
case Effect.ConditionallyMutate:
case Effect.Mutate: {
if (isMutable(instruction, operand)) {
const resolvedId = identifierMapping.get(operand.identifier);
if (resolvedId !== undefined) {
reactiveIdentifiers.markReactiveIdentifier(resolvedId);
}
reactiveIdentifiers.markReactive(operand);
}
break;
@@ -263,31 +258,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
}
}
}
switch (value.kind) {
case 'LoadLocal': {
identifierMapping.set(
instruction.lvalue.identifier,
value.place.identifier,
);
break;
}
case 'PropertyLoad':
case 'ComputedLoad': {
const resolvedId =
identifierMapping.get(value.object.identifier) ??
value.object.identifier;
identifierMapping.set(instruction.lvalue.identifier, resolvedId);
break;
}
case 'LoadContext': {
identifierMapping.set(
instruction.lvalue.identifier,
value.place.identifier,
);
break;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
reactiveIdentifiers.isReactive(operand);
@@ -372,27 +342,19 @@ class ReactivityMap {
}
isReactive(place: Place): boolean {
const reactive = this.isReactiveIdentifier(place.identifier);
const identifier =
this.aliasedIdentifiers.find(place.identifier) ?? place.identifier;
const reactive = this.reactive.has(identifier.id);
if (reactive) {
place.reactive = true;
}
return reactive;
}
isReactiveIdentifier(inputIdentifier: Identifier): boolean {
const identifier =
this.aliasedIdentifiers.find(inputIdentifier) ?? inputIdentifier;
return this.reactive.has(identifier.id);
}
markReactive(place: Place): void {
place.reactive = true;
this.markReactiveIdentifier(place.identifier);
}
markReactiveIdentifier(inputIdentifier: Identifier): void {
const identifier =
this.aliasedIdentifiers.find(inputIdentifier) ?? inputIdentifier;
this.aliasedIdentifiers.find(place.identifier) ?? place.identifier;
if (!this.reactive.has(identifier.id)) {
this.hasChanges = true;
this.reactive.add(identifier.id);

View File

@@ -8,7 +8,6 @@
import {isValidIdentifier} from '@babel/types';
import {CompilerError} from '../CompilerError';
import {
Environment,
GotoVariant,
HIRFunction,
IdentifierId,
@@ -117,7 +116,7 @@ function applyConstantPropagation(
for (const phi of block.phis) {
let value = evaluatePhi(phi, constants);
if (value !== null) {
constants.set(phi.id.id, value);
constants.set(phi.place.identifier.id, value);
}
}
@@ -130,7 +129,7 @@ function applyConstantPropagation(
continue;
}
const instr = block.instructions[i]!;
const value = evaluateInstruction(fn.env, constants, instr);
const value = evaluateInstruction(constants, instr);
if (value !== null) {
constants.set(instr.lvalue.identifier.id, value);
}
@@ -167,7 +166,7 @@ function applyConstantPropagation(
function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
let value: Constant | null = null;
for (const [, operand] of phi.operands) {
const operandValue = constants.get(operand.id) ?? null;
const operandValue = constants.get(operand.identifier.id) ?? null;
// did not find a constant, can't constant propogate
if (operandValue === null) {
return null;
@@ -223,7 +222,6 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
}
function evaluateInstruction(
env: Environment,
constants: Constants,
instr: Instruction,
): Constant | null {

View File

@@ -42,7 +42,7 @@ export function deadCodeElimination(fn: HIRFunction): void {
*/
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
if (!state.isIdOrNameUsed(phi.id)) {
if (!state.isIdOrNameUsed(phi.place.identifier)) {
block.phis.delete(phi);
}
}
@@ -159,9 +159,9 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
}
}
for (const phi of block.phis) {
if (state.isIdOrNameUsed(phi.id)) {
if (state.isIdOrNameUsed(phi.place.identifier)) {
for (const [_pred, operand] of phi.operands) {
state.reference(operand);
state.reference(operand.identifier);
}
}
}

View File

@@ -0,0 +1,758 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
BasicBlock,
BlockId,
BuiltinTag,
DeclarationId,
Effect,
forkTemporaryIdentifier,
GotoTerminal,
GotoVariant,
HIRFunction,
Identifier,
IfTerminal,
Instruction,
InstructionKind,
JsxAttribute,
makeInstructionId,
ObjectProperty,
Phi,
Place,
promoteTemporary,
SpreadPattern,
} from '../HIR';
import {
createTemporaryPlace,
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
} from '../HIR/HIRBuilder';
import {CompilerError, EnvironmentConfig} from '..';
import {
mapInstructionLValues,
mapInstructionOperands,
mapInstructionValueOperands,
mapTerminalOperands,
} from '../HIR/visitors';
type InlinedJsxDeclarationMap = Map<
DeclarationId,
{identifier: Identifier; blockIdsToIgnore: Set<BlockId>}
>;
/**
* A prod-only, RN optimization to replace JSX with inlined ReactElement object literals
*
* Example:
* <>foo</>
* _______________
* let t1;
* if (__DEV__) {
* t1 = <>foo</>
* } else {
* t1 = {...}
* }
*
*/
export function inlineJsxTransform(
fn: HIRFunction,
inlineJsxTransformConfig: NonNullable<
EnvironmentConfig['inlineJsxTransform']
>,
): void {
const inlinedJsxDeclarations: InlinedJsxDeclarationMap = new Map();
/**
* Step 1: Codegen the conditional and ReactElement object literal
*/
for (const [_, currentBlock] of [...fn.body.blocks]) {
let fallthroughBlockInstructions: Array<Instruction> | null = null;
const instructionCount = currentBlock.instructions.length;
for (let i = 0; i < instructionCount; i++) {
const instr = currentBlock.instructions[i]!;
// TODO: Support value blocks
if (currentBlock.kind === 'value') {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'CompileDiagnostic',
fnLoc: null,
detail: {
reason: 'JSX Inlining is not supported on value blocks',
loc: instr.loc,
},
});
continue;
}
switch (instr.value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
/**
* Split into blocks for new IfTerminal:
* current, then, else, fallthrough
*/
const currentBlockInstructions = currentBlock.instructions.slice(
0,
i,
);
const thenBlockInstructions = currentBlock.instructions.slice(
i,
i + 1,
);
const elseBlockInstructions: Array<Instruction> = [];
fallthroughBlockInstructions ??= currentBlock.instructions.slice(
i + 1,
);
const fallthroughBlockId = fn.env.nextBlockId;
const fallthroughBlock: BasicBlock = {
kind: currentBlock.kind,
id: fallthroughBlockId,
instructions: fallthroughBlockInstructions,
terminal: currentBlock.terminal,
preds: new Set(),
phis: new Set(),
};
/**
* Complete current block
* - Add instruction for variable declaration
* - Add instruction for LoadGlobal used by conditional
* - End block with a new IfTerminal
*/
const varPlace = createTemporaryPlace(fn.env, instr.value.loc);
promoteTemporary(varPlace.identifier);
const varLValuePlace = createTemporaryPlace(fn.env, instr.value.loc);
const thenVarPlace = {
...varPlace,
identifier: forkTemporaryIdentifier(
fn.env.nextIdentifierId,
varPlace.identifier,
),
};
const elseVarPlace = {
...varPlace,
identifier: forkTemporaryIdentifier(
fn.env.nextIdentifierId,
varPlace.identifier,
),
};
const varInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...varLValuePlace},
value: {
kind: 'DeclareLocal',
lvalue: {place: {...varPlace}, kind: InstructionKind.Let},
type: null,
loc: instr.value.loc,
},
loc: instr.loc,
};
currentBlockInstructions.push(varInstruction);
const devGlobalPlace = createTemporaryPlace(fn.env, instr.value.loc);
const devGlobalInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...devGlobalPlace, effect: Effect.Mutate},
value: {
kind: 'LoadGlobal',
binding: {
kind: 'Global',
name: inlineJsxTransformConfig.globalDevVar,
},
loc: instr.value.loc,
},
loc: instr.loc,
};
currentBlockInstructions.push(devGlobalInstruction);
const thenBlockId = fn.env.nextBlockId;
const elseBlockId = fn.env.nextBlockId;
const ifTerminal: IfTerminal = {
kind: 'if',
test: {...devGlobalPlace, effect: Effect.Read},
consequent: thenBlockId,
alternate: elseBlockId,
fallthrough: fallthroughBlockId,
loc: instr.loc,
id: makeInstructionId(0),
};
currentBlock.instructions = currentBlockInstructions;
currentBlock.terminal = ifTerminal;
/**
* Set up then block where we put the original JSX return
*/
const thenBlock: BasicBlock = {
id: thenBlockId,
instructions: thenBlockInstructions,
kind: 'block',
phis: new Set(),
preds: new Set(),
terminal: {
kind: 'goto',
block: fallthroughBlockId,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: instr.loc,
},
};
fn.body.blocks.set(thenBlockId, thenBlock);
const resassignElsePlace = createTemporaryPlace(
fn.env,
instr.value.loc,
);
const reassignElseInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...resassignElsePlace},
value: {
kind: 'StoreLocal',
lvalue: {
place: elseVarPlace,
kind: InstructionKind.Reassign,
},
value: {...instr.lvalue},
type: null,
loc: instr.value.loc,
},
loc: instr.loc,
};
thenBlockInstructions.push(reassignElseInstruction);
/**
* Set up else block where we add new codegen
*/
const elseBlockTerminal: GotoTerminal = {
kind: 'goto',
block: fallthroughBlockId,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: instr.loc,
};
const elseBlock: BasicBlock = {
id: elseBlockId,
instructions: elseBlockInstructions,
kind: 'block',
phis: new Set(),
preds: new Set(),
terminal: elseBlockTerminal,
};
fn.body.blocks.set(elseBlockId, elseBlock);
/**
* ReactElement object literal codegen
*/
const {refProperty, keyProperty, propsProperty} =
createPropsProperties(
fn,
instr,
elseBlockInstructions,
instr.value.kind === 'JsxExpression' ? instr.value.props : [],
instr.value.children,
);
const reactElementInstructionPlace = createTemporaryPlace(
fn.env,
instr.value.loc,
);
const reactElementInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...reactElementInstructionPlace, effect: Effect.Store},
value: {
kind: 'ObjectExpression',
properties: [
createSymbolProperty(
fn,
instr,
elseBlockInstructions,
'$$typeof',
inlineJsxTransformConfig.elementSymbol,
),
instr.value.kind === 'JsxExpression'
? createTagProperty(
fn,
instr,
elseBlockInstructions,
instr.value.tag,
)
: createSymbolProperty(
fn,
instr,
elseBlockInstructions,
'type',
'react.fragment',
),
refProperty,
keyProperty,
propsProperty,
],
loc: instr.value.loc,
},
loc: instr.loc,
};
elseBlockInstructions.push(reactElementInstruction);
const reassignConditionalInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...createTemporaryPlace(fn.env, instr.value.loc)},
value: {
kind: 'StoreLocal',
lvalue: {
place: {...elseVarPlace},
kind: InstructionKind.Reassign,
},
value: {...reactElementInstruction.lvalue},
type: null,
loc: instr.value.loc,
},
loc: instr.loc,
};
elseBlockInstructions.push(reassignConditionalInstruction);
/**
* Create phis to reassign the var
*/
const operands: Map<BlockId, Place> = new Map();
operands.set(thenBlockId, {
...elseVarPlace,
});
operands.set(elseBlockId, {
...thenVarPlace,
});
const phiIdentifier = forkTemporaryIdentifier(
fn.env.nextIdentifierId,
varPlace.identifier,
);
const phiPlace = {
...createTemporaryPlace(fn.env, instr.value.loc),
identifier: phiIdentifier,
};
const phis: Set<Phi> = new Set([
{
kind: 'Phi',
operands,
place: phiPlace,
},
]);
fallthroughBlock.phis = phis;
fn.body.blocks.set(fallthroughBlockId, fallthroughBlock);
/**
* Track this JSX instruction so we can replace references in step 2
*/
inlinedJsxDeclarations.set(instr.lvalue.identifier.declarationId, {
identifier: phiIdentifier,
blockIdsToIgnore: new Set([thenBlockId, elseBlockId]),
});
break;
}
case 'FunctionExpression':
case 'ObjectMethod': {
inlineJsxTransform(
instr.value.loweredFunc.func,
inlineJsxTransformConfig,
);
break;
}
}
}
}
/**
* Step 2: Replace declarations with new phi values
*/
for (const [blockId, block] of fn.body.blocks) {
for (const instr of block.instructions) {
mapInstructionOperands(instr, place =>
handlePlace(place, blockId, inlinedJsxDeclarations),
);
mapInstructionLValues(instr, lvalue =>
handlelValue(lvalue, blockId, inlinedJsxDeclarations),
);
mapInstructionValueOperands(instr.value, place =>
handlePlace(place, blockId, inlinedJsxDeclarations),
);
}
mapTerminalOperands(block.terminal, place =>
handlePlace(place, blockId, inlinedJsxDeclarations),
);
if (block.terminal.kind === 'scope') {
const scope = block.terminal.scope;
for (const dep of scope.dependencies) {
dep.identifier = handleIdentifier(
dep.identifier,
inlinedJsxDeclarations,
);
}
for (const [origId, decl] of [...scope.declarations]) {
const newDecl = handleIdentifier(
decl.identifier,
inlinedJsxDeclarations,
);
if (newDecl.id !== origId) {
scope.declarations.delete(origId);
scope.declarations.set(decl.identifier.id, {
identifier: newDecl,
scope: decl.scope,
});
}
}
}
}
/**
* Step 3: Fixup the HIR
* Restore RPO, ensure correct predecessors, renumber instructions, fix scope and ranges.
*/
reversePostorderBlocks(fn.body);
markPredecessors(fn.body);
markInstructionIds(fn.body);
fixScopeAndIdentifierRanges(fn.body);
}
function createSymbolProperty(
fn: HIRFunction,
instr: Instruction,
nextInstructions: Array<Instruction>,
propertyName: string,
symbolName: string,
): ObjectProperty {
const symbolPlace = createTemporaryPlace(fn.env, instr.value.loc);
const symbolInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...symbolPlace, effect: Effect.Mutate},
value: {
kind: 'LoadGlobal',
binding: {kind: 'Global', name: 'Symbol'},
loc: instr.value.loc,
},
loc: instr.loc,
};
nextInstructions.push(symbolInstruction);
const symbolForPlace = createTemporaryPlace(fn.env, instr.value.loc);
const symbolForInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...symbolForPlace, effect: Effect.Read},
value: {
kind: 'PropertyLoad',
object: {...symbolInstruction.lvalue},
property: 'for',
loc: instr.value.loc,
},
loc: instr.loc,
};
nextInstructions.push(symbolForInstruction);
const symbolValuePlace = createTemporaryPlace(fn.env, instr.value.loc);
const symbolValueInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...symbolValuePlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: symbolName,
loc: instr.value.loc,
},
loc: instr.loc,
};
nextInstructions.push(symbolValueInstruction);
const $$typeofPlace = createTemporaryPlace(fn.env, instr.value.loc);
const $$typeofInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...$$typeofPlace, effect: Effect.Mutate},
value: {
kind: 'MethodCall',
receiver: symbolInstruction.lvalue,
property: symbolForInstruction.lvalue,
args: [symbolValueInstruction.lvalue],
loc: instr.value.loc,
},
loc: instr.loc,
};
const $$typeofProperty: ObjectProperty = {
kind: 'ObjectProperty',
key: {name: propertyName, kind: 'string'},
type: 'property',
place: {...$$typeofPlace, effect: Effect.Capture},
};
nextInstructions.push($$typeofInstruction);
return $$typeofProperty;
}
function createTagProperty(
fn: HIRFunction,
instr: Instruction,
nextInstructions: Array<Instruction>,
componentTag: BuiltinTag | Place,
): ObjectProperty {
let tagProperty: ObjectProperty;
switch (componentTag.kind) {
case 'BuiltinTag': {
const tagPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
const tagInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...tagPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: componentTag.name,
loc: instr.value.loc,
},
loc: instr.loc,
};
tagProperty = {
kind: 'ObjectProperty',
key: {name: 'type', kind: 'string'},
type: 'property',
place: {...tagPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(tagInstruction);
break;
}
case 'Identifier': {
tagProperty = {
kind: 'ObjectProperty',
key: {name: 'type', kind: 'string'},
type: 'property',
place: {...componentTag, effect: Effect.Capture},
};
break;
}
}
return tagProperty;
}
function createPropsProperties(
fn: HIRFunction,
instr: Instruction,
nextInstructions: Array<Instruction>,
propAttributes: Array<JsxAttribute>,
children: Array<Place> | null,
): {
refProperty: ObjectProperty;
keyProperty: ObjectProperty;
propsProperty: ObjectProperty;
} {
let refProperty: ObjectProperty | undefined;
let keyProperty: ObjectProperty | undefined;
const props: Array<ObjectProperty | SpreadPattern> = [];
const jsxAttributesWithoutKeyAndRef = propAttributes.filter(
p => p.kind === 'JsxAttribute' && p.name !== 'key' && p.name !== 'ref',
);
const jsxSpreadAttributes = propAttributes.filter(
p => p.kind === 'JsxSpreadAttribute',
);
const spreadPropsOnly =
jsxAttributesWithoutKeyAndRef.length === 0 &&
jsxSpreadAttributes.length === 1;
propAttributes.forEach(prop => {
switch (prop.kind) {
case 'JsxAttribute': {
if (prop.name === 'ref') {
refProperty = {
kind: 'ObjectProperty',
key: {name: 'ref', kind: 'string'},
type: 'property',
place: {...prop.place},
};
} else if (prop.name === 'key') {
keyProperty = {
kind: 'ObjectProperty',
key: {name: 'key', kind: 'string'},
type: 'property',
place: {...prop.place},
};
} else {
const attributeProperty: ObjectProperty = {
kind: 'ObjectProperty',
key: {name: prop.name, kind: 'string'},
type: 'property',
place: {...prop.place},
};
props.push(attributeProperty);
}
break;
}
case 'JsxSpreadAttribute': {
props.push({
kind: 'Spread',
place: {...prop.argument},
});
break;
}
}
});
const propsPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
if (children) {
let childrenPropProperty: ObjectProperty;
if (children.length === 1) {
childrenPropProperty = {
kind: 'ObjectProperty',
key: {name: 'children', kind: 'string'},
type: 'property',
place: {...children[0], effect: Effect.Capture},
};
} else {
const childrenPropPropertyPlace = createTemporaryPlace(
fn.env,
instr.value.loc,
);
const childrenPropInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...childrenPropPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'ArrayExpression',
elements: [...children],
loc: instr.value.loc,
},
loc: instr.loc,
};
nextInstructions.push(childrenPropInstruction);
childrenPropProperty = {
kind: 'ObjectProperty',
key: {name: 'children', kind: 'string'},
type: 'property',
place: {...childrenPropPropertyPlace, effect: Effect.Capture},
};
}
props.push(childrenPropProperty);
}
if (refProperty == null) {
const refPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
const refInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...refPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: null,
loc: instr.value.loc,
},
loc: instr.loc,
};
refProperty = {
kind: 'ObjectProperty',
key: {name: 'ref', kind: 'string'},
type: 'property',
place: {...refPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(refInstruction);
}
if (keyProperty == null) {
const keyPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
const keyInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...keyPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'Primitive',
value: null,
loc: instr.value.loc,
},
loc: instr.loc,
};
keyProperty = {
kind: 'ObjectProperty',
key: {name: 'key', kind: 'string'},
type: 'property',
place: {...keyPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(keyInstruction);
}
let propsProperty: ObjectProperty;
if (spreadPropsOnly) {
const spreadProp = jsxSpreadAttributes[0];
CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
loc: instr.loc,
});
propsProperty = {
kind: 'ObjectProperty',
key: {name: 'props', kind: 'string'},
type: 'property',
place: {...spreadProp.argument, effect: Effect.Mutate},
};
} else {
const propsInstruction: Instruction = {
id: makeInstructionId(0),
lvalue: {...propsPropertyPlace, effect: Effect.Mutate},
value: {
kind: 'ObjectExpression',
properties: props,
loc: instr.value.loc,
},
loc: instr.loc,
};
propsProperty = {
kind: 'ObjectProperty',
key: {name: 'props', kind: 'string'},
type: 'property',
place: {...propsPropertyPlace, effect: Effect.Capture},
};
nextInstructions.push(propsInstruction);
}
return {refProperty, keyProperty, propsProperty};
}
function handlePlace(
place: Place,
blockId: BlockId,
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
): Place {
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
place.identifier.declarationId,
);
if (
inlinedJsxDeclaration == null ||
inlinedJsxDeclaration.blockIdsToIgnore.has(blockId)
) {
return place;
}
return {...place, identifier: inlinedJsxDeclaration.identifier};
}
function handlelValue(
lvalue: Place,
blockId: BlockId,
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
): Place {
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
lvalue.identifier.declarationId,
);
if (
inlinedJsxDeclaration == null ||
inlinedJsxDeclaration.blockIdsToIgnore.has(blockId)
) {
return lvalue;
}
return {...lvalue, identifier: inlinedJsxDeclaration.identifier};
}
function handleIdentifier(
identifier: Identifier,
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
): Identifier {
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
identifier.declarationId,
);
return inlinedJsxDeclaration == null
? identifier
: inlinedJsxDeclaration.identifier;
}

View File

@@ -0,0 +1,520 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import invariant from 'invariant';
import {Environment} from '../HIR';
import {
BasicBlock,
GeneratedSource,
HIRFunction,
IdentifierId,
Instruction,
InstructionId,
InstructionKind,
JsxAttribute,
JsxExpression,
LoadGlobal,
makeBlockId,
makeIdentifierName,
makeInstructionId,
makeType,
ObjectProperty,
Place,
promoteTemporary,
promoteTemporaryJsxTag,
} from '../HIR/HIR';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {printIdentifier} from '../HIR/PrintHIR';
import {deadCodeElimination} from './DeadCodeElimination';
import {assertExhaustive} from '../Utils/utils';
export function outlineJSX(fn: HIRFunction): void {
const outlinedFns: Array<HIRFunction> = [];
outlineJsxImpl(fn, outlinedFns);
for (const outlinedFn of outlinedFns) {
fn.env.outlineFunction(outlinedFn, 'Component');
}
}
type JsxInstruction = Instruction & {value: JsxExpression};
type LoadGlobalInstruction = Instruction & {value: LoadGlobal};
type LoadGlobalMap = Map<IdentifierId, LoadGlobalInstruction>;
type State = {
jsx: Array<JsxInstruction>;
children: Set<IdentifierId>;
};
function outlineJsxImpl(
fn: HIRFunction,
outlinedFns: Array<HIRFunction>,
): void {
const globals: LoadGlobalMap = new Map();
function processAndOutlineJSX(
state: State,
rewriteInstr: Map<InstructionId, Array<Instruction>>,
): void {
if (state.jsx.length <= 1) {
return;
}
const result = process(
fn,
[...state.jsx].sort((a, b) => a.id - b.id),
globals,
);
if (result) {
outlinedFns.push(result.fn);
rewriteInstr.set(state.jsx.at(0)!.id, result.instrs);
}
}
for (const [, block] of fn.body.blocks) {
const rewriteInstr = new Map();
let state: State = {
jsx: [],
children: new Set(),
};
for (let i = block.instructions.length - 1; i >= 0; i--) {
const instr = block.instructions[i];
const {value, lvalue} = instr;
switch (value.kind) {
case 'LoadGlobal': {
globals.set(lvalue.identifier.id, instr as LoadGlobalInstruction);
break;
}
case 'FunctionExpression': {
outlineJsxImpl(value.loweredFunc.func, outlinedFns);
break;
}
case 'JsxExpression': {
if (!state.children.has(lvalue.identifier.id)) {
processAndOutlineJSX(state, rewriteInstr);
state = {
jsx: [],
children: new Set(),
};
}
state.jsx.push(instr as JsxInstruction);
if (value.children) {
for (const child of value.children) {
state.children.add(child.identifier.id);
}
}
break;
}
case 'ArrayExpression':
case 'Await':
case 'BinaryExpression':
case 'CallExpression':
case 'ComputedDelete':
case 'ComputedLoad':
case 'ComputedStore':
case 'Debugger':
case 'DeclareContext':
case 'DeclareLocal':
case 'Destructure':
case 'FinishMemoize':
case 'GetIterator':
case 'IteratorNext':
case 'JSXText':
case 'JsxFragment':
case 'LoadContext':
case 'LoadLocal':
case 'MetaProperty':
case 'MethodCall':
case 'NewExpression':
case 'NextPropertyOf':
case 'ObjectExpression':
case 'ObjectMethod':
case 'PostfixUpdate':
case 'PrefixUpdate':
case 'Primitive':
case 'PropertyDelete':
case 'PropertyLoad':
case 'PropertyStore':
case 'RegExpLiteral':
case 'StartMemoize':
case 'StoreContext':
case 'StoreGlobal':
case 'StoreLocal':
case 'TaggedTemplateExpression':
case 'TemplateLiteral':
case 'TypeCastExpression':
case 'UnsupportedNode':
case 'UnaryExpression': {
break;
}
default: {
assertExhaustive(value, `Unexpected instruction: ${value}`);
}
}
}
processAndOutlineJSX(state, rewriteInstr);
if (rewriteInstr.size > 0) {
const newInstrs = [];
for (let i = 0; i < block.instructions.length; i++) {
// InstructionId's are one-indexed, so add one to account for them.
const id = i + 1;
if (rewriteInstr.has(id)) {
const instrs = rewriteInstr.get(id);
newInstrs.push(...instrs);
} else {
newInstrs.push(block.instructions[i]);
}
}
block.instructions = newInstrs;
}
deadCodeElimination(fn);
}
}
type OutlinedResult = {
instrs: Array<Instruction>;
fn: HIRFunction;
};
function process(
fn: HIRFunction,
jsx: Array<JsxInstruction>,
globals: LoadGlobalMap,
): OutlinedResult | null {
/**
* In the future, add a check for backedge to outline jsx inside loops in a
* top level component. For now, only outline jsx in callbacks.
*/
if (fn.fnType === 'Component') {
return null;
}
const props = collectProps(jsx);
if (!props) return null;
const outlinedTag = fn.env.generateGloballyUniqueIdentifierName(null).value;
const newInstrs = emitOutlinedJsx(fn.env, jsx, props, outlinedTag);
if (!newInstrs) return null;
const outlinedFn = emitOutlinedFn(fn.env, jsx, props, globals);
if (!outlinedFn) return null;
outlinedFn.id = outlinedTag;
return {instrs: newInstrs, fn: outlinedFn};
}
type OutlinedJsxAttribute = {
originalName: string;
newName: string;
place: Place;
};
function collectProps(
instructions: Array<JsxInstruction>,
): Array<OutlinedJsxAttribute> | null {
let id = 1;
function generateName(oldName: string): string {
let newName = oldName;
while (seen.has(newName)) {
newName = `${oldName}${id++}`;
}
seen.add(newName);
return newName;
}
const attributes: Array<OutlinedJsxAttribute> = [];
const jsxIds = new Set(instructions.map(i => i.lvalue.identifier.id));
const seen: Set<string> = new Set();
for (const instr of instructions) {
const {value} = instr;
for (const at of value.props) {
if (at.kind === 'JsxSpreadAttribute') {
return null;
}
if (at.kind === 'JsxAttribute') {
const newName = generateName(at.name);
attributes.push({
originalName: at.name,
newName,
place: at.place,
});
}
}
if (value.children) {
for (const child of value.children) {
if (jsxIds.has(child.identifier.id)) {
continue;
}
promoteTemporary(child.identifier);
const newName = generateName('t');
attributes.push({
originalName: child.identifier.name!.value,
newName: newName,
place: child,
});
}
}
}
return attributes;
}
function emitOutlinedJsx(
env: Environment,
instructions: Array<Instruction>,
outlinedProps: Array<OutlinedJsxAttribute>,
outlinedTag: string,
): Array<Instruction> {
const props: Array<JsxAttribute> = outlinedProps.map(p => ({
kind: 'JsxAttribute',
name: p.newName,
place: p.place,
}));
const loadJsx: Instruction = {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: createTemporaryPlace(env, GeneratedSource),
value: {
kind: 'LoadGlobal',
binding: {
kind: 'ModuleLocal',
name: outlinedTag,
},
loc: GeneratedSource,
},
};
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
const jsxExpr: Instruction = {
id: makeInstructionId(0),
loc: GeneratedSource,
lvalue: instructions.at(-1)!.lvalue,
value: {
kind: 'JsxExpression',
tag: {...loadJsx.lvalue},
props,
children: null,
loc: GeneratedSource,
openingLoc: GeneratedSource,
closingLoc: GeneratedSource,
},
};
return [loadJsx, jsxExpr];
}
function emitOutlinedFn(
env: Environment,
jsx: Array<JsxInstruction>,
oldProps: Array<OutlinedJsxAttribute>,
globals: LoadGlobalMap,
): HIRFunction | null {
const instructions: Array<Instruction> = [];
const oldToNewProps = createOldToNewPropsMapping(env, oldProps);
const propsObj: Place = createTemporaryPlace(env, GeneratedSource);
promoteTemporary(propsObj.identifier);
const destructurePropsInstr = emitDestructureProps(
env,
propsObj,
oldToNewProps,
);
instructions.push(destructurePropsInstr);
const updatedJsxInstructions = emitUpdatedJsx(jsx, oldToNewProps);
const loadGlobalInstrs = emitLoadGlobals(jsx, globals);
if (!loadGlobalInstrs) {
return null;
}
instructions.push(...loadGlobalInstrs);
instructions.push(...updatedJsxInstructions);
const block: BasicBlock = {
kind: 'block',
id: makeBlockId(0),
instructions,
terminal: {
id: makeInstructionId(0),
kind: 'return',
loc: GeneratedSource,
value: instructions.at(-1)!.lvalue,
},
preds: new Set(),
phis: new Set(),
};
const fn: HIRFunction = {
loc: GeneratedSource,
id: null,
fnType: 'Other',
env,
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
},
generator: false,
async: false,
directives: [],
};
return fn;
}
function emitLoadGlobals(
jsx: Array<JsxInstruction>,
globals: LoadGlobalMap,
): Array<Instruction> | null {
const instructions: Array<Instruction> = [];
for (const {value} of jsx) {
// Add load globals instructions for jsx tags
if (value.tag.kind === 'Identifier') {
const loadGlobalInstr = globals.get(value.tag.identifier.id);
if (!loadGlobalInstr) {
return null;
}
instructions.push(loadGlobalInstr);
}
}
return instructions;
}
function emitUpdatedJsx(
jsx: Array<JsxInstruction>,
oldToNewProps: Map<IdentifierId, OutlinedJsxAttribute>,
): Array<JsxInstruction> {
const newInstrs: Array<JsxInstruction> = [];
const jsxIds = new Set(jsx.map(i => i.lvalue.identifier.id));
for (const instr of jsx) {
const {value} = instr;
const newProps: Array<JsxAttribute> = [];
// Update old props references to use the newly destructured props param
for (const prop of value.props) {
invariant(
prop.kind === 'JsxAttribute',
`Expected only attributes but found ${prop.kind}`,
);
if (prop.name === 'key') {
continue;
}
const newProp = oldToNewProps.get(prop.place.identifier.id);
invariant(
newProp !== undefined,
`Expected a new property for ${printIdentifier(prop.place.identifier)}`,
);
newProps.push({
kind: 'JsxAttribute',
name: newProp.originalName,
place: newProp.place,
});
}
let newChildren: Array<Place> | null = null;
if (value.children) {
newChildren = [];
for (const child of value.children) {
if (jsxIds.has(child.identifier.id)) {
newChildren.push({...child});
continue;
}
const newChild = oldToNewProps.get(child.identifier.id);
invariant(
newChild !== undefined,
`Expected a new prop for ${printIdentifier(child.identifier)}`,
);
newChildren.push({...newChild.place});
}
}
newInstrs.push({
...instr,
value: {
...value,
props: newProps,
children: newChildren,
},
});
}
return newInstrs;
}
function createOldToNewPropsMapping(
env: Environment,
oldProps: Array<OutlinedJsxAttribute>,
): Map<IdentifierId, OutlinedJsxAttribute> {
const oldToNewProps = new Map();
for (const oldProp of oldProps) {
// Do not read key prop in the outlined component
if (oldProp.originalName === 'key') {
continue;
}
const newProp: OutlinedJsxAttribute = {
...oldProp,
place: createTemporaryPlace(env, GeneratedSource),
};
newProp.place.identifier.name = makeIdentifierName(oldProp.newName);
oldToNewProps.set(oldProp.place.identifier.id, newProp);
}
return oldToNewProps;
}
function emitDestructureProps(
env: Environment,
propsObj: Place,
oldToNewProps: Map<IdentifierId, OutlinedJsxAttribute>,
): Instruction {
const properties: Array<ObjectProperty> = [];
for (const [_, prop] of oldToNewProps) {
properties.push({
kind: 'ObjectProperty',
key: {
kind: 'string',
name: prop.newName,
},
type: 'property',
place: prop.place,
});
}
const destructurePropsInstr: Instruction = {
id: makeInstructionId(0),
lvalue: createTemporaryPlace(env, GeneratedSource),
loc: GeneratedSource,
value: {
kind: 'Destructure',
lvalue: {
pattern: {
kind: 'ObjectPattern',
properties,
},
kind: InstructionKind.Let,
},
loc: GeneratedSource,
value: propsObj,
},
};
return destructurePropsInstr;
}

View File

@@ -23,7 +23,7 @@ import {
removeUnnecessaryTryCatch,
removeUnreachableForUpdates,
} from '../HIR/HIRBuilder';
import {printIdentifier} from '../HIR/PrintHIR';
import {printPlace} from '../HIR/PrintHIR';
/*
* This pass prunes `maybe-throw` terminals for blocks that can provably *never* throw.
@@ -55,7 +55,7 @@ export function pruneMaybeThrows(fn: HIRFunction): void {
loc: GeneratedSource,
description: `Could not find mapping for predecessor bb${predecessor} in block bb${
block.id
} for phi ${printIdentifier(phi.id)}`,
} for phi ${printPlace(phi.place)}`,
suggestions: null,
});
phi.operands.delete(predecessor);

View File

@@ -8,3 +8,4 @@
export {constantPropagation} from './ConstantPropagation';
export {deadCodeElimination} from './DeadCodeElimination';
export {pruneMaybeThrows} from './PruneMaybeThrows';
export {inlineJsxTransform} from './InlineJsxTransform';

View File

@@ -1,187 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
InstructionId,
Place,
ReactiveBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScope,
ScopeId,
makeInstructionId,
} from '../HIR/HIR';
import {getPlaceScope} from './BuildReactiveBlocks';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
/*
* Note: this is the 2nd of 4 passes that determine how to break a function into discrete
* reactive scopes (independently memoizeable units of code):
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
* them a unique reactive scope.
* 2. AlignReactiveScopesToBlockScopes (this pass, on ReactiveFunction) aligns reactive scopes
* to block scopes.
* 3. MergeOverlappingReactiveScopes (on ReactiveFunction) ensures that reactive scopes do not
* overlap, merging any such scopes.
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
* a ReactiveScopeBlock.
*
* Prior inference passes assign a reactive scope to each operand, but the ranges of these
* scopes are based on specific instructions at arbitrary points in the control-flow graph.
* However, to codegen blocks around the instructions in each scope, the scopes must be
* aligned to block-scope boundaries - we can't memoize half of a loop!
*
* This pass updates reactive scope boundaries to align to control flow boundaries, for
* example:
*
* ```javascript
* function foo(cond, a) {
* ⌵ original scope
* ⌵ expanded scope
* const x = []; ⌝ ⌝
* if (cond) { ⎮ ⎮
* ... ⎮ ⎮
* x.push(a); ⌟ ⎮
* ... ⎮
* } ⌟
* }
* ```
*
* Here the original scope for `x` ended partway through the if consequent, but we can't
* memoize part of that block. This pass would align the scope to the end of the consequent.
*
* The more general rule is that a reactive scope may only end at the same block scope as it
* began: this pass therefore finds, for each scope, the block where that scope started and
* finds the first instruction after the scope's mutable range in that same block scope (which
* will be the updated end for that scope).
*/
export function alignReactiveScopesToBlockScopes(fn: ReactiveFunction): void {
const context = new Context();
visitReactiveFunction(fn, new Visitor(), context);
}
class Visitor extends ReactiveFunctionVisitor<Context> {
override visitID(id: InstructionId, state: Context): void {
state.visitId(id);
}
override visitPlace(id: InstructionId, place: Place, state: Context): void {
const scope = getPlaceScope(id, place);
if (scope !== null) {
state.visitScope(scope);
}
}
override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
const scope = getPlaceScope(id, lvalue);
if (scope !== null) {
state.visitScope(scope);
}
}
override visitInstruction(instr: ReactiveInstruction, state: Context): void {
switch (instr.value.kind) {
case 'OptionalExpression':
case 'SequenceExpression':
case 'ConditionalExpression':
case 'LogicalExpression': {
const prevScopeCount = state.currentScopes().length;
this.traverseInstruction(instr, state);
/**
* These compound value types can have nested sequences of instructions
* with scopes that start "partway" through a block-level instruction.
* This would cause the start of the scope to not align with any block-level
* instruction and get skipped by the later BuildReactiveBlocks pass.
*
* Here we detect scopes created within compound instructions and align the
* start of these scopes to the outer instruction id to ensure the scopes
* aren't skipped.
*/
const scopes = state.currentScopes();
for (let i = prevScopeCount; i < scopes.length; i++) {
const scope = scopes[i];
scope.scope.range.start = makeInstructionId(
Math.min(instr.id, scope.scope.range.start),
);
}
break;
}
default: {
this.traverseInstruction(instr, state);
}
}
}
override visitBlock(block: ReactiveBlock, state: Context): void {
state.enter(() => {
this.traverseBlock(block, state);
});
}
}
type PendingReactiveScope = {active: boolean; scope: ReactiveScope};
class Context {
/*
* For each block scope (outer array) stores a list of ReactiveScopes that start
* in that block scope.
*/
#blockScopes: Array<Array<PendingReactiveScope>> = [];
/*
* ReactiveScopes whose declaring block scope has ended but may still need to
* be "closed" (ie have their range.end be updated). A given scope can be in
* blockScopes OR this array but not both.
*/
#unclosedScopes: Array<PendingReactiveScope> = [];
/*
* Set of all scope ids that have been seen so far, regardless of which of
* the above data structures they're in, to avoid tracking the same scope twice.
*/
#seenScopes: Set<ScopeId> = new Set();
currentScopes(): Array<PendingReactiveScope> {
return this.#blockScopes.at(-1) ?? [];
}
enter(fn: () => void): void {
this.#blockScopes.push([]);
fn();
const lastScope = this.#blockScopes.pop()!;
for (const scope of lastScope) {
if (scope.active) {
this.#unclosedScopes.push(scope);
}
}
}
visitId(id: InstructionId): void {
const currentScopes = this.#blockScopes.at(-1)!;
const scopes = [...currentScopes, ...this.#unclosedScopes];
for (const pending of scopes) {
if (!pending.active) {
continue;
}
if (id >= pending.scope.range.end) {
pending.active = false;
pending.scope.range.end = id;
}
}
}
visitScope(scope: ReactiveScope): void {
if (!this.#seenScopes.has(scope.id)) {
const currentScopes = this.#blockScopes.at(-1)!;
this.#seenScopes.add(scope.id);
currentScopes.push({
active: true,
scope,
});
}
}
}

View File

@@ -13,6 +13,7 @@ import {
MutableRange,
Place,
ReactiveScope,
getPlaceScope,
makeInstructionId,
} from '../HIR/HIR';
import {
@@ -23,7 +24,6 @@ import {
terminalFallthrough,
} from '../HIR/visitors';
import {retainWhere_Set} from '../Utils/utils';
import {getPlaceScope} from './BuildReactiveBlocks';
type InstructionRange = MutableRange;
/*

View File

@@ -14,7 +14,7 @@ import {
ReactiveScopeBlock,
ScopeId,
} from '../HIR';
import {getPlaceScope} from './BuildReactiveBlocks';
import {getPlaceScope} from '../HIR/HIR';
import {ReactiveFunctionVisitor} from './visitors';
/*

View File

@@ -1,244 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {
BlockId,
InstructionId,
Place,
ReactiveBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScope,
ReactiveScopeBlock,
ReactiveStatement,
ScopeId,
} from '../HIR';
import {eachInstructionLValue} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
import {eachReactiveValueOperand, mapTerminalBlocks} from './visitors';
/*
* Note: this is the 4th of 4 passes that determine how to break a function into discrete
* reactive scopes (independently memoizeable units of code):
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
* them a unique reactive scope.
* 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
* to block scopes.
* 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
* scopes do not overlap, merging any such scopes.
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
* a ReactiveScopeBlock.
*
* Given a function where the reactive scopes have been correctly aligned and merged,
* this pass groups the instructions for each reactive scope into ReactiveBlocks.
*/
export function buildReactiveBlocks(fn: ReactiveFunction): void {
const context = new Context();
fn.body = context.enter(() => {
visitBlock(context, fn.body);
});
}
class Context {
#builders: Array<Builder> = [];
#scopes: Set<ScopeId> = new Set();
visitId(id: InstructionId): void {
const builder = this.#builders.at(-1)!;
builder.visitId(id);
}
visitScope(scope: ReactiveScope): void {
if (this.#scopes.has(scope.id)) {
return;
}
this.#scopes.add(scope.id);
this.#builders.at(-1)!.startScope(scope);
}
append(
stmt: ReactiveStatement,
label: {id: BlockId; implicit: boolean} | null,
): void {
this.#builders.at(-1)!.append(stmt, label);
}
enter(fn: () => void): ReactiveBlock {
const builder = new Builder();
this.#builders.push(builder);
fn();
const popped = this.#builders.pop();
CompilerError.invariant(popped === builder, {
reason: 'Expected push/pop to be called 1:1',
description: null,
loc: null,
suggestions: null,
});
return builder.complete();
}
}
class Builder {
#instructions: ReactiveBlock;
#stack: Array<
| {kind: 'scope'; block: ReactiveScopeBlock}
| {kind: 'block'; block: ReactiveBlock}
>;
constructor() {
const block: ReactiveBlock = [];
this.#instructions = block;
this.#stack = [{kind: 'block', block}];
}
append(
item: ReactiveStatement,
label: {id: BlockId; implicit: boolean} | null,
): void {
if (label !== null) {
CompilerError.invariant(item.kind === 'terminal', {
reason: 'Only terminals may have a label',
description: null,
loc: null,
suggestions: null,
});
item.label = label;
}
this.#instructions.push(item);
}
startScope(scope: ReactiveScope): void {
const block: ReactiveScopeBlock = {
kind: 'scope',
scope,
instructions: [],
};
this.append(block, null);
this.#instructions = block.instructions;
this.#stack.push({kind: 'scope', block});
}
visitId(id: InstructionId): void {
for (let i = 0; i < this.#stack.length; i++) {
const entry = this.#stack[i]!;
if (entry.kind === 'scope' && id >= entry.block.scope.range.end) {
this.#stack.length = i;
break;
}
}
const last = this.#stack[this.#stack.length - 1]!;
if (last.kind === 'block') {
this.#instructions = last.block;
} else {
this.#instructions = last.block.instructions;
}
}
complete(): ReactiveBlock {
/*
* TODO: @josephsavona debug violations of this invariant
* invariant(
* this.#stack.length === 1,
* "Expected all scopes to be closed when exiting a block"
* );
*/
const first = this.#stack[0]!;
CompilerError.invariant(first.kind === 'block', {
reason: 'Expected first stack item to be a basic block',
description: null,
loc: null,
suggestions: null,
});
return first.block;
}
}
function visitBlock(context: Context, block: ReactiveBlock): void {
for (const stmt of block) {
switch (stmt.kind) {
case 'instruction': {
context.visitId(stmt.instruction.id);
const scope = getInstructionScope(stmt.instruction);
if (scope !== null) {
context.visitScope(scope);
}
context.append(stmt, null);
break;
}
case 'terminal': {
const id = stmt.terminal.id;
if (id !== null) {
context.visitId(id);
}
mapTerminalBlocks(stmt.terminal, block => {
return context.enter(() => {
visitBlock(context, block);
});
});
context.append(stmt, stmt.label);
break;
}
case 'pruned-scope':
case 'scope': {
CompilerError.invariant(false, {
reason: 'Expected the function to not have scopes already assigned',
description: null,
loc: null,
suggestions: null,
});
}
default: {
assertExhaustive(
stmt,
`Unexpected statement kind \`${(stmt as any).kind}\``,
);
}
}
}
}
export function getInstructionScope(
instr: ReactiveInstruction,
): ReactiveScope | null {
CompilerError.invariant(instr.lvalue !== null, {
reason:
'Expected lvalues to not be null when assigning scopes. ' +
'Pruning lvalues too early can result in missing scope information.',
description: null,
loc: instr.loc,
suggestions: null,
});
for (const operand of eachInstructionLValue(instr)) {
const operandScope = getPlaceScope(instr.id, operand);
if (operandScope !== null) {
return operandScope;
}
}
for (const operand of eachReactiveValueOperand(instr.value)) {
const operandScope = getPlaceScope(instr.id, operand);
if (operandScope !== null) {
return operandScope;
}
}
return null;
}
export function getPlaceScope(
id: InstructionId,
place: Place,
): ReactiveScope | null {
const scope = place.identifier.scope;
if (scope !== null && isScopeActive(scope, id)) {
return scope;
}
return null;
}
function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
return id >= scope.range.start && id < scope.range.end;
}

View File

@@ -34,6 +34,7 @@ import {
ReactiveInstruction,
ReactiveScope,
ReactiveScopeBlock,
ReactiveScopeDeclaration,
ReactiveScopeDependency,
ReactiveTerminal,
ReactiveValue,
@@ -572,7 +573,8 @@ function codegenReactiveScope(
const changeExpressions: Array<t.Expression> = [];
const changeExpressionComments: Array<string> = [];
const outputComments: Array<string> = [];
for (const dep of scope.dependencies) {
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
const index = cx.nextCacheIndex;
changeExpressionComments.push(printDependencyComment(dep));
const comparison = t.binaryExpression(
@@ -615,7 +617,10 @@ function codegenReactiveScope(
);
}
let firstOutputIndex: number | null = null;
for (const [, {identifier}] of scope.declarations) {
for (const [, {identifier}] of [...scope.declarations].sort(([, a], [, b]) =>
compareScopeDeclaration(a, b),
)) {
const index = cx.nextCacheIndex;
if (firstOutputIndex === null) {
firstOutputIndex = index;
@@ -981,22 +986,12 @@ function codegenTerminal(
suggestions: null,
});
case InstructionKind.Catch:
CompilerError.invariant(false, {
reason: 'Unexpected catch variable as for..in collection',
description: null,
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.HoistedConst:
CompilerError.invariant(false, {
reason: 'Unexpected HoistedConst variable in for..in collection',
description: null,
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.HoistedLet:
case InstructionKind.HoistedFunction:
case InstructionKind.Function:
CompilerError.invariant(false, {
reason: 'Unexpected HoistedLet variable in for..in collection',
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`,
description: null,
loc: iterableItem.loc,
suggestions: null,
@@ -1075,30 +1070,13 @@ function codegenTerminal(
varDeclKind = 'let' as const;
break;
case InstructionKind.Reassign:
CompilerError.invariant(false, {
reason:
'Destructure should never be Reassign as it would be an Object/ArrayPattern',
description: null,
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.Catch:
CompilerError.invariant(false, {
reason: 'Unexpected catch variable as for..of collection',
description: null,
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.HoistedConst:
CompilerError.invariant(false, {
reason: 'Unexpected HoistedConst variable in for..of collection',
description: null,
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.HoistedLet:
case InstructionKind.HoistedFunction:
case InstructionKind.Function:
CompilerError.invariant(false, {
reason: 'Unexpected HoistedLet variable in for..of collection',
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`,
description: null,
loc: iterableItem.loc,
suggestions: null,
@@ -1261,6 +1239,35 @@ function codegenInstructionNullable(
t.variableDeclarator(codegenLValue(cx, lvalue), value),
]);
}
case InstructionKind.Function: {
CompilerError.invariant(instr.lvalue === null, {
reason: `Function declaration cannot be referenced as an expression`,
description: null,
loc: instr.value.loc,
suggestions: null,
});
const genLvalue = codegenLValue(cx, lvalue);
CompilerError.invariant(genLvalue.type === 'Identifier', {
reason: 'Expected an identifier as a function declaration lvalue',
description: null,
loc: instr.value.loc,
suggestions: null,
});
CompilerError.invariant(value?.type === 'FunctionExpression', {
reason: 'Expected a function as a function declaration value',
description: null,
loc: instr.value.loc,
suggestions: null,
});
return createFunctionDeclaration(
instr.loc,
genLvalue,
value.params,
value.body,
value.generator,
value.async,
);
}
case InstructionKind.Let: {
CompilerError.invariant(instr.lvalue === null, {
reason: `Const declaration cannot be referenced as an expression`,
@@ -1303,19 +1310,11 @@ function codegenInstructionNullable(
case InstructionKind.Catch: {
return t.emptyStatement();
}
case InstructionKind.HoistedLet: {
case InstructionKind.HoistedLet:
case InstructionKind.HoistedConst:
case InstructionKind.HoistedFunction: {
CompilerError.invariant(false, {
reason:
'Expected HoistedLet to have been pruned in PruneHoistedContexts',
description: null,
loc: instr.loc,
suggestions: null,
});
}
case InstructionKind.HoistedConst: {
CompilerError.invariant(false, {
reason:
'Expected HoistedConsts to have been pruned in PruneHoistedContexts',
reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`,
description: null,
loc: instr.loc,
suggestions: null,
@@ -1486,6 +1485,7 @@ const createBinaryExpression = withLoc(t.binaryExpression);
const createExpressionStatement = withLoc(t.expressionStatement);
const _createLabelledStatement = withLoc(t.labeledStatement);
const createVariableDeclaration = withLoc(t.variableDeclaration);
const createFunctionDeclaration = withLoc(t.functionDeclaration);
const _createWhileStatement = withLoc(t.whileStatement);
const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
const createLogicalExpression = withLoc(t.logicalExpression);
@@ -2571,3 +2571,45 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
);
return t.identifier(identifier.name.value);
}
function compareScopeDependency(
a: ReactiveScopeDependency,
b: ReactiveScopeDependency,
): number {
CompilerError.invariant(
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
{
reason: '[Codegen] Expected named identifier for dependency',
loc: a.identifier.loc,
},
);
const aName = [
a.identifier.name.value,
...a.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
].join('.');
const bName = [
b.identifier.name.value,
...b.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
].join('.');
if (aName < bName) return -1;
else if (aName > bName) return 1;
else return 0;
}
function compareScopeDeclaration(
a: ReactiveScopeDeclaration,
b: ReactiveScopeDeclaration,
): number {
CompilerError.invariant(
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
{
reason: '[Codegen] Expected named identifier for declaration',
loc: a.identifier.loc,
},
);
const aName = a.identifier.name.value;
const bName = b.identifier.name.value;
if (aName < bName) return -1;
else if (aName > bName) return 1;
else return 0;
}

View File

@@ -1,84 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
ReactiveFunction,
ReactiveScopeBlock,
ReactiveStatement,
ReactiveTerminal,
ReactiveTerminalStatement,
} from '../HIR/HIR';
import {assertExhaustive} from '../Utils/utils';
import {
ReactiveFunctionTransform,
Transformed,
visitReactiveFunction,
} from './visitors';
/*
* Given a reactive function, flattens any scopes contained within a loop construct.
* We won't initially support memoization within loops though this is possible in the future.
*/
export function flattenReactiveLoops(fn: ReactiveFunction): void {
visitReactiveFunction(fn, new Transform(), false);
}
class Transform extends ReactiveFunctionTransform<boolean> {
override transformScope(
scope: ReactiveScopeBlock,
isWithinLoop: boolean,
): Transformed<ReactiveStatement> {
this.visitScope(scope, isWithinLoop);
if (isWithinLoop) {
return {
kind: 'replace',
value: {
kind: 'pruned-scope',
scope: scope.scope,
instructions: scope.instructions,
},
};
} else {
return {kind: 'keep'};
}
}
override visitTerminal(
stmt: ReactiveTerminalStatement<ReactiveTerminal>,
isWithinLoop: boolean,
): void {
switch (stmt.terminal.kind) {
// Loop terminals flatten nested scopes
case 'do-while':
case 'while':
case 'for':
case 'for-of':
case 'for-in': {
this.traverseTerminal(stmt, true);
break;
}
// Non-loop terminals passthrough is contextual, inherits the parent isWithinScope
case 'try':
case 'label':
case 'break':
case 'continue':
case 'if':
case 'return':
case 'switch':
case 'throw': {
this.traverseTerminal(stmt, isWithinLoop);
break;
}
default: {
assertExhaustive(
stmt.terminal,
`Unexpected terminal kind \`${(stmt.terminal as any).kind}\``,
);
}
}
}
}

View File

@@ -1,123 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
Environment,
InstructionId,
ReactiveFunction,
ReactiveScopeBlock,
ReactiveStatement,
ReactiveValue,
getHookKind,
isUseOperator,
} from '../HIR';
import {
ReactiveFunctionTransform,
Transformed,
visitReactiveFunction,
} from './visitors';
/**
* For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are different
* from regular functions in two key ways:
* - They can introduce reactivity even when their arguments are non-reactive (accounted for in InferReactivePlaces)
* - They cannot be called conditionally
*
* The `use` operator is similar:
* - It can access context, and therefore introduce reactivity
* - It can be called conditionally, but _it must be called if the component needs the return value_. This is because
* React uses the fact that use was called to remember that the component needs the value, and that changes to the
* input should invalidate the component itself.
*
* This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the reasoning is slightly
* different for reach, the result is that we can't memoize scopes that call hooks or use since this would make them
* called conditionally in the output.
*
* The pass finds and removes any scopes that transitively contain a hook or use call. By running all
* the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
* describe the set of values which "construct together", and remove _all_ that memoization in order
* to ensure the hook call does not inadvertently become conditional.
*/
export function flattenScopesWithHooksOrUse(fn: ReactiveFunction): void {
visitReactiveFunction(fn, new Transform(), {
env: fn.env,
hasHook: false,
});
}
type State = {
env: Environment;
hasHook: boolean;
};
class Transform extends ReactiveFunctionTransform<State> {
override transformScope(
scope: ReactiveScopeBlock,
outerState: State,
): Transformed<ReactiveStatement> {
const innerState: State = {
env: outerState.env,
hasHook: false,
};
this.visitScope(scope, innerState);
outerState.hasHook ||= innerState.hasHook;
if (innerState.hasHook) {
if (scope.instructions.length === 1) {
/*
* This was a scope just for a hook call, which doesn't need memoization.
* flatten it away
*/
return {
kind: 'replace-many',
value: scope.instructions,
};
}
/*
* else this scope had multiple instructions and produced some other value:
* mark it as pruned
*/
return {
kind: 'replace',
value: {
kind: 'pruned-scope',
scope: scope.scope,
instructions: scope.instructions,
},
};
} else {
return {kind: 'keep'};
}
}
override visitValue(
id: InstructionId,
value: ReactiveValue,
state: State,
): void {
this.traverseValue(id, value, state);
switch (value.kind) {
case 'CallExpression': {
if (
getHookKind(state.env, value.callee.identifier) != null ||
isUseOperator(value.callee.identifier)
) {
state.hasHook = true;
}
break;
}
case 'MethodCall': {
if (
getHookKind(state.env, value.property.identifier) != null ||
isUseOperator(value.property.identifier)
) {
state.hasHook = true;
}
break;
}
}
}
}

View File

@@ -13,6 +13,8 @@ import {
HIRFunction,
Identifier,
Instruction,
InstructionId,
MutableRange,
Place,
ReactiveScope,
makeInstructionId,
@@ -186,8 +188,14 @@ function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation {
}
// Is the operand mutable at this given instruction
export function isMutable({id}: Instruction, place: Place): boolean {
const range = place.identifier.mutableRange;
export function isMutable(instr: {id: InstructionId}, place: Place): boolean {
return inRange(instr, place.identifier.mutableRange);
}
export function inRange(
{id}: {id: InstructionId},
range: MutableRange,
): boolean {
return id >= range.start && id < range.end;
}
@@ -227,6 +235,7 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
case 'StoreGlobal': {
return false;
}
case 'TaggedTemplateExpression':
case 'CallExpression':
case 'MethodCall': {
return instruction.lvalue.identifier.type.kind !== 'Primitive';
@@ -241,8 +250,7 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
case 'ObjectExpression':
case 'UnsupportedNode':
case 'ObjectMethod':
case 'FunctionExpression':
case 'TaggedTemplateExpression': {
case 'FunctionExpression': {
return true;
}
default: {
@@ -273,22 +281,25 @@ export function findDisjointMutableValues(
*/
for (const phi of block.phis) {
if (
phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
phi.id.mutableRange.end >
phi.place.identifier.mutableRange.start + 1 !==
phi.place.identifier.mutableRange.end &&
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id)
) {
const operands = [phi.id];
const declaration = declarations.get(phi.id.declarationId);
const operands = [phi.place.identifier];
const declaration = declarations.get(
phi.place.identifier.declarationId,
);
if (declaration !== undefined) {
operands.push(declaration);
}
for (const [_, phiId] of phi.operands) {
operands.push(phiId);
operands.push(phiId.identifier);
}
scopeIdentifiers.union(operands);
} else if (fn.env.config.enableForest) {
for (const [, phiId] of phi.operands) {
scopeIdentifiers.union([phi.id, phiId]);
scopeIdentifiers.union([phi.place.identifier, phiId.identifier]);
}
}
}

View File

@@ -1,281 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
InstructionId,
makeInstructionId,
Place,
ReactiveBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveScope,
ScopeId,
} from '../HIR';
import DisjointSet from '../Utils/DisjointSet';
import {retainWhere} from '../Utils/utils';
import {getPlaceScope} from './BuildReactiveBlocks';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
/*
* Note: this is the 3rd of 4 passes that determine how to break a function into discrete
* reactive scopes (independently memoizeable units of code):
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
* them a unique reactive scope.
* 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
* to block scopes.
* 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
* scopes do not overlap, merging any such scopes.
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
* a ReactiveScopeBlock.
*
* Previous passes may leave "overlapping" scopes, ie where one or more instructions are within
* the mutable range of multiple reactive scopes. We prefer to avoid executing instructions twice
* for performance reasons (side effects are less of a concern bc components are required to be
* idempotent), so we cannot simply repeat the instruction once for each scope. Instead, the only
* option is to combine the two scopes into one. This is an area where an eventual Forget IDE
* could provide real-time feedback to the developer that two computations are accidentally merged.
*
* ## Detailed Walkthrough
*
* Two scopes overlap if there is one or more instruction that is inside the range
* of both scopes. In general, overlapping scopes are merged togther. The only
* exception to this is when one scope *shadows* another scope. For example:
*
* ```javascript
* function foo(cond, a) {
* ⌵ scope for x
* let x = []; ⌝
* if (cond) { ⎮
* ⌵ scope for y ⎮
* let y = []; ⌝ ⎮
* if (b) { ⎮ ⎮
* y.push(b); ⌟ ⎮
* } ⎮
* x.push(<div>{y}</div>); ⎮
* } ⌟
* }
* ```
*
* In this example the two scopes overlap, but mutation of the two scopes is not
* interleaved. Specifically within the y scope there are no instructions that
* modify any other scope: the inner scope "shadows" the outer one. This category
* of overlap does *NOT* merge the scopes together.
*
* The implementation is inspired by the Rust notion of "stacked borrows". We traverse
* the control-flow graph in tree form, at each point keeping track of which scopes are
* active. So initially we see
*
* `let x = []`
* active scopes: [x]
*
* and mark the x scope as active.
*
* Then we later encounter
*
* `let y = [];`
* active scopes: [x, y]
*
* Here we first check to see if 'y' is already in the list of active scopes. It isn't,
* so we push it to the stop of the stack.
*
* Then
*
* `y.push(b)`
* active scopes: [x, y]
*
* Mutates y, so we check if y is the top of the stack. It is, so no merging must occur.
*
* If instead we saw eg
*
* `x.push(b)`
* active scopes: [x, y]
*
* Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
* to be merged.
*/
export function mergeOverlappingReactiveScopes(fn: ReactiveFunction): void {
const context = new Context();
visitReactiveFunction(fn, new Visitor(), context);
context.complete();
}
class Visitor extends ReactiveFunctionVisitor<Context> {
override visitID(id: InstructionId, state: Context): void {
state.visitId(id);
}
override visitPlace(id: InstructionId, place: Place, state: Context): void {
state.visitPlace(id, place);
}
override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
state.visitPlace(id, lvalue);
}
override visitBlock(block: ReactiveBlock, state: Context): void {
state.enter(() => {
this.traverseBlock(block, state);
});
}
override visitInstruction(
instruction: ReactiveInstruction,
state: Context,
): void {
if (
instruction.value.kind === 'ConditionalExpression' ||
instruction.value.kind === 'LogicalExpression' ||
instruction.value.kind === 'OptionalExpression'
) {
state.enter(() => {
super.visitInstruction(instruction, state);
});
} else {
super.visitInstruction(instruction, state);
}
}
}
class BlockScope {
seen: Set<ScopeId> = new Set();
scopes: Array<ShadowableReactiveScope> = [];
}
type ShadowableReactiveScope = {
scope: ReactiveScope;
shadowedBy: ReactiveScope | null;
};
class Context {
scopes: Array<BlockScope> = [];
seenScopes: Set<ScopeId> = new Set();
joinedScopes: DisjointSet<ReactiveScope> = new DisjointSet();
operandScopes: Map<Place, ReactiveScope> = new Map();
visitId(id: InstructionId): void {
const currentBlock = this.scopes[this.scopes.length - 1]!;
retainWhere(currentBlock.scopes, pending => {
if (pending.scope.range.end > id) {
return true;
} else {
currentBlock.seen.delete(pending.scope.id);
return false;
}
});
}
visitPlace(id: InstructionId, place: Place): void {
const scope = getPlaceScope(id, place);
if (scope === null) {
return;
}
this.operandScopes.set(place, scope);
const currentBlock = this.scopes[this.scopes.length - 1]!;
// Fast-path for the first time we see a new scope
if (!this.seenScopes.has(scope.id)) {
this.seenScopes.add(scope.id);
currentBlock.seen.add(scope.id);
currentBlock.scopes.push({shadowedBy: null, scope});
return;
}
// Scope has already been seen, find it in the current block or a parent
let index = this.scopes.length - 1;
let nextBlock = currentBlock;
while (!nextBlock.seen.has(scope.id)) {
/*
* scopes that cross control-flow boundaries are merged with overlapping
* scopes
*/
this.joinedScopes.union([scope, ...nextBlock.scopes.map(s => s.scope)]);
index--;
if (index < 0) {
/*
* TODO: handle reassignments in multiple branches. these create new identifiers that
* add an entry to this.seenScopes but which are then removed when their blocks exit.
* this is also wrong for codegen, different versions of an identifier could be cached
* differently and so a reassigned version of a variable needs a separate declaration.
* console.log(`scope ${scope.id} not found`);
*/
/*
* for (let i = this.scopes.length - 1; i > index; i--) {
* const s = this.scopes[i];
* console.log(
* JSON.stringify(
* {
* seen: Array.from(s.seen),
* scopes: s.scopes,
* },
* null,
* 2
* )
* );
* }
*/
currentBlock.seen.add(scope.id);
currentBlock.scopes.push({shadowedBy: null, scope});
return;
}
nextBlock = this.scopes[index]!;
}
// Handle interleaving within a given block scope
let found = false;
for (let i = 0; i < nextBlock.scopes.length; i++) {
const current = nextBlock.scopes[i]!;
if (current.scope.id === scope.id) {
found = true;
if (current.shadowedBy !== null) {
this.joinedScopes.union([current.shadowedBy, current.scope]);
}
} else if (found && current.shadowedBy === null) {
// `scope` is shadowing `current` and may interleave
current.shadowedBy = scope;
if (current.scope.range.end > scope.range.end) {
/*
* Current is shadowed by `scope`, and we know that `current` will mutate
* again (per its range), so the scopes are already known to interleave.
*
* Eagerly extend the ranges of the scopes so that we don't prematurely end
* a scope relative to its eventual post-merge mutable range
*/
const end = makeInstructionId(
Math.max(current.scope.range.end, scope.range.end),
);
current.scope.range.end = end;
scope.range.end = end;
this.joinedScopes.union([current.scope, scope]);
}
}
}
if (!currentBlock.seen.has(scope.id)) {
currentBlock.seen.add(scope.id);
currentBlock.scopes.push({shadowedBy: null, scope});
}
}
enter(fn: () => void): void {
this.scopes.push(new BlockScope());
fn();
this.scopes.pop();
}
complete(): void {
this.joinedScopes.forEach((scope, groupScope) => {
if (scope !== groupScope) {
groupScope.range.start = makeInstructionId(
Math.min(groupScope.range.start, scope.range.start),
);
groupScope.range.end = makeInstructionId(
Math.max(groupScope.range.end, scope.range.end),
);
}
});
for (const [operand, originalScope] of this.operandScopes) {
const mergedScope = this.joinedScopes.find(originalScope);
if (mergedScope !== null) {
operand.identifier.scope = mergedScope;
}
}
}
}

View File

@@ -57,6 +57,17 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
return {kind: 'remove'};
}
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedFunction'
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Function,
);
return {kind: 'remove'};
}
if (
instruction.value.kind === 'StoreContext' &&
state.has(instruction.value.lvalue.place.identifier.declarationId)

View File

@@ -26,7 +26,7 @@ import {
} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
import {getPlaceScope} from './BuildReactiveBlocks';
import {getPlaceScope} from '../HIR/HIR';
import {
ReactiveFunctionTransform,
ReactiveFunctionVisitor,
@@ -671,12 +671,11 @@ function computeMemoizationInputs(
],
};
}
case 'CallExpression': {
case 'TaggedTemplateExpression': {
const signature = getFunctionCallSignature(
env,
value.callee.identifier.type,
value.tag.identifier.type,
);
const operands = [...eachReactiveValueOperand(value)];
let lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
@@ -687,6 +686,33 @@ function computeMemoizationInputs(
rvalues: [],
};
}
const operands = [...eachReactiveValueOperand(value)];
lvalues.push(
...operands
.filter(operand => isMutableEffect(operand.effect, operand.loc))
.map(place => ({place, level: MemoizationLevel.Memoized})),
);
return {
lvalues,
rvalues: operands,
};
}
case 'CallExpression': {
const signature = getFunctionCallSignature(
env,
value.callee.identifier.type,
);
let lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
}
if (signature?.noAlias === true) {
return {
lvalues,
rvalues: [],
};
}
const operands = [...eachReactiveValueOperand(value)];
lvalues.push(
...operands
.filter(operand => isMutableEffect(operand.effect, operand.loc))
@@ -702,7 +728,6 @@ function computeMemoizationInputs(
env,
value.property.identifier.type,
);
const operands = [...eachReactiveValueOperand(value)];
let lvalues = [];
if (lvalue !== null) {
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
@@ -713,6 +738,7 @@ function computeMemoizationInputs(
rvalues: [],
};
}
const operands = [...eachReactiveValueOperand(value)];
lvalues.push(
...operands
.filter(operand => isMutableEffect(operand.effect, operand.loc))
@@ -726,7 +752,6 @@ function computeMemoizationInputs(
case 'RegExpLiteral':
case 'ObjectMethod':
case 'FunctionExpression':
case 'TaggedTemplateExpression':
case 'ArrayExpression':
case 'NewExpression':
case 'ObjectExpression':

View File

@@ -6,23 +6,17 @@
*/
export {alignObjectMethodScopes} from './AlignObjectMethodScopes';
export {alignReactiveScopesToBlockScopes} from './AlignReactiveScopesToBlockScopes';
export {assertScopeInstructionsWithinScopes} from './AssertScopeInstructionsWithinScope';
export {assertWellFormedBreakTargets} from './AssertWellFormedBreakTargets';
export {buildReactiveBlocks} from './BuildReactiveBlocks';
export {buildReactiveFunction} from './BuildReactiveFunction';
export {codegenFunction, type CodegenFunction} from './CodegenReactiveFunction';
export {extractScopeDeclarationsFromDestructuring} from './ExtractScopeDeclarationsFromDestructuring';
export {flattenReactiveLoops} from './FlattenReactiveLoops';
export {flattenScopesWithHooksOrUse} from './FlattenScopesWithHooksOrUse';
export {inferReactiveScopeVariables} from './InferReactiveScopeVariables';
export {memoizeFbtAndMacroOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope';
export {mergeOverlappingReactiveScopes} from './MergeOverlappingReactiveScopes';
export {mergeReactiveScopesThatInvalidateTogether} from './MergeReactiveScopesThatInvalidateTogether';
export {printReactiveFunction} from './PrintReactiveFunction';
export {promoteUsedTemporaries} from './PromoteUsedTemporaries';
export {propagateEarlyReturns} from './PropagateEarlyReturns';
export {propagateScopeDependencies} from './PropagateScopeDependencies';
export {pruneAllReactiveScopes} from './PruneAllReactiveScopes';
export {pruneHoistedContexts} from './PruneHoistedContexts';
export {pruneNonEscapingScopes} from './PruneNonEscapingScopes';

View File

@@ -68,18 +68,13 @@ export function eliminateRedundantPhi(
// Find any redundant phis
phis: for (const phi of block.phis) {
// Remap phis in case operands are from eliminated phis
phi.operands = new Map(
Array.from(phi.operands).map(([block, id]) => [
block,
rewrites.get(id) ?? id,
]),
);
phi.operands.forEach((place, _) => rewritePlace(place, rewrites));
// Find if the phi can be eliminated
let same: Identifier | null = null;
for (const [_, operand] of phi.operands) {
if (
(same !== null && operand.id === same.id) ||
operand.id === phi.id.id
(same !== null && operand.identifier.id === same.id) ||
operand.identifier.id === phi.place.identifier.id
) {
/*
* This operand is the same as the phi or is the same as the
@@ -94,7 +89,7 @@ export function eliminateRedundantPhi(
continue phis;
} else {
// First non-phi operand
same = operand;
same = operand.identifier;
}
}
CompilerError.invariant(same !== null, {
@@ -103,7 +98,7 @@ export function eliminateRedundantPhi(
loc: null,
suggestions: null,
});
rewrites.set(phi.id, same);
rewrites.set(phi.place.identifier, same);
block.phis.delete(phi);
}

View File

@@ -18,7 +18,7 @@ import {
Phi,
Place,
} from '../HIR/HIR';
import {printIdentifier} from '../HIR/PrintHIR';
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
import {
eachTerminalSuccessor,
mapInstructionLValues,
@@ -27,8 +27,8 @@ import {
} from '../HIR/visitors';
type IncompletePhi = {
oldId: Identifier;
newId: Identifier;
oldPlace: Place;
newPlace: Place;
};
type State = {
@@ -122,33 +122,33 @@ class SSABuilder {
}
getPlace(oldPlace: Place): Place {
const newId = this.getIdAt(oldPlace.identifier, this.#current!.id);
const newId = this.getIdAt(oldPlace, this.#current!.id);
return {
...oldPlace,
identifier: newId,
};
}
getIdAt(oldId: Identifier, blockId: BlockId): Identifier {
getIdAt(oldPlace: Place, blockId: BlockId): Identifier {
// check if Place is defined locally
const block = this.#blocks.get(blockId)!;
const state = this.#states.get(block)!;
if (state.defs.has(oldId)) {
return state.defs.get(oldId)!;
if (state.defs.has(oldPlace.identifier)) {
return state.defs.get(oldPlace.identifier)!;
}
if (block.preds.size == 0) {
/*
* We're at the entry block and haven't found our defintion yet.
* console.log(
* `Unable to find "${printIdentifier(
* oldId
* `Unable to find "${printPlace(
* oldPlace
* )}" in bb${blockId}, assuming it's a global`
* );
*/
this.#unknown.add(oldId);
return oldId;
this.#unknown.add(oldPlace.identifier);
return oldPlace.identifier;
}
if (this.unsealedPreds.get(block)! > 0) {
@@ -156,52 +156,55 @@ class SSABuilder {
* We haven't visited all our predecessors, let's place an incomplete phi
* for now.
*/
const newId = this.makeId(oldId);
state.incompletePhis.push({oldId, newId});
state.defs.set(oldId, newId);
const newId = this.makeId(oldPlace.identifier);
state.incompletePhis.push({
oldPlace,
newPlace: {...oldPlace, identifier: newId},
});
state.defs.set(oldPlace.identifier, newId);
return newId;
}
// Only one predecessor, let's check there
if (block.preds.size == 1) {
const [pred] = block.preds;
const newId = this.getIdAt(oldId, pred);
state.defs.set(oldId, newId);
const newId = this.getIdAt(oldPlace, pred);
state.defs.set(oldPlace.identifier, newId);
return newId;
}
// There are multiple predecessors, we may need a phi.
const newId = this.makeId(oldId);
const newId = this.makeId(oldPlace.identifier);
/*
* Adding a phi may loop back to our block if there is a loop in the CFG. We
* update our defs before adding the phi to terminate the recursion rather than
* looping infinitely.
*/
state.defs.set(oldId, newId);
return this.addPhi(block, oldId, newId);
state.defs.set(oldPlace.identifier, newId);
return this.addPhi(block, oldPlace, {...oldPlace, identifier: newId});
}
addPhi(block: BasicBlock, oldId: Identifier, newId: Identifier): Identifier {
const predDefs: Map<BlockId, Identifier> = new Map();
addPhi(block: BasicBlock, oldPlace: Place, newPlace: Place): Identifier {
const predDefs: Map<BlockId, Place> = new Map();
for (const predBlockId of block.preds) {
const predId = this.getIdAt(oldId, predBlockId);
predDefs.set(predBlockId, predId);
const predId = this.getIdAt(oldPlace, predBlockId);
predDefs.set(predBlockId, {...oldPlace, identifier: predId});
}
const phi: Phi = {
kind: 'Phi',
id: newId,
place: newPlace,
operands: predDefs,
};
block.phis.add(phi);
return newId;
return newPlace.identifier;
}
fixIncompletePhis(block: BasicBlock): void {
const state = this.#states.get(block)!;
for (const phi of state.incompletePhis) {
this.addPhi(block, phi.oldId, phi.newId);
this.addPhi(block, phi.oldPlace, phi.newPlace);
}
}
@@ -223,9 +226,9 @@ class SSABuilder {
for (const incompletePhi of state.incompletePhis) {
text.push(
` iphi \$${printIdentifier(
incompletePhi.newId,
)} = \$${printIdentifier(incompletePhi.oldId)}`,
` iphi \$${printPlace(
incompletePhi.newPlace,
)} = \$${printPlace(incompletePhi.oldPlace)}`,
);
}
}

View File

@@ -69,7 +69,7 @@ export function inferTypes(func: HIRFunction): void {
function apply(func: HIRFunction, unifier: Unifier): void {
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
phi.id.type = unifier.get(phi.id.type);
phi.place.identifier.type = unifier.get(phi.place.identifier.type);
}
for (const instr of block.instructions) {
for (const operand of eachInstructionLValue(instr)) {
@@ -107,7 +107,7 @@ function equation(left: Type, right: Type): TypeEquation {
function* generate(
func: HIRFunction,
): Generator<TypeEquation, void, undefined> {
if (func.env.fnType === 'Component') {
if (func.fnType === 'Component') {
const [props, ref] = func.params;
if (props && props.kind === 'Identifier') {
yield equation(props.identifier.type, {
@@ -127,9 +127,9 @@ function* generate(
const returnTypes: Array<Type> = [];
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
yield equation(phi.id.type, {
yield equation(phi.place.identifier.type, {
kind: 'Phi',
operands: [...phi.operands.values()].map(id => id.type),
operands: [...phi.operands.values()].map(id => id.identifier.type),
});
}
@@ -250,6 +250,7 @@ function* generateInstructionTypes(
}
case 'CallExpression': {
const returnType = makeType();
/*
* TODO: callee could be a hook or a function, so this type equation isn't correct.
* We should change Hook to a subtype of Function or change unifier logic.
@@ -258,8 +259,25 @@ function* generateInstructionTypes(
yield equation(value.callee.identifier.type, {
kind: 'Function',
shapeId: null,
return: left,
return: returnType,
});
yield equation(left, returnType);
break;
}
case 'TaggedTemplateExpression': {
const returnType = makeType();
/*
* TODO: callee could be a hook or a function, so this type equation isn't correct.
* We should change Hook to a subtype of Function or change unifier logic.
* (see https://github.com/facebook/react-forget/pull/1427)
*/
yield equation(value.tag.identifier.type, {
kind: 'Function',
shapeId: null,
return: returnType,
});
yield equation(left, returnType);
break;
}
@@ -392,7 +410,6 @@ function* generateInstructionTypes(
case 'MetaProperty':
case 'ComputedStore':
case 'ComputedLoad':
case 'TaggedTemplateExpression':
case 'Await':
case 'GetIterator':
case 'IteratorNext':

View File

@@ -62,21 +62,24 @@ export function propagatePhiTypes(fn: HIRFunction): void {
* We also don't propagate scopes for named variables, to preserve compatibility
* with previous LeaveSSA behavior.
*/
if (phi.id.type.kind !== 'Type' || phi.id.name !== null) {
if (
phi.place.identifier.type.kind !== 'Type' ||
phi.place.identifier.name !== null
) {
continue;
}
let type: Type | null = null;
for (const [, operand] of phi.operands) {
if (type === null) {
type = operand.type;
} else if (!typeEquals(type, operand.type)) {
type = operand.identifier.type;
} else if (!typeEquals(type, operand.identifier.type)) {
type = null;
break;
}
}
if (type !== null) {
phi.id.type = type;
propagated.add(phi.id.id);
phi.place.identifier.type = type;
propagated.add(phi.place.identifier.id);
}
}
for (const instr of block.instructions) {

View File

@@ -82,17 +82,45 @@ export function getOrInsertDefault<U, V>(
return defaultValue;
}
}
export function Set_union<T>(a: Set<T>, b: Set<T>): Set<T> {
const union = new Set<T>();
export function Set_equal<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
if (a.size !== b.size) {
return false;
}
for (const item of a) {
if (b.has(item)) {
union.add(item);
if (!b.has(item)) {
return false;
}
}
return true;
}
export function Set_union<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
const union = new Set<T>(a);
for (const item of b) {
union.add(item);
}
return union;
}
export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
if (sets.length === 0 || sets.some(s => s.size === 0)) {
return new Set();
} else if (sets.length === 1) {
return new Set(sets[0]);
}
const result: Set<T> = new Set();
const first = sets[0];
outer: for (const e of first) {
for (let i = 1; i < sets.length; i++) {
if (!sets[i].has(e)) {
continue outer;
}
}
result.add(e);
}
return result;
}
export function Iterable_some<T>(
iter: Iterable<T>,
pred: (item: T) => boolean,
@@ -111,6 +139,19 @@ export function nonNull<T extends NonNullable<U>, U>(
return value != null;
}
export function Set_filter<T>(
source: ReadonlySet<T>,
fn: (arg: T) => boolean,
): Set<T> {
const result = new Set<T>();
for (const entry of source) {
if (fn(entry)) {
result.add(entry);
}
}
return result;
}
export function hasNode<T>(
input: NodePath<T | null | undefined>,
): input is NodePath<NonNullable<T>> {

View File

@@ -198,11 +198,12 @@ export function validateHooksUsage(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
let kind: Kind =
phi.id.name !== null && isHookName(phi.id.name.value)
phi.place.identifier.name !== null &&
isHookName(phi.place.identifier.name.value)
? Kind.PotentialHook
: Kind.Local;
for (const [, operand] of phi.operands) {
const operandKind = valueKinds.get(operand.id);
const operandKind = valueKinds.get(operand.identifier.id);
/*
* NOTE: we currently skip operands whose value is unknown
* (which can only occur for functions with loops), we may
@@ -213,7 +214,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
kind = joinKinds(kind, operandKind);
}
}
valueKinds.set(phi.id.id, kind);
valueKinds.set(phi.place.identifier.id, kind);
}
for (const instr of block.instructions) {
switch (instr.value.kind) {

View File

@@ -161,6 +161,14 @@ function getContextReassignment(
if (signature?.noAlias) {
operands = [value.receiver, value.property];
}
} else if (value.kind === 'TaggedTemplateExpression') {
const signature = getFunctionCallSignature(
fn.env,
value.tag.identifier.type,
);
if (signature?.noAlias) {
operands = [value.tag];
}
}
for (const operand of operands) {
CompilerError.invariant(operand.effect !== Effect.Unknown, {

View File

@@ -7,20 +7,22 @@
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
BlockId,
HIRFunction,
IdentifierId,
Place,
SourceLocation,
getHookKindForType,
isRefValueType,
isUseRefType,
} from '../HIR';
import {
eachInstructionOperand,
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {isEffectHook} from './ValidateMemoizedEffectDependencies';
/**
* Validates that a function does not access a ref value during render. This includes a partial check
@@ -42,25 +44,207 @@ import {isEffectHook} from './ValidateMemoizedEffectDependencies';
* In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref)
* or based on property name alone (`foo.current` might be a ref).
*/
type State = {
refs: Set<IdentifierId>;
refValues: Map<IdentifierId, SourceLocation | null>;
refAccessingFunctions: Set<IdentifierId>;
};
const opaqueRefId = Symbol();
type RefId = number & {[opaqueRefId]: 'RefId'};
function makeRefId(id: number): RefId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected identifier id to be a non-negative integer',
description: null,
loc: null,
suggestions: null,
});
return id as RefId;
}
let _refId = 0;
function nextRefId(): RefId {
return makeRefId(_refId++);
}
type RefAccessType =
| {kind: 'None'}
| {kind: 'Nullable'}
| {kind: 'Guard'; refId: RefId}
| RefAccessRefType;
type RefAccessRefType =
| {kind: 'Ref'; refId: RefId}
| {kind: 'RefValue'; loc?: SourceLocation; refId?: RefId}
| {kind: 'Structure'; value: null | RefAccessRefType; fn: null | RefFnType};
type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
class Env extends Map<IdentifierId, RefAccessType> {
#changed = false;
resetChanged(): void {
this.#changed = false;
}
hasChanged(): boolean {
return this.#changed;
}
override set(key: IdentifierId, value: RefAccessType): this {
const cur = this.get(key);
const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'});
if (
!(cur == null && widenedValue.kind === 'None') &&
(cur == null || !tyEqual(cur, widenedValue))
) {
this.#changed = true;
}
return super.set(key, widenedValue);
}
}
export function validateNoRefAccessInRender(fn: HIRFunction): void {
const state = {
refs: new Set<IdentifierId>(),
refValues: new Map<IdentifierId, SourceLocation | null>(),
refAccessingFunctions: new Set<IdentifierId>(),
};
validateNoRefAccessInRenderImpl(fn, state).unwrap();
const env = new Env();
validateNoRefAccessInRenderImpl(fn, env).unwrap();
}
function refTypeOfType(place: Place): RefAccessType {
if (isRefValueType(place.identifier)) {
return {kind: 'RefValue'};
} else if (isUseRefType(place.identifier)) {
return {kind: 'Ref', refId: nextRefId()};
} else {
return {kind: 'None'};
}
}
function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
if (a.kind !== b.kind) {
return false;
}
switch (a.kind) {
case 'None':
return true;
case 'Ref':
return true;
case 'Nullable':
return true;
case 'Guard':
CompilerError.invariant(b.kind === 'Guard', {
reason: 'Expected ref value',
loc: null,
});
return a.refId === b.refId;
case 'RefValue':
CompilerError.invariant(b.kind === 'RefValue', {
reason: 'Expected ref value',
loc: null,
});
return a.loc == b.loc;
case 'Structure': {
CompilerError.invariant(b.kind === 'Structure', {
reason: 'Expected structure',
loc: null,
});
const fnTypesEqual =
(a.fn === null && b.fn === null) ||
(a.fn !== null &&
b.fn !== null &&
a.fn.readRefEffect === b.fn.readRefEffect &&
tyEqual(a.fn.returnType, b.fn.returnType));
return (
fnTypesEqual &&
(a.value === b.value ||
(a.value !== null && b.value !== null && tyEqual(a.value, b.value)))
);
}
}
}
function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
function joinRefAccessRefTypes(
a: RefAccessRefType,
b: RefAccessRefType,
): RefAccessRefType {
if (a.kind === 'RefValue') {
if (b.kind === 'RefValue' && a.refId === b.refId) {
return a;
}
return {kind: 'RefValue'};
} else if (b.kind === 'RefValue') {
return b;
} else if (a.kind === 'Ref' || b.kind === 'Ref') {
if (a.kind === 'Ref' && b.kind === 'Ref' && a.refId === b.refId) {
return a;
}
return {kind: 'Ref', refId: nextRefId()};
} else {
CompilerError.invariant(
a.kind === 'Structure' && b.kind === 'Structure',
{
reason: 'Expected structure',
loc: null,
},
);
const fn =
a.fn === null
? b.fn
: b.fn === null
? a.fn
: {
readRefEffect: a.fn.readRefEffect || b.fn.readRefEffect,
returnType: joinRefAccessTypes(
a.fn.returnType,
b.fn.returnType,
),
};
const value =
a.value === null
? b.value
: b.value === null
? a.value
: joinRefAccessRefTypes(a.value, b.value);
return {
kind: 'Structure',
fn,
value,
};
}
}
return types.reduce(
(a, b) => {
if (a.kind === 'None') {
return b;
} else if (b.kind === 'None') {
return a;
} else if (a.kind === 'Guard') {
if (b.kind === 'Guard' && a.refId === b.refId) {
return a;
} else if (b.kind === 'Nullable' || b.kind === 'Guard') {
return {kind: 'None'};
} else {
return b;
}
} else if (b.kind === 'Guard') {
if (a.kind === 'Nullable') {
return {kind: 'None'};
} else {
return b;
}
} else if (a.kind === 'Nullable') {
return b;
} else if (b.kind === 'Nullable') {
return a;
} else {
return joinRefAccessRefTypes(a, b);
}
},
{kind: 'None'},
);
}
function validateNoRefAccessInRenderImpl(
fn: HIRFunction,
state: State,
): Result<void, CompilerError> {
env: Env,
): Result<RefAccessType, CompilerError> {
let returnValues: Array<undefined | RefAccessType> = [];
let place;
for (const param of fn.params) {
if (param.kind === 'Identifier') {
@@ -68,293 +252,357 @@ function validateNoRefAccessInRenderImpl(
} else {
place = param.place;
}
if (isRefValueType(place.identifier)) {
state.refValues.set(place.identifier.id, null);
}
if (isUseRefType(place.identifier)) {
state.refs.add(place.identifier.id);
}
const type = refTypeOfType(place);
env.set(place.identifier.id, type);
}
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
phi.operands.forEach(operand => {
if (state.refs.has(operand.id) || isUseRefType(phi.id)) {
state.refs.add(phi.id.id);
}
const refValue = state.refValues.get(operand.id);
if (refValue !== undefined || isRefValueType(operand)) {
state.refValues.set(
phi.id.id,
refValue ?? state.refValues.get(phi.id.id) ?? null,
);
}
if (state.refAccessingFunctions.has(operand.id)) {
state.refAccessingFunctions.add(phi.id.id);
}
});
}
for (const instr of block.instructions) {
for (const operand of eachInstructionValueOperand(instr.value)) {
if (isRefValueType(operand.identifier)) {
CompilerError.invariant(state.refValues.has(operand.identifier.id), {
reason: 'Expected ref value to be in state',
loc: operand.loc,
});
}
if (isUseRefType(operand.identifier)) {
CompilerError.invariant(state.refs.has(operand.identifier.id), {
reason: 'Expected ref to be in state',
loc: operand.loc,
});
}
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
env.resetChanged();
returnValues = [];
const safeBlocks = new Map<BlockId, RefId>();
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
env.set(
phi.place.identifier.id,
joinRefAccessTypes(
...Array(...phi.operands.values()).map(
operand =>
env.get(operand.identifier.id) ?? ({kind: 'None'} as const),
),
),
);
}
switch (instr.value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoDirectRefValueAccess(errors, operand, state);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad': {
if (typeof instr.value.property !== 'string') {
validateNoRefValueAccess(errors, state, instr.value.property);
}
if (
state.refAccessingFunctions.has(instr.value.object.identifier.id)
) {
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
if (state.refs.has(instr.value.object.identifier.id)) {
/*
* Once an object contains a ref at any level, we treat it as a ref.
* If we look something up from it, that value may either be a ref
* or the ref value (or neither), so we conservatively assume it's both.
*/
state.refs.add(instr.lvalue.identifier.id);
state.refValues.set(instr.lvalue.identifier.id, instr.loc);
}
break;
}
case 'LoadContext':
case 'LoadLocal': {
if (
state.refAccessingFunctions.has(instr.value.place.identifier.id)
) {
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
const refValue = state.refValues.get(instr.value.place.identifier.id);
if (refValue !== undefined) {
state.refValues.set(instr.lvalue.identifier.id, refValue);
}
if (state.refs.has(instr.value.place.identifier.id)) {
state.refs.add(instr.lvalue.identifier.id);
}
break;
}
case 'StoreContext':
case 'StoreLocal': {
if (
state.refAccessingFunctions.has(instr.value.value.identifier.id)
) {
state.refAccessingFunctions.add(
instr.value.lvalue.place.identifier.id,
);
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
const refValue = state.refValues.get(instr.value.value.identifier.id);
if (
refValue !== undefined ||
isRefValueType(instr.value.lvalue.place.identifier)
) {
state.refValues.set(
instr.value.lvalue.place.identifier.id,
refValue ?? null,
);
state.refValues.set(instr.lvalue.identifier.id, refValue ?? null);
}
if (state.refs.has(instr.value.value.identifier.id)) {
state.refs.add(instr.value.lvalue.place.identifier.id);
state.refs.add(instr.lvalue.identifier.id);
}
break;
}
case 'Destructure': {
const destructuredFunction = state.refAccessingFunctions.has(
instr.value.value.identifier.id,
);
const destructuredRef = state.refs.has(
instr.value.value.identifier.id,
);
for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) {
if (isUseRefType(lval.identifier)) {
state.refs.add(lval.identifier.id);
}
if (destructuredRef || isRefValueType(lval.identifier)) {
state.refs.add(lval.identifier.id);
state.refValues.set(lval.identifier.id, null);
}
if (destructuredFunction) {
state.refAccessingFunctions.add(lval.identifier.id);
}
}
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
if (
/*
* check if the function expression accesses a ref *or* some other
* function which accesses a ref
*/
[...eachInstructionValueOperand(instr.value)].some(
operand =>
state.refValues.has(operand.identifier.id) ||
state.refAccessingFunctions.has(operand.identifier.id),
) ||
// check for cases where .current is accessed through an aliased ref
([...eachInstructionValueOperand(instr.value)].some(operand =>
state.refs.has(operand.identifier.id),
) &&
validateNoRefAccessInRenderImpl(
instr.value.loweredFunc.func,
state,
).isErr())
) {
// This function expression unconditionally accesses a ref
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
break;
}
case 'MethodCall': {
if (!isEffectHook(instr.value.property.identifier)) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefAccess(errors, state, operand, operand.loc);
validateNoDirectRefValueAccess(errors, operand, env);
}
break;
}
break;
}
case 'CallExpression': {
const callee = instr.value.callee;
const isUseEffect = isEffectHook(callee.identifier);
if (!isUseEffect) {
// Report a more precise error when calling a local function that accesses a ref
if (state.refAccessingFunctions.has(callee.identifier.id)) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: callee.loc,
description:
callee.identifier.name !== null &&
callee.identifier.name.kind === 'named'
? `Function \`${callee.identifier.name.value}\` accesses a ref`
: null,
suggestions: null,
case 'ComputedLoad':
case 'PropertyLoad': {
if (typeof instr.value.property !== 'string') {
validateNoDirectRefValueAccess(errors, instr.value.property, env);
}
const objType = env.get(instr.value.object.identifier.id);
let lookupType: null | RefAccessType = null;
if (objType?.kind === 'Structure') {
lookupType = objType.value;
} else if (objType?.kind === 'Ref') {
lookupType = {
kind: 'RefValue',
loc: instr.loc,
refId: objType.refId,
};
}
env.set(
instr.lvalue.identifier.id,
lookupType ?? refTypeOfType(instr.lvalue),
);
break;
}
case 'LoadContext':
case 'LoadLocal': {
env.set(
instr.lvalue.identifier.id,
env.get(instr.value.place.identifier.id) ??
refTypeOfType(instr.lvalue),
);
break;
}
case 'StoreContext':
case 'StoreLocal': {
env.set(
instr.value.lvalue.place.identifier.id,
env.get(instr.value.value.identifier.id) ??
refTypeOfType(instr.value.lvalue.place),
);
env.set(
instr.lvalue.identifier.id,
env.get(instr.value.value.identifier.id) ??
refTypeOfType(instr.lvalue),
);
break;
}
case 'Destructure': {
const objType = env.get(instr.value.value.identifier.id);
let lookupType = null;
if (objType?.kind === 'Structure') {
lookupType = objType.value;
}
env.set(
instr.lvalue.identifier.id,
lookupType ?? refTypeOfType(instr.lvalue),
);
for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) {
env.set(lval.identifier.id, lookupType ?? refTypeOfType(lval));
}
break;
}
case 'ObjectMethod':
case 'FunctionExpression': {
let returnType: RefAccessType = {kind: 'None'};
let readRefEffect = false;
const result = validateNoRefAccessInRenderImpl(
instr.value.loweredFunc.func,
env,
);
if (result.isOk()) {
returnType = result.unwrap();
} else if (result.isErr()) {
readRefEffect = true;
}
env.set(instr.lvalue.identifier.id, {
kind: 'Structure',
fn: {
readRefEffect,
returnType,
},
value: null,
});
break;
}
case 'MethodCall':
case 'CallExpression': {
const callee =
instr.value.kind === 'CallExpression'
? instr.value.callee
: instr.value.property;
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
let returnType: RefAccessType = {kind: 'None'};
const fnType = env.get(callee.identifier.id);
if (fnType?.kind === 'Structure' && fnType.fn !== null) {
returnType = fnType.fn.returnType;
if (fnType.fn.readRefEffect) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: callee.loc,
description:
callee.identifier.name !== null &&
callee.identifier.name.kind === 'named'
? `Function \`${callee.identifier.name.value}\` accesses a ref`
: null,
suggestions: null,
});
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (hookKind != null) {
validateNoDirectRefValueAccess(errors, operand, env);
} else {
validateNoRefAccess(errors, env, operand, operand.loc);
}
}
env.set(instr.lvalue.identifier.id, returnType);
break;
}
case 'ObjectExpression':
case 'ArrayExpression': {
const types: Array<RefAccessType> = [];
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoDirectRefValueAccess(errors, operand, env);
types.push(env.get(operand.identifier.id) ?? {kind: 'None'});
}
const value = joinRefAccessTypes(...types);
if (
value.kind === 'None' ||
value.kind === 'Guard' ||
value.kind === 'Nullable'
) {
env.set(instr.lvalue.identifier.id, {kind: 'None'});
} else {
env.set(instr.lvalue.identifier.id, {
kind: 'Structure',
value,
fn: null,
});
}
break;
}
case 'PropertyDelete':
case 'PropertyStore':
case 'ComputedDelete':
case 'ComputedStore': {
const safe = safeBlocks.get(block.id);
const target = env.get(instr.value.object.identifier.id);
if (
instr.value.kind === 'PropertyStore' &&
safe != null &&
target?.kind === 'Ref' &&
target.refId === safe
) {
safeBlocks.delete(block.id);
} else {
validateNoRefAccess(errors, env, instr.value.object, instr.loc);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefAccess(
errors,
state,
operand,
state.refValues.get(operand.identifier.id) ?? operand.loc,
);
if (operand === instr.value.object) {
continue;
}
validateNoRefValueAccess(errors, env, operand);
}
break;
}
break;
}
case 'ObjectExpression':
case 'ArrayExpression': {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoDirectRefValueAccess(errors, operand, state);
if (state.refAccessingFunctions.has(operand.identifier.id)) {
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
if (state.refs.has(operand.identifier.id)) {
state.refs.add(instr.lvalue.identifier.id);
}
const refValue = state.refValues.get(operand.identifier.id);
if (refValue !== undefined) {
state.refValues.set(instr.lvalue.identifier.id, refValue);
case 'StartMemoize':
case 'FinishMemoize':
break;
case 'Primitive': {
if (instr.value.value == null) {
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
}
break;
}
case 'BinaryExpression': {
const left = env.get(instr.value.left.identifier.id);
const right = env.get(instr.value.right.identifier.id);
let nullish: boolean = false;
let refId: RefId | null = null;
if (left?.kind === 'RefValue' && left.refId != null) {
refId = left.refId;
} else if (right?.kind === 'RefValue' && right.refId != null) {
refId = right.refId;
}
if (left?.kind === 'Nullable') {
nullish = true;
} else if (right?.kind === 'Nullable') {
nullish = true;
}
if (refId !== null && nullish) {
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
} else {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefValueAccess(errors, env, operand);
}
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefValueAccess(errors, env, operand);
}
break;
}
break;
}
case 'PropertyDelete':
case 'PropertyStore':
case 'ComputedDelete':
case 'ComputedStore': {
validateNoRefAccess(
errors,
state,
instr.value.object,
state.refValues.get(instr.value.object.identifier.id) ?? instr.loc,
// Guard values are derived from ref.current, so they can only be used in if statement targets
for (const operand of eachInstructionOperand(instr)) {
guardCheck(errors, operand, env);
}
if (
isUseRefType(instr.lvalue.identifier) &&
env.get(instr.lvalue.identifier.id)?.kind !== 'Ref'
) {
env.set(
instr.lvalue.identifier.id,
joinRefAccessTypes(
env.get(instr.lvalue.identifier.id) ?? {kind: 'None'},
{kind: 'Ref', refId: nextRefId()},
),
);
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand === instr.value.object) {
continue;
}
validateNoRefValueAccess(errors, state, operand);
}
break;
}
case 'StartMemoize':
case 'FinishMemoize':
break;
default: {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefValueAccess(errors, state, operand);
}
break;
if (
isRefValueType(instr.lvalue.identifier) &&
env.get(instr.lvalue.identifier.id)?.kind !== 'RefValue'
) {
env.set(
instr.lvalue.identifier.id,
joinRefAccessTypes(
env.get(instr.lvalue.identifier.id) ?? {kind: 'None'},
{kind: 'RefValue', loc: instr.loc},
),
);
}
}
if (isUseRefType(instr.lvalue.identifier)) {
state.refs.add(instr.lvalue.identifier.id);
if (block.terminal.kind === 'if') {
const test = env.get(block.terminal.test.identifier.id);
if (test?.kind === 'Guard') {
safeBlocks.set(block.terminal.consequent, test.refId);
}
}
if (
isRefValueType(instr.lvalue.identifier) &&
!state.refValues.has(instr.lvalue.identifier.id)
) {
state.refValues.set(instr.lvalue.identifier.id, instr.loc);
for (const operand of eachTerminalOperand(block.terminal)) {
if (block.terminal.kind !== 'return') {
validateNoRefValueAccess(errors, env, operand);
if (block.terminal.kind !== 'if') {
guardCheck(errors, operand, env);
}
} else {
// Allow functions containing refs to be returned, but not direct ref values
validateNoDirectRefValueAccess(errors, operand, env);
guardCheck(errors, operand, env);
returnValues.push(env.get(operand.identifier.id));
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (block.terminal.kind !== 'return') {
validateNoRefValueAccess(errors, state, operand);
} else {
// Allow functions containing refs to be returned, but not direct ref values
validateNoDirectRefValueAccess(errors, operand, state);
}
if (errors.hasErrors()) {
return Err(errors);
}
}
if (errors.hasErrors()) {
return Err(errors);
} else {
return Ok(undefined);
CompilerError.invariant(!env.hasChanged(), {
reason: 'Ref type environment did not converge',
loc: null,
});
return Ok(
joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
),
);
}
function destructure(
type: RefAccessType | undefined,
): RefAccessType | undefined {
if (type?.kind === 'Structure' && type.value !== null) {
return destructure(type.value);
}
return type;
}
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
? `Cannot access ref value \`${operand.identifier.name.value}\``
: null,
suggestions: null,
});
}
}
function validateNoRefValueAccess(
errors: CompilerError,
state: State,
env: Env,
operand: Place,
): void {
const type = destructure(env.get(operand.identifier.id));
if (
state.refValues.has(operand.identifier.id) ||
state.refAccessingFunctions.has(operand.identifier.id)
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
@@ -367,20 +615,21 @@ function validateNoRefValueAccess(
function validateNoRefAccess(
errors: CompilerError,
state: State,
env: Env,
operand: Place,
loc: SourceLocation,
): void {
const type = destructure(env.get(operand.identifier.id));
if (
state.refs.has(operand.identifier.id) ||
state.refValues.has(operand.identifier.id) ||
state.refAccessingFunctions.has(operand.identifier.id)
type?.kind === 'Ref' ||
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: loc,
loc: (type.kind === 'RefValue' && type.loc) || loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
@@ -394,14 +643,15 @@ function validateNoRefAccess(
function validateNoDirectRefValueAccess(
errors: CompilerError,
operand: Place,
state: State,
env: Env,
): void {
if (state.refValues.has(operand.identifier.id)) {
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'RefValue') {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
loc: type.loc ?? operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'

View File

@@ -57,4 +57,4 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) [[{"a":0,"b":"value1","c":true}],"[[ cyclic ref *2 ]]"]
(kind: ok) [[{"a":0,"b":"value1","c":true},"joe"],"[[ cyclic ref *2 ]]"]

View File

@@ -60,6 +60,6 @@ export const FIXTURE_ENTRYPOINT = {
```
### Eval output
(kind: ok) [2]
[2]
[3]
(kind: ok) [2,"joe"]
[2,"joe"]
[3,"joe"]

View File

@@ -2,8 +2,6 @@
## Input
```javascript
// @enableReactiveScopesInHIR:false
import {Stringify, identity, makeArray, mutate} from 'shared-runtime';
/**
@@ -37,8 +35,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableReactiveScopesInHIR:false
import { c as _c } from "react/compiler-runtime";
import { Stringify, identity, makeArray, mutate } from "shared-runtime";
/**
@@ -52,11 +49,12 @@ import { Stringify, identity, makeArray, mutate } from "shared-runtime";
* handles this correctly.
*/
function Foo(t0) {
const $ = _c(4);
const $ = _c(3);
const { cond1, cond2 } = t0;
const arr = makeArray({ a: 2 }, 2, []);
let t1;
if ($[0] !== cond1 || $[1] !== cond2 || $[2] !== arr) {
if ($[0] !== cond1 || $[1] !== cond2) {
const arr = makeArray({ a: 2 }, 2, []);
t1 = cond1 ? (
<>
<div>{identity("foo")}</div>
@@ -65,10 +63,9 @@ function Foo(t0) {
) : null;
$[0] = cond1;
$[1] = cond2;
$[2] = arr;
$[3] = t1;
$[2] = t1;
} else {
t1 = $[3];
t1 = $[2];
}
return t1;
}

Some files were not shown because too many files have changed in this diff Show More