mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge 6f28d52c3a into sapling-pr-archive-poteto
This commit is contained in:
@@ -330,6 +330,7 @@ module.exports = {
|
||||
'packages/react-server-dom-esm/**/*.js',
|
||||
'packages/react-server-dom-webpack/**/*.js',
|
||||
'packages/react-server-dom-turbopack/**/*.js',
|
||||
'packages/react-server-dom-parcel/**/*.js',
|
||||
'packages/react-server-dom-fb/**/*.js',
|
||||
'packages/react-test-renderer/**/*.js',
|
||||
'packages/react-debug-tools/**/*.js',
|
||||
@@ -481,6 +482,12 @@ module.exports = {
|
||||
__turbopack_require__: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/react-server-dom-parcel/**/*.js'],
|
||||
globals: {
|
||||
parcelRequire: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/scheduler/**/*.js'],
|
||||
globals: {
|
||||
|
||||
+272
@@ -1,3 +1,275 @@
|
||||
## 19.0.0 (December 5, 2024)
|
||||
|
||||
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.
|
||||
|
||||
> Note: To help make the upgrade to React 19 easier, we’ve published a react@18.3 release that is identical to 18.2 but adds warnings for deprecated APIs and other changes that are needed for React 19. We recommend upgrading to React 18.3.1 first to help identify any issues before upgrading to React 19.
|
||||
|
||||
### New Features
|
||||
|
||||
#### React
|
||||
|
||||
* Actions: `startTransition` can now accept async functions. Functions passed to `startTransition` are called “Actions”. A given Transition can include one or more Actions which update state in the background and update the UI with one commit. In addition to updating state, Actions can now perform side effects including async requests, and the Action will wait for the work to finish before finishing the Transition. This feature allows Transitions to include side effects like `fetch()` in the pending state, and provides support for error handling, and optimistic updates.
|
||||
* `useActionState`: is a new hook to order Actions inside of a Transition with access to the state of the action, and the pending state. It accepts a reducer that can call Actions, and the initial state used for first render. It also accepts an optional string that is used if the action is passed to a form `action` prop to support progressive enhancement in forms.
|
||||
* `useOptimistic`: is a new hook to update state while a Transition is in progress. It returns the state, and a set function that can be called inside a transition to “optimistically” update the state to expected final value immediately while the Transition completes in the background. When the transition finishes, the state is updated to the new value.
|
||||
* `use`: is a new API that allows reading resources in render. In React 19, `use` accepts a promise or Context. If provided a promise, `use` will suspend until a value is resolved. `use` can only be used in render but can be called conditionally.
|
||||
* `ref` as a prop: Refs can now be used as props, removing the need for `forwardRef`.
|
||||
* **Suspense sibling pre-warming**: When a component suspends, React will immediately commit the fallback of the nearest Suspense boundary, without waiting for the entire sibling tree to render. After the fallback commits, React will schedule another render for the suspended siblings to “pre-warm” lazy requests.
|
||||
|
||||
#### React DOM Client
|
||||
|
||||
* `<form> action` prop: Form Actions allow you to manage forms automatically and integrate with `useFormStatus`. When a `<form> action` succeeds, React will automatically reset the form for uncontrolled components. The form can be reset manually with the new `requestFormReset` API.
|
||||
* `<button> and <input> formAction` prop: Actions can be passed to the `formAction` prop to configure form submission behavior. This allows using different Actions depending on the input.
|
||||
* `useFormStatus`: is a new hook that provides the status of the parent `<form> action`, as if the form was a Context provider. The hook returns the values: `pending`, `data`, `method`, and `action`.
|
||||
* Support for Document Metadata: We’ve added support for rendering document metadata tags in components natively. React will automatically hoist them into the `<head>` section of the document.
|
||||
* Support for Stylesheets: React 19 will ensure stylesheets are inserted into the `<head>` on the client before revealing the content of a Suspense boundary that depends on that stylesheet.
|
||||
* Support for async scripts: Async scripts can be rendered anywhere in the component tree and React will handle ordering and deduplication.
|
||||
* Support for preloading resources: React 19 ships with `preinit`, `preload`, `prefetchDNS`, and `preconnect` APIs to optimize initial page loads by moving discovery of additional resources like fonts out of stylesheet loading. They can also be used to prefetch resources used by an anticipated navigation.
|
||||
|
||||
#### React DOM Server
|
||||
|
||||
* Added `prerender` and `prerenderToNodeStream` APIs for static site generation. They are designed to work with streaming environments like Node.js Streams and Web Streams. Unlike `renderToString`, they wait for data to load for HTML generation.
|
||||
|
||||
#### React Server Components
|
||||
|
||||
* RSC features such as directives, server components, and server functions are now stable. This means libraries that ship with Server Components can now target React 19 as a peer dependency with a react-server export condition for use in frameworks that support the Full-stack React Architecture. The underlying APIs used to implement a React Server Components bundler or framework do not follow semver and may break between minors in React 19.x. See [docs](https://19.react.dev/reference/rsc/server-components) for how to support React Server Components.
|
||||
|
||||
### Deprecations
|
||||
|
||||
* Deprecated: `element.ref` access: React 19 supports ref as a prop, so we’re deprecating `element.ref` in favor of `element.props.ref`. Accessing will result in a warning.
|
||||
* `react-test-renderer`: In React 19, react-test-renderer logs a deprecation warning and has switched to concurrent rendering for web usage. We recommend migrating your tests to [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) or [@testing-library/react-native](https://testing-library.com/docs/react-native-testing-library/intro)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
React 19 brings in a number of breaking changes, including the removals of long-deprecated APIs. We recommend first upgrading to `18.3.1`, where we've added additional deprecation warnings. Check out the [upgrade guide](https://19.react.dev/blog/2024/04/25/react-19-upgrade-guide) for more details and guidance on codemodding.
|
||||
|
||||
### React
|
||||
|
||||
* New JSX Transform is now required: We introduced [a new JSX transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) in 2020 to improve bundle size and use JSX without importing React. In React 19, we’re adding additional improvements like using ref as a prop and JSX speed improvements that require the new transform.
|
||||
* Errors in render are not re-thrown: Errors that are not caught by an Error Boundary are now reported to window.reportError. Errors that are caught by an Error Boundary are reported to console.error. We’ve introduced `onUncaughtError` and `onCaughtError` methods to `createRoot` and `hydrateRoot` to customize this error handling.
|
||||
* Removed: `propTypes`: Using `propTypes` will now be silently ignored. If required, we recommend migrating to TypeScript or another type-checking solution.
|
||||
* Removed: `defaultProps` for functions: ES6 default parameters can be used in place. Class components continue to support `defaultProps` since there is no ES6 alternative.
|
||||
* Removed: `contextTypes` and `getChildContext`: Legacy Context for class components has been removed in favor of the `contextType` API.
|
||||
* Removed: string refs: Any usage of string refs need to be migrated to ref callbacks.
|
||||
* Removed: Module pattern factories: A rarely used pattern that can be migrated to regular functions.
|
||||
* Removed: `React.createFactory`: Now that JSX is broadly supported, all `createFactory` usage can be migrated to JSX components.
|
||||
* Removed: `react-test-renderer/shallow`: This has been a re-export of [react-shallow-renderer](https://github.com/enzymejs/react-shallow-renderer) since React 18\. If needed, you can continue to use the third-party package directly. We recommend using [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) or [@testing-library/react-native](https://testing-library.com/docs/react-native-testing-library/intro) instead.
|
||||
|
||||
#### React DOM
|
||||
|
||||
* Removed: `react-dom/test-utils`: We’ve moved `act` from `react-dom/test-utils` to react. All other utilities have been removed.
|
||||
* Removed: `ReactDOM`.`render`, `ReactDOM`.`hydrate`: These have been removed in favor of the concurrent equivalents: `ReactDOM`.`createRoot` and `ReactDOM.hydrateRoot`.
|
||||
* Removed: `unmountComponentAtNode`: Removed in favor of `root.unmount()`.
|
||||
* Removed: `ReactDOM`.`findDOMNode`: You can replace `ReactDOM`.`findDOMNode` with DOM Refs.
|
||||
|
||||
### Notable Changes
|
||||
|
||||
#### React
|
||||
|
||||
* `<Context>` as a provider: You can now render `<Context>` as a provider instead of `<Context.Provider>`.
|
||||
* Cleanup functions for refs: When the component unmounts, React will call the cleanup function returned from the ref callback.
|
||||
* `useDeferredValue` initial value argument: When provided, `useDeferredValue` will return the initial value for the initial render of a component, then schedule a re-render in the background with the `deferredValue` returned.
|
||||
* Support for Custom Elements: React 19 now passes all tests on [Custom Elements Everywhere](https://custom-elements-everywhere.com/).
|
||||
* StrictMode changes: `useMemo` and `useCallback` will now reuse the memoized results from the first render, during the second render. Additionally, StrictMode will now double-invoke ref callback functions on initial mount.
|
||||
* UMD builds removed: To load React 19 with a script tag, we recommend using an ESM-based CDN such as [esm.sh](http://esm.sh).
|
||||
|
||||
#### React DOM
|
||||
|
||||
* Diffs for hydration errors: In the case of a mismatch, React 19 logs a single error with a diff of the mismatched content.
|
||||
* Compatibility with third-party scripts and extensions: React will now force a client re-render to fix up any mismatched content caused by elements inserted by third-party JS.
|
||||
|
||||
### TypeScript Changes
|
||||
|
||||
The most common changes can be codemodded with `npx types-react-codemod@latest preset-19 ./path-to-your-react-ts-files`.
|
||||
|
||||
* Removed deprecated TypeScript types:
|
||||
* `ReactChild` (replacement: `React.ReactElement | number | string)`
|
||||
* `ReactFragment` (replacement: `Iterable<React.ReactNode>`)
|
||||
* `ReactNodeArray` (replacement: `ReadonlyArray<React.ReactNode>`)
|
||||
* `ReactText` (replacement: `number | string`)
|
||||
* `VoidFunctionComponent` (replacement: `FunctionComponent`)
|
||||
* `VFC` (replacement: `FC`)
|
||||
* Moved to `prop-types`: `Requireable`, `ValidationMap`, `Validator`, `WeakValidationMap`
|
||||
* Moved to `create-react-class`: `ClassicComponentClass`, `ClassicComponent`, `ClassicElement`, `ComponentSpec`, `Mixin`, `ReactChildren`, `ReactHTML`, `ReactSVG`, `SFCFactory`
|
||||
* Disallow implicit return in refs: refs can now accept cleanup functions. When you return something else, we can’t tell if you intentionally returned something not meant to clean up or returned the wrong value. Implicit returns of anything but functions will now error.
|
||||
* Require initial argument to `useRef`: The initial argument is now required to match `useState`, `createContext` etc
|
||||
* Refs are mutable by default: Ref objects returned from `useRef()` are now always mutable instead of sometimes being immutable. This feature was too confusing for users and conflicted with legit cases where refs were managed by React and manually written to.
|
||||
* Strict `ReactElement` typing: The props of React elements now default to `unknown` instead of `any` if the element is typed as `ReactElement`
|
||||
* JSX namespace in TypeScript: The global `JSX` namespace is removed to improve interoperability with other libraries using JSX. Instead, the JSX namespace is available from the React package: `import { JSX } from 'react'`
|
||||
* Better `useReducer` typings: Most `useReducer` usage should not require explicit type arguments.
|
||||
For example,
|
||||
```diff
|
||||
-useReducer<React.Reducer<State, Action>>(reducer)
|
||||
+useReducer(reducer)
|
||||
```
|
||||
or
|
||||
```diff
|
||||
-useReducer<React.Reducer<State, Action>>(reducer)
|
||||
+useReducer<State, Action>(reducer)
|
||||
```
|
||||
|
||||
### All Changes
|
||||
|
||||
#### React
|
||||
|
||||
* Add support for async Actions ([\#26621](https://github.com/facebook/react/pull/26621), [\#26726](https://github.com/facebook/react/pull/26726), [\#28078](https://github.com/facebook/react/pull/28078), [\#28097](https://github.com/facebook/react/pull/28097), [\#29226](https://github.com/facebook/react/pull/29226), [\#29618](https://github.com/facebook/react/pull/29618), [\#29670](https://github.com/facebook/react/pull/29670), [\#26716](https://github.com/facebook/react/pull/26716) by [@acdlite](https://github.com/acdlite) and [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Add `useActionState()` hook to update state based on the result of a Form Action ([\#27270](https://github.com/facebook/react/pull/27270), [\#27278](https://github.com/facebook/react/pull/27278), [\#27309](https://github.com/facebook/react/pull/27309), [\#27302](https://github.com/facebook/react/pull/27302), [\#27307](https://github.com/facebook/react/pull/27307), [\#27366](https://github.com/facebook/react/pull/27366), [\#27370](https://github.com/facebook/react/pull/27370), [\#27321](https://github.com/facebook/react/pull/27321), [\#27374](https://github.com/facebook/react/pull/27374), [\#27372](https://github.com/facebook/react/pull/27372), [\#27397](https://github.com/facebook/react/pull/27397), [\#27399](https://github.com/facebook/react/pull/27399), [\#27460](https://github.com/facebook/react/pull/27460), [\#28557](https://github.com/facebook/react/pull/28557), [\#27570](https://github.com/facebook/react/pull/27570), [\#27571](https://github.com/facebook/react/pull/27571), [\#28631](https://github.com/facebook/react/pull/28631), [\#28788](https://github.com/facebook/react/pull/28788), [\#29694](https://github.com/facebook/react/pull/29694), [\#29695](https://github.com/facebook/react/pull/29695), [\#29694](https://github.com/facebook/react/pull/29694), [\#29665](https://github.com/facebook/react/pull/29665), [\#28232](https://github.com/facebook/react/pull/28232), [\#28319](https://github.com/facebook/react/pull/28319) by [@acdlite](https://github.com/acdlite), [@eps1lon](https://github.com/eps1lon), and [@rickhanlonii](https://github.com/rickhanlonii))
|
||||
* Add `use()` API to read resources in render ([\#25084](https://github.com/facebook/react/pull/25084), [\#25202](https://github.com/facebook/react/pull/25202), [\#25207](https://github.com/facebook/react/pull/25207), [\#25214](https://github.com/facebook/react/pull/25214), [\#25226](https://github.com/facebook/react/pull/25226), [\#25247](https://github.com/facebook/react/pull/25247), [\#25539](https://github.com/facebook/react/pull/25539), [\#25538](https://github.com/facebook/react/pull/25538), [\#25537](https://github.com/facebook/react/pull/25537), [\#25543](https://github.com/facebook/react/pull/25543), [\#25561](https://github.com/facebook/react/pull/25561), [\#25620](https://github.com/facebook/react/pull/25620), [\#25615](https://github.com/facebook/react/pull/25615), [\#25922](https://github.com/facebook/react/pull/25922), [\#25641](https://github.com/facebook/react/pull/25641), [\#25634](https://github.com/facebook/react/pull/25634), [\#26232](https://github.com/facebook/react/pull/26232), [\#26536](https://github.com/facebook/react/pull/26535), [\#26739](https://github.com/facebook/react/pull/26739), [\#28233](https://github.com/facebook/react/pull/28233) by [@acdlite](https://github.com/acdlite), [@MofeiZ](https://github.com/mofeiZ), [@sebmarkbage](https://github.com/sebmarkbage), [@sophiebits](https://github.com/sophiebits), [@eps1lon](https://github.com/eps1lon), and [@hansottowirtz](https://github.com/hansottowirtz))
|
||||
* Add `useOptimistic()` hook to display mutated state optimistically during an async mutation ([\#26740](https://github.com/facebook/react/pull/26740), [\#26772](https://github.com/facebook/react/pull/26772), [\#27277](https://github.com/facebook/react/pull/27277), [\#27453](https://github.com/facebook/react/pull/27453), [\#27454](https://github.com/facebook/react/pull/27454), [\#27936](https://github.com/facebook/react/pull/27936) by [@acdlite](https://github.com/acdlite))
|
||||
* Added an `initialValue` argument to `useDeferredValue()` hook ([\#27500](https://github.com/facebook/react/pull/27500), [\#27509](https://github.com/facebook/react/pull/27509), [\#27512](https://github.com/facebook/react/pull/27512), [\#27888](https://github.com/facebook/react/pull/27888), [\#27550](https://github.com/facebook/react/pull/27550) by [@acdlite](https://github.com/acdlite))
|
||||
* Support refs as props, warn on `element.ref` access ([\#28348](https://github.com/facebook/react/pull/28348), [\#28464](https://github.com/facebook/react/pull/28464), [\#28731](https://github.com/facebook/react/pull/28731) by [@acdlite](https://github.com/acdlite))
|
||||
* Support Custom Elements ([\#22184](https://github.com/facebook/react/pull/22184), [\#26524](https://github.com/facebook/react/pull/26524), [\#26523](https://github.com/facebook/react/pull/26523), [\#27511](https://github.com/facebook/react/pull/27511), [\#24541](https://github.com/facebook/react/pull/24541) by [@josepharhar](https://github.com/josepharhar), [@sebmarkbage](https://github.com/sebmarkbage), [@gnoff](https://github.com/gnoff) and [@eps1lon](https://github.com/eps1lon))
|
||||
* Add ref cleanup function ([\#25686](https://github.com/facebook/react/pull/25686), [\#28883](https://github.com/facebook/react/pull/28883), [\#28910](https://github.com/facebook/react/pull/28910) by [@sammy-SC](https://github.com/sammy-SC), [@jackpope](https://github.com/jackpope), and [@kassens](https://github.com/kassens))
|
||||
* Sibling pre-rendering replaced by sibling pre-warming ([\#26380](https://github.com/facebook/react/pull/26380), [\#26549](https://github.com/facebook/react/pull/26549), [\#30761](https://github.com/facebook/react/pull/30761), [\#30800](https://github.com/facebook/react/pull/30800), [\#30762](https://github.com/facebook/react/pull/30762), [\#30879](https://github.com/facebook/react/pull/30879), [\#30934](https://github.com/facebook/react/pull/30934), [\#30952](https://github.com/facebook/react/pull/30952), [\#31056](https://github.com/facebook/react/pull/31056), [\#31452](https://github.com/facebook/react/pull/31452) by [@sammy-SC](https://github.com/sammy-SC), [@acdlite](https://github.com/acdlite), [@gnoff](https://github.com/gnoff), [@jackpope](https://github.com/jackpope), [@rickhanlonii](https://github.com/rickhanlonii))
|
||||
* Don’t rethrow errors at the root ([\#28627](https://github.com/facebook/react/pull/28627), [\#28641](https://github.com/facebook/react/pull/28641) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Batch sync discrete, continuous, and default lanes ([\#25700](https://github.com/facebook/react/pull/25700) by [@tyao1](https://github.com/tyao1))
|
||||
* Switch `<Context>` to mean `<Context.Provider>` ([\#28226](https://github.com/facebook/react/pull/28226) by [@gaearon](https://github.com/gaearon))
|
||||
* Changes to *StrictMode*
|
||||
* Handle `info`, `group`, and `groupCollapsed` in *StrictMode* logging ([\#25172](https://github.com/facebook/react/pull/25172) by [@timneutkens](https://github.com/timneutkens))
|
||||
* Refs are now attached/detached/attached in *StrictMode* ([\#25049](https://github.com/facebook/react/pull/25049) by [@sammy-SC](https://github.com/sammy-SC))
|
||||
* Fix `useSyncExternalStore()` hydration in *StrictMode* ([\#26791](https://github.com/facebook/react/pull/26791) by [@sophiebits](https://github.com/sophiebits))
|
||||
* Always trigger `componentWillUnmount()` in *StrictMode* ([\#26842](https://github.com/facebook/react/pull/26842) by [@tyao1](https://github.com/tyao1))
|
||||
* Restore double invoking `useState()` and `useReducer()` initializer functions in *StrictMode* ([\#28248](https://github.com/facebook/react/pull/28248) by [@eps1lon](https://github.com/eps1lon))
|
||||
* Reuse memoized result from first pass ([\#25583](https://github.com/facebook/react/pull/25583) by [@acdlite](https://github.com/acdlite))
|
||||
* Fix `useId()` in *StrictMode* ([\#25713](https://github.com/facebook/react/pull/25713) by [@gnoff](https://github.com/gnoff))
|
||||
* Add component name to *StrictMode* error messages ([\#25718](https://github.com/facebook/react/pull/25718) by [@sammy-SC](https://github.com/sammy-SC))
|
||||
* Add support for rendering BigInt ([\#24580](https://github.com/facebook/react/pull/24580) by [@eps1lon](https://github.com/eps1lon))
|
||||
* `act()` no longer checks `shouldYield` which can be inaccurate in test environments ([\#26317](https://github.com/facebook/react/pull/26317) by [@acdlite](https://github.com/acdlite))
|
||||
* Warn when keys are spread with props ([\#25697](https://github.com/facebook/react/pull/25697), [\#26080](https://github.com/facebook/react/pull/26080) by [@sebmarkbage](https://github.com/sebmarkbage) and [@kassens](https://github.com/kassens))
|
||||
* Generate sourcemaps for production build artifacts ([\#26446](https://github.com/facebook/react/pull/26446) by [@markerikson](https://github.com/markerikson))
|
||||
* Improve stack diffing algorithm ([\#27132](https://github.com/facebook/react/pull/27132) by [@KarimP](https://github.com/KarimP))
|
||||
* Suspense throttling lowered from 500ms to 300ms ([\#26803](https://github.com/facebook/react/pull/26803) by [@acdlite](https://github.com/acdlite))
|
||||
* Lazily propagate context changes ([\#20890](https://github.com/facebook/react/pull/20890) by [@acdlite](https://github.com/acdlite) and [@gnoff](https://github.com/gnoff))
|
||||
* Immediately rerender pinged fiber ([\#25074](https://github.com/facebook/react/pull/25074) by [@acdlite](https://github.com/acdlite))
|
||||
* Move update scheduling to microtask ([\#26512](https://github.com/facebook/react/pull/26512) by [@acdlite](https://github.com/acdlite))
|
||||
* Consistently apply throttled retries ([\#26611](https://github.com/facebook/react/pull/26611), [\#26802](https://github.com/facebook/react/pull/26802) by [@acdlite](https://github.com/acdlite))
|
||||
* Suspend Thenable/Lazy if it's used in React.Children ([\#28284](https://github.com/facebook/react/pull/28284) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Detect infinite update loops caused by render phase updates ([\#26625](https://github.com/facebook/react/pull/26625) by [@acdlite](https://github.com/acdlite))
|
||||
* Update conditional hooks warning ([\#29626](https://github.com/facebook/react/pull/29626) by [@sophiebits](https://github.com/sophiebits))
|
||||
* Update error URLs to go to new docs ([\#27240](https://github.com/facebook/react/pull/27240) by [@rickhanlonii](https://github.com/rickhanlonii))
|
||||
* Rename the `react.element` symbol to `react.transitional.element` ([\#28813](https://github.com/facebook/react/pull/28813) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Fix crash when suspending in shell during `useSyncExternalStore()` re-render ([\#27199](https://github.com/facebook/react/pull/27199) by [@acdlite](https://github.com/acdlite))
|
||||
* Fix incorrect “detected multiple renderers" error in tests ([\#22797](https://github.com/facebook/react/pull/22797) by [@eps1lon](https://github.com/eps1lon))
|
||||
* Fix bug where effect cleanup may be called twice after bailout ([\#26561](https://github.com/facebook/react/pull/26561) by [@acdlite](https://github.com/acdlite))
|
||||
* Fix suspending in shell during discrete update ([\#25495](https://github.com/facebook/react/pull/25495) by [@acdlite](https://github.com/acdlite))
|
||||
* Fix memory leak after repeated setState bailouts ([\#25309](https://github.com/facebook/react/pull/25309) by [@acdlite](https://github.com/acdlite))
|
||||
* Fix `useSyncExternalStore()` dropped update when state is dispatched in render phase ([\#25578](https://github.com/facebook/react/pull/25578) by [@pandaiolo](https://github.com/pandaiolo))
|
||||
* Fix logging when rendering a lazy fragment ([\#30372](https://github.com/facebook/react/pull/30372) by [@tom-sherman](https://github.com/tom-sherman))
|
||||
* Remove string refs ([\#25383](https://github.com/facebook/react/pull/25383), [\#28322](https://github.com/facebook/react/pull/28322) by [@eps1lon](https://github.com/eps1lon) and [@acdlite](https://github.com/acdlite))
|
||||
* Remove Legacy Context (\#30319 by [@kassens](https://github.com/kassens))
|
||||
* Remove `RefreshRuntime.findAffectedHostInstances` ([\#30538](https://github.com/facebook/react/pull/30538) by [@gaearon](https://github.com/gaearon))
|
||||
* Remove client caching from `cache()` API ([\#27977](https://github.com/facebook/react/pull/27977), [\#28250](https://github.com/facebook/react/pull/28250) by [@acdlite](https://github.com/acdlite) and [@gnoff](https://github.com/gnoff))
|
||||
* Remove `propTypes` ([\#28324](https://github.com/facebook/react/pull/28324), [\#28326](https://github.com/facebook/react/pull/28326) by [@gaearon](https://github.com/gaearon))
|
||||
* Remove `defaultProps` support, except for classes ([\#28733](https://github.com/facebook/react/pull/28733) by [@acdlite](https://github.com/acdlite))
|
||||
* Remove UMD builds ([\#28735](https://github.com/facebook/react/pull/28735) by [@gnoff](https://github.com/gnoff))
|
||||
* Remove delay for non-transition updates ([\#26597](https://github.com/facebook/react/pull/26597) by [@acdlite](https://github.com/acdlite))
|
||||
* Remove `createFactory` ([\#27798](https://github.com/facebook/react/pull/27798) by [@kassens](https://github.com/kassens))
|
||||
|
||||
#### React DOM
|
||||
|
||||
* Adds Form Actions to handle form submission ([\#26379](https://github.com/facebook/react/pull/26379), [\#26674](https://github.com/facebook/react/pull/26674), [\#26689](https://github.com/facebook/react/pull/26689), [\#26708](https://github.com/facebook/react/pull/26708), [\#26714](https://github.com/facebook/react/pull/26714), [\#26735](https://github.com/facebook/react/pull/26735), [\#26846](https://github.com/facebook/react/pull/26846), [\#27358](https://github.com/facebook/react/pull/27358), [\#28056](https://github.com/facebook/react/pull/28056) by [@sebmarkbage](https://github.com/sebmarkbage), [@acdlite](https://github.com/acdlite), and [@jupapios](https://github.com/jupapios))
|
||||
* Add `useFormStatus()` hook to provide status information of the last form submission ([\#26719](https://github.com/facebook/react/pull/26719), [\#26722](https://github.com/facebook/react/pull/26722), [\#26788](https://github.com/facebook/react/pull/26788), [\#29019](https://github.com/facebook/react/pull/29019), [\#28728](https://github.com/facebook/react/pull/28728), [\#28413](https://github.com/facebook/react/pull/28413) by [@acdlite](https://github.com/acdlite) and [@eps1lon](https://github.com/eps1lon))
|
||||
* Support for Document Metadata. Adds `preinit`, `preinitModule`, `preconnect`, `prefetchDNS`, `preload`, and `preloadModule` APIs.
|
||||
* [\#25060](https://github.com/facebook/react/pull/25060), [\#25243](https://github.com/facebook/react/pull/25243), [\#25388](https://github.com/facebook/react/pull/25388), [\#25432](https://github.com/facebook/react/pull/25432), [\#25436](https://github.com/facebook/react/pull/25436), [\#25426](https://github.com/facebook/react/pull/25426), [\#25500](https://github.com/facebook/react/pull/25500), [\#25480](https://github.com/facebook/react/pull/25480), [\#25508](https://github.com/facebook/react/pull/25508), [\#25515](https://github.com/facebook/react/pull/25515), [\#25514](https://github.com/facebook/react/pull/25514), [\#25532](https://github.com/facebook/react/pull/25532), [\#25536](https://github.com/facebook/react/pull/25536), [\#25534](https://github.com/facebook/react/pull/25534), [\#25546](https://github.com/facebook/react/pull/25546), [\#25559](https://github.com/facebook/react/pull/25559), [\#25569](https://github.com/facebook/react/pull/25569), [\#25599](https://github.com/facebook/react/pull/25599), [\#25689](https://github.com/facebook/react/pull/25689), [\#26106](https://github.com/facebook/react/pull/26106), [\#26152](https://github.com/facebook/react/pull/26152), [\#26239](https://github.com/facebook/react/pull/26239), [\#26237](https://github.com/facebook/react/pull/26237), [\#26280](https://github.com/facebook/react/pull/26280), [\#26154](https://github.com/facebook/react/pull/26154), [\#26256](https://github.com/facebook/react/pull/26256), [\#26353](https://github.com/facebook/react/pull/26353), [\#26427](https://github.com/facebook/react/pull/26427), [\#26450](https://github.com/facebook/react/pull/26450), [\#26502](https://github.com/facebook/react/pull/26502), [\#26514](https://github.com/facebook/react/pull/26514), [\#26531](https://github.com/facebook/react/pull/26531), [\#26532](https://github.com/facebook/react/pull/26532), [\#26557](https://github.com/facebook/react/pull/26557), [\#26871](https://github.com/facebook/react/pull/26871), [\#26881](https://github.com/facebook/react/pull/26881), [\#26877](https://github.com/facebook/react/pull/26877), [\#26873](https://github.com/facebook/react/pull/26873), [\#26880](https://github.com/facebook/react/pull/26880), [\#26942](https://github.com/facebook/react/pull/26942), [\#26938](https://github.com/facebook/react/pull/26938), [\#26940](https://github.com/facebook/react/pull/26940), [\#26939](https://github.com/facebook/react/pull/26939), [\#27030](https://github.com/facebook/react/pull/27030), [\#27201](https://github.com/facebook/react/pull/27201), [\#27212](https://github.com/facebook/react/pull/27212), [\#27217](https://github.com/facebook/react/pull/27217), [\#27218](https://github.com/facebook/react/pull/27218), [\#27220](https://github.com/facebook/react/pull/27220), [\#27224](https://github.com/facebook/react/pull/27224), [\#27223](https://github.com/facebook/react/pull/27223), [\#27269](https://github.com/facebook/react/pull/27269), [\#27260](https://github.com/facebook/react/pull/27260), [\#27347](https://github.com/facebook/react/pull/27347), [\#27346](https://github.com/facebook/react/pull/27346), [\#27361](https://github.com/facebook/react/pull/27361), [\#27400](https://github.com/facebook/react/pull/27400), [\#27541](https://github.com/facebook/react/pull/27541), [\#27610](https://github.com/facebook/react/pull/27610), [\#28110](https://github.com/facebook/react/pull/28110), [\#29693](https://github.com/facebook/react/pull/29693), [\#29732](https://github.com/facebook/react/pull/29732), [\#29811](https://github.com/facebook/react/pull/29811), [\#27586](https://github.com/facebook/react/pull/27586), [\#28069](https://github.com/facebook/react/pull/28069) by [@gnoff](https://github.com/gnoff), [@sebmarkbage](https://github.com/sebmarkbage), [@acdlite](https://github.com/acdlite), [@kassens](https://github.com/kassens), [@sokra](https://github.com/sokra), [@sweetliquid](https://github.com/sweetliquid)
|
||||
* Add `fetchPriority` to `<img>` and `<link>` ([\#25927](https://github.com/facebook/react/pull/25927) by [@styfle](https://github.com/styfle))
|
||||
* Add support for SVG `transformOrigin` prop ([\#26130](https://github.com/facebook/react/pull/26130) by [@arav-ind](https://github.com/arav-ind))
|
||||
* Add support for `onScrollEnd` event ([\#26789](https://github.com/facebook/react/pull/26789) by [@devongovett](https://github.com/devongovett))
|
||||
* Allow `<hr>` as child of `<select>` ([\#27632](https://github.com/facebook/react/pull/27632) by [@SouSingh](https://github.com/SouSingh))
|
||||
* Add support for Popover API ([\#27981](https://github.com/facebook/react/pull/27981) by [@eps1lon](https://github.com/eps1lon))
|
||||
* Add support for `inert` ([\#24730](https://github.com/facebook/react/pull/24730) by [@eps1lon](https://github.com/eps1lon))
|
||||
* Add support for `imageSizes` and `imageSrcSet` ([\#22550](https://github.com/facebook/react/pull/22550) by [@eps1lon](https://github.com/eps1lon))
|
||||
* Synchronously flush transitions in popstate events ([\#26025](https://github.com/facebook/react/pull/26025), [\#27559](https://github.com/facebook/react/pull/27559), [\#27505](https://github.com/facebook/react/pull/27505), [\#30759](https://github.com/facebook/react/pull/30759) by [@tyao1](https://github.com/tyao1) and [@acdlite](https://github.com/acdlite))
|
||||
* `flushSync` exhausts queue even if something throws ([\#26366](https://github.com/facebook/react/pull/26366) by [@acdlite](https://github.com/acdlite))
|
||||
* Throw error if `react` and `react-dom` versions don’t match ([\#29236](https://github.com/facebook/react/pull/29236) by [@acdlite](https://github.com/acdlite))
|
||||
* Ensure `srcset` and `src` are assigned last on `<img>` instances ([\#30340](https://github.com/facebook/react/pull/30340) by [@gnoff](https://github.com/gnoff))
|
||||
* Javascript URLs are replaced with functions that throw errors ([\#26507](https://github.com/facebook/react/pull/26507), [\#29808](https://github.com/facebook/react/pull/29808) by [@sebmarkbage](https://github.com/sebmarkbage) and [@kassens](https://github.com/kassens))
|
||||
* Treat toggle and beforetoggle as discrete events ([\#29176](https://github.com/facebook/react/pull/29176) by [@eps1lon](https://github.com/eps1lon))
|
||||
* Filter out empty `src` and `href` attributes (unless for `<a href=”” />`) ([\#18513](https://github.com/facebook/react/pull/18513), [\#28124](https://github.com/facebook/react/pull/28124) by [@bvaughn](https://github.com/bvaughn) and [@eps1lon](https://github.com/eps1lon))
|
||||
* Fix unitless `scale` style property ([\#25601](https://github.com/facebook/react/pull/25601) by [@JonnyBurger](https://github.com/JonnyBurger))
|
||||
* Fix `onChange` error message for controlled `<select>` ([\#27740](https://github.com/facebook/react/pull/27740) by [@Biki-das](https://github.com/Biki-das))
|
||||
* Fix focus restore in child windows after element reorder ([\#30951](https://github.com/facebook/react/pull/30951) by [@ling1726](https://github.com/ling1726))
|
||||
* Remove `render`, `hydrate`, `findDOMNode`, `unmountComponentAtNode`, `unstable_createEventHandle`, `unstable_renderSubtreeIntoContainer`, and `unstable_runWithPriority`. Move `createRoot` and `hydrateRoot` to `react-dom/client`. ([\#28271](https://github.com/facebook/react/pull/28271) by [@gnoff](https://github.com/gnoff))
|
||||
* Remove `test-utils` ([\#28541](https://github.com/facebook/react/pull/28541) by [@eps1lon](https://github.com/eps1lon))
|
||||
* Remove `unstable_flushControlled` ([\#26397](https://github.com/facebook/react/pull/26397) by [@kassens](https://github.com/kassens))
|
||||
* Remove legacy mode ([\#28468](https://github.com/facebook/react/pull/28468) by [@gnoff](https://github.com/gnoff))
|
||||
* Remove `renderToStaticNodeStream()` ([\#28873](https://github.com/facebook/react/pull/28873) by @gnoff)
|
||||
* Remove `unstable_renderSubtreeIntoContainer` ([\#29771](https://github.com/facebook/react/pull/29771) by [@kassens](https://github.com/kassens))
|
||||
|
||||
#### React DOM Server
|
||||
|
||||
* Stable release of React Server Components ([Many, many PRs](https://github.com/facebook/react/pulls?q=is%3Apr+is%3Aclosed+%5BFlight%5D+in%3Atitle+created%3A%3C2024-12-01+) by [@sebmarkbage](https://github.com/sebmarkbage), [@acdlite](https://github.com/acdlite), [@gnoff](https://github.com/gnoff), [@sammy-SC](https://github.com/sammy-SC), [@gaearon](https://github.com/gaearon), [@sophiebits](https://github.com/sophiebits), [@unstubbable](https://github.com/unstubbable), [@lubieowoce](https://github.com/lubieowoce))
|
||||
* Support Server Actions ([\#26124](https://github.com/facebook/react/pull/26124), [\#26632](https://github.com/facebook/react/pull/26632), [\#27459](https://github.com/facebook/react/pull/27459) by [@sebmarkbage](https://github.com/sebmarkbage) and [@acdlite](https://github.com/acdlite))
|
||||
* Changes to SSR
|
||||
* Add external runtime which bootstraps hydration on the client for binary transparency ([\#25437](https://github.com/facebook/react/pull/25437), [\#26169](https://github.com/facebook/react/pull/26169), [\#25499](https://github.com/facebook/react/pull/25499) by [@MofeiZ](https://github.com/mofeiZ) and [@acdlite](https://github.com/acdlite))
|
||||
* Support subresource integrity for `bootstrapScripts` and `bootstrapModules` ([\#25104](https://github.com/facebook/react/pull/25104) by [@gnoff](https://github.com/gnoff))
|
||||
* Fix null bytes written at text chunk boundaries ([\#26228](https://github.com/facebook/react/pull/26228) by [@sophiebits](https://github.com/sophiebits))
|
||||
* Fix logic around attribute serialization ([\#26526](https://github.com/facebook/react/pull/26526) by [@gnoff](https://github.com/gnoff))
|
||||
* Fix precomputed chunk cleared on Node 18 ([\#25645](https://github.com/facebook/react/pull/25645) by [@feedthejim](https://github.com/feedthejim))
|
||||
* Optimize end tag chunks ([\#27522](https://github.com/facebook/react/pull/27522) by [@yujunjung](https://github.com/yujunjung))
|
||||
* Gracefully handle suspending in DOM configs ([\#26768](https://github.com/facebook/react/pull/26768) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Check for nullish values on ReactCustomFormAction ([\#26770](https://github.com/facebook/react/pull/26770) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Preload `bootstrapModules`, `bootstrapScripts`, and update priority queue ([\#26754](https://github.com/facebook/react/pull/26754), [\#26753](https://github.com/facebook/react/pull/26753), [\#27190](https://github.com/facebook/react/pull/27190), [\#27189](https://github.com/facebook/react/pull/27189) by [@gnoff](https://github.com/gnoff))
|
||||
* Client render the nearest child or parent suspense boundary if replay errors or is aborted ([\#27386](https://github.com/facebook/react/pull/27386) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Don't bail out of flushing if we still have pending root tasks ([\#27385](https://github.com/facebook/react/pull/27385) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Ensure Resumable State is Serializable ([\#27388](https://github.com/facebook/react/pull/27388) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Remove extra render pass when reverting to client render ([\#26445](https://github.com/facebook/react/pull/26445) by [@acdlite](https://github.com/acdlite))
|
||||
* Fix unwinding context during selective hydration ([\#25876](https://github.com/facebook/react/pull/25876) by [@tyao1](https://github.com/tyao1))
|
||||
* Stop flowing and then abort if a stream is cancelled ([\#27405](https://github.com/facebook/react/pull/27405) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Pass cancellation reason to abort ([\#27536](https://github.com/facebook/react/pull/27536) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Add `onHeaders` entrypoint option ([\#27641](https://github.com/facebook/react/pull/27641), [\#27712](https://github.com/facebook/react/pull/27712) by [@gnoff](https://github.com/gnoff))
|
||||
* Escape `<style>` and `<script>` textContent to enable rendering inner content without dangerouslySetInnerHTML ([\#28870](https://github.com/facebook/react/pull/28870), [\#28871](https://github.com/facebook/react/pull/28871) by [@gnoff](https://github.com/gnoff))
|
||||
* Fallback to client replaying actions for Blob serialization ([\#28987](https://github.com/facebook/react/pull/28987) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Render Suspense fallback if boundary contains new stylesheet during sync update ([\#28965](https://github.com/facebook/react/pull/28965) by [@gnoff](https://github.com/gnoff))
|
||||
* Fix header length tracking (\#30327 by [@gnoff](https://github.com/gnoff))
|
||||
* Use `srcset` to trigger load event on mount (\#30351 by [@gnoff](https://github.com/gnoff))
|
||||
* Don't perform work when closing stream (\#30497 by [@gnoff](https://github.com/gnoff))
|
||||
* Allow aborting during render (\#30488, [\#30730](https://github.com/facebook/react/pull/30730) by [@gnoff](https://github.com/gnoff))
|
||||
* Start initial work immediately (\#31079 by [@gnoff](https://github.com/gnoff))
|
||||
* A transition flowing into a dehydrated boundary no longer suspends when showing fallback ([\#27230](https://github.com/facebook/react/pull/27230) by [@acdlite](https://github.com/acdlite))
|
||||
* Fix selective hydration triggers false update loop error ([\#27439](https://github.com/facebook/react/pull/27439) by [@acdlite](https://github.com/acdlite))
|
||||
* Warn for Child Iterator of all types but allow Generator Components ([\#28853](https://github.com/facebook/react/pull/28853) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Include regular stack trace in serialized errors ([\#28684](https://github.com/facebook/react/pull/28684), [\#28738](https://github.com/facebook/react/pull/28738) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Aborting early no longer infinitely suspends ([\#24751](https://github.com/facebook/react/pull/24751) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Fix hydration warning suppression in text comparisons ([\#24784](https://github.com/facebook/react/pull/24784) by [@gnoff](https://github.com/gnoff))
|
||||
* Changes to error handling in SSR
|
||||
* Add diffs to hydration warnings ([\#28502](https://github.com/facebook/react/pull/28502), [\#28512](https://github.com/facebook/react/pull/28512) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Make Error creation lazy ([\#24728](https://github.com/facebook/react/pull/24728) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Remove recoverable error when a sync update flows into a dehydrated boundary ([\#25692](https://github.com/facebook/react/pull/25692) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Don't "fix up" mismatched text content with suppressedHydrationWarning ([\#26391](https://github.com/facebook/react/pull/26391) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Fix component stacks in errors ([\#27456](https://github.com/facebook/react/pull/27456) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Add component stacks to `onError` ([\#27761](https://github.com/facebook/react/pull/27761), [\#27850](https://github.com/facebook/react/pull/27850) by [@gnoff](https://github.com/gnoff) and [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Throw hydration mismatch errors once ([\#28502](https://github.com/facebook/react/pull/28502) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Add Bun streaming server renderer ([\#25597](https://github.com/facebook/react/pull/25597) by [@colinhacks](https://github.com/colinhacks))
|
||||
* Add nonce support to bootstrap scripts ([\#26738](https://github.com/facebook/react/pull/26738) by [@danieltott](https://github.com/danieltott))
|
||||
* Add `crossorigin` support to bootstrap scripts ([\#26844](https://github.com/facebook/react/pull/26844) by [@HenriqueLimas](https://github.com/HenriqueLimas))
|
||||
* Support `nonce` and `fetchpriority` in preload links ([\#26826](https://github.com/facebook/react/pull/26826) by [@liuyenwei](https://github.com/liuyenwei))
|
||||
* Add `referrerPolicy` to `ReactDOM.preload()` ([\#27096](https://github.com/facebook/react/pull/27096) by [@styfle](https://github.com/styfle))
|
||||
* Add server condition for `react/jsx-dev-runtime` ([\#28921](https://github.com/facebook/react/pull/28921) by [@himself65](https://github.com/himself65))
|
||||
* Export version ([\#29596](https://github.com/facebook/react/pull/29596) by [@unstubbable](https://github.com/unstubbable))
|
||||
* Rename the secret export of Client and Server internals ([\#28786](https://github.com/facebook/react/pull/28786), [\#28789](https://github.com/facebook/react/pull/28789) by [@sebmarkbage](https://github.com/sebmarkbage))
|
||||
* Remove layout effect warning on server ([\#26395](https://github.com/facebook/react/pull/26395) by [@rickhanlonii](https://github.com/rickhanlonii))
|
||||
* Remove `errorInfo.digest` from `onRecoverableError` ([\#28222](https://github.com/facebook/react/pull/28222) by [@gnoff](https://github.com/gnoff))
|
||||
|
||||
#### ReactTestRenderer
|
||||
|
||||
* Add deprecation error to `react-test-renderer` on web ([\#27903](https://github.com/facebook/react/pull/27903), [\#28904](https://github.com/facebook/react/pull/28904) by [@jackpope](https://github.com/jackpope) and [@acdlite](https://github.com/acdlite))
|
||||
* Render with ConcurrentRoot on web ([\#28498](https://github.com/facebook/react/pull/28498) by [@jackpope](https://github.com/jackpope))
|
||||
* Remove `react-test-renderer/shallow` export ([\#25475](https://github.com/facebook/react/pull/25475), [\#28497](https://github.com/facebook/react/pull/28497) by [@sebmarkbage](https://github.com/sebmarkbage) and [@jackpope](https://github.com/jackpope))
|
||||
|
||||
#### React Reconciler
|
||||
|
||||
* Enable suspending commits without blocking render ([\#26398](https://github.com/facebook/react/pull/26398), [\#26427](https://github.com/facebook/react/pull/26427) by [@acdlite](https://github.com/acdlite))
|
||||
* Remove `prepareUpdate` ([\#26583](https://github.com/facebook/react/pull/26583), [\#27409](http://github.com/facebook/react/pull/27409) by [@sebmarkbage](https://github.com/sebmarkbage) and [@sophiebits](https://github.com/sophiebits))
|
||||
|
||||
#### React-Is
|
||||
|
||||
* Enable tree shaking ([\#27701](https://github.com/facebook/react/pull/27701) by [@markerikson](https://github.com/markerikson))
|
||||
* Remove `isConcurrentMode` and `isAsyncMode` methods ([\#28224](https://github.com/facebook/react/pull/28224) by @gaearon)
|
||||
|
||||
#### useSyncExternalStore
|
||||
|
||||
* Remove React internals access ([\#29868](https://github.com/facebook/react/pull/29868) by [@phryneas](https://github.com/phryneas))
|
||||
* Fix stale selectors keeping previous store references ([\#25969](https://github.com/facebook/react/pull/25968) by [@jellevoost](https://github.com/jellevoost))
|
||||
|
||||
## 18.3.1 (April 26, 2024)
|
||||
|
||||
- Export `act` from `react` [f1338f](https://github.com/facebook/react/commit/f1338f8080abd1386454a10bbf93d67bfe37ce85)
|
||||
|
||||
+13
-12
@@ -7,18 +7,18 @@
|
||||
//
|
||||
// The @latest channel uses the version as-is, e.g.:
|
||||
//
|
||||
// 19.0.0
|
||||
// 19.1.0
|
||||
//
|
||||
// The @canary channel appends additional information, with the scheme
|
||||
// <version>-<label>-<commit_sha>, e.g.:
|
||||
//
|
||||
// 19.0.0-canary-a1c2d3e4
|
||||
// 19.1.0-canary-a1c2d3e4
|
||||
//
|
||||
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
|
||||
//
|
||||
// 0.0.0-experimental-241c4467e-20200129
|
||||
|
||||
const ReactVersion = '19.0.0';
|
||||
const ReactVersion = '19.1.0';
|
||||
|
||||
// The label used by the @canary channel. Represents the upcoming release's
|
||||
// stability. Most of the time, this will be "canary", but we may temporarily
|
||||
@@ -26,27 +26,28 @@ const ReactVersion = '19.0.0';
|
||||
//
|
||||
// It only affects the label used in the version string. To customize the
|
||||
// npm dist tags used during publish, refer to .github/workflows/runtime_prereleases_*.yml.
|
||||
const canaryChannelLabel = 'rc';
|
||||
const canaryChannelLabel = 'canary';
|
||||
|
||||
// If the canaryChannelLabel is "rc", the build pipeline will use this to build
|
||||
// an RC version of the packages.
|
||||
const rcNumber = 1;
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '5.1.0',
|
||||
'jest-react': '0.16.0',
|
||||
'eslint-plugin-react-hooks': '5.2.0',
|
||||
'jest-react': '0.17.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
'react-dom': ReactVersion,
|
||||
'react-server-dom-webpack': ReactVersion,
|
||||
'react-server-dom-turbopack': ReactVersion,
|
||||
'react-server-dom-parcel': ReactVersion,
|
||||
'react-is': ReactVersion,
|
||||
'react-reconciler': '0.31.0',
|
||||
'react-refresh': '0.16.0',
|
||||
'react-reconciler': '0.32.0',
|
||||
'react-refresh': '0.17.0',
|
||||
'react-test-renderer': ReactVersion,
|
||||
'use-subscription': '1.10.0',
|
||||
'use-sync-external-store': '1.4.0',
|
||||
scheduler: '0.25.0',
|
||||
'use-subscription': '1.11.0',
|
||||
'use-sync-external-store': '1.5.0',
|
||||
scheduler: '0.26.0',
|
||||
};
|
||||
|
||||
// These packages do not exist in the @canary or @latest channel, only
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
function TestComponent(t0) {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function TestComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
function MyApp() {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function MyApp() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
function TestComponent(t0) {
|
||||
"use memo";
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function TestComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
function TestComponent({ x }) {
|
||||
"use no memo";
|
||||
export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function useFoo(propVal) {
|
||||
const $ = _c(2);
|
||||
const t0 = (propVal.baz: number);
|
||||
let t1;
|
||||
if ($[0] !== t0) {
|
||||
t1 = <div>{t0}</div>;
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Foo() {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = foo();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const x = t0 as number;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <div>{x}</div>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
"use no memo";
|
||||
function TestComponent({ x }) {
|
||||
"use memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function TestComponent(t0) {
|
||||
"use memo";
|
||||
const $ = _c(2);
|
||||
@@ -12,7 +13,7 @@ function TestComponent(t0) {
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function anonymous_1(t0) {
|
||||
const TestComponent2 = (t0) => {
|
||||
"use memo";
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
@@ -25,4 +26,4 @@ function anonymous_1(t0) {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
};
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
function anonymous_1() {
|
||||
const TestComponent = function () {
|
||||
"use no memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
function anonymous_3({ x }) {
|
||||
};
|
||||
const TestComponent2 = ({ x }) => {
|
||||
"use no memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,11 +9,11 @@ import {expect, test} from '@playwright/test';
|
||||
import {encodeStore, type Store} from '../../lib/stores';
|
||||
import {format} from 'prettier';
|
||||
|
||||
function print(data: Array<string>): Promise<string> {
|
||||
function formatPrint(data: Array<string>): Promise<string> {
|
||||
return format(data.join(''), {parser: 'babel'});
|
||||
}
|
||||
|
||||
const DIRECTIVE_TEST_CASES = [
|
||||
const TEST_CASE_INPUTS = [
|
||||
{
|
||||
name: 'module-scope-use-memo',
|
||||
input: `
|
||||
@@ -55,7 +55,7 @@ const TestComponent2 = ({ x }) => {
|
||||
};`,
|
||||
},
|
||||
{
|
||||
name: 'function-scope-beats-module-scope',
|
||||
name: 'todo-function-scope-does-not-beat-module-scope',
|
||||
input: `
|
||||
'use no memo';
|
||||
function TestComponent({ x }) {
|
||||
@@ -63,6 +63,26 @@ function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: 'parse-typescript',
|
||||
input: `
|
||||
function Foo() {
|
||||
const x = foo() as number;
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
`,
|
||||
noFormat: true,
|
||||
},
|
||||
{
|
||||
name: 'parse-flow',
|
||||
input: `
|
||||
// @flow
|
||||
function useFoo(propVal: {+baz: number}) {
|
||||
return <div>{(propVal.baz as number)}</div>;
|
||||
}
|
||||
`,
|
||||
noFormat: true,
|
||||
},
|
||||
];
|
||||
|
||||
test('editor should open successfully', async ({page}) => {
|
||||
@@ -90,7 +110,7 @@ test('editor should compile from hash successfully', async ({page}) => {
|
||||
});
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
const output = await print(text);
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot('01-user-output.txt');
|
||||
@@ -115,14 +135,14 @@ test('reset button works', async ({page}) => {
|
||||
});
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
const output = await print(text);
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot('02-default-output.txt');
|
||||
});
|
||||
|
||||
DIRECTIVE_TEST_CASES.forEach((t, idx) =>
|
||||
test(`directives work: ${t.name}`, async ({page}) => {
|
||||
TEST_CASE_INPUTS.forEach((t, idx) =>
|
||||
test(`playground compiles: ${t.name}`, async ({page}) => {
|
||||
const store: Store = {
|
||||
source: t.input,
|
||||
};
|
||||
@@ -135,7 +155,12 @@ DIRECTIVE_TEST_CASES.forEach((t, idx) =>
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
|
||||
const output = await print(text);
|
||||
let output: string;
|
||||
if (t.noFormat) {
|
||||
output = text.join('');
|
||||
} else {
|
||||
output = await formatPrint(text);
|
||||
}
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot(`${t.name}-output.txt`);
|
||||
|
||||
@@ -5,23 +5,22 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {parse as babelParse} from '@babel/parser';
|
||||
import {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import traverse, {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
runPlayground,
|
||||
type Hook,
|
||||
findDirectiveDisablingMemoization,
|
||||
findDirectiveEnablingMemoization,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
|
||||
import {type EnvironmentConfig} from 'babel-plugin-react-compiler/src/HIR/Environment';
|
||||
import clsx from 'clsx';
|
||||
import invariant from 'invariant';
|
||||
import {useSnackbar} from 'notistack';
|
||||
@@ -39,32 +38,18 @@ import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import Input from './Input';
|
||||
import {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
default as Output,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
|
||||
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
|
||||
type FunctionLike =
|
||||
| NodePath<t.FunctionDeclaration>
|
||||
| NodePath<t.ArrowFunctionExpression>
|
||||
| NodePath<t.FunctionExpression>;
|
||||
enum MemoizeDirectiveState {
|
||||
Enabled = 'Enabled',
|
||||
Disabled = 'Disabled',
|
||||
Undefined = 'Undefined',
|
||||
}
|
||||
|
||||
const MEMOIZE_ENABLED_OR_UNDEFINED_STATES = new Set([
|
||||
MemoizeDirectiveState.Enabled,
|
||||
MemoizeDirectiveState.Undefined,
|
||||
]);
|
||||
|
||||
const MEMOIZE_ENABLED_OR_DISABLED_STATES = new Set([
|
||||
MemoizeDirectiveState.Enabled,
|
||||
MemoizeDirectiveState.Disabled,
|
||||
]);
|
||||
function parseInput(input: string, language: 'flow' | 'typescript'): any {
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
@@ -77,95 +62,45 @@ function parseInput(input: string, language: 'flow' | 'typescript'): any {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
});
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFunctions(
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): Array<{
|
||||
compilationEnabled: boolean;
|
||||
fn: FunctionLike;
|
||||
}> {
|
||||
const items: Array<{
|
||||
compilationEnabled: boolean;
|
||||
fn: FunctionLike;
|
||||
}> = [];
|
||||
try {
|
||||
const ast = parseInput(source, language);
|
||||
traverse(ast, {
|
||||
FunctionDeclaration(nodePath) {
|
||||
items.push({
|
||||
compilationEnabled: shouldCompile(nodePath),
|
||||
fn: nodePath,
|
||||
});
|
||||
nodePath.skip();
|
||||
},
|
||||
ArrowFunctionExpression(nodePath) {
|
||||
items.push({
|
||||
compilationEnabled: shouldCompile(nodePath),
|
||||
fn: nodePath,
|
||||
});
|
||||
nodePath.skip();
|
||||
},
|
||||
FunctionExpression(nodePath) {
|
||||
items.push({
|
||||
compilationEnabled: shouldCompile(nodePath),
|
||||
fn: nodePath,
|
||||
});
|
||||
nodePath.skip();
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: String(e),
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
environment: EnvironmentConfig,
|
||||
logIR: (pipelineValue: CompilerPipelineValue) => void,
|
||||
): CompilerTransformOutput {
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: () => {},
|
||||
},
|
||||
environment,
|
||||
compilationMode: 'all',
|
||||
panicThreshold: 'all_errors',
|
||||
});
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, opts]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function shouldCompile(fn: FunctionLike): boolean {
|
||||
const {body} = fn.node;
|
||||
if (t.isBlockStatement(body)) {
|
||||
const selfCheck = checkExplicitMemoizeDirectives(body.directives);
|
||||
if (selfCheck === MemoizeDirectiveState.Enabled) return true;
|
||||
if (selfCheck === MemoizeDirectiveState.Disabled) return false;
|
||||
|
||||
const parentWithDirective = fn.findParent(parentPath => {
|
||||
if (parentPath.isBlockStatement() || parentPath.isProgram()) {
|
||||
const directiveCheck = checkExplicitMemoizeDirectives(
|
||||
parentPath.node.directives,
|
||||
);
|
||||
return MEMOIZE_ENABLED_OR_DISABLED_STATES.has(directiveCheck);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!parentWithDirective) return true;
|
||||
const parentDirectiveCheck = checkExplicitMemoizeDirectives(
|
||||
(parentWithDirective.node as t.Program | t.BlockStatement).directives,
|
||||
);
|
||||
return MEMOIZE_ENABLED_OR_UNDEFINED_STATES.has(parentDirectiveCheck);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkExplicitMemoizeDirectives(
|
||||
directives: Array<t.Directive>,
|
||||
): MemoizeDirectiveState {
|
||||
if (findDirectiveEnablingMemoization(directives).length) {
|
||||
return MemoizeDirectiveState.Enabled;
|
||||
}
|
||||
if (findDirectiveDisablingMemoization(directives).length) {
|
||||
return MemoizeDirectiveState.Disabled;
|
||||
}
|
||||
return MemoizeDirectiveState.Undefined;
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
@@ -216,37 +151,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
function isHookName(s: string): boolean {
|
||||
return /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
|
||||
function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
|
||||
if (id != null) {
|
||||
if (isHookName(id.name)) {
|
||||
return 'Hook';
|
||||
}
|
||||
|
||||
const isPascalCaseNameSpace = /^[A-Z].*/;
|
||||
if (isPascalCaseNameSpace.test(id.name)) {
|
||||
return 'Component';
|
||||
}
|
||||
}
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
function getFunctionIdentifier(
|
||||
fn:
|
||||
| NodePath<t.FunctionDeclaration>
|
||||
| NodePath<t.ArrowFunctionExpression>
|
||||
| NodePath<t.FunctionExpression>,
|
||||
): t.Identifier | null {
|
||||
if (fn.isArrowFunctionExpression()) {
|
||||
return null;
|
||||
}
|
||||
const id = fn.get('id');
|
||||
return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
|
||||
}
|
||||
|
||||
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
@@ -264,71 +168,25 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let count = 0;
|
||||
const withIdentifier = (id: t.Identifier | null): t.Identifier => {
|
||||
if (id != null && id.name != null) {
|
||||
return id;
|
||||
} else {
|
||||
return t.identifier(`anonymous_${count++}`);
|
||||
}
|
||||
};
|
||||
let transformOutput;
|
||||
try {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const config = parseConfigPragmaForTests(pragma);
|
||||
const parsedFunctions = parseFunctions(source, language);
|
||||
for (const func of parsedFunctions) {
|
||||
const id = withIdentifier(getFunctionIdentifier(func.fn));
|
||||
const fnName = id.name;
|
||||
if (!func.compilationEnabled) {
|
||||
upsert({
|
||||
kind: 'ast',
|
||||
fnName,
|
||||
name: 'CodeGen',
|
||||
value: {
|
||||
type: 'FunctionDeclaration',
|
||||
id:
|
||||
func.fn.isArrowFunctionExpression() ||
|
||||
func.fn.isFunctionExpression()
|
||||
? withIdentifier(null)
|
||||
: func.fn.node.id,
|
||||
async: func.fn.node.async,
|
||||
generator: !!func.fn.node.generator,
|
||||
body: func.fn.node.body as t.BlockStatement,
|
||||
params: func.fn.node.params,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const result of runPlayground(
|
||||
func.fn,
|
||||
{
|
||||
...config,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
getReactFunctionType(id),
|
||||
)) {
|
||||
|
||||
transformOutput = invokeCompiler(
|
||||
source,
|
||||
language,
|
||||
{...config, customHooks: new Map([...COMMON_HOOKS])},
|
||||
result => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
upsert({
|
||||
kind: 'ast',
|
||||
fnName,
|
||||
name: result.name,
|
||||
value: {
|
||||
type: 'FunctionDeclaration',
|
||||
id: withIdentifier(result.value.id),
|
||||
async: result.value.async,
|
||||
generator: result.value.generator,
|
||||
body: result.value.body,
|
||||
params: result.value.params,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName,
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
@@ -337,7 +195,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName,
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
@@ -346,7 +204,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName,
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
@@ -357,8 +215,8 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
@@ -385,7 +243,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error: error}, language];
|
||||
}
|
||||
return [{kind: 'ok', results}, language];
|
||||
return [{kind: 'ok', results, transformOutput}, language];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
@@ -405,7 +263,7 @@ export default function Editor(): JSX.Element {
|
||||
} catch (e) {
|
||||
invariant(e instanceof Error, 'Only Error may be caught.');
|
||||
enqueueSnackbar(e.message, {
|
||||
variant: 'message',
|
||||
variant: 'warning',
|
||||
...createMessage(
|
||||
'Bad URL - fell back to the default Playground.',
|
||||
MessageLevel.Info,
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
CodeIcon,
|
||||
DocumentAddIcon,
|
||||
@@ -21,17 +19,12 @@ import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'ast';
|
||||
name: string;
|
||||
fnName: string | null;
|
||||
value: t.FunctionDeclaration;
|
||||
}
|
||||
| {
|
||||
kind: 'hir';
|
||||
name: string;
|
||||
@@ -41,8 +34,17 @@ export type PrintedCompilerPipelineValue =
|
||||
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
|
||||
| {kind: 'debug'; name: string; fnName: string | null; value: string};
|
||||
|
||||
export type CompilerTransformOutput = {
|
||||
code: string;
|
||||
sourceMaps: BabelFileResult['map'];
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
export type CompilerOutput =
|
||||
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
|
||||
| {
|
||||
kind: 'ok';
|
||||
transformOutput: CompilerTransformOutput;
|
||||
results: Map<string, Array<PrintedCompilerPipelineValue>>;
|
||||
}
|
||||
| {
|
||||
kind: 'err';
|
||||
results: Map<string, Array<PrintedCompilerPipelineValue>>;
|
||||
@@ -61,7 +63,6 @@ async function tabify(
|
||||
const tabs = new Map<string, React.ReactNode>();
|
||||
const reorderedTabs = new Map<string, React.ReactNode>();
|
||||
const concattedResults = new Map<string, string>();
|
||||
let topLevelFnDecls: Array<t.FunctionDeclaration> = [];
|
||||
// Concat all top level function declaration results into a single tab for each pass
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
for (const result of results) {
|
||||
@@ -87,9 +88,6 @@ async function tabify(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ast':
|
||||
topLevelFnDecls.push(result.value);
|
||||
break;
|
||||
case 'debug': {
|
||||
concattedResults.set(passName, result.value);
|
||||
break;
|
||||
@@ -114,13 +112,17 @@ async function tabify(
|
||||
lastPassOutput = text;
|
||||
}
|
||||
// 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
|
||||
*/
|
||||
const ast = t.program(topLevelFnDecls);
|
||||
const {code, sourceMapUrl} = await codegen(ast, source);
|
||||
if (compilerOutput.kind === 'ok') {
|
||||
const {transformOutput} = compilerOutput;
|
||||
const sourceMapUrl = getSourceMapUrl(
|
||||
transformOutput.code,
|
||||
JSON.stringify(transformOutput.sourceMaps),
|
||||
);
|
||||
const code = await prettier.format(transformOutput.code, {
|
||||
semi: true,
|
||||
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
reorderedTabs.set(
|
||||
'JS',
|
||||
<TextTabContent
|
||||
@@ -147,27 +149,6 @@ async function tabify(
|
||||
return reorderedTabs;
|
||||
}
|
||||
|
||||
async function codegen(
|
||||
ast: t.Program,
|
||||
source: string,
|
||||
): Promise<{code: any; sourceMapUrl: string | null}> {
|
||||
const generated = generate(
|
||||
ast,
|
||||
{sourceMaps: true, sourceFileName: 'input.js'},
|
||||
source,
|
||||
);
|
||||
const sourceMapUrl = getSourceMapUrl(
|
||||
generated.code,
|
||||
JSON.stringify(generated.map),
|
||||
);
|
||||
const codegenOutput = await prettier.format(generated.code, {
|
||||
semi: true,
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
return {code: codegenOutput, sourceMapUrl};
|
||||
}
|
||||
|
||||
function utf16ToUTF8(s: string): string {
|
||||
return unescape(encodeURIComponent(s));
|
||||
}
|
||||
|
||||
@@ -42,9 +42,7 @@
|
||||
"babel-jest": "^29.0.3",
|
||||
"babel-plugin-fbt": "^1.0.0",
|
||||
"babel-plugin-fbt-runtime": "^1.0.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",
|
||||
|
||||
@@ -39,7 +39,10 @@ export default function BabelPluginReactCompiler(
|
||||
) {
|
||||
opts = injectReanimatedFlag(opts);
|
||||
}
|
||||
if (isDev) {
|
||||
if (
|
||||
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
|
||||
isDev
|
||||
) {
|
||||
opts = {
|
||||
...opts,
|
||||
environment: {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
/*
|
||||
@@ -121,7 +122,22 @@ export type PluginOptions = {
|
||||
target: CompilerReactTarget;
|
||||
};
|
||||
|
||||
const CompilerReactTargetSchema = z.enum(['17', '18', '19']);
|
||||
const CompilerReactTargetSchema = z.union([
|
||||
z.literal('17'),
|
||||
z.literal('18'),
|
||||
z.literal('19'),
|
||||
/**
|
||||
* Used exclusively for Meta apps which are guaranteed to have compatible
|
||||
* react runtime and compiler versions. Note that only the FB-internal bundles
|
||||
* re-export useMemoCache (see
|
||||
* https://github.com/facebook/react/blob/5b0ef217ef32333a8e56f39be04327c89efa346f/packages/react/index.fb.js#L68-L70),
|
||||
* so this option is invalid / creates runtime errors for open-source users.
|
||||
*/
|
||||
z.object({
|
||||
kind: z.literal('donotuse_meta_internal'),
|
||||
runtimeModule: z.string().default('react'),
|
||||
}),
|
||||
]);
|
||||
export type CompilerReactTarget = z.infer<typeof CompilerReactTargetSchema>;
|
||||
|
||||
const CompilationModeSchema = z.enum([
|
||||
@@ -194,6 +210,7 @@ export type LoggerEvent =
|
||||
|
||||
export type Logger = {
|
||||
logEvent: (filename: string | null, event: LoggerEvent) => void;
|
||||
debugLogIRs?: (value: CompilerPipelineValue) => void;
|
||||
};
|
||||
|
||||
export const defaultOptions: PluginOptions = {
|
||||
|
||||
@@ -79,13 +79,6 @@ import {
|
||||
rewriteInstructionKindsBasedOnReassignment,
|
||||
} from '../SSA';
|
||||
import {inferTypes} from '../TypeInference';
|
||||
import {
|
||||
logCodegenFunction,
|
||||
logDebug,
|
||||
logHIRFunction,
|
||||
logReactiveFunction,
|
||||
} from '../Utils/logger';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {
|
||||
validateContextVariableLValues,
|
||||
validateHooksUsage,
|
||||
@@ -104,6 +97,7 @@ import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetSta
|
||||
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
|
||||
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -111,7 +105,7 @@ export type CompilerPipelineValue =
|
||||
| {kind: 'reactive'; name: string; value: ReactiveFunction}
|
||||
| {kind: 'debug'; name: string; value: string};
|
||||
|
||||
export function* run(
|
||||
function run(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
@@ -121,7 +115,7 @@ export function* run(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
): CodegenFunction {
|
||||
const contextIdentifiers = findContextIdentifiers(func);
|
||||
const env = new Environment(
|
||||
func.scope,
|
||||
@@ -133,30 +127,32 @@ export function* run(
|
||||
code,
|
||||
useMemoCacheIdentifier,
|
||||
);
|
||||
yield log({
|
||||
env.logger?.debugLogIRs?.({
|
||||
kind: 'debug',
|
||||
name: 'EnvironmentConfig',
|
||||
value: prettyFormat(env.config),
|
||||
});
|
||||
const ast = yield* runWithEnvironment(func, env);
|
||||
return ast;
|
||||
return runWithEnvironment(func, env);
|
||||
}
|
||||
|
||||
/*
|
||||
* Note: this is split from run() to make `config` out of scope, so that all
|
||||
* access to feature flags has to be through the Environment for consistency.
|
||||
*/
|
||||
function* runWithEnvironment(
|
||||
function runWithEnvironment(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
env: Environment,
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
): CodegenFunction {
|
||||
const log = (value: CompilerPipelineValue): void => {
|
||||
env.logger?.debugLogIRs?.(value);
|
||||
};
|
||||
const hir = lower(func, env).unwrap();
|
||||
yield log({kind: 'hir', name: 'HIR', value: hir});
|
||||
log({kind: 'hir', name: 'HIR', value: hir});
|
||||
|
||||
pruneMaybeThrows(hir);
|
||||
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
validateContextVariableLValues(hir);
|
||||
validateUseMemo(hir);
|
||||
@@ -167,35 +163,35 @@ function* runWithEnvironment(
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
) {
|
||||
dropManualMemoization(hir);
|
||||
yield log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
}
|
||||
|
||||
inlineImmediatelyInvokedFunctionExpressions(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'InlineImmediatelyInvokedFunctionExpressions',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
mergeConsecutiveBlocks(hir);
|
||||
yield log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
|
||||
log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
|
||||
enterSSA(hir);
|
||||
yield log({kind: 'hir', name: 'SSA', value: hir});
|
||||
log({kind: 'hir', name: 'SSA', value: hir});
|
||||
|
||||
eliminateRedundantPhi(hir);
|
||||
yield log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
|
||||
log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
|
||||
constantPropagation(hir);
|
||||
yield log({kind: 'hir', name: 'ConstantPropagation', value: hir});
|
||||
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
|
||||
|
||||
inferTypes(hir);
|
||||
yield log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
|
||||
if (env.config.validateHooksUsage) {
|
||||
validateHooksUsage(hir);
|
||||
@@ -209,28 +205,31 @@ function* runWithEnvironment(
|
||||
lowerContextAccess(hir, env.config.lowerContextAccess);
|
||||
}
|
||||
|
||||
optimizePropsMethodCalls(hir);
|
||||
log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
|
||||
|
||||
analyseFunctions(hir);
|
||||
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
inferReferenceEffects(hir);
|
||||
yield log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
deadCodeElimination(hir);
|
||||
yield log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
|
||||
if (env.config.enableInstructionReordering) {
|
||||
instructionReordering(hir);
|
||||
yield log({kind: 'hir', name: 'InstructionReordering', value: hir});
|
||||
log({kind: 'hir', name: 'InstructionReordering', value: hir});
|
||||
}
|
||||
|
||||
pruneMaybeThrows(hir);
|
||||
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
inferMutableRanges(hir);
|
||||
yield log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
assertValidMutableRanges(hir);
|
||||
@@ -253,27 +252,27 @@ function* runWithEnvironment(
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'RewriteInstructionKindsBasedOnReassignment',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
propagatePhiTypes(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagatePhiTypes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
inferReactiveScopeVariables(hir);
|
||||
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
|
||||
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
|
||||
|
||||
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MemoizeFbtAndMacroOperandsInSameScope',
|
||||
value: hir,
|
||||
@@ -285,39 +284,39 @@ function* runWithEnvironment(
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
}
|
||||
|
||||
alignMethodCallScopes(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignMethodCallScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
alignObjectMethodScopes(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignObjectMethodScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PruneUnusedLabelsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignReactiveScopesToBlockScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MergeOverlappingReactiveScopesHIR',
|
||||
value: hir,
|
||||
@@ -325,7 +324,7 @@ function* runWithEnvironment(
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'BuildReactiveScopeTerminalsHIR',
|
||||
value: hir,
|
||||
@@ -334,14 +333,14 @@ function* runWithEnvironment(
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenReactiveLoopsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenScopesWithHooksOrUseHIR',
|
||||
value: hir,
|
||||
@@ -349,19 +348,19 @@ function* runWithEnvironment(
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagateScopeDependenciesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
if (env.config.inferEffectDependencies) {
|
||||
inferEffectDependencies(env, hir);
|
||||
inferEffectDependencies(hir);
|
||||
}
|
||||
|
||||
if (env.config.inlineJsxTransform) {
|
||||
inlineJsxTransform(hir, env.config.inlineJsxTransform);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'inlineJsxTransform',
|
||||
value: hir,
|
||||
@@ -369,7 +368,7 @@ function* runWithEnvironment(
|
||||
}
|
||||
|
||||
const reactiveFunction = buildReactiveFunction(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'BuildReactiveFunction',
|
||||
value: reactiveFunction,
|
||||
@@ -378,7 +377,7 @@ function* runWithEnvironment(
|
||||
assertWellFormedBreakTargets(reactiveFunction);
|
||||
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLabels',
|
||||
value: reactiveFunction,
|
||||
@@ -386,35 +385,35 @@ function* runWithEnvironment(
|
||||
assertScopeInstructionsWithinScopes(reactiveFunction);
|
||||
|
||||
pruneNonEscapingScopes(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonEscapingScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneNonReactiveDependencies(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonReactiveDependencies',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneUnusedScopes(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'MergeReactiveScopesThatInvalidateTogether',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneAlwaysInvalidatingScopes(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneAlwaysInvalidatingScopes',
|
||||
value: reactiveFunction,
|
||||
@@ -422,7 +421,7 @@ function* runWithEnvironment(
|
||||
|
||||
if (env.config.enableChangeDetectionForDebugging != null) {
|
||||
pruneInitializationDependencies(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneInitializationDependencies',
|
||||
value: reactiveFunction,
|
||||
@@ -430,49 +429,49 @@ function* runWithEnvironment(
|
||||
}
|
||||
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PropagateEarlyReturns',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLValues',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PromoteUsedTemporaries',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
extractScopeDeclarationsFromDestructuring(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'ExtractScopeDeclarationsFromDestructuring',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
stabilizeBlockIds(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'StabilizeBlockIds',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
const uniqueIdentifiers = renameVariables(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'RenameVariables',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneHoistedContexts(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneHoistedContexts',
|
||||
value: reactiveFunction,
|
||||
@@ -493,9 +492,9 @@ function* runWithEnvironment(
|
||||
uniqueIdentifiers,
|
||||
fbtOperands,
|
||||
}).unwrap();
|
||||
yield log({kind: 'ast', name: 'Codegen', value: ast});
|
||||
log({kind: 'ast', name: 'Codegen', value: ast});
|
||||
for (const outlined of ast.outlined) {
|
||||
yield log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -521,7 +520,7 @@ export function compileFn(
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
): CodegenFunction {
|
||||
let generator = run(
|
||||
return run(
|
||||
func,
|
||||
config,
|
||||
fnType,
|
||||
@@ -530,46 +529,4 @@ export function compileFn(
|
||||
filename,
|
||||
code,
|
||||
);
|
||||
while (true) {
|
||||
const next = generator.next();
|
||||
if (next.done) {
|
||||
return next.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function log(value: CompilerPipelineValue): CompilerPipelineValue {
|
||||
switch (value.kind) {
|
||||
case 'ast': {
|
||||
logCodegenFunction(value.name, value.value);
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
logHIRFunction(value.name, value.value);
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
logReactiveFunction(value.name, value.value);
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
logDebug(value.name, value.value);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(value, 'Unexpected compilation kind');
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1123,30 +1123,23 @@ 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, {
|
||||
function getReactCompilerRuntimeModule(opts: PluginOptions): string {
|
||||
if (opts.target === '19') {
|
||||
return 'react/compiler-runtime'; // from react namespace
|
||||
} else if (opts.target === '17' || opts.target === '18') {
|
||||
return 'react-compiler-runtime'; // npm package
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
opts.target != null &&
|
||||
opts.target.kind === 'donotuse_meta_internal' &&
|
||||
typeof opts.target.runtimeModule === 'string',
|
||||
{
|
||||
reason: 'Expected target to already be validated',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
return opts.target.runtimeModule;
|
||||
}
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
@@ -1078,6 +1078,12 @@ function lowerStatement(
|
||||
const left = stmt.get('left');
|
||||
const leftLoc = left.node.loc ?? GeneratedSource;
|
||||
let test: Place;
|
||||
const advanceIterator = lowerValueToTemporary(builder, {
|
||||
kind: 'IteratorNext',
|
||||
loc: leftLoc,
|
||||
iterator: {...iterator},
|
||||
collection: {...value},
|
||||
});
|
||||
if (left.isVariableDeclaration()) {
|
||||
const declarations = left.get('declarations');
|
||||
CompilerError.invariant(declarations.length === 1, {
|
||||
@@ -1087,12 +1093,6 @@ function lowerStatement(
|
||||
suggestions: null,
|
||||
});
|
||||
const id = declarations[0].get('id');
|
||||
const advanceIterator = lowerValueToTemporary(builder, {
|
||||
kind: 'IteratorNext',
|
||||
loc: leftLoc,
|
||||
iterator: {...iterator},
|
||||
collection: {...value},
|
||||
});
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
leftLoc,
|
||||
@@ -1103,13 +1103,19 @@ function lowerStatement(
|
||||
);
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForOfStatement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: left.node.loc ?? null,
|
||||
suggestions: null,
|
||||
CompilerError.invariant(left.isLVal(), {
|
||||
loc: leftLoc,
|
||||
reason: 'Expected ForOf init to be a variable declaration or lval',
|
||||
});
|
||||
return;
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
leftLoc,
|
||||
InstructionKind.Reassign,
|
||||
left,
|
||||
advanceIterator,
|
||||
'Assignment',
|
||||
);
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
}
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
@@ -1166,6 +1172,11 @@ function lowerStatement(
|
||||
const left = stmt.get('left');
|
||||
const leftLoc = left.node.loc ?? GeneratedSource;
|
||||
let test: Place;
|
||||
const nextPropertyTemp = lowerValueToTemporary(builder, {
|
||||
kind: 'NextPropertyOf',
|
||||
loc: leftLoc,
|
||||
value,
|
||||
});
|
||||
if (left.isVariableDeclaration()) {
|
||||
const declarations = left.get('declarations');
|
||||
CompilerError.invariant(declarations.length === 1, {
|
||||
@@ -1175,11 +1186,6 @@ function lowerStatement(
|
||||
suggestions: null,
|
||||
});
|
||||
const id = declarations[0].get('id');
|
||||
const nextPropertyTemp = lowerValueToTemporary(builder, {
|
||||
kind: 'NextPropertyOf',
|
||||
loc: leftLoc,
|
||||
value,
|
||||
});
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
leftLoc,
|
||||
@@ -1190,13 +1196,19 @@ function lowerStatement(
|
||||
);
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
} else {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForInStatement`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: left.node.loc ?? null,
|
||||
suggestions: null,
|
||||
CompilerError.invariant(left.isLVal(), {
|
||||
loc: leftLoc,
|
||||
reason: 'Expected ForIn init to be a variable declaration or lval',
|
||||
});
|
||||
return;
|
||||
const assign = lowerAssignment(
|
||||
builder,
|
||||
leftLoc,
|
||||
InstructionKind.Reassign,
|
||||
left,
|
||||
nextPropertyTemp,
|
||||
'Assignment',
|
||||
);
|
||||
test = lowerValueToTemporary(builder, assign);
|
||||
}
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
|
||||
@@ -168,11 +168,19 @@ const EnvironmentConfigSchema = z.object({
|
||||
customMacros: z.nullable(z.array(MacroSchema)).default(null),
|
||||
|
||||
/**
|
||||
* Enable a check that resets the memoization cache when the source code of the file changes.
|
||||
* This is intended to support hot module reloading (HMR), where the same runtime component
|
||||
* instance will be reused across different versions of the component source.
|
||||
* Enable a check that resets the memoization cache when the source code of
|
||||
* the file changes. This is intended to support hot module reloading (HMR),
|
||||
* where the same runtime component instance will be reused across different
|
||||
* versions of the component source.
|
||||
*
|
||||
* When set to
|
||||
* - true: code for HMR support is always generated, regardless of NODE_ENV
|
||||
* or `globalThis.__DEV__`
|
||||
* - false: code for HMR support is not generated
|
||||
* - null: (default) code for HMR support is conditionally generated dependent
|
||||
* on `NODE_ENV` and `globalThis.__DEV__` at the time of compilation.
|
||||
*/
|
||||
enableResetCacheOnSourceFileChanges: z.boolean().default(false),
|
||||
enableResetCacheOnSourceFileChanges: z.nullable(z.boolean()).default(null),
|
||||
|
||||
/**
|
||||
* Enable using information from existing useMemo/useCallback to understand when a value is done
|
||||
@@ -241,10 +249,43 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableOptionalDependencies: z.boolean().default(true),
|
||||
|
||||
enableFire: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference and auto-insertion of effect dependencies. Still experimental.
|
||||
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
|
||||
* configurable module and import pairs to allow for user-land experimentation. For example,
|
||||
* [
|
||||
* {
|
||||
* module: 'react',
|
||||
* imported: 'useEffect',
|
||||
* numRequiredArgs: 1,
|
||||
* },{
|
||||
* module: 'MyExperimentalEffectHooks',
|
||||
* imported: 'useExperimentalEffect',
|
||||
* numRequiredArgs: 2,
|
||||
* },
|
||||
* ]
|
||||
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
|
||||
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
|
||||
*
|
||||
* `numRequiredArgs` tells the compiler the amount of arguments required to append a dependency
|
||||
* array to the end of the call. With the configuration above, we'd insert dependencies for
|
||||
* `useEffect` if it is only given a single argument and it would be appended to the argument list.
|
||||
*
|
||||
* numRequiredArgs must always be greater than 0, otherwise there is no function to analyze for dependencies
|
||||
*
|
||||
* Still experimental.
|
||||
*/
|
||||
inferEffectDependencies: z.boolean().default(false),
|
||||
inferEffectDependencies: z
|
||||
.nullable(
|
||||
z.array(
|
||||
z.object({
|
||||
function: ExternalFunctionSchema,
|
||||
numRequiredArgs: z.number(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.default(null),
|
||||
|
||||
/**
|
||||
* Enables inlining ReactElement object literals in place of JSX
|
||||
@@ -614,6 +655,29 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useContext_withSelector',
|
||||
},
|
||||
inferEffectDependencies: [
|
||||
{
|
||||
function: {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'shared-runtime',
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'useEffectWrapper',
|
||||
importSpecifierName: 'default',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -654,7 +718,10 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
|
||||
if (
|
||||
key !== 'enableResetCacheOnSourceFileChanges' &&
|
||||
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
|
||||
) {
|
||||
// skip parsing non-boolean properties
|
||||
continue;
|
||||
}
|
||||
@@ -664,9 +731,15 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
|
||||
maybeConfig[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
|
||||
if (config.success) {
|
||||
/**
|
||||
* Unless explicitly enabled, do not insert HMR handling code
|
||||
* in test fixtures or playground to reduce visual noise.
|
||||
*/
|
||||
if (config.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
config.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
@@ -1100,3 +1173,5 @@ export function tryParseExternalFunction(
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
export const DEFAULT_EXPORT = 'default';
|
||||
|
||||
@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInFireId,
|
||||
BuiltInMixedReadonlyId,
|
||||
BuiltInUseActionStateId,
|
||||
BuiltInUseContextHookId,
|
||||
@@ -87,6 +88,21 @@ const UNTYPED_GLOBALS: Set<string> = new Set([
|
||||
]);
|
||||
|
||||
const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
[
|
||||
'Object',
|
||||
addObject(DEFAULT_SHAPES, 'Object', [
|
||||
[
|
||||
'keys',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
addObject(DEFAULT_SHAPES, 'Array', [
|
||||
@@ -468,6 +484,21 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInUseOperatorId,
|
||||
),
|
||||
],
|
||||
[
|
||||
'fire',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
},
|
||||
BuiltInFireId,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
TYPED_GLOBALS.push(
|
||||
|
||||
@@ -840,6 +840,11 @@ export type LoadLocal = {
|
||||
place: Place;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type LoadContext = {
|
||||
kind: 'LoadContext';
|
||||
place: Place;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
/*
|
||||
* The value of a given instruction. Note that values are not recursive: complex
|
||||
@@ -852,11 +857,7 @@ export type LoadLocal = {
|
||||
|
||||
export type InstructionValue =
|
||||
| LoadLocal
|
||||
| {
|
||||
kind: 'LoadContext';
|
||||
place: Place;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| LoadContext
|
||||
| {
|
||||
kind: 'DeclareLocal';
|
||||
lvalue: LValue;
|
||||
@@ -1644,6 +1645,10 @@ export function isArrayType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
|
||||
}
|
||||
|
||||
export function isPropsType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInProps';
|
||||
}
|
||||
|
||||
export function isRefValueType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInRefValue';
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@ export const BuiltInDispatchId = 'BuiltInDispatch';
|
||||
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
|
||||
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
|
||||
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
|
||||
// ShapeRegistry with default definitions for built-ins.
|
||||
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
|
||||
|
||||
+99
-34
@@ -17,6 +17,11 @@ import {
|
||||
areEqualPaths,
|
||||
IdentifierId,
|
||||
Terminal,
|
||||
InstructionValue,
|
||||
LoadContext,
|
||||
TInstruction,
|
||||
FunctionExpression,
|
||||
ObjectMethod,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -223,11 +228,25 @@ export function collectTemporariesSidemap(
|
||||
fn,
|
||||
usedOutsideDeclaringScope,
|
||||
temporaries,
|
||||
false,
|
||||
null,
|
||||
);
|
||||
return temporaries;
|
||||
}
|
||||
|
||||
function isLoadContextMutable(
|
||||
instrValue: InstructionValue,
|
||||
id: InstructionId,
|
||||
): instrValue is LoadContext {
|
||||
if (instrValue.kind === 'LoadContext') {
|
||||
CompilerError.invariant(instrValue.place.identifier.scope != null, {
|
||||
reason:
|
||||
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
|
||||
loc: instrValue.loc,
|
||||
});
|
||||
return id >= instrValue.place.identifier.scope.range.end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
|
||||
* function and all nested functions.
|
||||
@@ -239,17 +258,21 @@ function collectTemporariesSidemapImpl(
|
||||
fn: HIRFunction,
|
||||
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
|
||||
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
|
||||
isInnerFn: boolean,
|
||||
innerFnContext: {instrId: InstructionId} | null,
|
||||
): void {
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
for (const {value, lvalue, id: origInstrId} of block.instructions) {
|
||||
const instrId =
|
||||
innerFnContext != null ? innerFnContext.instrId : origInstrId;
|
||||
const usedOutside = usedOutsideDeclaringScope.has(
|
||||
lvalue.identifier.declarationId,
|
||||
);
|
||||
|
||||
if (value.kind === 'PropertyLoad' && !usedOutside) {
|
||||
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
|
||||
if (
|
||||
innerFnContext == null ||
|
||||
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
|
||||
@@ -265,13 +288,13 @@ function collectTemporariesSidemapImpl(
|
||||
temporaries.set(lvalue.identifier.id, property);
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'LoadLocal' &&
|
||||
(value.kind === 'LoadLocal' || isLoadContextMutable(value, instrId)) &&
|
||||
lvalue.identifier.name == null &&
|
||||
value.place.identifier.name !== null &&
|
||||
!usedOutside
|
||||
) {
|
||||
if (
|
||||
!isInnerFn ||
|
||||
innerFnContext == null ||
|
||||
fn.context.some(
|
||||
context => context.identifier.id === value.place.identifier.id,
|
||||
)
|
||||
@@ -289,7 +312,7 @@ function collectTemporariesSidemapImpl(
|
||||
value.loweredFunc.func,
|
||||
usedOutsideDeclaringScope,
|
||||
temporaries,
|
||||
true,
|
||||
innerFnContext ?? {instrId},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -358,19 +381,22 @@ class Context {
|
||||
|
||||
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
|
||||
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
|
||||
#processedInstrsInOptional: ReadonlySet<Instruction | Terminal>;
|
||||
|
||||
/**
|
||||
* Tracks the traversal state. See Context.declare for explanation of why this
|
||||
* is needed.
|
||||
*/
|
||||
inInnerFn: boolean = false;
|
||||
#innerFnContext: {outerInstrId: InstructionId} | null = null;
|
||||
|
||||
constructor(
|
||||
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
|
||||
) {
|
||||
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
|
||||
this.#temporaries = temporaries;
|
||||
this.#processedInstrsInOptional = processedInstrsInOptional;
|
||||
}
|
||||
|
||||
enterScope(scope: ReactiveScope): void {
|
||||
@@ -431,7 +457,7 @@ class Context {
|
||||
* by root identifier mutable ranges).
|
||||
*/
|
||||
declare(identifier: Identifier, decl: Decl): void {
|
||||
if (this.inInnerFn) return;
|
||||
if (this.#innerFnContext != null) return;
|
||||
if (!this.#declarations.has(identifier.declarationId)) {
|
||||
this.#declarations.set(identifier.declarationId, decl);
|
||||
}
|
||||
@@ -574,22 +600,52 @@ class Context {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
}
|
||||
enterInnerFn<T>(
|
||||
innerFn: TInstruction<FunctionExpression> | TInstruction<ObjectMethod>,
|
||||
cb: () => T,
|
||||
): T {
|
||||
const prevContext = this.#innerFnContext;
|
||||
this.#innerFnContext = this.#innerFnContext ?? {outerInstrId: innerFn.id};
|
||||
const result = cb();
|
||||
this.#innerFnContext = prevContext;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip dependencies that are subexpressions of other dependencies. e.g. if a
|
||||
* dependency is tracked in the temporaries sidemap, it can be added at
|
||||
* site-of-use
|
||||
*/
|
||||
isDeferredDependency(
|
||||
instr:
|
||||
| {kind: HIRValue.Instruction; value: Instruction}
|
||||
| {kind: HIRValue.Terminal; value: Terminal},
|
||||
): boolean {
|
||||
return (
|
||||
this.#processedInstrsInOptional.has(instr.value) ||
|
||||
(instr.kind === HIRValue.Instruction &&
|
||||
this.#temporaries.has(instr.value.lvalue.identifier.id))
|
||||
);
|
||||
}
|
||||
}
|
||||
enum HIRValue {
|
||||
Instruction = 1,
|
||||
Terminal,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
context.declare(lvalue.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
if (
|
||||
context.isDeferredDependency({kind: HIRValue.Instruction, value: instr})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (value.kind === 'PropertyLoad') {
|
||||
context.visitProperty(value.object, value.property, false);
|
||||
} else if (value.kind === 'StoreLocal') {
|
||||
context.visitOperand(value.value);
|
||||
if (value.lvalue.kind === InstructionKind.Reassign) {
|
||||
@@ -632,11 +688,6 @@ function handleInstruction(instr: Instruction, context: Context): void {
|
||||
context.visitOperand(operand);
|
||||
}
|
||||
}
|
||||
|
||||
context.declare(lvalue.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
|
||||
function collectDependencies(
|
||||
@@ -645,7 +696,11 @@ function collectDependencies(
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
|
||||
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
|
||||
const context = new Context(usedOutsideDeclaringScope, temporaries);
|
||||
const context = new Context(
|
||||
usedOutsideDeclaringScope,
|
||||
temporaries,
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
@@ -694,16 +749,26 @@ function collectDependencies(
|
||||
/**
|
||||
* Recursively visit the inner function to extract dependencies there
|
||||
*/
|
||||
const wasInInnerFn = context.inInnerFn;
|
||||
context.inInnerFn = true;
|
||||
handleFunction(instr.value.loweredFunc.func);
|
||||
context.inInnerFn = wasInInnerFn;
|
||||
} else if (!processedInstrsInOptional.has(instr)) {
|
||||
const innerFn = instr.value.loweredFunc.func;
|
||||
context.enterInnerFn(
|
||||
instr as
|
||||
| TInstruction<FunctionExpression>
|
||||
| TInstruction<ObjectMethod>,
|
||||
() => {
|
||||
handleFunction(innerFn);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
handleInstruction(instr, context);
|
||||
}
|
||||
}
|
||||
|
||||
if (!processedInstrsInOptional.has(block.terminal)) {
|
||||
if (
|
||||
!context.isDeferredDependency({
|
||||
kind: HIRValue.Terminal,
|
||||
value: block.terminal,
|
||||
})
|
||||
) {
|
||||
for (const place of eachTerminalOperand(block.terminal)) {
|
||||
context.visitOperand(place);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {logHIRFunction} from '../Utils/logger';
|
||||
import {inferMutableContextVariables} from './InferMutableContextVariables';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
@@ -112,7 +111,11 @@ function lower(func: HIRFunction): void {
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
inferMutableContextVariables(func);
|
||||
logHIRFunction('AnalyseFunction (inner)', func);
|
||||
func.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: func,
|
||||
});
|
||||
}
|
||||
|
||||
function infer(
|
||||
|
||||
+72
-23
@@ -8,7 +8,6 @@ import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
isUseEffectHookType,
|
||||
makeInstructionId,
|
||||
TInstruction,
|
||||
InstructionId,
|
||||
@@ -17,31 +16,47 @@ import {
|
||||
Place,
|
||||
ReactiveScopeDependencies,
|
||||
} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Infers reactive dependencies captured by useEffect lambdas and adds them as
|
||||
* a second argument to the useEffect call if no dependency array is provided.
|
||||
*/
|
||||
export function inferEffectDependencies(
|
||||
env: Environment,
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
let hasRewrite = false;
|
||||
const fnExpressions = new Map<
|
||||
IdentifierId,
|
||||
TInstruction<FunctionExpression>
|
||||
>();
|
||||
|
||||
const autodepFnConfigs = new Map<string, Map<string, number>>();
|
||||
for (const effectTarget of fn.env.config.inferEffectDependencies!) {
|
||||
const moduleTargets = getOrInsertWith(
|
||||
autodepFnConfigs,
|
||||
effectTarget.function.source,
|
||||
() => new Map<string, number>(),
|
||||
);
|
||||
moduleTargets.set(
|
||||
effectTarget.function.importSpecifierName,
|
||||
effectTarget.numRequiredArgs,
|
||||
);
|
||||
}
|
||||
const autodepFnLoads = new Map<IdentifierId, number>();
|
||||
|
||||
const scopeInfos = new Map<
|
||||
ScopeId,
|
||||
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
|
||||
>();
|
||||
|
||||
const loadGlobals = new Set<IdentifierId>();
|
||||
|
||||
/**
|
||||
* When inserting LoadLocals, we need to retain the reactivity of the base
|
||||
* identifier, as later passes e.g. PruneNonReactiveDeps take the reactivity of
|
||||
@@ -74,19 +89,46 @@ export function inferEffectDependencies(
|
||||
lvalue.identifier.id,
|
||||
instr as TInstruction<FunctionExpression>,
|
||||
);
|
||||
} else if (value.kind === 'LoadGlobal') {
|
||||
loadGlobals.add(lvalue.identifier.id);
|
||||
|
||||
if (
|
||||
value.binding.kind === 'ImportSpecifier' ||
|
||||
value.binding.kind === 'ImportDefault'
|
||||
) {
|
||||
const moduleTargets = autodepFnConfigs.get(value.binding.module);
|
||||
if (moduleTargets != null) {
|
||||
const importSpecifierName =
|
||||
value.binding.kind === 'ImportSpecifier'
|
||||
? value.binding.imported
|
||||
: DEFAULT_EXPORT;
|
||||
const numRequiredArgs = moduleTargets.get(importSpecifierName);
|
||||
if (numRequiredArgs != null) {
|
||||
autodepFnLoads.set(lvalue.identifier.id, numRequiredArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
/*
|
||||
* This check is not final. Right now we only look for useEffects without a dependency array.
|
||||
* This is likely not how we will ship this feature, but it is good enough for us to make progress
|
||||
* on the implementation and test it.
|
||||
* TODO: Handle method calls
|
||||
*/
|
||||
value.kind === 'CallExpression' &&
|
||||
isUseEffectHookType(value.callee.identifier) &&
|
||||
value.args.length === 1 &&
|
||||
autodepFnLoads.get(value.callee.identifier.id) === value.args.length &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const effectDeps: Array<Place> = [];
|
||||
const newInstructions: Array<Instruction> = [];
|
||||
const deps: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements: effectDeps,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const depsPlace = createTemporaryPlace(fn.env, GeneratedSource);
|
||||
depsPlace.effect = Effect.Read;
|
||||
|
||||
const fnExpr = fnExpressions.get(value.args[0].identifier.id);
|
||||
if (fnExpr != null) {
|
||||
// We have a function expression, so we can infer its dependencies
|
||||
const scopeInfo =
|
||||
fnExpr.lvalue.identifier.scope != null
|
||||
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
|
||||
@@ -108,14 +150,12 @@ export function inferEffectDependencies(
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: write new instructions to insert a dependency array
|
||||
* Step 1: push dependencies to the effect deps array
|
||||
*
|
||||
* Note that it's invalid to prune non-reactive deps in this pass, see
|
||||
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
|
||||
* explanation.
|
||||
*/
|
||||
const effectDeps: Array<Place> = [];
|
||||
const newInstructions: Array<Instruction> = [];
|
||||
for (const dep of scopeInfo.deps) {
|
||||
const {place, instructions} = writeDependencyToInstructions(
|
||||
dep,
|
||||
@@ -126,14 +166,6 @@ export function inferEffectDependencies(
|
||||
newInstructions.push(...instructions);
|
||||
effectDeps.push(place);
|
||||
}
|
||||
const deps: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements: effectDeps,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
const depsPlace = createTemporaryPlace(env, GeneratedSource);
|
||||
depsPlace.effect = Effect.Read;
|
||||
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
@@ -142,8 +174,18 @@ export function inferEffectDependencies(
|
||||
value: deps,
|
||||
});
|
||||
|
||||
// Step 2: insert the deps array as an argument of the useEffect
|
||||
value.args[1] = {...depsPlace, effect: Effect.Freeze};
|
||||
// Step 2: push the inferred deps array as an argument of the useEffect
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
} else if (loadGlobals.has(value.args[0].identifier.id)) {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
}
|
||||
}
|
||||
@@ -202,6 +244,13 @@ function writeDependencyToInstructions(
|
||||
*/
|
||||
break;
|
||||
}
|
||||
if (path.property === 'current') {
|
||||
/*
|
||||
* Prune ref.current accesses. This may over-capture for non-ref values with
|
||||
* a current property, but that's fine.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
const nextValue = createTemporaryPlace(env, GeneratedSource);
|
||||
nextValue.reactive = reactive;
|
||||
instructions.push({
|
||||
|
||||
+3
-5
@@ -546,16 +546,14 @@ function createPropsProperties(
|
||||
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 jsxAttributesWithoutKey = propAttributes.filter(
|
||||
p => p.kind === 'JsxAttribute' && p.name !== 'key',
|
||||
);
|
||||
const jsxSpreadAttributes = propAttributes.filter(
|
||||
p => p.kind === 'JsxSpreadAttribute',
|
||||
);
|
||||
const spreadPropsOnly =
|
||||
jsxAttributesWithoutKeyAndRef.length === 0 &&
|
||||
jsxSpreadAttributes.length === 1;
|
||||
|
||||
jsxAttributesWithoutKey.length === 0 && jsxSpreadAttributes.length === 1;
|
||||
propAttributes.forEach(prop => {
|
||||
switch (prop.kind) {
|
||||
case 'JsxAttribute': {
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 {HIRFunction, isPropsType} from '../HIR';
|
||||
|
||||
/**
|
||||
* Converts method calls into regular calls where the receiver is the props object:
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // INPUT
|
||||
* props.foo();
|
||||
*
|
||||
* // OUTPUT
|
||||
* const t0 = props.foo;
|
||||
* t0();
|
||||
* ```
|
||||
*
|
||||
* Counter example:
|
||||
*
|
||||
* Here the receiver is `props.foo`, not the props object, so we don't rewrite it:
|
||||
*
|
||||
* // INPUT
|
||||
* props.foo.bar();
|
||||
*
|
||||
* // OUTPUT
|
||||
* props.foo.bar();
|
||||
* ```
|
||||
*/
|
||||
export function optimizePropsMethodCalls(fn: HIRFunction): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i]!;
|
||||
if (
|
||||
instr.value.kind === 'MethodCall' &&
|
||||
isPropsType(instr.value.receiver.identifier)
|
||||
) {
|
||||
instr.value = {
|
||||
kind: 'CallExpression',
|
||||
callee: instr.value.property,
|
||||
args: instr.value.args,
|
||||
loc: instr.value.loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-27
@@ -1354,20 +1354,6 @@ function codegenForInit(
|
||||
init: ReactiveValue,
|
||||
): t.Expression | t.VariableDeclaration | null {
|
||||
if (init.kind === 'SequenceExpression') {
|
||||
for (const instr of init.instructions) {
|
||||
if (instr.value.kind === 'DeclareContext') {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support for loops where the index variable is a context variable`,
|
||||
loc: instr.loc,
|
||||
description:
|
||||
instr.value.lvalue.place.identifier.name != null
|
||||
? `\`${instr.value.lvalue.place.identifier.name.value}\` is a context variable`
|
||||
: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const body = codegenBlock(
|
||||
cx,
|
||||
init.instructions.map(instruction => ({
|
||||
@@ -1378,20 +1364,33 @@ function codegenForInit(
|
||||
const declarators: Array<t.VariableDeclarator> = [];
|
||||
let kind: 'let' | 'const' = 'const';
|
||||
body.forEach(instr => {
|
||||
CompilerError.invariant(
|
||||
instr.type === 'VariableDeclaration' &&
|
||||
(instr.kind === 'let' || instr.kind === 'const'),
|
||||
{
|
||||
reason: 'Expected a variable declaration',
|
||||
loc: init.loc,
|
||||
description: `Got ${instr.type}`,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
if (instr.kind === 'let') {
|
||||
kind = 'let';
|
||||
let top: undefined | t.VariableDeclarator = undefined;
|
||||
if (
|
||||
instr.type === 'ExpressionStatement' &&
|
||||
instr.expression.type === 'AssignmentExpression' &&
|
||||
instr.expression.operator === '=' &&
|
||||
instr.expression.left.type === 'Identifier' &&
|
||||
(top = declarators.at(-1))?.id.type === 'Identifier' &&
|
||||
top?.id.name === instr.expression.left.name &&
|
||||
top?.init == null
|
||||
) {
|
||||
top.init = instr.expression.right;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
instr.type === 'VariableDeclaration' &&
|
||||
(instr.kind === 'let' || instr.kind === 'const'),
|
||||
{
|
||||
reason: 'Expected a variable declaration',
|
||||
loc: init.loc,
|
||||
description: `Got ${instr.type}`,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
if (instr.kind === 'let') {
|
||||
kind = 'let';
|
||||
}
|
||||
declarators.push(...instr.declarations);
|
||||
}
|
||||
declarators.push(...instr.declarations);
|
||||
});
|
||||
CompilerError.invariant(declarators.length > 0, {
|
||||
reason: 'Expected a variable declaration',
|
||||
|
||||
+5
-2
@@ -25,7 +25,6 @@ import {
|
||||
eachPatternOperand,
|
||||
} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {logHIRFunction} from '../Utils/logger';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/*
|
||||
@@ -156,7 +155,11 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
scope.range.end > maxInstruction + 1
|
||||
) {
|
||||
// Make it easier to debug why the error occurred
|
||||
logHIRFunction('InferReactiveScopeVariables (invalid scope)', fn);
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'InferReactiveScopeVariables (invalid scope)',
|
||||
value: fn,
|
||||
});
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Invalid mutable range for scope`,
|
||||
loc: GeneratedSource,
|
||||
|
||||
@@ -1,110 +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 generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import chalk from 'chalk';
|
||||
import {HIR, HIRFunction, ReactiveFunction} from '../HIR/HIR';
|
||||
import {printFunctionWithOutlined, printHIR} from '../HIR/PrintHIR';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {printReactiveFunctionWithOutlined} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
|
||||
let ENABLED: boolean = false;
|
||||
|
||||
let lastLogged: string;
|
||||
|
||||
export function toggleLogging(enabled: boolean): void {
|
||||
ENABLED = enabled;
|
||||
}
|
||||
|
||||
export function logDebug(step: string, value: string): void {
|
||||
if (ENABLED) {
|
||||
process.stdout.write(`${chalk.green(step)}:\n${value}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
export function logHIR(step: string, ir: HIR): void {
|
||||
if (ENABLED) {
|
||||
const printed = printHIR(ir);
|
||||
if (printed !== lastLogged) {
|
||||
lastLogged = printed;
|
||||
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
|
||||
} else {
|
||||
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logCodegenFunction(step: string, fn: CodegenFunction): void {
|
||||
if (ENABLED) {
|
||||
let printed: string | null = null;
|
||||
try {
|
||||
const node = t.functionDeclaration(
|
||||
fn.id,
|
||||
fn.params,
|
||||
fn.body,
|
||||
fn.generator,
|
||||
fn.async,
|
||||
);
|
||||
const ast = generate(node);
|
||||
printed = ast.code;
|
||||
} catch (e) {
|
||||
let errMsg: string;
|
||||
if (
|
||||
typeof e === 'object' &&
|
||||
e != null &&
|
||||
'message' in e &&
|
||||
typeof e.message === 'string'
|
||||
) {
|
||||
errMsg = e.message.toString();
|
||||
} else {
|
||||
errMsg = '[empty]';
|
||||
}
|
||||
console.log('Error formatting AST: ' + errMsg);
|
||||
}
|
||||
if (printed === null) {
|
||||
return;
|
||||
}
|
||||
if (printed !== lastLogged) {
|
||||
lastLogged = printed;
|
||||
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
|
||||
} else {
|
||||
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logHIRFunction(step: string, fn: HIRFunction): void {
|
||||
if (ENABLED) {
|
||||
const printed = printFunctionWithOutlined(fn);
|
||||
if (printed !== lastLogged) {
|
||||
lastLogged = printed;
|
||||
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
|
||||
} else {
|
||||
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logReactiveFunction(step: string, fn: ReactiveFunction): void {
|
||||
if (ENABLED) {
|
||||
const printed = printReactiveFunctionWithOutlined(fn);
|
||||
if (printed !== lastLogged) {
|
||||
lastLogged = printed;
|
||||
process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`);
|
||||
} else {
|
||||
process.stdout.write(`${chalk.blue(step)}: (no change)\n\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function log(fn: () => string): void {
|
||||
if (ENABLED) {
|
||||
const message = fn();
|
||||
process.stdout.write(message.trim() + '\n\n');
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug repro:
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
|
||||
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
|
||||
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
|
||||
*
|
||||
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
|
||||
* aliasing `y` via `[y]`, we make an opaque call.
|
||||
*
|
||||
* Note that the bug here is that we don't infer that `a = makeArray(y)`
|
||||
* potentially captures a context variable into a local variable. As a result,
|
||||
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
|
||||
* currently inferring that this lambda captures `y` (for a potential later
|
||||
* mutation) and simply reads `x`.
|
||||
*
|
||||
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
|
||||
* used when we analyze CallExpressions.
|
||||
*/
|
||||
function Component({foo, bar}: {foo: number; bar: number}) {
|
||||
let x = {foo};
|
||||
let y: {bar: number; x?: {foo: number}} = {bar};
|
||||
const f0 = function () {
|
||||
let a = makeArray(y); // a = [y]
|
||||
let b = x;
|
||||
// this writes y.x = x
|
||||
a[0].x = b;
|
||||
};
|
||||
f0();
|
||||
mutate(y.x);
|
||||
return y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 3, bar: 4}],
|
||||
sequentialRenders: [
|
||||
{foo: 3, bar: 4},
|
||||
{foo: 3, bar: 5},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { makeArray, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Bug repro:
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
|
||||
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
|
||||
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
|
||||
*
|
||||
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
|
||||
* aliasing `y` via `[y]`, we make an opaque call.
|
||||
*
|
||||
* Note that the bug here is that we don't infer that `a = makeArray(y)`
|
||||
* potentially captures a context variable into a local variable. As a result,
|
||||
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
|
||||
* currently inferring that this lambda captures `y` (for a potential later
|
||||
* mutation) and simply reads `x`.
|
||||
*
|
||||
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
|
||||
* used when we analyze CallExpressions.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { foo, bar } = t0;
|
||||
let t1;
|
||||
if ($[0] !== foo) {
|
||||
t1 = { foo };
|
||||
$[0] = foo;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const x = t1;
|
||||
let y;
|
||||
if ($[2] !== bar || $[3] !== x) {
|
||||
y = { bar };
|
||||
const f0 = function () {
|
||||
const a = makeArray(y);
|
||||
const b = x;
|
||||
|
||||
a[0].x = b;
|
||||
};
|
||||
|
||||
f0();
|
||||
mutate(y.x);
|
||||
$[2] = bar;
|
||||
$[3] = x;
|
||||
$[4] = y;
|
||||
} else {
|
||||
y = $[4];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ foo: 3, bar: 4 }],
|
||||
sequentialRenders: [
|
||||
{ foo: 3, bar: 4 },
|
||||
{ foo: 3, bar: 5 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug repro:
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
|
||||
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
|
||||
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
|
||||
*
|
||||
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
|
||||
* aliasing `y` via `[y]`, we make an opaque call.
|
||||
*
|
||||
* Note that the bug here is that we don't infer that `a = makeArray(y)`
|
||||
* potentially captures a context variable into a local variable. As a result,
|
||||
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
|
||||
* currently inferring that this lambda captures `y` (for a potential later
|
||||
* mutation) and simply reads `x`.
|
||||
*
|
||||
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
|
||||
* used when we analyze CallExpressions.
|
||||
*/
|
||||
function Component({foo, bar}: {foo: number; bar: number}) {
|
||||
let x = {foo};
|
||||
let y: {bar: number; x?: {foo: number}} = {bar};
|
||||
const f0 = function () {
|
||||
let a = makeArray(y); // a = [y]
|
||||
let b = x;
|
||||
// this writes y.x = x
|
||||
a[0].x = b;
|
||||
};
|
||||
f0();
|
||||
mutate(y.x);
|
||||
return y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{foo: 3, bar: 4}],
|
||||
sequentialRenders: [
|
||||
{foo: 3, bar: 4},
|
||||
{foo: 3, bar: 5},
|
||||
],
|
||||
};
|
||||
+8
-10
@@ -58,18 +58,16 @@ function Foo(t0) {
|
||||
bar = $[1];
|
||||
result = $[2];
|
||||
}
|
||||
|
||||
const t1 = bar;
|
||||
let t2;
|
||||
if ($[3] !== result || $[4] !== t1) {
|
||||
t2 = <Stringify result={result} fn={t1} shouldInvokeFns={true} />;
|
||||
$[3] = result;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
let t1;
|
||||
if ($[3] !== bar || $[4] !== result) {
|
||||
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
$[3] = bar;
|
||||
$[4] = result;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
t1 = $[5];
|
||||
}
|
||||
return t2;
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -43,16 +43,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = { x: t0 };
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = { x };
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -42,16 +42,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <div>{t0}</div>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = <div>{x}</div>;
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -43,16 +43,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = { x: t0 };
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = { x };
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+7
-8
@@ -42,16 +42,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = { x: t0 };
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = { x };
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
const data = useData();
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(<Stringify key={i} onClick={() => data.set(i)} />);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
4 | // NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
5 | // within a closure, the `onClick` handler of each item
|
||||
> 6 | for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
| ^^^^^^^^^^^ Todo: Support for loops where the index variable is a context variable. `i` is a context variable (6:6)
|
||||
7 | items.push(<Stringify key={i} onClick={() => data.set(i)} />);
|
||||
8 | }
|
||||
9 | return items;
|
||||
```
|
||||
|
||||
|
||||
-6
@@ -98,12 +98,6 @@ Todo: (BuildHIR::lowerExpression) Handle tagged template with interpolations (30
|
||||
|
||||
Todo: (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value (34:34)
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle Identifier inits in ForOfStatement (36:36)
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle ArrayPattern inits in ForOfStatement (38:38)
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle ObjectPattern inits in ForOfStatement (40:40)
|
||||
|
||||
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `MemberExpression` cannot be safely reordered (57:57)
|
||||
|
||||
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `BinaryExpression` cannot be safely reordered (53:53)
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component() {
|
||||
const data = useData();
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
}
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const data = useData();
|
||||
let t0;
|
||||
if ($[0] !== data) {
|
||||
const items = [];
|
||||
for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
}
|
||||
|
||||
t0 = <>{items}</>;
|
||||
$[0] = data;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = new Map();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div></div><div></div><div></div><div></div>
|
||||
+15
-2
@@ -4,7 +4,20 @@ function Component() {
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(<Stringify key={i} onClick={() => data.set(i)} />);
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
}
|
||||
return items;
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {print, useSpecialEffect} from 'shared-runtime';
|
||||
|
||||
function CustomConfig({propVal}) {
|
||||
// Insertion
|
||||
useSpecialEffect(() => print(propVal), [propVal]);
|
||||
// No insertion
|
||||
useSpecialEffect(() => print(propVal), [propVal], [propVal]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { print, useSpecialEffect } from "shared-runtime";
|
||||
|
||||
function CustomConfig(t0) {
|
||||
const $ = _c(7);
|
||||
const { propVal } = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== propVal) {
|
||||
t1 = () => print(propVal);
|
||||
t2 = [propVal];
|
||||
$[0] = propVal;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useSpecialEffect(t1, t2, [propVal]);
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[3] !== propVal) {
|
||||
t3 = () => print(propVal);
|
||||
t4 = [propVal];
|
||||
t5 = [propVal];
|
||||
$[3] = propVal;
|
||||
$[4] = t3;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
t4 = $[5];
|
||||
t5 = $[6];
|
||||
}
|
||||
useSpecialEffect(t3, t4, t5);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// @inferEffectDependencies
|
||||
import {print, useSpecialEffect} from 'shared-runtime';
|
||||
|
||||
function CustomConfig({propVal}) {
|
||||
// Insertion
|
||||
useSpecialEffect(() => print(propVal), [propVal]);
|
||||
// No insertion
|
||||
useSpecialEffect(() => print(propVal), [propVal], [propVal]);
|
||||
}
|
||||
+22
-1
@@ -3,6 +3,9 @@
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect, useRef} from 'react';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
|
||||
const moduleNonReactive = 0;
|
||||
|
||||
function Component({foo, bar}) {
|
||||
@@ -37,6 +40,10 @@ function Component({foo, bar}) {
|
||||
|
||||
// No inferred dep array, the argument is not a lambda
|
||||
useEffect(f);
|
||||
|
||||
useEffectWrapper(() => {
|
||||
console.log(foo);
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
@@ -45,10 +52,13 @@ function Component({foo, bar}) {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect, useRef } from "react";
|
||||
import useEffectWrapper from "useEffectWrapper";
|
||||
|
||||
const moduleNonReactive = 0;
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(12);
|
||||
const $ = _c(14);
|
||||
const { foo, bar } = t0;
|
||||
|
||||
const ref = useRef(0);
|
||||
@@ -121,6 +131,17 @@ function Component(t0) {
|
||||
const f = t5;
|
||||
|
||||
useEffect(f);
|
||||
let t6;
|
||||
if ($[12] !== foo) {
|
||||
t6 = () => {
|
||||
console.log(foo);
|
||||
};
|
||||
$[12] = foo;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
useEffectWrapper(t6, [foo]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+7
@@ -1,4 +1,7 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect, useRef} from 'react';
|
||||
import useEffectWrapper from 'useEffectWrapper';
|
||||
|
||||
const moduleNonReactive = 0;
|
||||
|
||||
function Component({foo, bar}) {
|
||||
@@ -33,4 +36,8 @@ function Component({foo, bar}) {
|
||||
|
||||
// No inferred dep array, the argument is not a lambda
|
||||
useEffect(f);
|
||||
|
||||
useEffectWrapper(() => {
|
||||
console.log(foo);
|
||||
});
|
||||
}
|
||||
|
||||
+39
@@ -60,6 +60,10 @@ function ConditionalJsx({shouldWrap}) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function ComponentWithSpreadPropsAndRef({ref, ...other}) {
|
||||
return <Foo ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
// TODO: Support value blocks
|
||||
function TernaryJsx({cond}) {
|
||||
return cond ? <div /> : null;
|
||||
@@ -409,6 +413,41 @@ function ConditionalJsx(t0) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function ComponentWithSpreadPropsAndRef(t0) {
|
||||
const $ = _c2(6);
|
||||
let other;
|
||||
let ref;
|
||||
if ($[0] !== t0) {
|
||||
({ ref, ...other } = t0);
|
||||
$[0] = t0;
|
||||
$[1] = other;
|
||||
$[2] = ref;
|
||||
} else {
|
||||
other = $[1];
|
||||
ref = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== other || $[4] !== ref) {
|
||||
if (DEV) {
|
||||
t1 = <Foo ref={ref} {...other} />;
|
||||
} else {
|
||||
t1 = {
|
||||
$$typeof: Symbol.for("react.transitional.element"),
|
||||
type: Foo,
|
||||
ref: ref,
|
||||
key: null,
|
||||
props: { ref: ref, ...other },
|
||||
};
|
||||
}
|
||||
$[3] = other;
|
||||
$[4] = ref;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
// TODO: Support value blocks
|
||||
function TernaryJsx(t0) {
|
||||
const $ = _c2(2);
|
||||
|
||||
+4
@@ -56,6 +56,10 @@ function ConditionalJsx({shouldWrap}) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function ComponentWithSpreadPropsAndRef({ref, ...other}) {
|
||||
return <Foo ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
// TODO: Support value blocks
|
||||
function TernaryJsx({cond}) {
|
||||
return cond ? <div /> : null;
|
||||
|
||||
+9
-8
@@ -3,7 +3,7 @@
|
||||
|
||||
```javascript
|
||||
// @enableJsxOutlining
|
||||
function Component(arr) {
|
||||
function Component({arr}) {
|
||||
const x = useX();
|
||||
return arr.map(i => {
|
||||
<>
|
||||
@@ -49,12 +49,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableJsxOutlining
|
||||
function Component(arr) {
|
||||
function Component(t0) {
|
||||
const $ = _c(3);
|
||||
const { arr } = t0;
|
||||
const x = useX();
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== arr || $[1] !== x) {
|
||||
t0 = arr.map((i) => {
|
||||
t1 = arr.map((i) => {
|
||||
arr.map((i_0, id) => {
|
||||
const T0 = _temp;
|
||||
const child = <T0 i={i_0} x={x} />;
|
||||
@@ -65,11 +66,11 @@ function Component(arr) {
|
||||
});
|
||||
$[0] = arr;
|
||||
$[1] = x;
|
||||
$[2] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
t1 = $[2];
|
||||
}
|
||||
return t0;
|
||||
return t1;
|
||||
}
|
||||
function _temp(t0) {
|
||||
const $ = _c(5);
|
||||
@@ -140,4 +141,4 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) arr.map is not a function
|
||||
(kind: ok) [null,null]
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// @enableJsxOutlining
|
||||
function Component(arr) {
|
||||
function Component({arr}) {
|
||||
const x = useX();
|
||||
return arr.map(i => {
|
||||
<>
|
||||
|
||||
+7
-9
@@ -33,17 +33,15 @@ function f(a) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <div x={t0} />;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = <div x={x} />;
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* We never include a .current access in a dep array because it may be a ref access.
|
||||
* This might over-capture objects that are not refs and happen to have fields named
|
||||
* current, but that should be a rare case and the result would still be correct
|
||||
* (assuming the effect is idempotent). In the worst case, you can always write a manual
|
||||
* dep array.
|
||||
*/
|
||||
function RefsInEffects() {
|
||||
const ref = useRefHelper();
|
||||
const wrapped = useDeeperRefHelper();
|
||||
useEffect(() => {
|
||||
print(ref.current);
|
||||
print(wrapped.foo.current);
|
||||
});
|
||||
}
|
||||
|
||||
function useRefHelper() {
|
||||
return useRef(0);
|
||||
}
|
||||
|
||||
function useDeeperRefHelper() {
|
||||
return {foo: useRefHelper()};
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* We never include a .current access in a dep array because it may be a ref access.
|
||||
* This might over-capture objects that are not refs and happen to have fields named
|
||||
* current, but that should be a rare case and the result would still be correct
|
||||
* (assuming the effect is idempotent). In the worst case, you can always write a manual
|
||||
* dep array.
|
||||
*/
|
||||
function RefsInEffects() {
|
||||
const $ = _c(3);
|
||||
const ref = useRefHelper();
|
||||
const wrapped = useDeeperRefHelper();
|
||||
let t0;
|
||||
if ($[0] !== ref.current || $[1] !== wrapped.foo.current) {
|
||||
t0 = () => {
|
||||
print(ref.current);
|
||||
print(wrapped.foo.current);
|
||||
};
|
||||
$[0] = ref.current;
|
||||
$[1] = wrapped.foo.current;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
useEffect(t0, [ref, wrapped.foo]);
|
||||
}
|
||||
|
||||
function useRefHelper() {
|
||||
return useRef(0);
|
||||
}
|
||||
|
||||
function useDeeperRefHelper() {
|
||||
const $ = _c(2);
|
||||
const t0 = useRefHelper();
|
||||
let t1;
|
||||
if ($[0] !== t0) {
|
||||
t1 = { foo: t0 };
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* We never include a .current access in a dep array because it may be a ref access.
|
||||
* This might over-capture objects that are not refs and happen to have fields named
|
||||
* current, but that should be a rare case and the result would still be correct
|
||||
* (assuming the effect is idempotent). In the worst case, you can always write a manual
|
||||
* dep array.
|
||||
*/
|
||||
function RefsInEffects() {
|
||||
const ref = useRefHelper();
|
||||
const wrapped = useDeeperRefHelper();
|
||||
useEffect(() => {
|
||||
print(ref.current);
|
||||
print(wrapped.foo.current);
|
||||
});
|
||||
}
|
||||
|
||||
function useRefHelper() {
|
||||
return useRef(0);
|
||||
}
|
||||
|
||||
function useDeeperRefHelper() {
|
||||
return {foo: useRefHelper()};
|
||||
}
|
||||
+1
-1
@@ -34,7 +34,7 @@ import { print } from "shared-runtime";
|
||||
* before OutlineFunctions
|
||||
*/
|
||||
function OutlinedFunctionInEffect() {
|
||||
useEffect(_temp);
|
||||
useEffect(_temp, []);
|
||||
}
|
||||
function _temp() {
|
||||
return print("hello world!");
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* TODO: we're currently bailing out because `contextVar` is a context variable
|
||||
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
|
||||
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
|
||||
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
|
||||
* we took as eligible dependencies.
|
||||
*
|
||||
* One solution is to simply record `LoadContext` identifiers into the
|
||||
* temporaries sidemap when the instruction occurs *after* the context
|
||||
* variable's mutable range.
|
||||
*/
|
||||
function Foo(props) {
|
||||
let contextVar;
|
||||
if (props.cond) {
|
||||
contextVar = {val: 2};
|
||||
} else {
|
||||
contextVar = {};
|
||||
}
|
||||
|
||||
const cb = useCallback(() => [contextVar.val], [contextVar.val]);
|
||||
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{cond: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
22 | }
|
||||
23 |
|
||||
> 24 | const cb = useCallback(() => [contextVar.val], [contextVar.val]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (24:24)
|
||||
25 |
|
||||
26 | return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
27 | }
|
||||
```
|
||||
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {useCallback} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* TODO: we're currently bailing out because `contextVar` is a context variable
|
||||
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
|
||||
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
|
||||
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
|
||||
* we took as eligible dependencies.
|
||||
*
|
||||
* One solution is to simply record `LoadContext` identifiers into the
|
||||
* temporaries sidemap when the instruction occurs *after* the context
|
||||
* variable's mutable range.
|
||||
*/
|
||||
function Foo(props) {
|
||||
let contextVar;
|
||||
if (props.cond) {
|
||||
contextVar = {val: 2};
|
||||
} else {
|
||||
contextVar = {};
|
||||
}
|
||||
|
||||
const cb = useCallback(() => [contextVar.val], [contextVar.val]);
|
||||
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{cond: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
import { useCallback } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* TODO: we're currently bailing out because `contextVar` is a context variable
|
||||
* and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad
|
||||
* sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted
|
||||
* `LoadContext` and `PropertyLoad` instructions into the outer function, which
|
||||
* we took as eligible dependencies.
|
||||
*
|
||||
* One solution is to simply record `LoadContext` identifiers into the
|
||||
* temporaries sidemap when the instruction occurs *after* the context
|
||||
* variable's mutable range.
|
||||
*/
|
||||
function Foo(props) {
|
||||
const $ = _c(6);
|
||||
let contextVar;
|
||||
if ($[0] !== props.cond) {
|
||||
if (props.cond) {
|
||||
contextVar = { val: 2 };
|
||||
} else {
|
||||
contextVar = {};
|
||||
}
|
||||
$[0] = props.cond;
|
||||
$[1] = contextVar;
|
||||
} else {
|
||||
contextVar = $[1];
|
||||
}
|
||||
let t0;
|
||||
if ($[2] !== contextVar.val) {
|
||||
t0 = () => [contextVar.val];
|
||||
$[2] = contextVar.val;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
contextVar;
|
||||
const cb = t0;
|
||||
let t1;
|
||||
if ($[4] !== cb) {
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[4] = cb;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ cond: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}</div>
|
||||
+7
-8
@@ -44,16 +44,15 @@ function useFoo(arr1, arr2) {
|
||||
y = $[2];
|
||||
}
|
||||
let t0;
|
||||
const t1 = y;
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = { y: t1 };
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
let t1;
|
||||
if ($[3] !== y) {
|
||||
t1 = { y };
|
||||
$[3] = y;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = t2;
|
||||
t0 = t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
|
||||
+7
-9
@@ -36,17 +36,15 @@ function HomeDiscoStoreItemTileRating(props) {
|
||||
} else {
|
||||
count = $[1];
|
||||
}
|
||||
|
||||
const t0 = count;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Text>{t0}</Text>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== count) {
|
||||
t0 = <Text>{count}</Text>;
|
||||
$[2] = count;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const x = useMemo(() => props.x(), [props.x]);
|
||||
return <ValidateMemoization inputs={[props.x]} output={x} />;
|
||||
}
|
||||
|
||||
const f = () => ['React'];
|
||||
const g = () => ['Compiler'];
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: () => ['React']}],
|
||||
sequentialRenders: [{x: f}, {x: g}, {x: g}, {x: f}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
|
||||
import { useMemo } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(7);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.x) {
|
||||
t1 = props.x();
|
||||
$[0] = props.x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
t0 = t1;
|
||||
const x = t0;
|
||||
let t2;
|
||||
if ($[2] !== props.x) {
|
||||
t2 = [props.x];
|
||||
$[2] = props.x;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== t2 || $[5] !== x) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={x} />;
|
||||
$[4] = t2;
|
||||
$[5] = x;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
const f = () => ["React"];
|
||||
const g = () => ["Compiler"];
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: () => ["React"] }],
|
||||
sequentialRenders: [{ x: f }, { x: g }, { x: g }, { x: f }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":["[[ function params=0 ]]"],"output":["React"]}</div>
|
||||
<div>{"inputs":["[[ function params=0 ]]"],"output":["Compiler"]}</div>
|
||||
<div>{"inputs":["[[ function params=0 ]]"],"output":["Compiler"]}</div>
|
||||
<div>{"inputs":["[[ function params=0 ]]"],"output":["React"]}</div>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// @compilationMode(infer)
|
||||
import {useMemo} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const x = useMemo(() => props.x(), [props.x]);
|
||||
return <ValidateMemoization inputs={[props.x]} output={x} />;
|
||||
}
|
||||
|
||||
const f = () => ['React'];
|
||||
const g = () => ['Compiler'];
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: () => ['React']}],
|
||||
sequentialRenders: [{x: f}, {x: g}, {x: g}, {x: f}],
|
||||
};
|
||||
+7
-9
@@ -67,17 +67,15 @@ function Component(props) {
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
|
||||
const t0 = x;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = [t0];
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== x) {
|
||||
t0 = [x];
|
||||
$[2] = x;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Context variables are local variables that (1) have at least one reassignment
|
||||
* and (2) are captured into a function expression. These have a known mutable
|
||||
* range: from first declaration / assignment to the last direct or aliased,
|
||||
* mutable reference.
|
||||
*
|
||||
* This fixture validates that forget can take granular dependencies on context
|
||||
* variables when the reference to a context var happens *after* the end of its
|
||||
* mutable range.
|
||||
*/
|
||||
function Component({cond, a}) {
|
||||
let contextVar;
|
||||
if (cond) {
|
||||
contextVar = {val: a};
|
||||
} else {
|
||||
contextVar = {};
|
||||
throwErrorWithMessage('');
|
||||
}
|
||||
const cb = {cb: () => contextVar.val * 4};
|
||||
|
||||
/**
|
||||
* manually specify input to avoid adding a `PropertyLoad` from contextVar,
|
||||
* which might affect hoistable-objects analysis.
|
||||
*/
|
||||
return (
|
||||
<ValidateMemoization
|
||||
inputs={[cond ? a : undefined]}
|
||||
output={cb}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{cond: false, a: undefined}],
|
||||
sequentialRenders: [
|
||||
{cond: true, a: 2},
|
||||
{cond: true, a: 2},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { throwErrorWithMessage, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Context variables are local variables that (1) have at least one reassignment
|
||||
* and (2) are captured into a function expression. These have a known mutable
|
||||
* range: from first declaration / assignment to the last direct or aliased,
|
||||
* mutable reference.
|
||||
*
|
||||
* This fixture validates that forget can take granular dependencies on context
|
||||
* variables when the reference to a context var happens *after* the end of its
|
||||
* mutable range.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(10);
|
||||
const { cond, a } = t0;
|
||||
let contextVar;
|
||||
if ($[0] !== a || $[1] !== cond) {
|
||||
if (cond) {
|
||||
contextVar = { val: a };
|
||||
} else {
|
||||
contextVar = {};
|
||||
throwErrorWithMessage("");
|
||||
}
|
||||
$[0] = a;
|
||||
$[1] = cond;
|
||||
$[2] = contextVar;
|
||||
} else {
|
||||
contextVar = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== contextVar.val) {
|
||||
t1 = { cb: () => contextVar.val * 4 };
|
||||
$[3] = contextVar.val;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const cb = t1;
|
||||
|
||||
const t2 = cond ? a : undefined;
|
||||
let t3;
|
||||
if ($[5] !== t2) {
|
||||
t3 = [t2];
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== cb || $[8] !== t3) {
|
||||
t4 = (
|
||||
<ValidateMemoization inputs={t3} output={cb} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[7] = cb;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ cond: false, a: undefined }],
|
||||
sequentialRenders: [
|
||||
{ cond: true, a: 2 },
|
||||
{ cond: true, a: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>
|
||||
<div>{"inputs":[2],"output":{"cb":"[[ function params=0 ]]"}}</div>
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import {throwErrorWithMessage, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Context variables are local variables that (1) have at least one reassignment
|
||||
* and (2) are captured into a function expression. These have a known mutable
|
||||
* range: from first declaration / assignment to the last direct or aliased,
|
||||
* mutable reference.
|
||||
*
|
||||
* This fixture validates that forget can take granular dependencies on context
|
||||
* variables when the reference to a context var happens *after* the end of its
|
||||
* mutable range.
|
||||
*/
|
||||
function Component({cond, a}) {
|
||||
let contextVar;
|
||||
if (cond) {
|
||||
contextVar = {val: a};
|
||||
} else {
|
||||
contextVar = {};
|
||||
throwErrorWithMessage('');
|
||||
}
|
||||
const cb = {cb: () => contextVar.val * 4};
|
||||
|
||||
/**
|
||||
* manually specify input to avoid adding a `PropertyLoad` from contextVar,
|
||||
* which might affect hoistable-objects analysis.
|
||||
*/
|
||||
return (
|
||||
<ValidateMemoization
|
||||
inputs={[cond ? a : undefined]}
|
||||
output={cb}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{cond: false, a: undefined}],
|
||||
sequentialRenders: [
|
||||
{cond: true, a: 2},
|
||||
{cond: true, a: 2},
|
||||
],
|
||||
};
|
||||
+7
-9
@@ -35,17 +35,15 @@ function HomeDiscoStoreItemTileRating(props) {
|
||||
} else {
|
||||
count = $[1];
|
||||
}
|
||||
|
||||
const t0 = count;
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Text>{t0}</Text>;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
let t0;
|
||||
if ($[2] !== count) {
|
||||
t0 = <Text>{count}</Text>;
|
||||
$[2] = count;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
t0 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {arrayPush} from 'shared-runtime';
|
||||
|
||||
function useFoo({a, b}) {
|
||||
const obj = {a};
|
||||
arrayPush(Object.keys(obj), b);
|
||||
return obj;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 3}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { arrayPush } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(2);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
t1 = { a };
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const obj = t1;
|
||||
arrayPush(Object.keys(obj), b);
|
||||
return obj;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2, b: 3 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) {"a":2}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import {arrayPush} from 'shared-runtime';
|
||||
|
||||
function useFoo({a, b}) {
|
||||
const obj = {a};
|
||||
arrayPush(Object.keys(obj), b);
|
||||
return obj;
|
||||
}
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 3}],
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @target="donotuse_meta_internal"
|
||||
|
||||
function Component() {
|
||||
return <div>Hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react"; // @target="donotuse_meta_internal"
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div>Hello world</div>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// @target="donotuse_meta_internal"
|
||||
|
||||
function Component() {
|
||||
return <div>Hello world</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
isComponent: true,
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(all)
|
||||
'use no memo';
|
||||
|
||||
function TestComponent({x}) {
|
||||
'use memo';
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode(all)
|
||||
"use no memo";
|
||||
|
||||
function TestComponent({ x }) {
|
||||
"use memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// @compilationMode(all)
|
||||
'use no memo';
|
||||
|
||||
function TestComponent({x}) {
|
||||
'use memo';
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
+20
-22
@@ -88,36 +88,34 @@ function Inner(props) {
|
||||
input;
|
||||
input;
|
||||
let t0;
|
||||
const t1 = input;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = [t1];
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
let t1;
|
||||
if ($[0] !== input) {
|
||||
t1 = [input];
|
||||
$[0] = input;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t1 = $[1];
|
||||
}
|
||||
t0 = t2;
|
||||
t0 = t1;
|
||||
const output = t0;
|
||||
const t3 = input;
|
||||
let t4;
|
||||
if ($[2] !== t3) {
|
||||
t4 = [t3];
|
||||
$[2] = t3;
|
||||
$[3] = t4;
|
||||
let t2;
|
||||
if ($[2] !== input) {
|
||||
t2 = [input];
|
||||
$[2] = input;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
t2 = $[3];
|
||||
}
|
||||
let t5;
|
||||
if ($[4] !== output || $[5] !== t4) {
|
||||
t5 = <ValidateMemoization inputs={t4} output={output} />;
|
||||
let t3;
|
||||
if ($[4] !== output || $[5] !== t2) {
|
||||
t3 = <ValidateMemoization inputs={t2} output={output} />;
|
||||
$[4] = output;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
t3 = $[6];
|
||||
}
|
||||
return t5;
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('parseConfigPragmaForTests()', () => {
|
||||
enableUseTypeAnnotations: true,
|
||||
validateNoSetStateInPassiveEffects: true,
|
||||
validateNoSetStateInRender: false,
|
||||
enableResetCacheOnSourceFileChanges: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,8 +17,6 @@ export {
|
||||
compileFn as compile,
|
||||
compileProgram,
|
||||
parsePluginOptions,
|
||||
run,
|
||||
runPlayground,
|
||||
OPT_OUT_DIRECTIVES,
|
||||
OPT_IN_DIRECTIVES,
|
||||
findDirectiveEnablingMemoization,
|
||||
|
||||
@@ -479,6 +479,7 @@ const skipFilter = new Set([
|
||||
// bugs
|
||||
'fbt/bug-fbt-plural-multiple-function-calls',
|
||||
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
|
||||
`bug-capturing-func-maybealias-captured-mutate`,
|
||||
'bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr',
|
||||
'bug-invalid-hoisting-functionexpr',
|
||||
'bug-aliased-capture-aliased-mutate',
|
||||
@@ -504,6 +505,7 @@ const skipFilter = new Set([
|
||||
// Depends on external functions
|
||||
'idx-method-no-outlining-wildcard',
|
||||
'idx-method-no-outlining',
|
||||
'target-flag-meta-internal',
|
||||
|
||||
// needs to be executed as a module
|
||||
'meta-property',
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {
|
||||
LoggerEvent,
|
||||
PanicThresholdOptions,
|
||||
PluginOptions,
|
||||
CompilerReactTarget,
|
||||
CompilerPipelineValue,
|
||||
} from 'babel-plugin-react-compiler/src/Entrypoint';
|
||||
import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR';
|
||||
import type {
|
||||
@@ -44,6 +46,7 @@ export function parseLanguage(source: string): 'flow' | 'typescript' {
|
||||
function makePluginOptions(
|
||||
firstLine: string,
|
||||
parseConfigPragmaFn: typeof ParseConfigPragma,
|
||||
debugIRLogger: (value: CompilerPipelineValue) => void,
|
||||
EffectEnum: typeof Effect,
|
||||
ValueKindEnum: typeof ValueKind,
|
||||
): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
|
||||
@@ -55,7 +58,8 @@ function makePluginOptions(
|
||||
let validatePreserveExistingMemoizationGuarantees = false;
|
||||
let customMacros: null | Array<Macro> = null;
|
||||
let validateBlocklistedImports = null;
|
||||
let target = '19' as const;
|
||||
let enableFire = false;
|
||||
let target: CompilerReactTarget = '19';
|
||||
|
||||
if (firstLine.indexOf('@compilationMode(annotation)') !== -1) {
|
||||
assert(
|
||||
@@ -81,8 +85,15 @@ function makePluginOptions(
|
||||
|
||||
const targetMatch = /@target="([^"]+)"/.exec(firstLine);
|
||||
if (targetMatch) {
|
||||
// @ts-ignore
|
||||
target = targetMatch[1];
|
||||
if (targetMatch[1] === 'donotuse_meta_internal') {
|
||||
target = {
|
||||
kind: targetMatch[1],
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
// @ts-ignore
|
||||
target = targetMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (firstLine.includes('@panicThreshold(none)')) {
|
||||
@@ -119,6 +130,10 @@ function makePluginOptions(
|
||||
validatePreserveExistingMemoizationGuarantees = true;
|
||||
}
|
||||
|
||||
if (firstLine.includes('@enableFire')) {
|
||||
enableFire = true;
|
||||
}
|
||||
|
||||
const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
|
||||
if (
|
||||
hookPatternMatch &&
|
||||
@@ -174,20 +189,15 @@ function makePluginOptions(
|
||||
.filter(s => s.length > 0);
|
||||
}
|
||||
|
||||
let inferEffectDependencies = false;
|
||||
if (firstLine.includes('@inferEffectDependencies')) {
|
||||
inferEffectDependencies = true;
|
||||
}
|
||||
|
||||
let logs: Array<{filename: string | null; event: LoggerEvent}> = [];
|
||||
let logger: Logger | null = null;
|
||||
if (firstLine.includes('@logger')) {
|
||||
logger = {
|
||||
logEvent(filename: string | null, event: LoggerEvent): void {
|
||||
logs.push({filename, event});
|
||||
},
|
||||
};
|
||||
}
|
||||
const logs: Array<{filename: string | null; event: LoggerEvent}> = [];
|
||||
const logger: Logger = {
|
||||
logEvent: firstLine.includes('@logger')
|
||||
? (filename, event) => {
|
||||
logs.push({filename, event});
|
||||
}
|
||||
: () => {},
|
||||
debugLogIRs: debugIRLogger,
|
||||
};
|
||||
|
||||
const config = parseConfigPragmaFn(firstLine);
|
||||
const options = {
|
||||
@@ -202,7 +212,7 @@ function makePluginOptions(
|
||||
hookPattern,
|
||||
validatePreserveExistingMemoizationGuarantees,
|
||||
validateBlocklistedImports,
|
||||
inferEffectDependencies,
|
||||
enableFire,
|
||||
},
|
||||
compilationMode,
|
||||
logger,
|
||||
@@ -295,6 +305,8 @@ function getEvaluatorPresets(
|
||||
arg.value = './shared-runtime';
|
||||
} else if (arg.value === 'ReactForgetFeatureFlag') {
|
||||
arg.value = './ReactForgetFeatureFlag';
|
||||
} else if (arg.value === 'useEffectWrapper') {
|
||||
arg.value = './useEffectWrapper';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,6 +346,7 @@ export async function transformFixtureInput(
|
||||
parseConfigPragmaFn: typeof ParseConfigPragma,
|
||||
plugin: BabelCore.PluginObj,
|
||||
includeEvaluator: boolean,
|
||||
debugIRLogger: (value: CompilerPipelineValue) => void,
|
||||
EffectEnum: typeof Effect,
|
||||
ValueKindEnum: typeof ValueKind,
|
||||
): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> {
|
||||
@@ -361,6 +374,7 @@ export async function transformFixtureInput(
|
||||
const [options, logs] = makePluginOptions(
|
||||
firstLine,
|
||||
parseConfigPragmaFn,
|
||||
debugIRLogger,
|
||||
EffectEnum,
|
||||
ValueKindEnum,
|
||||
);
|
||||
|
||||
@@ -18,11 +18,17 @@ export const COMPILER_PATH = path.join(
|
||||
'BabelPlugin.js',
|
||||
);
|
||||
export const COMPILER_INDEX_PATH = path.join(process.cwd(), 'dist', 'index');
|
||||
export const LOGGER_PATH = path.join(
|
||||
export const PRINT_HIR_PATH = path.join(
|
||||
process.cwd(),
|
||||
'dist',
|
||||
'Utils',
|
||||
'logger.js',
|
||||
'HIR',
|
||||
'PrintHIR.js',
|
||||
);
|
||||
export const PRINT_REACTIVE_IR_PATH = path.join(
|
||||
process.cwd(),
|
||||
'dist',
|
||||
'ReactiveScopes',
|
||||
'PrintReactiveFunction.js',
|
||||
);
|
||||
export const PARSE_CONFIG_PRAGMA_PATH = path.join(
|
||||
process.cwd(),
|
||||
|
||||
@@ -8,16 +8,21 @@
|
||||
import {codeFrameColumns} from '@babel/code-frame';
|
||||
import type {PluginObj} from '@babel/core';
|
||||
import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/HIR/Environment';
|
||||
import type {printFunctionWithOutlined as PrintFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
|
||||
import type {printReactiveFunctionWithOutlined as PrintReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
|
||||
import {TransformResult, transformFixtureInput} from './compiler';
|
||||
import {
|
||||
COMPILER_PATH,
|
||||
COMPILER_INDEX_PATH,
|
||||
LOGGER_PATH,
|
||||
PARSE_CONFIG_PRAGMA_PATH,
|
||||
PRINT_HIR_PATH,
|
||||
PRINT_REACTIVE_IR_PATH,
|
||||
} from './constants';
|
||||
import {TestFixture, getBasename, isExpectError} from './fixture-utils';
|
||||
import {TestResult, writeOutputToString} from './reporter';
|
||||
import {runSprout} from './sprout';
|
||||
import {CompilerPipelineValue} from 'babel-plugin-react-compiler/src';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
@@ -64,20 +69,56 @@ async function compile(
|
||||
const {Effect: EffectEnum, ValueKind: ValueKindEnum} = require(
|
||||
COMPILER_INDEX_PATH,
|
||||
);
|
||||
const {toggleLogging} = require(LOGGER_PATH);
|
||||
const {printFunctionWithOutlined} = require(PRINT_HIR_PATH) as {
|
||||
printFunctionWithOutlined: typeof PrintFunctionWithOutlined;
|
||||
};
|
||||
const {printReactiveFunctionWithOutlined} = require(
|
||||
PRINT_REACTIVE_IR_PATH,
|
||||
) as {
|
||||
printReactiveFunctionWithOutlined: typeof PrintReactiveFunctionWithOutlined;
|
||||
};
|
||||
|
||||
let lastLogged: string | null = null;
|
||||
const debugIRLogger = shouldLog
|
||||
? (value: CompilerPipelineValue) => {
|
||||
let printed: string;
|
||||
switch (value.kind) {
|
||||
case 'hir':
|
||||
printed = printFunctionWithOutlined(value.value);
|
||||
break;
|
||||
case 'reactive':
|
||||
printed = printReactiveFunctionWithOutlined(value.value);
|
||||
break;
|
||||
case 'debug':
|
||||
printed = value.value;
|
||||
break;
|
||||
case 'ast':
|
||||
// skip printing ast as we already write fixture output JS
|
||||
printed = '(ast)';
|
||||
break;
|
||||
}
|
||||
|
||||
if (printed !== lastLogged) {
|
||||
lastLogged = printed;
|
||||
console.log(`${chalk.green(value.name)}:\n ${printed}\n`);
|
||||
} else {
|
||||
console.log(`${chalk.blue(value.name)}: (no change)\n`);
|
||||
}
|
||||
}
|
||||
: () => {};
|
||||
const {parseConfigPragmaForTests} = require(PARSE_CONFIG_PRAGMA_PATH) as {
|
||||
parseConfigPragmaForTests: typeof ParseConfigPragma;
|
||||
};
|
||||
|
||||
// only try logging if we filtered out all but one fixture,
|
||||
// since console log order is non-deterministic
|
||||
toggleLogging(shouldLog);
|
||||
const result = await transformFixtureInput(
|
||||
input,
|
||||
fixturePath,
|
||||
parseConfigPragmaForTests,
|
||||
BabelPluginReactCompiler,
|
||||
includeEvaluator,
|
||||
debugIRLogger,
|
||||
EffectEnum,
|
||||
ValueKindEnum,
|
||||
);
|
||||
|
||||
@@ -32,7 +32,15 @@ export function runSprout(
|
||||
originalCode: string,
|
||||
forgetCode: string,
|
||||
): SproutResult {
|
||||
const forgetResult = doEval(forgetCode);
|
||||
let forgetResult;
|
||||
try {
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE = 'forget';
|
||||
forgetResult = doEval(forgetCode);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE = undefined;
|
||||
}
|
||||
if (forgetResult.kind === 'UnexpectedError') {
|
||||
return makeError('Unexpected error in Forget runner', forgetResult.value);
|
||||
}
|
||||
|
||||
@@ -259,26 +259,35 @@ export function Throw() {
|
||||
|
||||
export function ValidateMemoization({
|
||||
inputs,
|
||||
output,
|
||||
output: rawOutput,
|
||||
onlyCheckCompiled = false,
|
||||
}: {
|
||||
inputs: Array<any>;
|
||||
output: any;
|
||||
onlyCheckCompiled: boolean;
|
||||
}): React.ReactElement {
|
||||
'use no forget';
|
||||
// Wrap rawOutput as it might be a function, which useState would invoke.
|
||||
const output = {value: rawOutput};
|
||||
const [previousInputs, setPreviousInputs] = React.useState(inputs);
|
||||
const [previousOutput, setPreviousOutput] = React.useState(output);
|
||||
if (
|
||||
inputs.length !== previousInputs.length ||
|
||||
inputs.some((item, i) => item !== previousInputs[i])
|
||||
onlyCheckCompiled &&
|
||||
(globalThis as any).__SNAP_EVALUATOR_MODE === 'forget'
|
||||
) {
|
||||
// Some input changed, we expect the output to change
|
||||
setPreviousInputs(inputs);
|
||||
setPreviousOutput(output);
|
||||
} else if (output !== previousOutput) {
|
||||
// Else output should be stable
|
||||
throw new Error('Output identity changed but inputs did not');
|
||||
if (
|
||||
inputs.length !== previousInputs.length ||
|
||||
inputs.some((item, i) => item !== previousInputs[i])
|
||||
) {
|
||||
// Some input changed, we expect the output to change
|
||||
setPreviousInputs(inputs);
|
||||
setPreviousOutput(output);
|
||||
} else if (output.value !== previousOutput.value) {
|
||||
// Else output should be stable
|
||||
throw new Error('Output identity changed but inputs did not');
|
||||
}
|
||||
}
|
||||
return React.createElement(Stringify, {inputs, output});
|
||||
return React.createElement(Stringify, {inputs, output: rawOutput});
|
||||
}
|
||||
|
||||
export function createHookWrapper<TProps, TRet>(
|
||||
@@ -363,6 +372,14 @@ export function useFragment(..._args: Array<any>): object {
|
||||
};
|
||||
}
|
||||
|
||||
export function useSpecialEffect(
|
||||
fn: () => any,
|
||||
_secondArg: any,
|
||||
deps: Array<any>,
|
||||
) {
|
||||
React.useEffect(fn, deps);
|
||||
}
|
||||
|
||||
export function typedArrayPush<T>(array: Array<T>, item: T): void {
|
||||
array.push(item);
|
||||
}
|
||||
@@ -370,4 +387,5 @@ export function typedArrayPush<T>(array: Array<T>, item: T): void {
|
||||
export function typedLog(...values: Array<any>): void {
|
||||
console.log(...values);
|
||||
}
|
||||
|
||||
export default typedLog;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* This file is used to test the effect auto-deps configuration, which
|
||||
* allows you to specify functions that should have dependencies added to
|
||||
* callsites.
|
||||
*/
|
||||
import {useEffect} from 'react';
|
||||
|
||||
export default function useEffectWrapper(f: () => void | (() => void)): void {
|
||||
useEffect(() => {
|
||||
f();
|
||||
}, [f]);
|
||||
}
|
||||
+3
-3
@@ -6,14 +6,14 @@
|
||||
*/
|
||||
|
||||
// v0.17.1
|
||||
declare module "hermes-parser" {
|
||||
declare module 'hermes-parser' {
|
||||
type HermesParserOptions = {
|
||||
allowReturnOutsideFunction?: boolean;
|
||||
babel?: boolean;
|
||||
flow?: "all" | "detect";
|
||||
flow?: 'all' | 'detect';
|
||||
enableExperimentalComponentSyntax?: boolean;
|
||||
sourceFilename?: string;
|
||||
sourceType?: "module" | "script" | "unambiguous";
|
||||
sourceType?: 'module' | 'script' | 'unambiguous';
|
||||
tokens?: boolean;
|
||||
};
|
||||
export function parse(code: string, options: Partial<HermesParserOptions>);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.parcel-cache
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
todos.json
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@parcel/config-default",
|
||||
"runtimes": ["...", "@parcel/runtime-rsc"]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "flight-parcel",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"examples/*"
|
||||
],
|
||||
"server": "dist/server.js",
|
||||
"targets": {
|
||||
"server": {
|
||||
"source": "src/server.tsx",
|
||||
"context": "react-server",
|
||||
"outputFormat": "commonjs",
|
||||
"includeNodeModules": {
|
||||
"express": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"dev": "concurrently \"npm run dev:watch\" \"npm run dev:start\"",
|
||||
"dev:watch": "NODE_ENV=development parcel watch",
|
||||
"dev:start": "NODE_ENV=development node dist/server.js",
|
||||
"build": "parcel build",
|
||||
"start": "node dist/server.js"
|
||||
},
|
||||
"@parcel/resolver-default": {
|
||||
"packageExports": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@parcel/config-default": "2.0.0-dev.1789",
|
||||
"@parcel/runtime-rsc": "2.13.3-dev.3412",
|
||||
"@types/parcel-env": "^0.0.6",
|
||||
"@types/express": "*",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"concurrently": "^7.3.0",
|
||||
"express": "^4.18.2",
|
||||
"parcel": "2.0.0-dev.1787",
|
||||
"process": "^0.11.10",
|
||||
"react": "experimental",
|
||||
"react-dom": "experimental",
|
||||
"react-server-dom-parcel": "experimental",
|
||||
"rsc-html-stream": "^0.0.4",
|
||||
"ws": "^8.8.1"
|
||||
},
|
||||
"@parcel/bundler-default": {
|
||||
"minBundleSize": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import {ReactNode, useRef} from 'react';
|
||||
|
||||
export function Dialog({
|
||||
trigger,
|
||||
children,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
let ref = useRef<HTMLDialogElement | null>(null);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => ref.current?.showModal()}>{trigger}</button>
|
||||
<dialog ref={ref} onSubmit={() => ref.current?.close()}>
|
||||
{children}
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {createTodo} from './actions';
|
||||
|
||||
export function TodoCreate() {
|
||||
return (
|
||||
<form action={createTodo}>
|
||||
<label>
|
||||
Title: <input name="title" />
|
||||
</label>
|
||||
<label>
|
||||
Description: <textarea name="description" />
|
||||
</label>
|
||||
<label>
|
||||
Due date: <input type="date" name="dueDate" />
|
||||
</label>
|
||||
<button>Add todo</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {getTodo, updateTodo} from './actions';
|
||||
|
||||
export async function TodoDetail({id}: {id: number}) {
|
||||
let todo = await getTodo(id);
|
||||
if (!todo) {
|
||||
return <p>Todo not found</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="todo" action={updateTodo.bind(null, todo.id)}>
|
||||
<label>
|
||||
Title: <input name="title" defaultValue={todo.title} />
|
||||
</label>
|
||||
<label>
|
||||
Description:{' '}
|
||||
<textarea name="description" defaultValue={todo.description} />
|
||||
</label>
|
||||
<label>
|
||||
Due date:{' '}
|
||||
<input type="date" name="dueDate" defaultValue={todo.dueDate} />
|
||||
</label>
|
||||
<button type="submit">Update todo</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import {startTransition, useOptimistic} from 'react';
|
||||
import {deleteTodo, setTodoComplete, type Todo as ITodo} from './actions';
|
||||
|
||||
export function TodoItem({
|
||||
todo,
|
||||
isSelected,
|
||||
}: {
|
||||
todo: ITodo;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
let [isOptimisticComplete, setOptimisticComplete] = useOptimistic(
|
||||
todo.isComplete,
|
||||
);
|
||||
|
||||
return (
|
||||
<li data-selected={isSelected || undefined}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isOptimisticComplete}
|
||||
onChange={e => {
|
||||
startTransition(async () => {
|
||||
setOptimisticComplete(e.target.checked);
|
||||
await setTodoComplete(todo.id, e.target.checked);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<a
|
||||
href={`/todos/${todo.id}`}
|
||||
aria-current={isSelected ? 'page' : undefined}>
|
||||
{todo.title}
|
||||
</a>
|
||||
<button onClick={() => deleteTodo(todo.id)}>x</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {TodoItem} from './TodoItem';
|
||||
import {getTodos} from './actions';
|
||||
|
||||
export async function TodoList({id}: {id: number | undefined}) {
|
||||
let todos = await getTodos();
|
||||
return (
|
||||
<ul className="todo-list">
|
||||
{todos.map(todo => (
|
||||
<TodoItem key={todo.id} todo={todo} isSelected={todo.id === id} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
body {
|
||||
font-family: system-ui;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.todo-column {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 250px;
|
||||
padding: 8px;
|
||||
padding-right: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
max-width: 250px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
padding-right: 32px;
|
||||
border-right: 1px solid gray;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
accent-color: light-dark(black, white);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&[data-selected] {
|
||||
background-color: light-dark(#222, #ddd);
|
||||
color: light-dark(#ddd, #222);
|
||||
accent-color: light-dark(white, black);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
'use server-entry';
|
||||
|
||||
import './client';
|
||||
import './Todos.css';
|
||||
import {Resources} from '@parcel/runtime-rsc';
|
||||
import {Dialog} from './Dialog';
|
||||
import {TodoDetail} from './TodoDetail';
|
||||
import {TodoCreate} from './TodoCreate';
|
||||
import {TodoList} from './TodoList';
|
||||
|
||||
export async function Todos({id}: {id?: number}) {
|
||||
return (
|
||||
<html style={{colorScheme: 'dark light'}}>
|
||||
<head>
|
||||
<title>Todos</title>
|
||||
<Resources />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Todos</h1>
|
||||
<Dialog trigger="+">
|
||||
<h2>Add todo</h2>
|
||||
<TodoCreate />
|
||||
</Dialog>
|
||||
</header>
|
||||
<main>
|
||||
<div className="todo-column">
|
||||
<TodoList id={id} />
|
||||
</div>
|
||||
{id != null ? <TodoDetail key={id} id={id} /> : <p>Select a todo</p>}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
'use server';
|
||||
|
||||
import fs from 'fs/promises';
|
||||
|
||||
export interface Todo {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
dueDate: string;
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
export async function getTodos(): Promise<Todo[]> {
|
||||
try {
|
||||
let contents = await fs.readFile('todos.json', 'utf8');
|
||||
return JSON.parse(contents);
|
||||
} catch {
|
||||
await fs.writeFile('todos.json', '[]');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTodo(id: number): Promise<Todo | undefined> {
|
||||
let todos = await getTodos();
|
||||
return todos.find(todo => todo.id === id);
|
||||
}
|
||||
|
||||
export async function createTodo(formData: FormData) {
|
||||
let todos = await getTodos();
|
||||
let title = formData.get('title');
|
||||
let description = formData.get('description');
|
||||
let dueDate = formData.get('dueDate');
|
||||
let id = todos.length > 0 ? Math.max(...todos.map(todo => todo.id)) + 1 : 0;
|
||||
todos.push({
|
||||
id,
|
||||
title: typeof title === 'string' ? title : '',
|
||||
description: typeof description === 'string' ? description : '',
|
||||
dueDate: typeof dueDate === 'string' ? dueDate : new Date().toISOString(),
|
||||
isComplete: false,
|
||||
});
|
||||
await fs.writeFile('todos.json', JSON.stringify(todos));
|
||||
}
|
||||
|
||||
export async function updateTodo(id: number, formData: FormData) {
|
||||
let todos = await getTodos();
|
||||
let title = formData.get('title');
|
||||
let description = formData.get('description');
|
||||
let dueDate = formData.get('dueDate');
|
||||
let todo = todos.find(todo => todo.id === id);
|
||||
if (todo) {
|
||||
todo.title = typeof title === 'string' ? title : '';
|
||||
todo.description = typeof description === 'string' ? description : '';
|
||||
todo.dueDate =
|
||||
typeof dueDate === 'string' ? dueDate : new Date().toISOString();
|
||||
await fs.writeFile('todos.json', JSON.stringify(todos));
|
||||
}
|
||||
}
|
||||
|
||||
export async function setTodoComplete(id: number, isComplete: boolean) {
|
||||
let todos = await getTodos();
|
||||
let todo = todos.find(todo => todo.id === id);
|
||||
if (todo) {
|
||||
todo.isComplete = isComplete;
|
||||
await fs.writeFile('todos.json', JSON.stringify(todos));
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTodo(id: number) {
|
||||
let todos = await getTodos();
|
||||
let index = todos.findIndex(todo => todo.id === id);
|
||||
if (index >= 0) {
|
||||
todos.splice(index, 1);
|
||||
await fs.writeFile('todos.json', JSON.stringify(todos));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client-entry';
|
||||
|
||||
import {
|
||||
useState,
|
||||
use,
|
||||
startTransition,
|
||||
useInsertionEffect,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import {
|
||||
createFromReadableStream,
|
||||
createFromFetch,
|
||||
encodeReply,
|
||||
setServerCallback,
|
||||
} from 'react-server-dom-parcel/client';
|
||||
import {rscStream} from 'rsc-html-stream/client';
|
||||
|
||||
// Stream in initial RSC payload embedded in the HTML.
|
||||
let initialRSCPayload = createFromReadableStream<ReactElement>(rscStream);
|
||||
let updateRoot:
|
||||
| ((root: ReactElement, cb?: (() => void) | null) => void)
|
||||
| null = null;
|
||||
|
||||
function Content() {
|
||||
// Store the current root element in state, along with a callback
|
||||
// to call once rendering is complete.
|
||||
let [[root, cb], setRoot] = useState<[ReactElement, (() => void) | null]>([
|
||||
use(initialRSCPayload),
|
||||
null,
|
||||
]);
|
||||
updateRoot = (root, cb) => setRoot([root, cb ?? null]);
|
||||
useInsertionEffect(() => cb?.());
|
||||
return root;
|
||||
}
|
||||
|
||||
// Hydrate initial page content.
|
||||
startTransition(() => {
|
||||
ReactDOM.hydrateRoot(document, <Content />);
|
||||
});
|
||||
|
||||
// A very simple router. When we navigate, we'll fetch a new RSC payload from the server,
|
||||
// and in a React transition, stream in the new page. Once complete, we'll pushState to
|
||||
// update the URL in the browser.
|
||||
async function navigate(pathname: string, push = false) {
|
||||
let res = fetch(pathname, {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
},
|
||||
});
|
||||
let root = await createFromFetch<ReactElement>(res);
|
||||
startTransition(() => {
|
||||
updateRoot!(root, () => {
|
||||
if (push) {
|
||||
history.pushState(null, '', pathname);
|
||||
push = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Intercept link clicks to perform RSC navigation.
|
||||
document.addEventListener('click', e => {
|
||||
let link = (e.target as Element).closest('a');
|
||||
if (
|
||||
link &&
|
||||
link instanceof HTMLAnchorElement &&
|
||||
link.href &&
|
||||
(!link.target || link.target === '_self') &&
|
||||
link.origin === location.origin &&
|
||||
!link.hasAttribute('download') &&
|
||||
e.button === 0 && // left clicks only
|
||||
!e.metaKey && // open in new tab (mac)
|
||||
!e.ctrlKey && // open in new tab (windows)
|
||||
!e.altKey && // download
|
||||
!e.shiftKey &&
|
||||
!e.defaultPrevented
|
||||
) {
|
||||
e.preventDefault();
|
||||
navigate(link.pathname, true);
|
||||
}
|
||||
});
|
||||
|
||||
// When the user clicks the back button, navigate with RSC.
|
||||
window.addEventListener('popstate', e => {
|
||||
navigate(location.pathname);
|
||||
});
|
||||
|
||||
// Intercept HMR window reloads, and do it with RSC instead.
|
||||
window.addEventListener('parcelhmrreload', e => {
|
||||
e.preventDefault();
|
||||
navigate(location.pathname);
|
||||
});
|
||||
|
||||
// Setup a callback to perform server actions.
|
||||
// This sends a POST request to the server, and updates the page with the response.
|
||||
setServerCallback(async function (id: string, args: any[]) {
|
||||
console.log('Handling server action', id, args);
|
||||
const response = fetch(location.pathname, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
'rsc-action-id': id,
|
||||
},
|
||||
body: await encodeReply(args),
|
||||
});
|
||||
const {result, root} = await createFromFetch<{
|
||||
root: JSX.Element;
|
||||
result: any;
|
||||
}>(response);
|
||||
startTransition(() => updateRoot!(root));
|
||||
return result;
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
// Server dependencies.
|
||||
import express, {
|
||||
type Request as ExpressRequest,
|
||||
type Response as ExpressResponse,
|
||||
} from 'express';
|
||||
import {Readable} from 'node:stream';
|
||||
import type {ReadableStream as NodeReadableStream} from 'stream/web';
|
||||
import {
|
||||
renderToReadableStream,
|
||||
loadServerAction,
|
||||
decodeReply,
|
||||
decodeAction,
|
||||
} from 'react-server-dom-parcel/server.edge';
|
||||
import {injectRSCPayload} from 'rsc-html-stream/server';
|
||||
|
||||
// Client dependencies, used for SSR.
|
||||
// These must run in the same environment as client components (e.g. same instance of React).
|
||||
import {createFromReadableStream} from 'react-server-dom-parcel/client' with {env: 'react-client'};
|
||||
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server' with {env: 'react-client'};
|
||||
import ReactClient, {ReactElement} from 'react' with {env: 'react-client'};
|
||||
|
||||
// Page components. These must have "use server-entry" so they are treated as code splitting entry points.
|
||||
import {Todos} from './Todos';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,POST');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'rsc-action');
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.static('dist'));
|
||||
|
||||
app.get('/', async (req, res) => {
|
||||
await render(req, res, <Todos />);
|
||||
});
|
||||
|
||||
app.post('/', async (req, res) => {
|
||||
await handleAction(req, res, <Todos />);
|
||||
});
|
||||
|
||||
app.get('/todos/:id', async (req, res) => {
|
||||
await render(req, res, <Todos id={Number(req.params.id)} />);
|
||||
});
|
||||
|
||||
app.post('/todos/:id', async (req, res) => {
|
||||
await handleAction(req, res, <Todos id={Number(req.params.id)} />);
|
||||
});
|
||||
|
||||
async function render(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
component: ReactElement,
|
||||
actionResult?: any,
|
||||
) {
|
||||
// Render RSC payload.
|
||||
let root: any = component;
|
||||
if (actionResult) {
|
||||
root = {result: actionResult, root};
|
||||
}
|
||||
let stream = renderToReadableStream(root);
|
||||
if (req.accepts('text/html')) {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
// Use client react to render the RSC payload to HTML.
|
||||
let [s1, s2] = stream.tee();
|
||||
let data = createFromReadableStream<ReactElement>(s1);
|
||||
function Content() {
|
||||
return ReactClient.use(data);
|
||||
}
|
||||
|
||||
let htmlStream = await renderHTMLToReadableStream(<Content />);
|
||||
let response = htmlStream.pipeThrough(injectRSCPayload(s2));
|
||||
Readable.fromWeb(response as NodeReadableStream).pipe(res);
|
||||
} else {
|
||||
res.set('Content-Type', 'text/x-component');
|
||||
Readable.fromWeb(stream as NodeReadableStream).pipe(res);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle server actions.
|
||||
async function handleAction(
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
component: ReactElement,
|
||||
) {
|
||||
let id = req.get('rsc-action-id');
|
||||
let request = new Request('http://localhost' + req.url, {
|
||||
method: 'POST',
|
||||
headers: req.headers as any,
|
||||
body: Readable.toWeb(req) as ReadableStream,
|
||||
// @ts-ignore
|
||||
duplex: 'half',
|
||||
});
|
||||
|
||||
if (id) {
|
||||
let action = await loadServerAction(id);
|
||||
let body = req.is('multipart/form-data')
|
||||
? await request.formData()
|
||||
: await request.text();
|
||||
let args = await decodeReply<any[]>(body);
|
||||
let result = action.apply(null, args);
|
||||
try {
|
||||
// Wait for any mutations
|
||||
await result;
|
||||
} catch (x) {
|
||||
// We handle the error on the client
|
||||
}
|
||||
|
||||
await render(req, res, component, result);
|
||||
} else {
|
||||
// Form submitted by browser (progressive enhancement).
|
||||
let formData = await request.formData();
|
||||
let action = await decodeAction(formData);
|
||||
try {
|
||||
// Wait for any mutations
|
||||
await action();
|
||||
} catch (err) {
|
||||
// TODO render error page?
|
||||
}
|
||||
await render(req, res, component);
|
||||
}
|
||||
}
|
||||
|
||||
let server = app.listen(3001);
|
||||
console.log('Server listening on port 3001');
|
||||
|
||||
// Restart the server when it changes.
|
||||
if (module.hot) {
|
||||
module.hot.dispose(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
module.hot.accept();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "esnext",
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "es2022"
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
// TODO: move these into their respective packages.
|
||||
|
||||
declare module 'react-server-dom-parcel/client' {
|
||||
export function createFromFetch<T>(res: Promise<Response>): Promise<T>;
|
||||
export function createFromReadableStream<T>(stream: ReadableStream): Promise<T>;
|
||||
export function encodeReply(value: any): Promise<string | URLSearchParams | FormData>;
|
||||
|
||||
type CallServerCallback = <T>(id: string, args: any[]) => Promise<T>;
|
||||
export function setServerCallback(cb: CallServerCallback): void;
|
||||
}
|
||||
|
||||
declare module 'react-server-dom-parcel/server.edge' {
|
||||
export function renderToReadableStream(value: any): ReadableStream;
|
||||
export function loadServerAction(id: string): Promise<(...args: any[]) => any>;
|
||||
export function decodeReply<T>(body: string | FormData): Promise<T>;
|
||||
export function decodeAction(body: FormData): Promise<(...args: any[]) => any>;
|
||||
}
|
||||
|
||||
declare module '@parcel/runtime-rsc' {
|
||||
export function Resources(): JSX.Element;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user