summaryrefslogtreecommitdiff
path: root/packages/integrations/react
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/react')
-rw-r--r--packages/integrations/react/CHANGELOG.md827
-rw-r--r--packages/integrations/react/README.md38
-rw-r--r--packages/integrations/react/client-v17.js22
-rw-r--r--packages/integrations/react/client.js116
-rw-r--r--packages/integrations/react/context.js24
-rw-r--r--packages/integrations/react/jsx-runtime.js8
-rw-r--r--packages/integrations/react/package.json77
-rw-r--r--packages/integrations/react/server-v17.js73
-rw-r--r--packages/integrations/react/server.d.ts4
-rw-r--r--packages/integrations/react/server.js213
-rw-r--r--packages/integrations/react/server17.d.ts2
-rw-r--r--packages/integrations/react/src/actions.ts104
-rw-r--r--packages/integrations/react/src/index.ts153
-rw-r--r--packages/integrations/react/src/version.ts31
-rw-r--r--packages/integrations/react/static-html.js29
-rw-r--r--packages/integrations/react/test/fixtures/react-component/astro.config.mjs10
-rw-r--r--packages/integrations/react/test/fixtures/react-component/package.json13
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/ArrowFunction.jsx5
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/CloneElement.jsx6
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/ForgotImport.jsx3
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/GetSearch.jsx7
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/Goodbye.vue11
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/Hello.jsx5
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx7
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/LazyComponent.jsx9
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/PragmaComment.jsx5
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/PragmaCommentTypeScript.tsx5
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/PropsSpread.jsx5
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/Pure.jsx13
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/Research.jsx7
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/Suspense.jsx14
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/ThrowsAnError.jsx15
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/TypeScriptComponent.tsx5
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/WithChildren.jsx10
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/components/WithId.jsx6
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/pages/children.astro18
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/pages/error-rendering.astro11
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/pages/index.astro41
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/pages/pragma-comment.astro14
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/pages/suspense.astro17
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/skipped-pages/forgot-import.astro12
-rw-r--r--packages/integrations/react/test/fixtures/react-component/src/skipped-pages/window.astro8
-rw-r--r--packages/integrations/react/test/parsed-react-children.test.js16
-rw-r--r--packages/integrations/react/test/react-component.test.js183
-rw-r--r--packages/integrations/react/tsconfig.json7
-rw-r--r--packages/integrations/react/vnode-children.js29
46 files changed, 2238 insertions, 0 deletions
diff --git a/packages/integrations/react/CHANGELOG.md b/packages/integrations/react/CHANGELOG.md
new file mode 100644
index 000000000..71e09c120
--- /dev/null
+++ b/packages/integrations/react/CHANGELOG.md
@@ -0,0 +1,827 @@
+# @astrojs/react
+
+## 4.2.0
+
+### Minor Changes
+
+- [#13036](https://github.com/withastro/astro/pull/13036) [`3c90d8f`](https://github.com/withastro/astro/commit/3c90d8f3e0baba1463a9022c2e8c777204ad2250) Thanks [@artmsilva](https://github.com/artmsilva)! - Adds experimental support for disabling streaming
+
+ This is useful to support libraries that are not compatible with streaming such as some CSS-in-JS libraries. To disable streaming for all React components in your project, set `experimentalDisableStreaming: true` as a configuration option for `@astrojs/react`:
+
+ ```diff
+ // astro.config.mjs
+ import { defineConfig } from 'astro/config';
+ import react from '@astrojs/react';
+
+ export default defineConfig({
+ integrations: [
+ react({
+ + experimentalDisableStreaming: true,
+ }),
+ ],
+ });
+ ```
+
+## 4.1.6
+
+### Patch Changes
+
+- [#12996](https://github.com/withastro/astro/pull/12996) [`80c6801`](https://github.com/withastro/astro/commit/80c6801b4f2b9da44ed69d6da7e4dbd4d65aae69) Thanks [@bluwy](https://github.com/bluwy)! - Removes hardcoded `ssr.external: ['react-dom/server', 'react-dom/client']` config that causes issues with adapters that bundle all dependencies (e.g. Cloudflare). These externals should already be inferred by default by Vite when deploying to a server environment.
+
+- [#13011](https://github.com/withastro/astro/pull/13011) [`cf30880`](https://github.com/withastro/astro/commit/cf3088060d45227dcb48e041c4ed5e0081d71398) Thanks [@ascorbic](https://github.com/ascorbic)! - Upgrades Vite
+
+## 4.1.5
+
+### Patch Changes
+
+- [#12887](https://github.com/withastro/astro/pull/12887) [`ea603ae`](https://github.com/withastro/astro/commit/ea603aec80531205d38fed11c525b3faa0271903) Thanks [@louisescher](https://github.com/louisescher)! - Adds a warning message when multiple JSX-based UI frameworks are being used without either the `include` or `exclude` property being set on the integration.
+
+## 4.1.4
+
+### Patch Changes
+
+- [#12923](https://github.com/withastro/astro/pull/12923) [`c7642fb`](https://github.com/withastro/astro/commit/c7642fb80b2a2b4d1ec18369b37700ff28b4c041) Thanks [@bluwy](https://github.com/bluwy)! - Removes react-specific entrypoints in `optimizeDeps.include` and rely on `@vitejs/plugin-react` to add
+
+## 4.1.3
+
+### Patch Changes
+
+- [#12948](https://github.com/withastro/astro/pull/12948) [`51ab7b5`](https://github.com/withastro/astro/commit/51ab7b5722acecce722fb404ca6bc152a109c9e5) Thanks [@bluwy](https://github.com/bluwy)! - Supports checking for React 19 components
+
+## 4.1.2
+
+### Patch Changes
+
+- [#12799](https://github.com/withastro/astro/pull/12799) [`739dbfb`](https://github.com/withastro/astro/commit/739dbfba4214107cf8fc40c702834dad33eed3b0) Thanks [@ascorbic](https://github.com/ascorbic)! - Upgrades Vite to pin esbuild
+
+## 4.1.1
+
+### Patch Changes
+
+- [#12755](https://github.com/withastro/astro/pull/12755) [`391df0e`](https://github.com/withastro/astro/commit/391df0e410aecc4bb8871c42548e2d9634c5ef3a) Thanks [@matthewp](https://github.com/matthewp)! - Preoptimize React compiler runtime
+
+## 4.1.0
+
+### Minor Changes
+
+- [#12678](https://github.com/withastro/astro/pull/12678) [`97c9265`](https://github.com/withastro/astro/commit/97c9265754b78af12ad1e399cc75028435028dfa) Thanks [@bskimball](https://github.com/bskimball)! - Add React 19 stable to peer dependencies
+
+## 4.0.0
+
+### Major Changes
+
+- [#12524](https://github.com/withastro/astro/pull/12524) [`9f44019`](https://github.com/withastro/astro/commit/9f440196dc39f36fce0198bf4c97131160e5bcc1) Thanks [@bluwy](https://github.com/bluwy)! - Updates Vite dependency to v6 to match Astro v5
+
+### Minor Changes
+
+- [#12539](https://github.com/withastro/astro/pull/12539) [`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7) Thanks [@bluwy](https://github.com/bluwy)! - Drops node 21 support
+
+- [#12510](https://github.com/withastro/astro/pull/12510) [`14feaf3`](https://github.com/withastro/astro/commit/14feaf30e1a4266b8422865722a4478d39202404) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Changes the generated URL query param from `_astroAction` to `_action` when submitting a form using Actions. This avoids leaking the framework name into the URL bar, which may be considered a security issue.
+
+## 4.0.0-beta.2
+
+### Major Changes
+
+- [#12524](https://github.com/withastro/astro/pull/12524) [`9f44019`](https://github.com/withastro/astro/commit/9f440196dc39f36fce0198bf4c97131160e5bcc1) Thanks [@bluwy](https://github.com/bluwy)! - Updates Vite dependency to v6 to match Astro v5
+
+### Minor Changes
+
+- [#12539](https://github.com/withastro/astro/pull/12539) [`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7) Thanks [@bluwy](https://github.com/bluwy)! - Drops node 21 support
+
+## 3.7.0-beta.1
+
+### Minor Changes
+
+- [#12510](https://github.com/withastro/astro/pull/12510) [`14feaf3`](https://github.com/withastro/astro/commit/14feaf30e1a4266b8422865722a4478d39202404) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Changes the generated URL query param from `_astroAction` to `_action` when submitting a form using Actions. This avoids leaking the framework name into the URL bar, which may be considered a security issue.
+
+## 3.6.3
+
+### Patch Changes
+
+- [#12481](https://github.com/withastro/astro/pull/12481) [`8a46e80`](https://github.com/withastro/astro/commit/8a46e8074d6afb4a23badbd59ed239d526294e8c) Thanks [@marbrex](https://github.com/marbrex)! - Resolve `vite` peer dependency problem for strict package managers like **Yarn in PnP mode**.
+
+## 3.6.2
+
+### Patch Changes
+
+- [#11624](https://github.com/withastro/astro/pull/11624) [`7adb350`](https://github.com/withastro/astro/commit/7adb350a37f3975c8c9db89a32bf63b9fd0b78c2) Thanks [@bluwy](https://github.com/bluwy)! - Prevents throwing errors when checking if a component is a React component in runtime
+
+## 3.6.1
+
+### Patch Changes
+
+- [#11571](https://github.com/withastro/astro/pull/11571) [`1c3265a`](https://github.com/withastro/astro/commit/1c3265a8c9c0b1b1bd597f756b63463146bacc3a) Thanks [@bholmesdev](https://github.com/bholmesdev)! - **BREAKING CHANGE to the experimental Actions API only.** Install the latest `@astrojs/react` integration as well if you're using React 19 features.
+
+ Make `.safe()` the default return value for actions. This means `{ data, error }` will be returned when calling an action directly. If you prefer to get the data while allowing errors to throw, chain the `.orThrow()` modifier.
+
+ ```ts
+ import { actions } from 'astro:actions';
+
+ // Before
+ const { data, error } = await actions.like.safe();
+ // After
+ const { data, error } = await actions.like();
+
+ // Before
+ const newLikes = await actions.like();
+ // After
+ const newLikes = await actions.like.orThrow();
+ ```
+
+ ## Migration
+
+ To migrate your existing action calls:
+
+ - Remove `.safe` from existing _safe_ action calls
+ - Add `.orThrow` to existing _unsafe_ action calls
+
+- [#11570](https://github.com/withastro/astro/pull/11570) [`84189b6`](https://github.com/withastro/astro/commit/84189b6511dc2a14bcfe608696f56a64c2046f39) Thanks [@bholmesdev](https://github.com/bholmesdev)! - **BREAKING CHANGE to the experimental Actions API only.** Install the latest `@astrojs/react` integration as well if you're using React 19 features.
+
+ Updates the Astro Actions fallback to support `action={actions.name}` instead of using `getActionProps().` This will submit a form to the server in zero-JS scenarios using a search parameter:
+
+ ```astro
+ ---
+ import { actions } from 'astro:actions';
+ ---
+
+ <form action={actions.logOut}>
+ <!--output: action="?_astroAction=logOut"-->
+ <button>Log Out</button>
+ </form>
+ ```
+
+ You may also construct form action URLs using string concatenation, or by using the `URL()` constructor, with the an action's `.queryString` property:
+
+ ```astro
+ ---
+ import { actions } from 'astro:actions';
+
+ const confirmationUrl = new URL('/confirmation', Astro.url);
+ confirmationUrl.search = actions.queryString;
+ ---
+
+ <form method="POST" action={confirmationUrl.pathname}>
+ <button>Submit</button>
+ </form>
+ ```
+
+ ## Migration
+
+ `getActionProps()` is now deprecated. To use the new fallback pattern, remove the `getActionProps()` input from your form and pass your action function to the form `action` attribute:
+
+ ```diff
+ ---
+ import {
+ actions,
+ - getActionProps,
+ } from 'astro:actions';
+ ---
+
+ + <form method="POST" action={actions.logOut}>
+ - <form method="POST">
+ - <input {...getActionProps(actions.logOut)} />
+ <button>Log Out</button>
+ </form>
+ ```
+
+## 3.6.0
+
+### Minor Changes
+
+- [#11234](https://github.com/withastro/astro/pull/11234) [`4385bf7`](https://github.com/withastro/astro/commit/4385bf7a4dc9c65bff53a30c660f7a909fcabfc9) Thanks [@ematipico](https://github.com/ematipico)! - Adds a new function called `addServerRenderer` to the Container API. Use this function to manually store renderers inside the instance of your container.
+
+ This new function should be preferred when using the Container API in environments like on-demand pages:
+
+ ```ts
+ import type { APIRoute } from 'astro';
+ import { experimental_AstroContainer } from 'astro/container';
+ import reactRenderer from '@astrojs/react/server.js';
+ import vueRenderer from '@astrojs/vue/server.js';
+ import ReactComponent from '../components/button.jsx';
+ import VueComponent from '../components/button.vue';
+
+ // MDX runtime is contained inside the Astro core
+ import mdxRenderer from 'astro/jsx/server.js';
+
+ // In case you need to import a custom renderer
+ import customRenderer from '../renderers/customRenderer.js';
+
+ export const GET: APIRoute = async (ctx) => {
+ const container = await experimental_AstroContainer.create();
+ container.addServerRenderer({ renderer: reactRenderer });
+ container.addServerRenderer({ renderer: vueRenderer });
+ container.addServerRenderer({ renderer: customRenderer });
+ // You can pass a custom name too
+ container.addServerRenderer({
+ name: 'customRenderer',
+ renderer: customRenderer,
+ });
+ const vueComponent = await container.renderToString(VueComponent);
+ return await container.renderToResponse(Component);
+ };
+ ```
+
+## 3.5.0
+
+### Minor Changes
+
+- [#11144](https://github.com/withastro/astro/pull/11144) [`803dd80`](https://github.com/withastro/astro/commit/803dd8061df02138b4928442bcb76e77dcf6f5e7) Thanks [@ematipico](https://github.com/ematipico)! - The integration now exposes a function called `getContainerRenderer`, that can be used inside the Container APIs to load the relative renderer.
+
+ ```js
+ import { experimental_AstroContainer as AstroContainer } from 'astro/container';
+ import ReactWrapper from '../src/components/ReactWrapper.astro';
+ import { loadRenderers } from 'astro:container';
+ import { getContainerRenderer } from '@astrojs/react';
+
+ test('ReactWrapper with react renderer', async () => {
+ const renderers = await loadRenderers([getContainerRenderer()]);
+ const container = await AstroContainer.create({
+ renderers,
+ });
+ const result = await container.renderToString(ReactWrapper);
+
+ expect(result).toContain('Counter');
+ expect(result).toContain('Count: <!-- -->5');
+ });
+ ```
+
+## 3.4.0
+
+### Minor Changes
+
+- [#11071](https://github.com/withastro/astro/pull/11071) [`8ca7c73`](https://github.com/withastro/astro/commit/8ca7c731dea894e77f84b314ebe3a141d5daa918) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Adds two new functions `experimental_getActionState()` and `experimental_withState()` to support [the React 19 `useActionState()` hook](https://react.dev/reference/react/useActionState) when using Astro Actions. This introduces progressive enhancement when calling an Action with the `withState()` utility.
+
+ This example calls a `like` action that accepts a `postId` and returns the number of likes. Pass this action to the `experimental_withState()` function to apply progressive enhancement info, and apply to `useActionState()` to track the result:
+
+ ```tsx
+ import { actions } from 'astro:actions';
+ import { experimental_withState } from '@astrojs/react/actions';
+
+ export function Like({ postId }: { postId: string }) {
+ const [state, action, pending] = useActionState(
+ experimental_withState(actions.like),
+ 0, // initial likes
+ );
+
+ return (
+ <form action={action}>
+ <input type="hidden" name="postId" value={postId} />
+ <button disabled={pending}>{state} ❤️</button>
+ </form>
+ );
+ }
+ ```
+
+ You can also access the state stored by `useActionState()` from your action `handler`. Call `experimental_getActionState()` with the API context, and optionally apply a type to the result:
+
+ ```ts
+ import { defineAction, z } from 'astro:actions';
+ import { experimental_getActionState } from '@astrojs/react/actions';
+
+ export const server = {
+ like: defineAction({
+ input: z.object({
+ postId: z.string(),
+ }),
+ handler: async ({ postId }, ctx) => {
+ const currentLikes = experimental_getActionState<number>(ctx);
+ // write to database
+ return currentLikes + 1;
+ },
+ }),
+ };
+ ```
+
+## 3.3.4
+
+### Patch Changes
+
+- [#10986](https://github.com/withastro/astro/pull/10986) [`4d16381`](https://github.com/withastro/astro/commit/4d163811e1a25affb2c3d9adb3af650b7f1c91b6) Thanks [@emish89](https://github.com/emish89)! - Fixes incorrect `peerDependencies` for `@types/react` and `@types/react-dom`
+
+## 3.3.3
+
+### Patch Changes
+
+- [#10942](https://github.com/withastro/astro/pull/10942) [`d47baa4`](https://github.com/withastro/astro/commit/d47baa466aaeedde9c79ed5375d0be34762ac8b6) Thanks [@matthewp](https://github.com/matthewp)! - Updates package to support React 19 beta
+
+## 3.3.2
+
+### Patch Changes
+
+- [#10893](https://github.com/withastro/astro/pull/10893) [`fd7a9ed`](https://github.com/withastro/astro/commit/fd7a9ed3379a123f02f297b69fa5da0053e84a89) Thanks [@Angrigo](https://github.com/Angrigo)! - Removes using deprecated `ReactDOMServer.renderToStaticNodeStream` API
+
+## 3.3.1
+
+### Patch Changes
+
+- [#10855](https://github.com/withastro/astro/pull/10855) [`f6bddd3`](https://github.com/withastro/astro/commit/f6bddd3a155cd10a9f85c92d43b1af8b74786a42) Thanks [@lamATnginx](https://github.com/lamATnginx)! - Fix Redoc usage in React integration
+
+## 3.3.0
+
+### Minor Changes
+
+- [#10689](https://github.com/withastro/astro/pull/10689) [`683d51a5eecafbbfbfed3910a3f1fbf0b3531b99`](https://github.com/withastro/astro/commit/683d51a5eecafbbfbfed3910a3f1fbf0b3531b99) Thanks [@ematipico](https://github.com/ematipico)! - Deprecate support for versions of Node.js older than `v18.17.1` for Node.js 18, older than `v20.0.3` for Node.js 20, and the complete Node.js v19 release line.
+
+ This change is in line with Astro's [Node.js support policy](https://docs.astro.build/en/upgrade-astro/#support).
+
+## 3.2.0
+
+### Minor Changes
+
+- [#10675](https://github.com/withastro/astro/pull/10675) [`14f1d49a10541fecc4c10def8a094322442ccf23`](https://github.com/withastro/astro/commit/14f1d49a10541fecc4c10def8a094322442ccf23) Thanks [@fightingcat](https://github.com/fightingcat)! - Expose Babel config for @astro/react.
+
+## 3.1.1
+
+### Patch Changes
+
+- [#10654](https://github.com/withastro/astro/pull/10654) [`195f51f82a44df32be73865949aabee0d46ffe61`](https://github.com/withastro/astro/commit/195f51f82a44df32be73865949aabee0d46ffe61) Thanks [@matthewp](https://github.com/matthewp)! - Mark @material-tailwind/react as noExternal
+
+## 3.1.0
+
+### Minor Changes
+
+- [#10136](https://github.com/withastro/astro/pull/10136) [`9cd84bd19b92fb43ae48809f575ee12ebd43ea8f`](https://github.com/withastro/astro/commit/9cd84bd19b92fb43ae48809f575ee12ebd43ea8f) Thanks [@matthewp](https://github.com/matthewp)! - Changes the default behavior of `transition:persist` to update the props of persisted islands upon navigation. Also adds a new view transitions option `transition:persist-props` (default: `false`) to prevent props from updating as needed.
+
+ Islands which have the `transition:persist` property to keep their state when using the `<ViewTransitions />` router will now have their props updated upon navigation. This is useful in cases where the component relies on page-specific props, such as the current page title, which should update upon navigation.
+
+ For example, the component below is set to persist across navigation. This component receives a `products` props and might have some internal state, such as which filters are applied:
+
+ ```astro
+ <ProductListing transition:persist products={products} />
+ ```
+
+ Upon navigation, this component persists, but the desired `products` might change, for example if you are visiting a category of products, or you are performing a search.
+
+ Previously the props would not change on navigation, and your island would have to handle updating them externally, such as with API calls.
+
+ With this change the props are now updated, while still preserving state.
+
+ You can override this new default behavior on a per-component basis using `transition:persist-props=true` to persist both props and state during navigation:
+
+ ```astro
+ <ProductListing transition:persist-props="true" products={products} />
+ ```
+
+## 3.0.10
+
+### Patch Changes
+
+- [#9849](https://github.com/withastro/astro/pull/9849) [`20ca3154fb37049cbcd51b06d9fa2ef25ac25a36`](https://github.com/withastro/astro/commit/20ca3154fb37049cbcd51b06d9fa2ef25ac25a36) Thanks [@StandardGage](https://github.com/StandardGage)! - Fixes an issue where passing void elements (img, etc..) did not work with the `experimentalReactChildren` option enabled
+
+## 3.0.9
+
+### Patch Changes
+
+- [#9482](https://github.com/withastro/astro/pull/9482) [`72b26daf694b213918f02d0fcbf90ab5b7ebc31f`](https://github.com/withastro/astro/commit/72b26daf694b213918f02d0fcbf90ab5b7ebc31f) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improves compatibility with the [Qwik adapter](https://github.com/QwikDev/astro)
+
+- [#9479](https://github.com/withastro/astro/pull/9479) [`1baf0b0d3cbd0564954c2366a7278794fad6726e`](https://github.com/withastro/astro/commit/1baf0b0d3cbd0564954c2366a7278794fad6726e) Thanks [@sarah11918](https://github.com/sarah11918)! - Updates README
+
+## 3.0.8
+
+### Patch Changes
+
+- [#9403](https://github.com/withastro/astro/pull/9403) [`7eb9fe8a7`](https://github.com/withastro/astro/commit/7eb9fe8a717dd2b66b1d541e1aa4d3eb5d959ddf) Thanks [@knpwrs](https://github.com/knpwrs)! - Prevents unsupported `forwardRef` components created by Preact from being rendered by React
+
+- [#9452](https://github.com/withastro/astro/pull/9452) [`e83b5095f`](https://github.com/withastro/astro/commit/e83b5095f164f48ba40fc715a805fc66a3e39dcf) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Upgrades vite to latest
+
+## 3.0.7
+
+### Patch Changes
+
+- [#9122](https://github.com/withastro/astro/pull/9122) [`1c48ed286`](https://github.com/withastro/astro/commit/1c48ed286538ab9e354eca4e4dcd7c6385c96721) Thanks [@bluwy](https://github.com/bluwy)! - Adds Vite 5 support. There are no breaking changes from Astro. Check the [Vite migration guide](https://vite.dev/guide/migration.html) for details of the breaking changes from Vite instead.
+
+## 3.0.7-beta.0
+
+### Patch Changes
+
+- [#9122](https://github.com/withastro/astro/pull/9122) [`1c48ed286`](https://github.com/withastro/astro/commit/1c48ed286538ab9e354eca4e4dcd7c6385c96721) Thanks [@bluwy](https://github.com/bluwy)! - Adds Vite 5 support. There are no breaking changes from Astro. Check the [Vite migration guide](https://vite.dev/guide/migration.html) for details of the breaking changes from Vite instead.
+
+## 3.0.6
+
+### Patch Changes
+
+- [#9141](https://github.com/withastro/astro/pull/9141) [`af43fb517`](https://github.com/withastro/astro/commit/af43fb51726fa2242cec03cb019fa4fa4a4403ef) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes an issue where slotting self-closing elements (img, br, hr) into react components with `experimentalReactChildren` enabled led to an error.
+
+## 3.0.5
+
+### Patch Changes
+
+- [#8925](https://github.com/withastro/astro/pull/8925) [`ac5633b8f`](https://github.com/withastro/astro/commit/ac5633b8f615fe90ea419e00c5c771d00783a6e2) Thanks [@brandonsdebt](https://github.com/brandonsdebt)! - Uses `node:stream` during server rendering for compatibility with Cloudflare
+
+## 3.0.4
+
+### Patch Changes
+
+- [#8898](https://github.com/withastro/astro/pull/8898) [`4dee38711`](https://github.com/withastro/astro/commit/4dee38711cbf83efb5e12fbfa8e69e2495c49acf) Thanks [@matthewp](https://github.com/matthewp)! - Fixes client hydration in islands when using experimentalReactChildren
+
+## 3.0.3
+
+### Patch Changes
+
+- [#8737](https://github.com/withastro/astro/pull/8737) [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c) Thanks [@ematipico](https://github.com/ematipico)! - Add provenance statement when publishing the library from CI
+
+## 3.0.2
+
+### Patch Changes
+
+- [#8455](https://github.com/withastro/astro/pull/8455) [`85fe213fe`](https://github.com/withastro/astro/commit/85fe213fe0e8de3227ac80a41119800c374214f6) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Update `experimentalReactChildren` behavior to support void tags
+
+## 3.0.1
+
+### Patch Changes
+
+- [#8428](https://github.com/withastro/astro/pull/8428) [`67e834859`](https://github.com/withastro/astro/commit/67e83485949cf21de62831731111413abf57718c) Thanks [@matthewp](https://github.com/matthewp)! - Fix React dev mode using a base
+
+## 3.0.0
+
+### Major Changes
+
+- [#8188](https://github.com/withastro/astro/pull/8188) [`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312) Thanks [@ematipico](https://github.com/ematipico)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023.
+
+- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate
+
+- [#7924](https://github.com/withastro/astro/pull/7924) [`519a1c4e8`](https://github.com/withastro/astro/commit/519a1c4e8407c7abcb8d879b67a9f4b960652cae) Thanks [@matthewp](https://github.com/matthewp)! - Support for React Refresh
+
+ The React integration now fully supports React Refresh and is backed by `@vitejs/plugin-react`.
+
+ Also included in this change are new `include` and `exclude` config options. Use these if you want to use React alongside another JSX framework; include specifies files to be compiled for React and `exclude` does the opposite.
+
+### Patch Changes
+
+- [#8228](https://github.com/withastro/astro/pull/8228) [`4bd2fac8d`](https://github.com/withastro/astro/commit/4bd2fac8da4efb7c532d8920077df1f61d6e1953) Thanks [@bluwy](https://github.com/bluwy)! - Publish missing `vnode-children.js` file
+
+- [#8264](https://github.com/withastro/astro/pull/8264) [`1f58a7a1b`](https://github.com/withastro/astro/commit/1f58a7a1bea6888868b689dac94801d554319b02) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Automatically unmount islands when `astro:unmount` is fired
+
+- Updated dependencies [[`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312), [`2aa6d8ace`](https://github.com/withastro/astro/commit/2aa6d8ace398a41c2dec5473521d758816b08191), [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7)]:
+ - @astrojs/internal-helpers@0.2.0
+
+## 3.0.0-rc.6
+
+### Patch Changes
+
+- [#8264](https://github.com/withastro/astro/pull/8264) [`1f58a7a1b`](https://github.com/withastro/astro/commit/1f58a7a1bea6888868b689dac94801d554319b02) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Automatically unmount islands when `astro:unmount` is fired
+
+## 3.0.0-rc.5
+
+### Patch Changes
+
+- [#8228](https://github.com/withastro/astro/pull/8228) [`4bd2fac8d`](https://github.com/withastro/astro/commit/4bd2fac8da4efb7c532d8920077df1f61d6e1953) Thanks [@bluwy](https://github.com/bluwy)! - Publish missing `vnode-children.js` file
+
+## 3.0.0-rc.4
+
+### Major Changes
+
+- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate
+
+### Patch Changes
+
+- Updated dependencies [[`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7)]:
+ - @astrojs/internal-helpers@0.2.0-rc.2
+
+## 3.0.0-beta.3
+
+### Minor Changes
+
+- [#8082](https://github.com/withastro/astro/pull/8082) [`16a3fdf93`](https://github.com/withastro/astro/commit/16a3fdf93165a1a0404c1db0973871345b2c591b) Thanks [@matthewp](https://github.com/matthewp)! - Optionally parse React slots as React children.
+
+ This adds a new configuration option for the React integration `experimentalReactChildren`:
+
+ ```js
+ export default {
+ integrations: [
+ react({
+ experimentalReactChildren: true,
+ }),
+ ],
+ };
+ ```
+
+ With this enabled, children passed to React from Astro components via the default slot are parsed as React components.
+
+ This enables better compatibility with certain React components which manipulate their children.
+
+## 3.0.0-beta.2
+
+### Patch Changes
+
+- Updated dependencies [[`2aa6d8ace`](https://github.com/withastro/astro/commit/2aa6d8ace398a41c2dec5473521d758816b08191)]:
+ - @astrojs/internal-helpers@0.2.0-beta.1
+
+## 3.0.0-beta.1
+
+### Major Changes
+
+- [#7924](https://github.com/withastro/astro/pull/7924) [`519a1c4e8`](https://github.com/withastro/astro/commit/519a1c4e8407c7abcb8d879b67a9f4b960652cae) Thanks [@matthewp](https://github.com/matthewp)! - Support for React Refresh
+
+ The React integration now fully supports React Refresh and is backed by `@vitejs/plugin-react`.
+
+ Also included in this change are new `include` and `exclude` config options. Use these if you want to use React alongside another JSX framework; include specifies files to be compiled for React and `exclude` does the opposite.
+
+## 3.0.0-beta.0
+
+### Major Changes
+
+- [`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023.
+
+## 2.3.2
+
+### Patch Changes
+
+- [#8149](https://github.com/withastro/astro/pull/8149) [`531cc3e49`](https://github.com/withastro/astro/commit/531cc3e490bc3bc1b896eeaec05664571df5bb24) Thanks [@matthewp](https://github.com/matthewp)! - Fix missing package file regression
+
+## 2.3.1
+
+### Patch Changes
+
+- [#8137](https://github.com/withastro/astro/pull/8137) [`8c0a4ed10`](https://github.com/withastro/astro/commit/8c0a4ed106efeda286f0aae8b959008f9462b5ec) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Fix missing export for new `experimentalReactChildren` option
+
+## 2.3.0
+
+### Minor Changes
+
+- [#8082](https://github.com/withastro/astro/pull/8082) [`16a3fdf93`](https://github.com/withastro/astro/commit/16a3fdf93165a1a0404c1db0973871345b2c591b) Thanks [@matthewp](https://github.com/matthewp)! - Optionally parse React slots as React children.
+
+ This adds a new configuration option for the React integration `experimentalReactChildren`:
+
+ ```js
+ export default {
+ integrations: [
+ react({
+ experimentalReactChildren: true,
+ }),
+ ],
+ };
+ ```
+
+ With this enabled, children passed to React from Astro components via the default slot are parsed as React components.
+
+ This enables better compatibility with certain React components which manipulate their children.
+
+## 2.2.2
+
+### Patch Changes
+
+- [#8075](https://github.com/withastro/astro/pull/8075) [`da517d405`](https://github.com/withastro/astro/commit/da517d4055825ee1b630cd4a6983818d6120a7b7) Thanks [@SudoCat](https://github.com/SudoCat)! - fix a bug where react identifierPrefix was set to null for client:only components causing React.useId to generate ids prefixed with null
+
+## 2.2.1
+
+### Patch Changes
+
+- [#7196](https://github.com/withastro/astro/pull/7196) [`1c77779dd`](https://github.com/withastro/astro/commit/1c77779dd66a6db77c81ed235da076a6118decde) Thanks [@bluwy](https://github.com/bluwy)! - Fix `astro-static-slot` hydration mismatch error
+
+## 2.2.0
+
+### Minor Changes
+
+- [#7093](https://github.com/withastro/astro/pull/7093) [`3d525efc9`](https://github.com/withastro/astro/commit/3d525efc95cfb2deb5d9e04856d02965d66901c9) Thanks [@matthewp](https://github.com/matthewp)! - Prevent removal of nested slots within islands
+
+ This change introduces a new flag that renderers can add called `supportsAstroStaticSlot`. What this does is let Astro know that the render is sending `<astro-static-slot>` as placeholder values for static (non-hydrated) slots which Astro will then remove.
+
+ This change is completely backwards compatible, but fixes bugs caused by combining ssr-only and client-side framework components like so:
+
+ ```astro
+ <Component>
+ <div>
+ <Component client:load>
+ <span>Nested</span>
+ </Component>
+ </div>
+ </Component>
+ ```
+
+### Patch Changes
+
+- [#7104](https://github.com/withastro/astro/pull/7104) [`826e02890`](https://github.com/withastro/astro/commit/826e0289005f645b902375b98d5549c6a95ccafa) Thanks [@bluwy](https://github.com/bluwy)! - Specify `"files"` field to only publish necessary files
+
+## 2.1.3
+
+### Patch Changes
+
+- [#6976](https://github.com/withastro/astro/pull/6976) [`ca329bbca`](https://github.com/withastro/astro/commit/ca329bbcae7a6075af4f428f6f64466e9d152c8f) Thanks [@SudoCat](https://github.com/SudoCat)! - Prevent ID collisions in React.useId
+
+## 2.1.2
+
+### Patch Changes
+
+- [#6933](https://github.com/withastro/astro/pull/6933) [`649d70934`](https://github.com/withastro/astro/commit/649d70934e709bb1aa6e5e7583b12fa1703377cb) Thanks [@matthewp](https://github.com/matthewp)! - Automatically configure redoc
+
+## 2.1.1
+
+### Patch Changes
+
+- [#6698](https://github.com/withastro/astro/pull/6698) [`fc71c3f18`](https://github.com/withastro/astro/commit/fc71c3f18819ac3ad62809a7eeff5fe7840f2c4b) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Update React README to reference the [new React docs](https://react.dev)
+
+- [#6696](https://github.com/withastro/astro/pull/6696) [`239b9a2fb`](https://github.com/withastro/astro/commit/239b9a2fb864fa785e4150cd8aa833de72dd3517) Thanks [@matthewp](https://github.com/matthewp)! - Add use-immer as a noExternal module
+
+## 2.1.0
+
+### Minor Changes
+
+- [#6213](https://github.com/withastro/astro/pull/6213) [`afbbc4d5b`](https://github.com/withastro/astro/commit/afbbc4d5bfafc1779bac00b41c2a1cb1c90f2808) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updated compilation settings to disable downlevelling for Node 14
+
+## 2.0.2
+
+### Patch Changes
+
+- [#5478](https://github.com/withastro/astro/pull/5478) [`1c7eef308`](https://github.com/withastro/astro/commit/1c7eef308e808aa5ed4662b53e67ec8d1b814d1f) Thanks [@nemo0](https://github.com/nemo0)! - Update READMEs for consistency
+
+## 2.0.1
+
+### Patch Changes
+
+- [#5886](https://github.com/withastro/astro/pull/5886) [`9d4bfc76e`](https://github.com/withastro/astro/commit/9d4bfc76e8de7cf85997100145532a6fa7d2b025) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Support passing `children` as props to a React component
+
+## 2.0.0
+
+### Major Changes
+
+- [#5782](https://github.com/withastro/astro/pull/5782) [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 14. Minimum supported Node version is now >=16.12.0
+
+## 2.0.0-beta.0
+
+<details>
+<summary>See changes in 2.0.0-beta.0</summary>
+
+### Major Changes
+
+- [#5782](https://github.com/withastro/astro/pull/5782) [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 14. Minimum supported Node version is now >=16.12.0
+
+</details>
+
+## 1.2.2
+
+### Patch Changes
+
+- [#5218](https://github.com/withastro/astro/pull/5218) [`0b1241431`](https://github.com/withastro/astro/commit/0b12414315fa81ded96587779c63c74400466078) Thanks [@MoustaphaDev](https://github.com/MoustaphaDev)! - remove unnecessary `ReactDOM.renderToString` operation
+
+## 1.2.1
+
+### Patch Changes
+
+- [#5095](https://github.com/withastro/astro/pull/5095) [`ddfbef5ac`](https://github.com/withastro/astro/commit/ddfbef5acbd4c56d8ce1626a458b5cbb27da47fe) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add `@types/` packages as peerDependencies
+
+## 1.2.0
+
+### Minor Changes
+
+- [#5016](https://github.com/withastro/astro/pull/5016) [`6efeaeb39`](https://github.com/withastro/astro/commit/6efeaeb39ed7e6642b31603745750ccb9fe0ff1e) Thanks [@matthewp](https://github.com/matthewp)! - Add support for mui
+
+ This adds support for [mui](https://mui.com/) through configuration. Users will now not need to configure this library to get it to work.
+
+## 1.1.4
+
+### Patch Changes
+
+- [#4816](https://github.com/withastro/astro/pull/4816) [`8d059faae`](https://github.com/withastro/astro/commit/8d059faaedf212426e0fb6d93843f6855f723f56) Thanks [@matthewp](https://github.com/matthewp)! - Prevent errors in React components from crashing the dev server
+
+## 1.1.3
+
+### Patch Changes
+
+- [#4756](https://github.com/withastro/astro/pull/4756) [`c271ed35e`](https://github.com/withastro/astro/commit/c271ed35ee634f2f8c9957ee04a3aadc7dd39b3e) Thanks [@matthewp](https://github.com/matthewp)! - Only pass through children prop if there are children
+
+## 1.1.2
+
+### Patch Changes
+
+- [#4679](https://github.com/withastro/astro/pull/4679) [`5986517b4`](https://github.com/withastro/astro/commit/5986517b4f29af90fcfe333d4bb69ac09d4f8778) Thanks [@matthewp](https://github.com/matthewp)! - Prevent decoder from leaking
+
+- [#4667](https://github.com/withastro/astro/pull/4667) [`9290b2414`](https://github.com/withastro/astro/commit/9290b24143d753edd3daf25945990c25a58e5bde) Thanks [@Holben888](https://github.com/Holben888)! - Fix framework components on Vercel Edge
+
+## 1.1.1
+
+### Patch Changes
+
+- [#4527](https://github.com/withastro/astro/pull/4527) [`9adb7cca3`](https://github.com/withastro/astro/commit/9adb7cca33f669082d0daf750b97b1496ee79d2f) Thanks [@bluwy](https://github.com/bluwy)! - Add vite-ignore comment to suppress unknown import warnings
+
+## 1.1.0
+
+### Minor Changes
+
+- [#4478](https://github.com/withastro/astro/pull/4478) [`243525b15`](https://github.com/withastro/astro/commit/243525b1565385753ae1464c5def0d7de799f906) Thanks [@matthewp](https://github.com/matthewp)! - Uses startTransition on React roots
+
+ This prevents hydration from blocking the main thread when multiple islands are rendering at the same time.
+
+## 1.1.0-next.0
+
+### Minor Changes
+
+- [#4478](https://github.com/withastro/astro/pull/4478) [`243525b15`](https://github.com/withastro/astro/commit/243525b1565385753ae1464c5def0d7de799f906) Thanks [@matthewp](https://github.com/matthewp)! - Uses startTransition on React roots
+
+ This prevents hydration from blocking the main thread when multiple islands are rendering at the same time.
+
+## 1.0.0
+
+### Major Changes
+
+- [`04ad44563`](https://github.com/withastro/astro/commit/04ad445632c67bdd60c1704e1e0dcbcaa27b9308) - > Astro v1.0 is out! Read the [official announcement post](https://astro.build/blog/astro-1/).
+
+ **No breaking changes**. This package is now officially stable and compatible with `astro@1.0.0`!
+
+## 0.4.3
+
+### Patch Changes
+
+- [#4174](https://github.com/withastro/astro/pull/4174) [`8eb3a8c6d`](https://github.com/withastro/astro/commit/8eb3a8c6d9554707963c3a3bc36ed8b68d3cf0fb) Thanks [@matthewp](https://github.com/matthewp)! - Allows using React with automatic imports alongside MDX
+
+## 0.4.2
+
+### Patch Changes
+
+- [#3937](https://github.com/withastro/astro/pull/3937) [`31f9c0bf0`](https://github.com/withastro/astro/commit/31f9c0bf029ffa4b470e620f2c32e1370643e81e) Thanks [@delucis](https://github.com/delucis)! - Roll back supported Node engines
+
+## 0.4.1
+
+### Patch Changes
+
+- [#3928](https://github.com/withastro/astro/pull/3928) [`d6dfef0ca`](https://github.com/withastro/astro/commit/d6dfef0caa25f4effd0ed548d92ff48ce7a39ab2) Thanks [@matthewp](https://github.com/matthewp)! - Removes @babel/core peerDependency warning
+
+## 0.4.0
+
+### Minor Changes
+
+- [#3914](https://github.com/withastro/astro/pull/3914) [`b48767985`](https://github.com/withastro/astro/commit/b48767985359bd359df8071324952ea5f2bc0d86) Thanks [@ran-dall](https://github.com/ran-dall)! - Rollback supported `node@16` version. Minimum versions are now `node@14.20.0` or `node@16.14.0`.
+
+## 0.3.1
+
+### Patch Changes
+
+- [#3885](https://github.com/withastro/astro/pull/3885) [`bf5d1cc1e`](https://github.com/withastro/astro/commit/bf5d1cc1e71da38a14658c615e9481f2145cc6e7) Thanks [@delucis](https://github.com/delucis)! - Integration README fixes
+
+## 0.3.0
+
+### Minor Changes
+
+- [#3871](https://github.com/withastro/astro/pull/3871) [`1cc5b7890`](https://github.com/withastro/astro/commit/1cc5b78905633608e5b07ad291f916f54e67feb1) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Update supported `node` versions. Minimum versions are now `node@14.20.0` or `node@16.16.0`.
+
+## 0.2.1
+
+### Patch Changes
+
+- [#3854](https://github.com/withastro/astro/pull/3854) [`b012ee55`](https://github.com/withastro/astro/commit/b012ee55b107dea0730286263b27d83e530fad5d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - [astro add] Support adapters and third party packages
+
+## 0.2.0
+
+### Minor Changes
+
+- [#3652](https://github.com/withastro/astro/pull/3652) [`7373d61c`](https://github.com/withastro/astro/commit/7373d61cdcaedd64bf5fd60521b157cfa4343558) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Add support for passing named slots from `.astro` => framework components.
+
+ Each `slot` is be passed as a top-level prop. For example:
+
+ ```jsx
+ // From .astro
+ <Component>
+ <h2 slot="title">Hello world!</h2>
+ <h2 slot="slot-with-dash">Dash</h2>
+ <div>Default</div>
+ </Component>;
+
+ // For .jsx
+ export default function Component({ title, slotWithDash, children }) {
+ return (
+ <>
+ <div id="title">{title}</div>
+ <div id="slot-with-dash">{slotWithDash}</div>
+ <div id="main">{children}</div>
+ </>
+ );
+ }
+ ```
+
+## 0.1.3
+
+### Patch Changes
+
+- [#3455](https://github.com/withastro/astro/pull/3455) [`e9a77d86`](https://github.com/withastro/astro/commit/e9a77d861907adccfa75811f9aaa555f186d78f8) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Update client hydration to check for `ssr` attribute. Requires `astro@^1.0.0-beta.36`.
+
+## 0.1.2
+
+### Patch Changes
+
+- [#3337](https://github.com/withastro/astro/pull/3337) [`678c2b75`](https://github.com/withastro/astro/commit/678c2b7523c7f10cfdf2eb5a73aa2bbb7e5cbc07) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: remove hydration failures on React v18 by exposing the "client" directive from Astro core.
+
+## 0.1.1
+
+### Patch Changes
+
+- [#3160](https://github.com/withastro/astro/pull/3160) [`ae9ac5cb`](https://github.com/withastro/astro/commit/ae9ac5cbdceba0687d83d56d9d5f80479ab88710) Thanks [@matthewp](https://github.com/matthewp)! - Allows using React.lazy, Suspense in SSR and with hydration
+
+## 0.1.0
+
+### Minor Changes
+
+- [`e425f896`](https://github.com/withastro/astro/commit/e425f896b668d98033ad3b998b50c1f28bc7f6ee) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Add support for React v18
+
+## 0.0.2
+
+### Patch Changes
+
+- [#2885](https://github.com/withastro/astro/pull/2885) [`6b004363`](https://github.com/withastro/astro/commit/6b004363f99f27e581d1e2d53a2ebff39d7afb8a) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Add README across Astro built-in integrations
+
+* [#2847](https://github.com/withastro/astro/pull/2847) [`3b621f7a`](https://github.com/withastro/astro/commit/3b621f7a613b45983b090794fa7c015f23ed6140) Thanks [@tony-sull](https://github.com/tony-sull)! - Adds keywords to the official integrations to support discoverability on Astro's Integrations site
+
+## 0.0.2-next.0
+
+### Patch Changes
+
+- [#2847](https://github.com/withastro/astro/pull/2847) [`3b621f7a`](https://github.com/withastro/astro/commit/3b621f7a613b45983b090794fa7c015f23ed6140) Thanks [@tony-sull](https://github.com/tony-sull)! - Adds keywords to the official integrations to support discoverability on Astro's Integrations site
diff --git a/packages/integrations/react/README.md b/packages/integrations/react/README.md
new file mode 100644
index 000000000..06eadb78e
--- /dev/null
+++ b/packages/integrations/react/README.md
@@ -0,0 +1,38 @@
+# @astrojs/react ⚛️
+
+This **[Astro integration][astro-integration]** enables server-side rendering and client-side hydration for your [React](https://react.dev/) components.
+
+## Documentation
+
+Read the [`@astrojs/react` docs][docs]
+
+## Support
+
+- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more!
+
+- Check our [Astro Integration Documentation][astro-integration] for more on integrations.
+
+- Submit bug reports and feature requests as [GitHub issues][issues].
+
+## Contributing
+
+This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started:
+
+- [Contributor Manual][contributing]
+- [Code of Conduct][coc]
+- [Community Guide][community]
+
+## License
+
+MIT
+
+Copyright (c) 2023–present [Astro][astro]
+
+[astro]: https://astro.build/
+[docs]: https://docs.astro.build/en/guides/integrations-guide/react/
+[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md
+[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md
+[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md
+[discord]: https://astro.build/chat/
+[issues]: https://github.com/withastro/astro/issues
+[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
diff --git a/packages/integrations/react/client-v17.js b/packages/integrations/react/client-v17.js
new file mode 100644
index 000000000..bd17050ea
--- /dev/null
+++ b/packages/integrations/react/client-v17.js
@@ -0,0 +1,22 @@
+import { createElement } from 'react';
+import { hydrate, render, unmountComponentAtNode } from 'react-dom';
+import StaticHtml from './static-html.js';
+
+export default (element) =>
+ (Component, props, { default: children, ...slotted }, { client }) => {
+ for (const [key, value] of Object.entries(slotted)) {
+ props[key] = createElement(StaticHtml, { value, name: key });
+ }
+ const componentEl = createElement(
+ Component,
+ props,
+ children != null ? createElement(StaticHtml, { value: children }) : children,
+ );
+
+ const isHydrate = client !== 'only';
+ const bootstrap = isHydrate ? hydrate : render;
+ bootstrap(componentEl, element);
+ element.addEventListener('astro:unmount', () => unmountComponentAtNode(element), {
+ once: true,
+ });
+ };
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js
new file mode 100644
index 000000000..044eaf26f
--- /dev/null
+++ b/packages/integrations/react/client.js
@@ -0,0 +1,116 @@
+import { createElement, startTransition } from 'react';
+import { createRoot, hydrateRoot } from 'react-dom/client';
+import StaticHtml from './static-html.js';
+
+function isAlreadyHydrated(element) {
+ for (const key in element) {
+ if (key.startsWith('__reactContainer')) {
+ return key;
+ }
+ }
+}
+
+function createReactElementFromDOMElement(element) {
+ let attrs = {};
+ for (const attr of element.attributes) {
+ attrs[attr.name] = attr.value;
+ }
+ // If the element has no children, we can create a simple React element
+ if (element.firstChild === null) {
+ return createElement(element.localName, attrs);
+ }
+
+ return createElement(
+ element.localName,
+ attrs,
+ Array.from(element.childNodes)
+ .map((c) => {
+ if (c.nodeType === Node.TEXT_NODE) {
+ return c.data;
+ } else if (c.nodeType === Node.ELEMENT_NODE) {
+ return createReactElementFromDOMElement(c);
+ } else {
+ return undefined;
+ }
+ })
+ .filter((a) => !!a),
+ );
+}
+
+function getChildren(childString, experimentalReactChildren) {
+ if (experimentalReactChildren && childString) {
+ let children = [];
+ let template = document.createElement('template');
+ template.innerHTML = childString;
+ for (let child of template.content.children) {
+ children.push(createReactElementFromDOMElement(child));
+ }
+ return children;
+ } else if (childString) {
+ return createElement(StaticHtml, { value: childString });
+ } else {
+ return undefined;
+ }
+}
+
+// Keep a map of roots so we can reuse them on re-renders
+let rootMap = new WeakMap();
+const getOrCreateRoot = (element, creator) => {
+ let root = rootMap.get(element);
+ if (!root) {
+ root = creator();
+ rootMap.set(element, root);
+ }
+ return root;
+};
+
+export default (element) =>
+ (Component, props, { default: children, ...slotted }, { client }) => {
+ if (!element.hasAttribute('ssr')) return;
+
+ const actionKey = element.getAttribute('data-action-key');
+ const actionName = element.getAttribute('data-action-name');
+ const stringifiedActionResult = element.getAttribute('data-action-result');
+
+ const formState =
+ actionKey && actionName && stringifiedActionResult
+ ? [JSON.parse(stringifiedActionResult), actionKey, actionName]
+ : undefined;
+
+ const renderOptions = {
+ identifierPrefix: element.getAttribute('prefix'),
+ formState,
+ };
+ for (const [key, value] of Object.entries(slotted)) {
+ props[key] = createElement(StaticHtml, { value, name: key });
+ }
+
+ const componentEl = createElement(
+ Component,
+ props,
+ getChildren(children, element.hasAttribute('data-react-children')),
+ );
+ const rootKey = isAlreadyHydrated(element);
+ // HACK: delete internal react marker for nested components to suppress aggressive warnings
+ if (rootKey) {
+ delete element[rootKey];
+ }
+ if (client === 'only') {
+ return startTransition(() => {
+ const root = getOrCreateRoot(element, () => {
+ const r = createRoot(element);
+ element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
+ return r;
+ });
+ root.render(componentEl);
+ });
+ }
+ startTransition(() => {
+ const root = getOrCreateRoot(element, () => {
+ const r = hydrateRoot(element, componentEl, renderOptions);
+ element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
+ return r;
+ });
+ root.render(componentEl);
+ });
+ };
diff --git a/packages/integrations/react/context.js b/packages/integrations/react/context.js
new file mode 100644
index 000000000..2e3e37fd5
--- /dev/null
+++ b/packages/integrations/react/context.js
@@ -0,0 +1,24 @@
+const contexts = new WeakMap();
+
+const ID_PREFIX = 'r';
+
+function getContext(rendererContextResult) {
+ if (contexts.has(rendererContextResult)) {
+ return contexts.get(rendererContextResult);
+ }
+ const ctx = {
+ currentIndex: 0,
+ get id() {
+ return ID_PREFIX + this.currentIndex.toString();
+ },
+ };
+ contexts.set(rendererContextResult, ctx);
+ return ctx;
+}
+
+export function incrementId(rendererContextResult) {
+ const ctx = getContext(rendererContextResult);
+ const id = ctx.id;
+ ctx.currentIndex++;
+ return id;
+}
diff --git a/packages/integrations/react/jsx-runtime.js b/packages/integrations/react/jsx-runtime.js
new file mode 100644
index 000000000..d86f698b9
--- /dev/null
+++ b/packages/integrations/react/jsx-runtime.js
@@ -0,0 +1,8 @@
+// This module is a simple wrapper around react/jsx-runtime so that
+// it can run in Node ESM. 'react' doesn't declare this module as an export map
+// So we have to use the .js. The .js is not added via the babel automatic JSX transform
+// hence this module as a workaround.
+import jsxr from 'react/jsx-runtime.js';
+const { jsx, jsxs, Fragment } = jsxr;
+
+export { jsx, jsxs, Fragment };
diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json
new file mode 100644
index 000000000..aa2d83609
--- /dev/null
+++ b/packages/integrations/react/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "@astrojs/react",
+ "description": "Use React components within Astro",
+ "version": "4.2.0",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/react"
+ },
+ "keywords": [
+ "astro-integration",
+ "astro-component",
+ "renderer",
+ "react"
+ ],
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://docs.astro.build/en/guides/integrations-guide/react/",
+ "exports": {
+ ".": "./dist/index.js",
+ "./actions": "./dist/actions.js",
+ "./client.js": "./client.js",
+ "./client-v17.js": "./client-v17.js",
+ "./server.js": "./server.js",
+ "./server-v17.js": "./server-v17.js",
+ "./package.json": "./package.json",
+ "./jsx-runtime": "./jsx-runtime.js"
+ },
+ "files": [
+ "dist",
+ "client.js",
+ "client-v17.js",
+ "context.js",
+ "jsx-runtime.js",
+ "server.js",
+ "server.d.ts",
+ "server-v17.js",
+ "server-v17.d.ts",
+ "static-html.js",
+ "vnode-children.js"
+ ],
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "build:ci": "astro-scripts build \"src/**/*.ts\"",
+ "dev": "astro-scripts dev \"src/**/*.ts\"",
+ "test": "astro-scripts test \"test/**/*.test.js\""
+ },
+ "dependencies": {
+ "@vitejs/plugin-react": "^4.3.4",
+ "ultrahtml": "^1.5.3",
+ "vite": "^6.0.11"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*",
+ "cheerio": "1.0.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0",
+ "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0",
+ "react": "^17.0.2 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0"
+ },
+ "engines": {
+ "node": "^18.17.1 || ^20.3.0 || >=22.0.0"
+ },
+ "publishConfig": {
+ "provenance": true
+ }
+}
diff --git a/packages/integrations/react/server-v17.js b/packages/integrations/react/server-v17.js
new file mode 100644
index 000000000..4e577883a
--- /dev/null
+++ b/packages/integrations/react/server-v17.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import ReactDOM from 'react-dom/server.js';
+import StaticHtml from './static-html.js';
+
+const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
+const reactTypeof = Symbol.for('react.element');
+
+function check(Component, props, children) {
+ // Note: there are packages that do some unholy things to create "components".
+ // Checking the $$typeof property catches most of these patterns.
+ if (typeof Component === 'object') {
+ return Component['$$typeof']?.toString().slice('Symbol('.length).startsWith('react');
+ }
+ if (typeof Component !== 'function') return false;
+ if (Component.name === 'QwikComponent') return false;
+
+ if (Component.prototype != null && typeof Component.prototype.render === 'function') {
+ return React.Component.isPrototypeOf(Component) || React.PureComponent.isPrototypeOf(Component);
+ }
+
+ let isReactComponent = false;
+ function Tester(...args) {
+ try {
+ const vnode = Component(...args);
+ if (vnode && vnode['$$typeof'] === reactTypeof) {
+ isReactComponent = true;
+ }
+ } catch {}
+
+ return React.createElement('div');
+ }
+
+ renderToStaticMarkup(Tester, props, children, {});
+
+ return isReactComponent;
+}
+
+function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
+ delete props['class'];
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ const name = slotName(key);
+ slots[name] = React.createElement(StaticHtml, { value, name });
+ }
+ // Note: create newProps to avoid mutating `props` before they are serialized
+ const newProps = {
+ ...props,
+ ...slots,
+ };
+ const newChildren = children ?? props.children;
+ if (newChildren != null) {
+ newProps.children = React.createElement(StaticHtml, {
+ // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
+ hydrate: metadata.astroStaticSlot ? !!metadata.hydrate : true,
+ value: newChildren,
+ });
+ }
+ const vnode = React.createElement(Component, newProps);
+ let html;
+ if (metadata?.hydrate) {
+ html = ReactDOM.renderToString(vnode);
+ } else {
+ html = ReactDOM.renderToStaticMarkup(vnode);
+ }
+ return { html };
+}
+
+export default {
+ name: '@astrojs/react',
+ check,
+ renderToStaticMarkup,
+ supportsAstroStaticSlot: true,
+};
diff --git a/packages/integrations/react/server.d.ts b/packages/integrations/react/server.d.ts
new file mode 100644
index 000000000..75cc3eb64
--- /dev/null
+++ b/packages/integrations/react/server.d.ts
@@ -0,0 +1,4 @@
+import type { NamedSSRLoadedRendererValue } from 'astro';
+
+declare const renderer: NamedSSRLoadedRendererValue;
+export default renderer;
diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js
new file mode 100644
index 000000000..67d9e9386
--- /dev/null
+++ b/packages/integrations/react/server.js
@@ -0,0 +1,213 @@
+import opts from 'astro:react:opts';
+import React from 'react';
+import ReactDOM from 'react-dom/server';
+import { incrementId } from './context.js';
+import StaticHtml from './static-html.js';
+
+const slotName = (str) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
+const reactTypeof = Symbol.for('react.element');
+const reactTransitionalTypeof = Symbol.for('react.transitional.element');
+
+async function check(Component, props, children) {
+ // Note: there are packages that do some unholy things to create "components".
+ // Checking the $$typeof property catches most of these patterns.
+ if (typeof Component === 'object') {
+ return Component['$$typeof'].toString().slice('Symbol('.length).startsWith('react');
+ }
+ if (typeof Component !== 'function') return false;
+ if (Component.name === 'QwikComponent') return false;
+
+ // Preact forwarded-ref components can be functions, which React does not support
+ if (typeof Component === 'function' && Component['$$typeof'] === Symbol.for('react.forward_ref'))
+ return false;
+
+ if (Component.prototype != null && typeof Component.prototype.render === 'function') {
+ return React.Component.isPrototypeOf(Component) || React.PureComponent.isPrototypeOf(Component);
+ }
+
+ let isReactComponent = false;
+ function Tester(...args) {
+ try {
+ const vnode = Component(...args);
+ if (
+ vnode &&
+ (vnode['$$typeof'] === reactTypeof || vnode['$$typeof'] === reactTransitionalTypeof)
+ ) {
+ isReactComponent = true;
+ }
+ } catch {}
+
+ return React.createElement('div');
+ }
+
+ await renderToStaticMarkup(Tester, props, children, {});
+
+ return isReactComponent;
+}
+
+async function getNodeWritable() {
+ let nodeStreamBuiltinModuleName = 'node:stream';
+ let { Writable } = await import(/* @vite-ignore */ nodeStreamBuiltinModuleName);
+ return Writable;
+}
+
+function needsHydration(metadata) {
+ // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
+ return metadata.astroStaticSlot ? !!metadata.hydrate : true;
+}
+
+async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
+ let prefix;
+ if (this && this.result) {
+ prefix = incrementId(this.result);
+ }
+ const attrs = { prefix };
+
+ delete props['class'];
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ const name = slotName(key);
+ slots[name] = React.createElement(StaticHtml, {
+ hydrate: needsHydration(metadata),
+ value,
+ name,
+ });
+ }
+ // Note: create newProps to avoid mutating `props` before they are serialized
+ const newProps = {
+ ...props,
+ ...slots,
+ };
+ const newChildren = children ?? props.children;
+ if (children && opts.experimentalReactChildren) {
+ attrs['data-react-children'] = true;
+ const convert = await import('./vnode-children.js').then((mod) => mod.default);
+ newProps.children = convert(children);
+ } else if (newChildren != null) {
+ newProps.children = React.createElement(StaticHtml, {
+ hydrate: needsHydration(metadata),
+ value: newChildren,
+ });
+ }
+ const formState = this ? await getFormState(this) : undefined;
+ if (formState) {
+ attrs['data-action-result'] = JSON.stringify(formState[0]);
+ attrs['data-action-key'] = formState[1];
+ attrs['data-action-name'] = formState[2];
+ }
+ const vnode = React.createElement(Component, newProps);
+ const renderOptions = {
+ identifierPrefix: prefix,
+ formState,
+ };
+ let html;
+ if (opts.experimentalDisableStreaming) {
+ html = ReactDOM.renderToString(vnode);
+ } else if ('renderToReadableStream' in ReactDOM) {
+ html = await renderToReadableStreamAsync(vnode, renderOptions);
+ } else {
+ html = await renderToPipeableStreamAsync(vnode, renderOptions);
+ }
+ return { html, attrs };
+}
+
+/**
+ * @returns {Promise<[actionResult: any, actionKey: string, actionName: string] | undefined>}
+ */
+async function getFormState({ result }) {
+ const { request, actionResult } = result;
+
+ if (!actionResult) return undefined;
+ if (!isFormRequest(request.headers.get('content-type'))) return undefined;
+
+ const { searchParams } = new URL(request.url);
+ const formData = await request.clone().formData();
+ /**
+ * The key generated by React to identify each `useActionState()` call.
+ * @example "k511f74df5a35d32e7cf266450d85cb6c"
+ */
+ const actionKey = formData.get('$ACTION_KEY')?.toString();
+ /**
+ * The action name returned by an action's `toString()` property.
+ * This matches the endpoint path.
+ * @example "/_actions/blog.like"
+ */
+ const actionName = searchParams.get('_action');
+
+ if (!actionKey || !actionName) return undefined;
+
+ return [actionResult, actionKey, actionName];
+}
+
+async function renderToPipeableStreamAsync(vnode, options) {
+ const Writable = await getNodeWritable();
+ let html = '';
+ return new Promise((resolve, reject) => {
+ let error = undefined;
+ let stream = ReactDOM.renderToPipeableStream(vnode, {
+ ...options,
+ onError(err) {
+ error = err;
+ reject(error);
+ },
+ onAllReady() {
+ stream.pipe(
+ new Writable({
+ write(chunk, _encoding, callback) {
+ html += chunk.toString('utf-8');
+ callback();
+ },
+ destroy() {
+ resolve(html);
+ },
+ }),
+ );
+ },
+ });
+ });
+}
+
+/**
+ * Use a while loop instead of "for await" due to cloudflare and Vercel Edge issues
+ * See https://github.com/facebook/react/issues/24169
+ */
+async function readResult(stream) {
+ const reader = stream.getReader();
+ let result = '';
+ const decoder = new TextDecoder('utf-8');
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ if (value) {
+ result += decoder.decode(value);
+ } else {
+ // This closes the decoder
+ decoder.decode(new Uint8Array());
+ }
+
+ return result;
+ }
+ result += decoder.decode(value, { stream: true });
+ }
+}
+
+async function renderToReadableStreamAsync(vnode, options) {
+ return await readResult(await ReactDOM.renderToReadableStream(vnode, options));
+}
+
+const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
+
+function isFormRequest(contentType) {
+ // Split off parameters like charset or boundary
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#content-type_in_html_forms
+ const type = contentType?.split(';')[0].toLowerCase();
+
+ return formContentTypes.some((t) => type === t);
+}
+
+export default {
+ name: '@astrojs/react',
+ check,
+ renderToStaticMarkup,
+ supportsAstroStaticSlot: true,
+};
diff --git a/packages/integrations/react/server17.d.ts b/packages/integrations/react/server17.d.ts
new file mode 100644
index 000000000..bb2f29556
--- /dev/null
+++ b/packages/integrations/react/server17.d.ts
@@ -0,0 +1,2 @@
+import type { NamedSSRLoadedRendererValue } from 'astro';
+export default NamedSSRLoadedRendererValue;
diff --git a/packages/integrations/react/src/actions.ts b/packages/integrations/react/src/actions.ts
new file mode 100644
index 000000000..06ecd4148
--- /dev/null
+++ b/packages/integrations/react/src/actions.ts
@@ -0,0 +1,104 @@
+import { AstroError } from 'astro/errors';
+
+type FormFn<T> = (formData: FormData) => Promise<T>;
+
+/**
+ * Use an Astro Action with React `useActionState()`.
+ * This function matches your action to the expected types,
+ * and preserves metadata for progressive enhancement.
+ * To read state from your action handler, use {@linkcode experimental_getActionState}.
+ */
+export function experimental_withState<T>(action: FormFn<T>) {
+ // React expects two positional arguments when using `useActionState()`:
+ // 1. The initial state value.
+ // 2. The form data object.
+
+ // Map this first argument to a hidden input
+ // for retrieval from `getActionState()`.
+ const callback = async function (state: T, formData: FormData) {
+ formData.set('_astroActionState', JSON.stringify(state));
+ return action(formData);
+ };
+ if (!('$$FORM_ACTION' in action)) return callback;
+
+ // Re-bind progressive enhancement info for React.
+ callback.$$FORM_ACTION = action.$$FORM_ACTION;
+ // Called by React when form state is passed from the server.
+ // If the action names match, React returns this state from `useActionState()`.
+ callback.$$IS_SIGNATURE_EQUAL = (incomingActionName: string) => {
+ const actionName = new URLSearchParams(action.toString()).get('_action');
+ return actionName === incomingActionName;
+ };
+
+ // React calls `.bind()` internally to pass the initial state value.
+ // Calling `.bind()` seems to remove our `$$FORM_ACTION` metadata,
+ // so we need to define our *own* `.bind()` method to preserve that metadata.
+ Object.defineProperty(callback, 'bind', {
+ value: (...args: Parameters<typeof callback>) =>
+ injectStateIntoFormActionData(callback, ...args),
+ });
+ return callback;
+}
+
+/**
+ * Retrieve the state object from your action handler when using `useActionState()`.
+ * To ensure this state is retrievable, use the {@linkcode experimental_withState} helper.
+ */
+export async function experimental_getActionState<T>({
+ request,
+}: {
+ request: Request;
+}): Promise<T> {
+ const contentType = request.headers.get('Content-Type');
+ if (!contentType || !isFormRequest(contentType)) {
+ throw new AstroError(
+ '`getActionState()` must be called with a form request.',
+ "Ensure your action uses the `accept: 'form'` option.",
+ );
+ }
+ const formData = await request.clone().formData();
+ const state = formData.get('_astroActionState')?.toString();
+ if (!state) {
+ throw new AstroError(
+ '`getActionState()` could not find a state object.',
+ 'Ensure your action was passed to `useActionState()` with the `experimental_withState()` wrapper.',
+ );
+ }
+ return JSON.parse(state) as T;
+}
+
+const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
+
+function isFormRequest(contentType: string) {
+ // Split off parameters like charset or boundary
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#content-type_in_html_forms
+ const type = contentType.split(';')[0].toLowerCase();
+
+ return formContentTypes.some((t) => type === t);
+}
+
+/**
+ * Override the default `.bind()` method to:
+ * 1. Inject the form state into the form data for progressive enhancement.
+ * 2. Preserve the `$$FORM_ACTION` metadata.
+ */
+function injectStateIntoFormActionData<R extends [this: unknown, state: unknown, ...unknown[]]>(
+ fn: (...args: R) => unknown,
+ ...args: R
+) {
+ const boundFn = Function.prototype.bind.call(fn, ...args);
+ Object.assign(boundFn, fn);
+ const [, state] = args;
+
+ if ('$$FORM_ACTION' in fn && typeof fn.$$FORM_ACTION === 'function') {
+ const metadata = fn.$$FORM_ACTION();
+ boundFn.$$FORM_ACTION = () => {
+ const data = (metadata.data as FormData) ?? new FormData();
+ data.set('_astroActionState', JSON.stringify(state));
+ metadata.data = data;
+
+ return metadata;
+ };
+ }
+ return boundFn;
+}
diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts
new file mode 100644
index 000000000..9e781d203
--- /dev/null
+++ b/packages/integrations/react/src/index.ts
@@ -0,0 +1,153 @@
+import react, { type Options as ViteReactPluginOptions } from '@vitejs/plugin-react';
+import type { AstroIntegration, ContainerRenderer } from 'astro';
+import type * as vite from 'vite';
+import {
+ type ReactVersionConfig,
+ type SupportedReactVersion,
+ getReactMajorVersion,
+ isUnsupportedVersion,
+ versionsConfig,
+} from './version.js';
+
+export type ReactIntegrationOptions = Pick<
+ ViteReactPluginOptions,
+ 'include' | 'exclude' | 'babel'
+> & {
+ experimentalReactChildren?: boolean;
+ /**
+ * Disable streaming in React components
+ */
+ experimentalDisableStreaming?: boolean;
+};
+
+const FAST_REFRESH_PREAMBLE = react.preambleCode;
+
+function getRenderer(reactConfig: ReactVersionConfig) {
+ return {
+ name: '@astrojs/react',
+ clientEntrypoint: reactConfig.client,
+ serverEntrypoint: reactConfig.server,
+ };
+}
+
+function optionsPlugin({
+ experimentalReactChildren = false,
+ experimentalDisableStreaming = false,
+}: {
+ experimentalReactChildren: boolean;
+ experimentalDisableStreaming: boolean;
+}): vite.Plugin {
+ const virtualModule = 'astro:react:opts';
+ const virtualModuleId = '\0' + virtualModule;
+ return {
+ name: '@astrojs/react:opts',
+ resolveId(id) {
+ if (id === virtualModule) {
+ return virtualModuleId;
+ }
+ },
+ load(id) {
+ if (id === virtualModuleId) {
+ return {
+ code: `export default {
+ experimentalReactChildren: ${JSON.stringify(experimentalReactChildren)},
+ experimentalDisableStreaming: ${JSON.stringify(experimentalDisableStreaming)}
+ }`,
+ };
+ }
+ },
+ };
+}
+
+function getViteConfiguration(
+ {
+ include,
+ exclude,
+ babel,
+ experimentalReactChildren,
+ experimentalDisableStreaming,
+ }: ReactIntegrationOptions = {},
+ reactConfig: ReactVersionConfig,
+) {
+ return {
+ optimizeDeps: {
+ include: [reactConfig.client],
+ exclude: [reactConfig.server],
+ },
+ plugins: [
+ react({ include, exclude, babel }),
+ optionsPlugin({
+ experimentalReactChildren: !!experimentalReactChildren,
+ experimentalDisableStreaming: !!experimentalDisableStreaming,
+ }),
+ ],
+ ssr: {
+ noExternal: [
+ // These are all needed to get mui to work.
+ '@mui/material',
+ '@mui/base',
+ '@babel/runtime',
+ 'use-immer',
+ '@material-tailwind/react',
+ ],
+ },
+ };
+}
+
+export default function ({
+ include,
+ exclude,
+ babel,
+ experimentalReactChildren,
+ experimentalDisableStreaming,
+}: ReactIntegrationOptions = {}): AstroIntegration {
+ const majorVersion = getReactMajorVersion();
+ if (isUnsupportedVersion(majorVersion)) {
+ throw new Error(`Unsupported React version: ${majorVersion}.`);
+ }
+ const versionConfig = versionsConfig[majorVersion as SupportedReactVersion];
+
+ return {
+ name: '@astrojs/react',
+ hooks: {
+ 'astro:config:setup': ({ command, addRenderer, updateConfig, injectScript }) => {
+ addRenderer(getRenderer(versionConfig));
+ updateConfig({
+ vite: getViteConfiguration(
+ { include, exclude, babel, experimentalReactChildren, experimentalDisableStreaming },
+ versionConfig,
+ ),
+ });
+ if (command === 'dev') {
+ const preamble = FAST_REFRESH_PREAMBLE.replace(`__BASE__`, '/');
+ injectScript('before-hydration', preamble);
+ }
+ },
+ 'astro:config:done': ({ logger, config }) => {
+ const knownJsxRenderers = ['@astrojs/react', '@astrojs/preact', '@astrojs/solid-js'];
+ const enabledKnownJsxRenderers = config.integrations.filter((renderer) =>
+ knownJsxRenderers.includes(renderer.name),
+ );
+
+ if (enabledKnownJsxRenderers.length > 1 && !include && !exclude) {
+ logger.warn(
+ 'More than one JSX renderer is enabled. This will lead to unexpected behavior unless you set the `include` or `exclude` option. See https://docs.astro.build/en/guides/integrations-guide/react/#combining-multiple-jsx-frameworks for more information.',
+ );
+ }
+ },
+ },
+ };
+}
+
+export function getContainerRenderer(): ContainerRenderer {
+ const majorVersion = getReactMajorVersion();
+ if (isUnsupportedVersion(majorVersion)) {
+ throw new Error(`Unsupported React version: ${majorVersion}.`);
+ }
+ const versionConfig = versionsConfig[majorVersion as SupportedReactVersion];
+
+ return {
+ name: '@astrojs/react',
+ serverEntrypoint: versionConfig.server,
+ };
+}
diff --git a/packages/integrations/react/src/version.ts b/packages/integrations/react/src/version.ts
new file mode 100644
index 000000000..29118f13b
--- /dev/null
+++ b/packages/integrations/react/src/version.ts
@@ -0,0 +1,31 @@
+import { version as ReactVersion } from 'react-dom';
+
+export type SupportedReactVersion = keyof typeof versionsConfig;
+export type ReactVersionConfig = (typeof versionsConfig)[SupportedReactVersion];
+
+export function getReactMajorVersion(): number {
+ const matches = /\d+\./.exec(ReactVersion);
+ if (!matches) {
+ return NaN;
+ }
+ return Number(matches[0]);
+}
+
+export function isUnsupportedVersion(majorVersion: number) {
+ return majorVersion < 17 || majorVersion > 19 || Number.isNaN(majorVersion);
+}
+
+export const versionsConfig = {
+ 17: {
+ server: '@astrojs/react/server-v17.js',
+ client: '@astrojs/react/client-v17.js',
+ },
+ 18: {
+ server: '@astrojs/react/server.js',
+ client: '@astrojs/react/client.js',
+ },
+ 19: {
+ server: '@astrojs/react/server.js',
+ client: '@astrojs/react/client.js',
+ },
+};
diff --git a/packages/integrations/react/static-html.js b/packages/integrations/react/static-html.js
new file mode 100644
index 000000000..e319a40c7
--- /dev/null
+++ b/packages/integrations/react/static-html.js
@@ -0,0 +1,29 @@
+import { createElement as h } from 'react';
+
+/**
+ * Astro passes `children` as a string of HTML, so we need
+ * a wrapper `div` to render that content as VNodes.
+ *
+ * As a bonus, we can signal to React that this subtree is
+ * entirely static and will never change via `shouldComponentUpdate`.
+ */
+const StaticHtml = ({ value, name, hydrate = true }) => {
+ if (!value) return null;
+ const tagName = hydrate ? 'astro-slot' : 'astro-static-slot';
+ return h(tagName, {
+ name,
+ suppressHydrationWarning: true,
+ dangerouslySetInnerHTML: { __html: value },
+ });
+};
+
+/**
+ * This tells React to opt-out of re-rendering this subtree,
+ * In addition to being a performance optimization,
+ * this also allows other frameworks to attach to `children`.
+ *
+ * See https://preactjs.com/guide/v8/external-dom-mutations
+ */
+StaticHtml.shouldComponentUpdate = () => false;
+
+export default StaticHtml;
diff --git a/packages/integrations/react/test/fixtures/react-component/astro.config.mjs b/packages/integrations/react/test/fixtures/react-component/astro.config.mjs
new file mode 100644
index 000000000..0c6434f8a
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/astro.config.mjs
@@ -0,0 +1,10 @@
+import react from '@astrojs/react';
+import vue from '@astrojs/vue';
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [react({
+ experimentalReactChildren: true,
+ }), vue()],
+});
diff --git a/packages/integrations/react/test/fixtures/react-component/package.json b/packages/integrations/react/test/fixtures/react-component/package.json
new file mode 100644
index 000000000..c5376e3b3
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@test/react-component",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/react": "workspace:*",
+ "@astrojs/vue": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "vue": "^3.5.13"
+ }
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/ArrowFunction.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/ArrowFunction.jsx
new file mode 100644
index 000000000..16fac5bb6
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/ArrowFunction.jsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default () => {
+ return <div id="arrow-fn-component"></div>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/CloneElement.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/CloneElement.jsx
new file mode 100644
index 000000000..809ac4aa4
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/CloneElement.jsx
@@ -0,0 +1,6 @@
+import { cloneElement } from 'react';
+
+const ClonedWithProps = (element) => (props) =>
+ cloneElement(element, props);
+
+export default ClonedWithProps(<div id="cloned">Cloned With Props</div>);
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/ForgotImport.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/ForgotImport.jsx
new file mode 100644
index 000000000..9ee27faca
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/ForgotImport.jsx
@@ -0,0 +1,3 @@
+export default function ({}) {
+ return <h2>oops</h2>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/GetSearch.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/GetSearch.jsx
new file mode 100644
index 000000000..d3fee2f9a
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/GetSearch.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+function GetSearch() {
+ return (<div>{window.location.search}</div>);
+}
+
+export default GetSearch
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/Goodbye.vue b/packages/integrations/react/test/fixtures/react-component/src/components/Goodbye.vue
new file mode 100644
index 000000000..430dfdb71
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/Goodbye.vue
@@ -0,0 +1,11 @@
+<template>
+ <h2 id="vue-h2">Hasta la vista, {{ name }}</h2>
+</template>
+
+<script>
+export default {
+ props: {
+ name: String,
+ },
+};
+</script>
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/Hello.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/Hello.jsx
new file mode 100644
index 000000000..4c241162d
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/Hello.jsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default function ({ name, unused }) {
+ return <h2 id={`react-${name}`}>Hello {name}!</h2>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx
new file mode 100644
index 000000000..d6ff21dc3
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/ImportsThrowsAnError.jsx
@@ -0,0 +1,7 @@
+import ThrowsAnError from "./ThrowsAnError";
+
+export default function() {
+ return <>
+ <ThrowsAnError />
+ </>
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/LazyComponent.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/LazyComponent.jsx
new file mode 100644
index 000000000..b43aa36be
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/LazyComponent.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+export const LazyComponent = () => {
+ return (
+ <span id="lazy">inner content</span>
+ );
+};
+
+export default LazyComponent;
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/PragmaComment.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/PragmaComment.jsx
new file mode 100644
index 000000000..d8ea77810
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/PragmaComment.jsx
@@ -0,0 +1,5 @@
+/** @jsxImportSource react */
+
+export default function() {
+ return <div className="pragma-comment">Hello world</div>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/PragmaCommentTypeScript.tsx b/packages/integrations/react/test/fixtures/react-component/src/components/PragmaCommentTypeScript.tsx
new file mode 100644
index 000000000..9f2256fbf
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/PragmaCommentTypeScript.tsx
@@ -0,0 +1,5 @@
+/** @jsxImportSource react */
+
+export default function({}: object) {
+ return <div className="pragma-comment">Hello world</div>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/PropsSpread.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/PropsSpread.jsx
new file mode 100644
index 000000000..044c2a019
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/PropsSpread.jsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default (props) => {
+ return <div id="component-spread-props">{props.text}</div>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/Pure.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/Pure.jsx
new file mode 100644
index 000000000..6fae8613b
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/Pure.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+
+export default class StaticComponent extends React.PureComponent {
+
+ render() {
+ return (
+ <div id="pure">
+ <h1>Static component</h1>
+ </div>
+ )
+ }
+
+} \ No newline at end of file
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/Research.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/Research.jsx
new file mode 100644
index 000000000..9ab83e5f3
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/Research.jsx
@@ -0,0 +1,7 @@
+import * as React from 'react'
+
+export function Research2() {
+ const [value] = React.useState(1)
+
+ return <div id="research">foo bar {value}</div>
+} \ No newline at end of file
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/Suspense.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/Suspense.jsx
new file mode 100644
index 000000000..87dc82625
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/Suspense.jsx
@@ -0,0 +1,14 @@
+import React, { Suspense } from 'react';
+const LazyComponent = React.lazy(() => import('./LazyComponent.jsx'));
+
+export const ParentComponent = () => {
+ return (
+ <div id="outer">
+ <Suspense>
+ <LazyComponent />
+ </Suspense>
+ </div>
+ );
+};
+
+export default ParentComponent;
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/ThrowsAnError.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/ThrowsAnError.jsx
new file mode 100644
index 000000000..cf970e38c
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/ThrowsAnError.jsx
@@ -0,0 +1,15 @@
+import { useState } from 'react';
+
+export default function() {
+ let player = undefined;
+ // This is tested in dev mode, so make it work during the build to prevent
+ // breaking other tests.
+ if(import.meta.env.MODE === 'production') {
+ player = {};
+ }
+ const [] = useState(player.currentTime || null);
+
+ return (
+ <div>Should have thrown</div>
+ )
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/TypeScriptComponent.tsx b/packages/integrations/react/test/fixtures/react-component/src/components/TypeScriptComponent.tsx
new file mode 100644
index 000000000..bde96da84
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/TypeScriptComponent.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export default function({}) {
+ return <div className="ts-component">Hello world</div>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/WithChildren.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/WithChildren.jsx
new file mode 100644
index 000000000..a522bf95e
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/WithChildren.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+
+export default function ({ id, children }) {
+ return (
+ <div id={id}>
+ <div className="with-children">{children}</div>
+ <div className="with-children-count">{children.length}</div>
+ </div>
+ );
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/components/WithId.jsx b/packages/integrations/react/test/fixtures/react-component/src/components/WithId.jsx
new file mode 100644
index 000000000..0abe91c72
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/components/WithId.jsx
@@ -0,0 +1,6 @@
+import React from 'react';
+
+export default function () {
+ const id = React.useId();
+ return <p className='react-use-id' id={id}>{id}</p>;
+}
diff --git a/packages/integrations/react/test/fixtures/react-component/src/pages/children.astro b/packages/integrations/react/test/fixtures/react-component/src/pages/children.astro
new file mode 100644
index 000000000..3f83eafcb
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/pages/children.astro
@@ -0,0 +1,18 @@
+---
+import WithChildren from '../components/WithChildren';
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <WithChildren id="one">
+ <div>child 1</div><div>child 2</div>
+ </WithChildren>
+
+ <WithChildren id="two" client:load>
+ <div>child 1</div><div>child 2</div>
+ </WithChildren>
+ </body>
+</html>
diff --git a/packages/integrations/react/test/fixtures/react-component/src/pages/error-rendering.astro b/packages/integrations/react/test/fixtures/react-component/src/pages/error-rendering.astro
new file mode 100644
index 000000000..6984a6da5
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/pages/error-rendering.astro
@@ -0,0 +1,11 @@
+---
+import ImportsThrowsAnError from '../components/ImportsThrowsAnError';
+---
+<html>
+<head>
+ <title>Testing</title>
+</head>
+<body>
+ <ImportsThrowsAnError />
+</body>
+</html>
diff --git a/packages/integrations/react/test/fixtures/react-component/src/pages/index.astro b/packages/integrations/react/test/fixtures/react-component/src/pages/index.astro
new file mode 100644
index 000000000..b3b95c4b3
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/pages/index.astro
@@ -0,0 +1,41 @@
+---
+import ArrowFunction from '../components/ArrowFunction.jsx';
+import CloneElement from '../components/CloneElement';
+import Later from '../components/Goodbye.vue';
+import Hello from '../components/Hello.jsx';
+import PropsSpread from '../components/PropsSpread.jsx';
+import Pure from '../components/Pure.jsx';
+import {Research2} from '../components/Research.jsx';
+import TypeScriptComponent from '../components/TypeScriptComponent';
+import WithChildren from '../components/WithChildren';
+import WithId from '../components/WithId';
+
+const someProps = {
+ text: 'Hello world!',
+};
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <Hello name="static" />
+ <Hello name="load" client:load />
+ <!-- Test island deduplication, i.e. same UID as the component above. -->
+ <Hello name="load" client:load />
+ <!-- Test island deduplication account for non-render affecting props. -->
+ <Hello name="load" unused="" client:load />
+ <Later name="baby" />
+ <ArrowFunction />
+ <PropsSpread {...someProps}/>
+ <Research2 client:idle />
+ <TypeScriptComponent client:load />
+ <Pure />
+ <CloneElement />
+ <WithChildren client:load>test</WithChildren>
+ <WithChildren client:load children="test" />
+ <WithId client:idle />
+ <WithId client:idle />
+ </body>
+</html>
diff --git a/packages/integrations/react/test/fixtures/react-component/src/pages/pragma-comment.astro b/packages/integrations/react/test/fixtures/react-component/src/pages/pragma-comment.astro
new file mode 100644
index 000000000..b3ddba639
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/pages/pragma-comment.astro
@@ -0,0 +1,14 @@
+---
+import PragmaComponent from '../components/PragmaComment.jsx';
+import PragmaComponentTypeScript from '../components/PragmaCommentTypeScript.tsx';
+---
+
+<html>
+<head>
+ <title>React component works with Pragma comment</title>
+</head>
+<body>
+ <PragmaComponent client:load/>
+ <PragmaComponentTypeScript client:load/>
+</body>
+</html>
diff --git a/packages/integrations/react/test/fixtures/react-component/src/pages/suspense.astro b/packages/integrations/react/test/fixtures/react-component/src/pages/suspense.astro
new file mode 100644
index 000000000..5a9d15644
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/pages/suspense.astro
@@ -0,0 +1,17 @@
+---
+import Suspense from '../components/Suspense.jsx';
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <div id="client">
+ <Suspense client:load />
+ </div>
+ <div id="server">
+ <Suspense />
+ </div>
+ </body>
+</html>
diff --git a/packages/integrations/react/test/fixtures/react-component/src/skipped-pages/forgot-import.astro b/packages/integrations/react/test/fixtures/react-component/src/skipped-pages/forgot-import.astro
new file mode 100644
index 000000000..de5d319d9
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/skipped-pages/forgot-import.astro
@@ -0,0 +1,12 @@
+---
+import ForgotImport from '../components/ForgotImport.jsx';
+---
+
+<html>
+<head>
+ <title>Here we are</title>
+</head>
+<body>
+ <ForgotImport />
+</body>
+</html> \ No newline at end of file
diff --git a/packages/integrations/react/test/fixtures/react-component/src/skipped-pages/window.astro b/packages/integrations/react/test/fixtures/react-component/src/skipped-pages/window.astro
new file mode 100644
index 000000000..e780f3c44
--- /dev/null
+++ b/packages/integrations/react/test/fixtures/react-component/src/skipped-pages/window.astro
@@ -0,0 +1,8 @@
+---
+import GetSearch from '../components/GetSearch.jsx';
+---
+<html>
+<body>
+ <GetSearch />
+</body>
+</html>
diff --git a/packages/integrations/react/test/parsed-react-children.test.js b/packages/integrations/react/test/parsed-react-children.test.js
new file mode 100644
index 000000000..75604e5d3
--- /dev/null
+++ b/packages/integrations/react/test/parsed-react-children.test.js
@@ -0,0 +1,16 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import convert from '../vnode-children.js';
+
+describe('experimental react children', () => {
+ it('has undefined as children for direct children', () => {
+ const [imgVNode] = convert('<img src="abc"></img>');
+ assert.deepEqual(imgVNode.props.children, undefined);
+ });
+
+ it('has undefined as children for nested children', () => {
+ const [divVNode] = convert('<div><img src="xyz"></img></div>');
+ const [imgVNode] = divVNode.props.children;
+ assert.deepEqual(imgVNode.props.children, undefined);
+ });
+});
diff --git a/packages/integrations/react/test/react-component.test.js b/packages/integrations/react/test/react-component.test.js
new file mode 100644
index 000000000..fbe88d65f
--- /dev/null
+++ b/packages/integrations/react/test/react-component.test.js
@@ -0,0 +1,183 @@
+import assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import { load as cheerioLoad } from 'cheerio';
+import { isWindows, loadFixture } from '../../../astro/test/test-utils.js';
+
+let fixture;
+
+describe('React Components', () => {
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/react-component/', import.meta.url),
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('Can load React', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerioLoad(html);
+
+ // test 1: basic component renders
+ assert.equal($('#react-static').text(), 'Hello static!');
+
+ // test 2: no reactroot
+ assert.equal($('#react-static').attr('data-reactroot'), undefined);
+
+ // test 3: Can use function components
+ assert.equal($('#arrow-fn-component').length, 1);
+
+ // test 4: Can use spread for components
+ assert.equal($('#component-spread-props').length, 1);
+
+ // test 5: spread props renders
+ assert.equal($('#component-spread-props').text(), 'Hello world!');
+
+ // test 6: Can use TS components
+ assert.equal($('.ts-component').length, 1);
+
+ // test 7: Can use Pure components
+ assert.equal($('#pure').length, 1);
+
+ // test 8: Check number of islands
+ assert.equal($('astro-island[uid]').length, 9);
+
+ // test 9: Check island deduplication
+ const uniqueRootUIDs = new Set($('astro-island').map((_i, el) => $(el).attr('uid')));
+ assert.equal(uniqueRootUIDs.size, 8);
+
+ // test 10: Should properly render children passed as props
+ const islandsWithChildren = $('.with-children');
+ assert.equal(islandsWithChildren.length, 2);
+ assert.equal(
+ $(islandsWithChildren[0]).html(),
+ $(islandsWithChildren[1]).find('astro-slot').html(),
+ );
+
+ // test 11: Should generate unique React.useId per island
+ const islandsWithId = $('.react-use-id');
+ assert.equal(islandsWithId.length, 2);
+ assert.notEqual($(islandsWithId[0]).attr('id'), $(islandsWithId[1]).attr('id'));
+ });
+
+ it('Can load Vue', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerioLoad(html);
+ assert.equal($('#vue-h2').text(), 'Hasta la vista, baby');
+ });
+
+ it('Can use a pragma comment', async () => {
+ const html = await fixture.readFile('/pragma-comment/index.html');
+ const $ = cheerioLoad(html);
+
+ // test 1: rendered the PragmaComment component
+ assert.equal($('.pragma-comment').length, 2);
+ });
+
+ // TODO: is this still a relevant test?
+ it.skip('Includes reactroot on hydrating components', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerioLoad(html);
+
+ const div = $('#research');
+
+ // test 1: has the hydration attr
+ assert.ok(div.attr('data-reactroot'));
+
+ // test 2: renders correctly
+ assert.equal(div.html(), 'foo bar <!-- -->1');
+ });
+
+ it('Can load Suspense-using components', async () => {
+ const html = await fixture.readFile('/suspense/index.html');
+ const $ = cheerioLoad(html);
+ assert.equal($('#client #lazy').length, 1);
+ assert.equal($('#server #lazy').length, 1);
+ });
+
+ it('Can pass through props with cloneElement', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerioLoad(html);
+ assert.equal($('#cloned').text(), 'Cloned With Props');
+ });
+
+ it('Children are parsed as React components, can be manipulated', async () => {
+ const html = await fixture.readFile('/children/index.html');
+ const $ = cheerioLoad(html);
+ assert.equal($('#one .with-children-count').text(), '2');
+ });
+
+ it('Client children passes option to the client', async () => {
+ const html = await fixture.readFile('/children/index.html');
+ const $ = cheerioLoad(html);
+ assert.equal($('[data-react-children]').length, 1);
+ });
+ });
+
+ if (isWindows) return;
+
+ describe('dev', () => {
+ /** @type {import('../../../astro/test/test-utils.js').Fixture} */
+ let devServer;
+
+ before(async () => {
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('scripts proxy correctly', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerioLoad(html);
+
+ for (const script of $('script').toArray()) {
+ const { src } = script.attribs;
+ if (!src) continue;
+ assert.equal((await fixture.fetch(src)).status, 200, `404: ${src}`);
+ }
+ });
+
+ // TODO: move this to separate dev test?
+ it.skip('Throws helpful error message on window SSR', async () => {
+ const html = await fixture.fetch('/window/index.html');
+ assert.ok(
+ (await html.text()).includes(
+ `[/window]
+ The window object is not available during server-side rendering (SSR).
+ Try using \`import.meta.env.SSR\` to write SSR-friendly code.
+ https://docs.astro.build/reference/api-reference/#importmeta`,
+ ),
+ );
+ });
+
+ // In moving over to Vite, the jsx-runtime import is now obscured. TODO: update the method of finding this.
+ it.skip('uses the new JSX transform', async () => {
+ const html = await fixture.fetch('/index.html');
+
+ // Grab the imports
+ const exp = /import\("(.+?)"\)/g;
+ let match, componentUrl;
+ while ((match = exp.exec(html))) {
+ if (match[1].includes('Research.js')) {
+ componentUrl = match[1];
+ break;
+ }
+ }
+ const component = await fixture.readFile(componentUrl);
+ const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime'));
+
+ // test 1: react/jsx-runtime is used for the component
+ assert.ok(jsxRuntime);
+ });
+
+ it('When a nested component throws it does not crash the server', async () => {
+ const res = await fixture.fetch('/error-rendering');
+ await res.arrayBuffer();
+ });
+ });
+});
diff --git a/packages/integrations/react/tsconfig.json b/packages/integrations/react/tsconfig.json
new file mode 100644
index 000000000..1504b4b6d
--- /dev/null
+++ b/packages/integrations/react/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "outDir": "./dist"
+ }
+}
diff --git a/packages/integrations/react/vnode-children.js b/packages/integrations/react/vnode-children.js
new file mode 100644
index 000000000..e0751c95a
--- /dev/null
+++ b/packages/integrations/react/vnode-children.js
@@ -0,0 +1,29 @@
+import { Fragment, createElement } from 'react';
+import { DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE, parse } from 'ultrahtml';
+
+let ids = 0;
+export default function convert(children) {
+ let doc = parse(children.toString().trim());
+ let id = ids++;
+ let key = 0;
+
+ function createReactElementFromNode(node) {
+ const childVnodes =
+ Array.isArray(node.children) && node.children.length
+ ? node.children.map((child) => createReactElementFromNode(child)).filter(Boolean)
+ : undefined;
+
+ if (node.type === DOCUMENT_NODE) {
+ return createElement(Fragment, {}, childVnodes);
+ } else if (node.type === ELEMENT_NODE) {
+ const { class: className, ...props } = node.attributes;
+ return createElement(node.name, { ...props, className, key: `${id}-${key++}` }, childVnodes);
+ } else if (node.type === TEXT_NODE) {
+ // 0-length text gets omitted in JSX
+ return node.value.trim() ? node.value : undefined;
+ }
+ }
+
+ const root = createReactElementFromNode(doc);
+ return root.props.children;
+}