├── src ├── server.js ├── tests │ ├── fixtures │ │ ├── CaveatExample.svelte │ │ ├── NoScript.svelte │ │ ├── Element.svelte │ │ ├── Typescript.svelte │ │ ├── Slots.svelte │ │ ├── TrailingComma.svelte │ │ ├── Binding.svelte │ │ ├── Container.svelte │ │ ├── Forwarding.svelte │ │ ├── RestProps.svelte │ │ ├── Provider.svelte │ │ ├── Clicker.tsx │ │ ├── List.svelte │ │ ├── SlottedText.svelte │ │ ├── Children.svelte │ │ ├── Dog.svelte │ │ ├── Multiple.svelte │ │ ├── List.tsx │ │ └── Blocks.svelte │ ├── react-router │ │ ├── TestRouter.tsx │ │ ├── locationToUrl.spec.ts │ │ ├── NavLink.spec.tsx │ │ └── Link.spec.tsx │ ├── reactify-tsx.spec.tsx │ ├── types.spec.ts │ ├── reactify.spec.ts │ ├── preprocess.spec.ts │ └── __snapshots__ │ │ └── preprocess.spec.ts.snap ├── global.d.ts ├── routes │ ├── intrinsic-elements │ │ └── +page.svelte │ ├── youtube │ │ ├── svelte │ │ │ ├── +page.svelte │ │ │ └── YouTubeWrapper.tsx │ │ └── react │ │ │ └── +page.svelte │ ├── +layout.svelte │ ├── input │ │ └── +page.svelte │ ├── signals │ │ ├── useUsername.svelte.ts │ │ └── +page.svelte │ ├── lazy │ │ └── +page.svelte │ ├── mini │ │ └── +page.svelte │ ├── react-router │ │ ├── [slug] │ │ │ └── +page.svelte │ │ ├── +layout.svelte │ │ └── Menu.tsx │ ├── playwright │ │ ├── StatefulClicker.svelte │ │ └── +page.svelte │ ├── broken │ │ └── +page.svelte │ ├── hooks │ │ ├── HookWithContext.svelte │ │ ├── react-auth.ts │ │ └── +page.svelte │ ├── render-prop │ │ ├── Search.tsx │ │ └── +page.svelte │ ├── context-svelte │ │ └── +page.svelte │ ├── fixtures │ │ ├── [fixture] │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ │ └── +page.svelte │ ├── suspense │ │ └── +page.svelte │ ├── typesafe │ │ └── +page.svelte │ ├── context-react │ │ └── +page.svelte │ ├── dynamic │ │ └── +page.svelte │ ├── preprocessor │ │ └── +page.svelte │ ├── sveltify-react │ │ └── +page.svelte │ ├── context-react-nested │ │ └── +page.svelte │ ├── +page.svelte │ └── react-ssr │ │ └── +server.ts ├── lib │ ├── react-router │ │ ├── useParams.ts │ │ ├── types.ts │ │ ├── internal │ │ │ ├── RouterContext.ts │ │ │ ├── useRouterContext.ts │ │ │ └── locationToUrl.ts │ │ ├── useLocation.ts │ │ ├── index.ts │ │ ├── useHistory.ts │ │ ├── Link.ts │ │ └── NavLink.ts │ ├── internal │ │ ├── ExtractContexts.svelte │ │ ├── SnippetComponent.svelte │ │ ├── ReactContext.ts │ │ ├── renderToStringAsync.ts │ │ ├── SveltifiedUniversal.svelte │ │ ├── ReactifiedSSR.svelte │ │ ├── SvelteContext.ts │ │ ├── reactifySSR.ts │ │ ├── ReactifiedCSR.svelte │ │ ├── types.ts │ │ ├── SveltifiedCSR.svelte │ │ └── SveltifiedSSR.svelte │ ├── index.ts │ ├── global.d.ts │ ├── useStore.ts │ ├── useSignals.svelte.ts │ ├── hooks.svelte.ts │ ├── sveltify.ts │ ├── reactify.ts │ └── preprocessReact.js ├── demo │ ├── components │ │ ├── DebugContextProvider.svelte │ │ ├── DebugContext.svelte │ │ └── Button.svelte │ └── react-components │ │ ├── Alert.tsx │ │ ├── Alert.module.css │ │ ├── Counter.tsx │ │ └── App.tsx └── app.html ├── .browserslistrc ├── .husky ├── pre-push └── pre-commit ├── dev-preprocess.sh ├── .prettierrc ├── .prettierignore ├── postcss.config.cjs ├── static ├── favicon.ico ├── react-spa.html └── svelte-preprocess-react.svg ├── CHANGELOG.md ├── playwright └── tests │ ├── ssr.test.ts-snapshots │ ├── ssr-client-rendered-1-darwin.png │ ├── ssr-client-rendered-10-darwin.png │ ├── ssr-client-rendered-11-darwin.png │ ├── ssr-client-rendered-12-darwin.png │ ├── ssr-client-rendered-13-darwin.png │ ├── ssr-client-rendered-14-darwin.png │ ├── ssr-client-rendered-2-darwin.png │ ├── ssr-client-rendered-3-darwin.png │ ├── ssr-client-rendered-4-darwin.png │ ├── ssr-client-rendered-5-darwin.png │ ├── ssr-client-rendered-6-darwin.png │ ├── ssr-client-rendered-7-darwin.png │ ├── ssr-client-rendered-8-darwin.png │ ├── ssr-client-rendered-9-darwin.png │ ├── ssr-no-js-server-rendered-1-darwin.png │ ├── ssr-no-js-server-rendered-10-darwin.png │ ├── ssr-no-js-server-rendered-11-darwin.png │ ├── ssr-no-js-server-rendered-12-darwin.png │ ├── ssr-no-js-server-rendered-13-darwin.png │ ├── ssr-no-js-server-rendered-14-darwin.png │ ├── ssr-no-js-server-rendered-2-darwin.png │ ├── ssr-no-js-server-rendered-3-darwin.png │ ├── ssr-no-js-server-rendered-4-darwin.png │ ├── ssr-no-js-server-rendered-5-darwin.png │ ├── ssr-no-js-server-rendered-6-darwin.png │ ├── ssr-no-js-server-rendered-7-darwin.png │ ├── ssr-no-js-server-rendered-8-darwin.png │ └── ssr-no-js-server-rendered-9-darwin.png │ ├── react-first.test.ts-snapshots │ ├── react-first-render-reactified-Svelte-component-inside-a-React-SPA-1-darwin.png │ ├── react-first-render-reactified-Svelte-component-inside-a-React-SPA-2-darwin.png │ ├── react-first-render-reactified-Svelte-component-inside-a-React-SPA-3-darwin.png │ ├── react-first-no-js-Server-render-reactified-Svelte-component-inside-React-server-1-darwin.png │ ├── react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-1-darwin.png │ ├── react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-2-darwin.png │ └── react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-3-darwin.png │ ├── useSignals.spec.ts │ ├── hooks.spec.ts │ ├── sveltify.spec.ts │ ├── react-first.test.ts │ └── ssr.test.ts ├── .gitignore ├── docs ├── utilities.md ├── migration-to-3.0.md ├── useStore.md ├── hooks.md ├── reactify.md ├── migration-to-2.0.md ├── react-router.md ├── architecture.md └── caveats.md ├── tsconfig.eslint.json ├── vite.config.ts ├── playwright.config.ts ├── tsconfig.json ├── svelte.config.js ├── LICENCE ├── eslint.config.js ├── package.json └── README.md /src/server.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run test 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /dev-preprocess.sh: -------------------------------------------------------------------------------- 1 | "preprocessor":"echo '' |" 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-svelte"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.svelte-kit 2 | /build 3 | /dist 4 | /node_modules 5 | /pnpm-lock.yaml -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /src/tests/fixtures/CaveatExample.svelte: -------------------------------------------------------------------------------- 1 | 2 | Content 3 | 4 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Visit https://github.com/bfanger/svelte-preprocess-react/releases for the release notes. 2 | -------------------------------------------------------------------------------- /src/tests/fixtures/NoScript.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/tests/fixtures/Element.svelte: -------------------------------------------------------------------------------- 1 | console.info("clicked")}> 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/tests/fixtures/Typescript.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

{title}

6 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /src/routes/intrinsic-elements/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | Heading 3 | 4 | Subheading 5 | -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-1-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-10-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-10-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-11-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-11-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-12-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-12-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-13-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-13-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-14-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-14-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-2-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-2-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-3-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-3-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-4-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-4-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-5-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-5-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-6-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-6-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-7-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-7-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-8-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-8-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-9-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-9-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-1-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-10-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-10-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-11-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-11-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-12-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-12-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-13-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-13-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-14-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-14-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-2-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-2-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-3-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-3-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-4-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-4-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-5-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-5-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-6-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-6-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-7-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-7-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-8-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-8-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-9-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-9-darwin.png -------------------------------------------------------------------------------- /src/lib/react-router/useParams.ts: -------------------------------------------------------------------------------- 1 | import useRouterContext from "./internal/useRouterContext.js"; 2 | 3 | export default function useParams() { 4 | const { params } = useRouterContext(); 5 | return params; 6 | } 7 | -------------------------------------------------------------------------------- /src/tests/fixtures/Slots.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | A simple primary alert. Check it out! 8 | -------------------------------------------------------------------------------- /src/routes/youtube/svelte/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {@render children()} 11 | -------------------------------------------------------------------------------- /src/routes/input/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | {value} 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /.svelte-kit 4 | /build 5 | /dist 6 | /functions 7 | /storybook-static 8 | /test-results 9 | .env 10 | /test-results/ 11 | /playwright-report/ 12 | /playwright/.cache/ 13 | /vite.config.ts.timestamp-*.mjs 14 | /.vscode/ -------------------------------------------------------------------------------- /src/routes/youtube/react/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /src/tests/fixtures/TrailingComma.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Title 9 | Description 10 | 11 | -------------------------------------------------------------------------------- /src/routes/youtube/svelte/YouTubeWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { reactify } from "svelte-preprocess-react"; 2 | import { Youtube } from "svelte-youtube-lite"; 3 | 4 | const svelte = reactify({ Youtube }); 5 | const YoutubeWrapper = svelte.Youtube; 6 | export default YoutubeWrapper; 7 | -------------------------------------------------------------------------------- /src/tests/fixtures/Binding.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | console.info(update)} /> 10 | -------------------------------------------------------------------------------- /src/lib/react-router/types.ts: -------------------------------------------------------------------------------- 1 | export type Location = { 2 | pathname: string; 3 | search: string; 4 | hash: string; 5 | }; 6 | 7 | export type Params = Record; 8 | 9 | export type RouteCondition = { isActive: boolean }; 10 | 11 | export type To = string | Location; 12 | -------------------------------------------------------------------------------- /src/routes/signals/useUsername.svelte.ts: -------------------------------------------------------------------------------- 1 | import { useSignals } from "svelte-preprocess-react"; 2 | 3 | export const user = new (class { 4 | name = $state("John"); 5 | })(); 6 | 7 | export default function useUsername() { 8 | useSignals(() => user.name); 9 | return user.name; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/internal/ExtractContexts.svelte: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts-snapshots/react-first-render-reactified-Svelte-component-inside-a-React-SPA-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/react-first.test.ts-snapshots/react-first-render-reactified-Svelte-component-inside-a-React-SPA-1-darwin.png -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts-snapshots/react-first-render-reactified-Svelte-component-inside-a-React-SPA-2-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/react-first.test.ts-snapshots/react-first-render-reactified-Svelte-component-inside-a-React-SPA-2-darwin.png -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts-snapshots/react-first-render-reactified-Svelte-component-inside-a-React-SPA-3-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/react-first.test.ts-snapshots/react-first-render-reactified-Svelte-component-inside-a-React-SPA-3-darwin.png -------------------------------------------------------------------------------- /src/tests/fixtures/Container.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | { 12 | count = next; 13 | }} 14 | > 15 | -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts-snapshots/react-first-no-js-Server-render-reactified-Svelte-component-inside-React-server-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/react-first.test.ts-snapshots/react-first-no-js-Server-render-reactified-Svelte-component-inside-React-server-1-darwin.png -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts-snapshots/react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/react-first.test.ts-snapshots/react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-1-darwin.png -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts-snapshots/react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-2-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/react-first.test.ts-snapshots/react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-2-darwin.png -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts-snapshots/react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-3-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/HEAD/playwright/tests/react-first.test.ts-snapshots/react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-3-darwin.png -------------------------------------------------------------------------------- /src/routes/lazy/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if browser} 10 | 11 | {/if} 12 | -------------------------------------------------------------------------------- /src/routes/mini/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export { default as hooks } from "./hooks.svelte.js"; 4 | export { default as reactify } from "./reactify.js"; 5 | export { default as sveltify } from "./sveltify.js"; 6 | export { default as useStore } from "./useStore.js"; 7 | export { default as useSignals } from "./useSignals.svelte.js"; 8 | -------------------------------------------------------------------------------- /src/routes/react-router/[slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

You're on the {$page.params.slug} page.

9 | 10 | Back to home 11 | -------------------------------------------------------------------------------- /src/lib/internal/SnippetComponent.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {@render snippet()} 14 | -------------------------------------------------------------------------------- /src/tests/fixtures/Forwarding.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/demo/components/DebugContextProvider.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {@render children?.()} 15 | -------------------------------------------------------------------------------- /docs/utilities.md: -------------------------------------------------------------------------------- 1 | # Utilities 2 | 3 | svelte-preprocess-react comes with utilities to make using Svelte primitives inside React components easier and vice versa. 4 | 5 | - [reactify](./reactify.md) - Create a React component from a Svelte component 6 | - [useStore](./useStore.md) - Use a Svelte store in a React component 7 | - [hooks](./hooks.md) - Use React hooks in a Svelte component 8 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | %sveltekit.body% 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/tests/fixtures/RestProps.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Hi 12 | 13 | -------------------------------------------------------------------------------- /src/tests/fixtures/Provider.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | content 13 | -------------------------------------------------------------------------------- /src/routes/playwright/StatefulClicker.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/routes/broken/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/tests/fixtures/Clicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Props = { 4 | count: number; 5 | onCount: (count: number) => void; 6 | }; 7 | const Clicker: React.FC = ({ count, onCount }) => ( 8 |
9 |

You clicked {count} times

10 | 11 |
12 | ); 13 | 14 | export default Clicker; 15 | -------------------------------------------------------------------------------- /src/lib/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "svelte"; 2 | import type { 3 | IntrinsicElementComponents, 4 | StaticPropComponents, 5 | } from "./internal/types.js"; 6 | import type SveltifyType from "./sveltify.js"; 7 | 8 | declare global { 9 | const sveltify: typeof SveltifyType; 10 | 11 | const react: IntrinsicElementComponents & 12 | Record; 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/hooks/HookWithContext.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if auth?.authenticated} 9 |
Authenticated
10 | {:else} 11 |
Not authenticated
12 | {/if} 13 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.d.ts", 5 | "src/**/*.svelte", 6 | // Additional extensions for eslint 7 | "**/*.svelte", 8 | "**/*.ts", 9 | "**/*.tsx", 10 | "**/*.spec.tsx", 11 | "**/*.cjs", 12 | ".*/**/*.cjs", 13 | ".*.cjs", 14 | "**/*.js", 15 | ".*.js" 16 | ], 17 | "exclude": ["build", "package"] 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/react-router/internal/RouterContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Params } from "../types"; 3 | 4 | export type RouterContextType = { 5 | url: URL; 6 | params: Params; 7 | goto(url: string, opts?: { replaceState?: boolean }): void | Promise; 8 | }; 9 | const RouterContext = React.createContext( 10 | undefined, 11 | ); 12 | export default RouterContext; 13 | -------------------------------------------------------------------------------- /src/tests/fixtures/List.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | Item 2 10 | Item 3 11 | 14 | 15 | -------------------------------------------------------------------------------- /docs/migration-to-3.0.md: -------------------------------------------------------------------------------- 1 | # Migration to 3.0 2 | 3 | `sveltify()` now only accepts a mapping: 4 | 5 | ```svelte 6 | 9 | 10 | 11 | ``` 12 | 13 | ```svelte 14 | 17 | 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /src/lib/react-router/useLocation.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Location } from "./types"; 3 | import useRouterContext from "./internal/useRouterContext.js"; 4 | 5 | export default function useLocation(): Location { 6 | const { 7 | url: { pathname, search, hash }, 8 | } = useRouterContext(); 9 | 10 | return React.useMemo( 11 | () => ({ pathname, search, hash }), 12 | [hash, pathname, search], 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/render-prop/Search.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | query: string; 3 | children: (results: string[]) => React.ReactNode; 4 | }; 5 | export default function Search({ query, children }: Props) { 6 | // The number of results is determined by the length of the query string. 7 | const results: string[] = new Array(query.length) 8 | .fill(null) 9 | .map((_, i) => `result ${i + 1} with ${query}`); 10 | 11 | return children(results); 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/signals/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/react-router/index.ts: -------------------------------------------------------------------------------- 1 | import RouterContext from "./internal/RouterContext.js"; 2 | 3 | export type * from "./types.js"; 4 | export const RouterProvider = RouterContext.Provider; 5 | export { default as Link } from "./Link.js"; 6 | export { default as NavLink } from "./NavLink.js"; 7 | export { default as useHistory } from "./useHistory.js"; 8 | export { default as useLocation } from "./useLocation.js"; 9 | export { default as useParams } from "./useParams.js"; 10 | -------------------------------------------------------------------------------- /src/tests/fixtures/SlottedText.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | Static Text 10 | 11 | 12 | "Multiline content". Lorem ipsum dolor sit amet consectetur adipisicing elit. 13 | Suscipit nisi atque asperiores. 14 | 15 | 16 | Value: {count + 1} 17 | -------------------------------------------------------------------------------- /src/lib/internal/ReactContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import type { getAllContexts } from "svelte"; 3 | 4 | const ReactContext = createContext< 5 | | { 6 | /** suffix that will be added to the rendered tags for identification */ 7 | suffix: string; 8 | /** Svelte context for mount or render */ 9 | context: ReturnType; 10 | } 11 | | undefined 12 | >(undefined); 13 | 14 | export default ReactContext; 15 | -------------------------------------------------------------------------------- /src/routes/render-prop/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 14 | results.map((result, i) => 15 | createElement("react-children-container", { key: i }, result), 16 | )} 17 | /> 18 | -------------------------------------------------------------------------------- /src/demo/components/DebugContext.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
{text}
14 | 15 | 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { sveltekit } from "@sveltejs/kit/vite"; 3 | import { configDefaults, defineConfig } from "vitest/config"; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit()], 7 | css: { devSourcemap: true }, 8 | test: { 9 | exclude: [...configDefaults.exclude, "dist", "playwright"], 10 | }, 11 | resolve: { 12 | alias: { 13 | "svelte-preprocess-react": path.resolve("./src/lib"), 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | const config: PlaywrightTestConfig = { 4 | testDir: "./playwright/tests", 5 | fullyParallel: true, 6 | forbidOnly: !!process.env.CI, 7 | use: { 8 | baseURL: "http://localhost:5173", 9 | trace: "retain-on-failure", 10 | }, 11 | webServer: { 12 | command: "npm run dev", 13 | port: 5173, 14 | reuseExistingServer: true, 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /src/routes/context-svelte/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/fixtures/[fixture]/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

{data.title}

7 |
8 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/routes/hooks/react-auth.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | export type Auth = { authenticated: boolean }; 4 | const AuthContext = createContext({ authenticated: false }); 5 | 6 | export const AuthProvider = AuthContext.Provider; 7 | 8 | export function useAuth() { 9 | const ctx = useContext(AuthContext); 10 | if (typeof ctx === "undefined") { 11 | throw new Error("useAuth must be used within an AuthProvider"); 12 | } 13 | return ctx; 14 | } 15 | -------------------------------------------------------------------------------- /src/tests/fixtures/Children.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | {@render children()} 12 |
13 | 14 | 24 | -------------------------------------------------------------------------------- /src/routes/react-router/+layout.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes/suspense/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/routes/typesafe/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

{count}

11 | 12 | { 15 | count = val; 16 | }} 17 | /> 18 | 19 | 20 | The count is {count} 21 | 22 | -------------------------------------------------------------------------------- /playwright/tests/useSignals.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.use({ viewport: { width: 480, height: 360 } }); 4 | test("useSignals", async ({ page }) => { 5 | await page.goto("/signals", { waitUntil: "networkidle" }); 6 | await expect(page.getByRole("heading", { name: "Hello John" })).toBeVisible(); 7 | await page.getByRole("textbox", { name: "Enter your name" }).fill("James"); 8 | await expect( 9 | page.getByRole("heading", { name: "Hello James" }), 10 | ).toBeVisible(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/routes/fixtures/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | {#each getFiles() as file} 13 | {#if file}
  • 14 | {file} 15 |
  • 16 | {/if} 17 | {/each} 18 |
19 | -------------------------------------------------------------------------------- /src/demo/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/lib/react-router/useHistory.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import useRouterContext from "./internal/useRouterContext.js"; 3 | 4 | export default function useHistory() { 5 | const router = useRouterContext(); 6 | return React.useMemo( 7 | () => ({ 8 | push(url: string) { 9 | void router.goto(url); 10 | }, 11 | replace(url: string) { 12 | void router.goto(url, { replaceState: true }); 13 | }, 14 | }), 15 | // eslint-disable-next-line @typescript-eslint/unbound-method 16 | [router.goto], 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "bundler", 5 | "module": "es2020", 6 | "lib": ["es2020", "DOM"], 7 | "target": "es2020", 8 | "resolveJsonModule": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "allowJs": true, 14 | "checkJs": true, 15 | "strict": true, 16 | "noEmit": true, 17 | "jsx": "preserve" 18 | }, 19 | "exclude": ["build", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/context-react/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/demo/react-components/Alert.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * A react component using a Svelte Button component 3 | */ 4 | import * as React from "react"; 5 | 6 | import $ from "./Alert.module.css"; 7 | 8 | type AlertProps = { 9 | type?: "primary"; 10 | children?: React.ReactNode; 11 | }; 12 | const Tooltip: React.FC = ({ type = "primary", children }) => { 13 | const classNames = [$.alert]; 14 | if (type === "primary") { 15 | classNames.push($["alert-primary"]); 16 | } 17 | 18 | return
{children}
; 19 | }; 20 | export default Tooltip; 21 | -------------------------------------------------------------------------------- /src/tests/fixtures/Dog.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {name} 16 | 17 | 27 | -------------------------------------------------------------------------------- /src/tests/fixtures/Multiple.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

prop and event

11 | { 14 | count = next; 15 | }} 16 | > 17 |

prop and Prop event

18 | { 21 | console.info("count"); 22 | }} 23 | > 24 |

prop

25 | 26 |

.

27 | 28 | -------------------------------------------------------------------------------- /src/lib/internal/renderToStringAsync.ts: -------------------------------------------------------------------------------- 1 | import { renderToReadableStream } from "react-dom/server"; 2 | 3 | export default async function renderToStringAsync( 4 | ...args: Parameters 5 | ): Promise { 6 | return await streamToString(await renderToReadableStream(...args)); 7 | } 8 | 9 | async function streamToString(stream: ReadableStream) { 10 | let output = ""; 11 | const decoder = new TextDecoder(); 12 | await stream.pipeTo( 13 | new WritableStream({ 14 | write(chunk) { 15 | output += decoder.decode(chunk); 16 | }, 17 | }), 18 | ); 19 | return output; 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/dynamic/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/routes/preprocessor/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | 11 | 12 | A simple alert 13 | 14 | 15 | "Multiline content". {10 ** 4} Lorem ipsum dolor sit amet consectetur adipisicing 16 | elit. Suscipit nisi atque asperiores. 17 | 18 | a div 19 | -------------------------------------------------------------------------------- /src/routes/sveltify-react/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | { 17 | count = Math.random(); 18 | }} 19 | /> 20 | 21 | A simple alert 22 | -------------------------------------------------------------------------------- /src/tests/react-router/TestRouter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import RouterContext from "../../lib/react-router/internal/RouterContext"; 3 | 4 | type Props = { 5 | url: string; 6 | children: React.ReactNode; 7 | goto?: (url: string, opts?: { replaceState?: boolean }) => void; 8 | }; 9 | 10 | const TestRouter: React.FC = ({ 11 | children, 12 | url, 13 | goto = () => undefined, 14 | }) => ( 15 | 22 | {children} 23 | 24 | ); 25 | export default TestRouter; 26 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 | import adapter from "@sveltejs/adapter-static"; 3 | import preprocessReact from "./src/lib/preprocessReact.js"; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | export default { 7 | preprocess: [vitePreprocess(), preprocessReact()], 8 | compilerOptions: { 9 | experimental: { async: true }, 10 | // css: "injected" 11 | }, 12 | kit: { 13 | alias: { 14 | "svelte-preprocess-react": "src/lib", 15 | }, 16 | adapter: adapter({ 17 | fallback: "index.html", 18 | }), 19 | }, 20 | vitePlugin: { 21 | inspector: true, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/routes/fixtures/[fixture]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "svelte"; 2 | 3 | export async function load({ params }) { 4 | return { 5 | title: params.fixture, 6 | Fixture: await getModule(params.fixture), 7 | }; 8 | } 9 | 10 | async function getModule(name: string) { 11 | const modules = import.meta.glob("../../../tests/fixtures/*.svelte"); 12 | const loader = Object.entries(modules).find( 13 | ([path]) => /([^/]+)\.svelte$/.exec(path)?.[1] === name, 14 | ); 15 | if (!loader) { 16 | throw new Error(`Fixture not found: ${name}`); 17 | } 18 | const module = await loader[1](); 19 | return (module as { default: Component }).default; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/internal/SveltifiedUniversal.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#if SveltifiedSSR} 13 | 14 | {:else} 15 | 16 | {/if} 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/tests/fixtures/List.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | }; 6 | export default function List({ children }: Props) { 7 | return
    {children}
; 8 | } 9 | 10 | type ItemProps = { 11 | label: string; 12 | children: React.ReactNode; 13 | }; 14 | 15 | function ListItem({ label, children }: ItemProps) { 16 | return ( 17 |
  • 18 | {label} 19 | {children} 20 |
  • 21 | ); 22 | } 23 | type IconProps = { 24 | icon: string; 25 | }; 26 | ListItem.Icon = ({ icon }: IconProps) => { 27 | return {icon}; 28 | }; 29 | 30 | List.Item = ListItem; 31 | -------------------------------------------------------------------------------- /static/react-spa.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React SPA 7 | 8 | 9 | 10 | 11 |

    Loading...

    12 |
    13 | 14 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/routes/react-router/Menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { NavLink } from "svelte-preprocess-react/react-router"; 3 | 4 | type Props = { 5 | to: string; 6 | title: string; 7 | }; 8 | const MenuItem: React.FC = ({ to, title }) => ( 9 |
  • 10 | ({ fontWeight: isActive ? "bold" : "normal" })} 13 | > 14 | {title} 15 | 16 |
  • 17 | ); 18 | 19 | const Menu: React.FC = () => ( 20 |
      21 | 22 | 23 | 24 |
    25 | ); 26 | export default Menu; 27 | -------------------------------------------------------------------------------- /src/lib/internal/ReactifiedSSR.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if reactChildren !== undefined} 17 | 18 | {#if children} 19 | {@render children()} 20 | {/if} 21 | 22 | {:else} 23 | 24 | {/if} 25 | -------------------------------------------------------------------------------- /src/routes/context-react-nested/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | BEFORE 18 | 19 | after 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/demo/react-components/Alert.module.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | --alert-bg: transparent; 3 | --alert-padding-x: 1rem; 4 | --alert-padding-y: 1rem; 5 | --alert-margin-bottom: 1rem; 6 | --alert-color: inherit; 7 | --alert-border-color: transparent; 8 | --alert-border: 1px solid var(--alert-border-color); 9 | --alert-border-radius: 0.375rem; 10 | position: relative; 11 | padding: var(--alert-padding-y) var(--alert-padding-x); 12 | margin-bottom: var(--alert-margin-bottom); 13 | color: var(--alert-color); 14 | background-color: var(--alert-bg); 15 | border: var(--alert-border); 16 | border-radius: var(--alert-border-radius, 0); 17 | } 18 | .alert-primary { 19 | --alert-color: #084298; 20 | --alert-bg: #cfe2ff; 21 | --alert-border-color: #b6d4fe; 22 | } 23 | -------------------------------------------------------------------------------- /src/tests/reactify-tsx.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { describe, expect, it } from "vitest"; 3 | import reactify from "../lib/reactify"; 4 | import renderToStringAsync from "../lib/internal/renderToStringAsync"; 5 | import Dog from "./fixtures/Dog.svelte"; 6 | 7 | describe("reactify-tsx", () => { 8 | const svelte = reactify({ Dog }); // in a tsx file, Dog is of type "any" :-( 9 | 10 | it("renders the Svelte component output into React component", async () => { 11 | expect( 12 | await renderToStringAsync(), 13 | ).toMatchInlineSnapshot( 14 | `"Fido"`, 15 | ); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/tests/fixtures/Blocks.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {#if number === 1} 12 | 13 | 14 | {:else if number === 2} 15 | 16 | {:else} 17 | 18 | {/if} 19 | 20 | {#each [] as _} 21 | 22 | {:else} 23 | 24 | {/each} 25 | 26 | {#await Promise.resolve()} 27 | 28 | {:then} 29 | 30 | {:catch} 31 | 32 | {/await} 33 | -------------------------------------------------------------------------------- /src/lib/useStore.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { get, type Readable } from "svelte/store"; 3 | 4 | /** 5 | * Hook for using Svelte stores in React. 6 | * 7 | * Usage: 8 | * 9 | * const User: React.FC = () => { 10 | * const $user = useStore(userStore); 11 | * return

    Hello, {$user.name}

    ; 12 | * } 13 | */ 14 | export default function useStore(store: Readable): T { 15 | const [value, setValue] = React.useState(() => get(store)); 16 | React.useEffect(() => { 17 | let first = true; 18 | const cancel = store.subscribe((next) => { 19 | if (first) { 20 | first = false; 21 | if (next === value) { 22 | return; 23 | } 24 | } 25 | setValue(next); 26 | }); 27 | return cancel; 28 | }, [store]); 29 | return value; 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/react-router/internal/useRouterContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import RouterContext, { type RouterContextType } from "./RouterContext.js"; 3 | 4 | export default function useRouterContext(): RouterContextType { 5 | const context = React.useContext(RouterContext); 6 | if (!context) { 7 | console.warn("Component was not wrapped inside a "); 8 | return { 9 | url: new URL( 10 | typeof window !== "undefined" 11 | ? window.location.href 12 | : "http://localhost/", 13 | ), 14 | params: {}, 15 | goto, 16 | }; 17 | } 18 | return context; 19 | } 20 | 21 | function goto(url: string) { 22 | console.warn( 23 | "No access to , falling back to using browser navigation", 24 | ); 25 | window.location.href = url; 26 | } 27 | -------------------------------------------------------------------------------- /src/routes/playwright/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    17 | {#if loading} 18 |

    Loading...

    19 | {:else} 20 |

    Ready

    21 | {/if} 22 |
    23 | 24 |
    25 | 26 | 42 | -------------------------------------------------------------------------------- /src/demo/react-components/Counter.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * A react component using a Svelte Button component 3 | */ 4 | import * as React from "react"; 5 | import Button from "../components/Button.svelte"; 6 | import { reactify } from "svelte-preprocess-react"; 7 | 8 | const svelte = reactify({ Button }); 9 | 10 | type CounterProps = { 11 | initial?: number; 12 | onCount?: (count: number) => void; 13 | }; 14 | const Counter: React.FC = ({ initial = 0, onCount }) => { 15 | const [count, setCount] = React.useState(initial); 16 | 17 | function decrease() { 18 | setCount(count - 1); 19 | onCount?.(count - 1); 20 | } 21 | function increase() { 22 | setCount(count + 1); 23 | onCount?.(count + 1); 24 | } 25 | return ( 26 | <> 27 | - 28 | {count} 29 | + 30 | 31 | ); 32 | }; 33 | export default Counter; 34 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | Svelte logo in React colors and the React logo in Svelte colors 9 | 26 | -------------------------------------------------------------------------------- /src/routes/hooks/+page.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
    Count: {count}
    21 | 27 |
    28 | 29 | 30 | 31 | 32 | 33 | {#if auth.authenticated} 34 | 35 | {:else} 36 | 37 | {/if} 38 | -------------------------------------------------------------------------------- /playwright/tests/hooks.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.use({ viewport: { width: 480, height: 360 } }); 4 | test.describe("hooks", () => { 5 | test("counter", async ({ page }) => { 6 | await page.goto("/hooks", { waitUntil: "networkidle" }); 7 | await expect(page.getByTestId("count")).toHaveText("0"); 8 | await page.getByTestId("add").click(); 9 | await expect(page.getByTestId("count")).toHaveText("1"); 10 | await page.getByTestId("add").click(); 11 | await expect(page.getByTestId("count")).toHaveText("2"); 12 | }); 13 | 14 | test("authentication", async ({ page }) => { 15 | await page.goto("/hooks", { waitUntil: "networkidle" }); 16 | await expect(page.getByTestId("not-authenticated")).toBeVisible(); 17 | await expect(page.getByTestId("authenticated")).not.toBeVisible(); 18 | await page.getByTestId("login").click(); 19 | await expect(page.getByTestId("authenticated")).toBeVisible(); 20 | await page.getByTestId("logout").click(); 21 | await expect(page.getByTestId("not-authenticated")).toBeVisible(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/lib/internal/SvelteContext.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { createRoot, type Root } from "react-dom/client"; 3 | import { flushSync } from "react-dom"; 4 | import { getContext, setContext } from "svelte"; 5 | 6 | type SvelteContext = () => Root; 7 | 8 | export function getSvelteContext(): SvelteContext { 9 | const ctx = getContext(getSvelteContext); 10 | if (ctx) { 11 | return ctx; 12 | } 13 | function createBranch(): Root { 14 | const rootEl = document.createElement("sveltify-csr-react-root"); 15 | const reactRoot = createRoot(rootEl); 16 | document.body.appendChild(rootEl); 17 | 18 | return { 19 | render(vdom: ReactNode) { 20 | reactRoot.render(vdom); 21 | }, 22 | unmount() { 23 | flushSync(() => { 24 | reactRoot.unmount(); 25 | }); 26 | document.body.removeChild(rootEl); 27 | }, 28 | }; 29 | } 30 | return createBranch; 31 | } 32 | 33 | export function setSvelteContext(createBranch: () => Root) { 34 | return setContext(getSvelteContext, createBranch); 35 | } 36 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bob Fanger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/useStore.md: -------------------------------------------------------------------------------- 1 | ## useStore 2 | 3 | useStore is a React hook that allows using a Svelte Store in a React component. 4 | 5 | ### Usage: 6 | 7 | ```ts 8 | import { useStore } from "svelte-preprocess-react"; 9 | const userStore = writable({ name: "John Doe" }); 10 | 11 | const UserGreet: React.FC = () => { 12 | const $user = useStore(userStore); 13 | return

    Hello, {$user.name}

    ; 14 | }; 15 | export default UserGreet; 16 | ``` 17 | 18 | When the Svelte Store is updated the component will rerender and receive the new value from the useStore hook. 19 | 20 | ### Writable stores 21 | 22 | Inside a Svelte component `$user = { name:'Jane Doe' }` or `$user.name = 'Jane Doe'` will trigger an update. 23 | In React and other regular javascript files this does _not_ work. 24 | To update the value and trigger an update use the `set` or `update` methods: 25 | 26 | ```ts 27 | // Instead of `$user = { name: "Jane Doe" }` 28 | user.set({ name: "Jane Doe" }); 29 | 30 | // Instead of `$user.name = "Jane Doe"` 31 | user.update((user) => ({ ...user, name: "Jane Doe" })); 32 | ``` 33 | 34 | See https://svelte.dev/docs#run-time-svelte-store for more information. 35 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # hooks 2 | 3 | Using React hooks inside Svelte components. 4 | 5 | The `hooks()` function uses Svelte lifecycle functions, so you can only call the function during component initialization. 6 | 7 | ### Usage: 8 | 9 | ```svelte 10 | 15 | 16 |

    Count: {count}

    17 | 18 | ``` 19 | 20 | hooks() returns a function, when that function retrieves the reactive state, by using $derived.by, the updates from React are applied. Inside the callback you can call multiple hooks, but [the rules of hooks](https://reactjs.org/docs/hooks-rules.html) still apply. 21 | 22 | ```ts 23 | const actions = $derived.by( 24 | await hooks(() => { 25 | const multiplier = useContext(MultiplierContext); 26 | const [count, setCount] = useState(0); 27 | return { 28 | multiply: () => setCount(count * multiplier), 29 | reset: () => setCount(0), 30 | }; 31 | }), 32 | ); 33 | 34 | function onReset() { 35 | actions.reset(); 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /src/lib/useSignals.svelte.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * Hook for using Svelte signals inside a React hook/component. 5 | * useSignals() accepts a watch function that must be defined in a *.svelte or *.svelte.ts file. 6 | * When any of the signals change value, the React component will re-render. 7 | * 8 | * The useSignal has no return value, because the signal value can already be accessed directly, no need to name the same thing something else. 9 | * 10 | * Usage: 11 | * 12 | * const user = $state({ name: "John" }); 13 | * 14 | * const User: React.FC = () => { 15 | * useSignals(() => user.name); 16 | * return

    Hello, {user.name}

    ; 17 | * } 18 | */ 19 | export default function useSignals(watch: () => void): void { 20 | const [, rerender] = React.useState({}); 21 | const initial = React.useRef(true); 22 | React.useEffect( 23 | () => 24 | $effect.root(() => { 25 | $effect(() => { 26 | watch(); 27 | if (initial.current) { 28 | initial.current = false; 29 | return; 30 | } 31 | rerender({}); 32 | }); 33 | }), 34 | [], 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/demo/react-components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot, hydrateRoot } from "react-dom/client"; 3 | import { reactify } from "svelte-preprocess-react"; 4 | import Dog from "../../tests/fixtures/Dog.svelte"; 5 | import Children from "../../tests/fixtures/Children.svelte"; 6 | import DebugContextProvider from "../../demo/components/DebugContextProvider.svelte"; 7 | import DebugContext from "../../demo/components/DebugContext.svelte"; 8 | 9 | const svelte = reactify({ Dog, Children, DebugContextProvider, DebugContext }); 10 | 11 | export default function App() { 12 | return ( 13 |
    14 |

    React app

    15 | console.info("Zoinks!")} /> 16 | 17 |
    18 | React element inside a reactified Svelte component 19 |
    20 |
    21 | 22 | 23 | 24 |
    25 | ); 26 | } 27 | const createElement = React.createElement; 28 | export { createElement, hydrateRoot, createRoot }; 29 | -------------------------------------------------------------------------------- /src/lib/react-router/Link.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { To } from "./types"; 3 | import locationToUrl from "./internal/locationToUrl.js"; 4 | import RouterContext from "./internal/RouterContext.js"; 5 | 6 | export type LinkProps = Omit< 7 | React.AnchorHTMLAttributes, 8 | "href" 9 | > & { 10 | replace?: boolean; 11 | to: To; 12 | }; 13 | 14 | const Link = React.forwardRef( 15 | ({ to, replace, children, ...rest }, ref) => { 16 | const attrs = rest; 17 | const context = React.useContext(RouterContext); 18 | if (!context) { 19 | if (replace) { 20 | console.warn("replace attribute needs a "); 21 | } 22 | } 23 | 24 | const href = locationToUrl(to, context?.url).toString(); 25 | if (replace) { 26 | const { onClick } = attrs; 27 | attrs.onClick = (event) => { 28 | onClick?.(event); 29 | if (!event.defaultPrevented) { 30 | event.preventDefault(); 31 | void context?.goto(href, { replaceState: true }); 32 | } 33 | }; 34 | } 35 | return React.createElement("a", { ...attrs, ref, href }, children); 36 | }, 37 | ); 38 | export default Link; 39 | -------------------------------------------------------------------------------- /src/lib/hooks.svelte.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { flushSync } from "react-dom"; 3 | import { onDestroy } from "svelte"; 4 | import renderToStringAsync from "svelte-preprocess-react/internal/renderToStringAsync"; 5 | import { getSvelteContext } from "svelte-preprocess-react/internal/SvelteContext"; 6 | 7 | export default async function hooks(fn: () => T): Promise<() => T> { 8 | if (typeof document === "undefined") { 9 | return hooksSSR(fn); 10 | } 11 | // Client-side 12 | const createBranch = getSvelteContext(); 13 | const branch = createBranch(); 14 | onDestroy(() => { 15 | branch.unmount(); 16 | }); 17 | let result = $state(); 18 | flushSync(() => 19 | branch.render( 20 | React.createElement( 21 | React.memo(() => { 22 | result = fn(); 23 | return null; 24 | }), 25 | ), 26 | ), 27 | ); 28 | 29 | return () => { 30 | return result as T; 31 | }; 32 | } 33 | 34 | async function hooksSSR(fn: () => T): Promise<() => T> { 35 | // @TODO: run hook inside nested react context 36 | let result: T; 37 | await renderToStringAsync( 38 | React.createElement(() => { 39 | result = fn(); 40 | return null; 41 | }), 42 | ); 43 | 44 | return () => { 45 | return result; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/tests/react-router/locationToUrl.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import locationToUrl from "svelte-preprocess-react/react-router/internal/locationToUrl"; 3 | 4 | describe("locationToUrl", () => { 5 | it("absolute urls", () => { 6 | expect(`${locationToUrl("/foo")}`).toBe("/foo"); 7 | expect(`${locationToUrl("/foo?arg=1#hash")}`).toBe("/foo?arg=1#hash"); 8 | expect( 9 | `${locationToUrl("/foo", new URL("https://example.com/test/123"))}`, 10 | ).toBe("/foo"); 11 | }); 12 | it(". should return the current absolute url", () => { 13 | const base = new URL("https://example.com/path"); 14 | expect(`${locationToUrl(".", base)}`).toBe("/path"); 15 | }); 16 | it("same folder", () => { 17 | const base = new URL("https://example.com/folder/filename"); 18 | expect(`${locationToUrl("file2", base)}`).toBe("/folder/file2"); 19 | expect(`${locationToUrl("./abc", base)}`).toBe("/folder/abc"); 20 | }); 21 | it("up level", () => { 22 | const base = new URL("https://example.com/folder1/folder2/filename"); 23 | expect(`${locationToUrl("..", base)}`).toBe("/folder1/folder2"); 24 | expect(`${locationToUrl("../xyz", base)}`).toBe("/folder1/xyz"); 25 | expect(`${locationToUrl("../../abc", base)}`).toBe("/abc"); 26 | expect(`${locationToUrl("../../../abc", base)}`).toBe("/abc"); // too much 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/tests/types.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "vitest"; 2 | import type { 3 | EventName, 4 | EventProps, 5 | HandlerName, 6 | OmitEventProps, 7 | SvelteEventHandlers, 8 | } from "svelte-preprocess-react/internal/types"; 9 | 10 | const fn: any = () => undefined; 11 | 12 | describe("types", () => { 13 | it("bogus test", () => { 14 | // Types don't fail at runtime, 15 | // Use `tsc --noEmit` to verify that the types are correct. 16 | 17 | type ReactProps = { label: string; onClick(): void }; 18 | 19 | const testEventProps = fn as (_: EventProps) => void; 20 | testEventProps({ onClick: () => undefined }); 21 | 22 | const testPropsOmitEventProps = fn as ( 23 | _: OmitEventProps, 24 | ) => void; 25 | testPropsOmitEventProps({ label: "test" }); 26 | 27 | const testHandlerName = fn as (_: HandlerName<"click">) => void; 28 | testHandlerName("onClick"); 29 | 30 | const testEventName = fn as (_: EventName<"onClick">) => void; 31 | testEventName("click"); 32 | 33 | type SvelteEvents = { 34 | click: MouseEvent; 35 | }; 36 | const testSvelteEventHandlers = fn as ( 37 | _: SvelteEventHandlers, 38 | ) => void; 39 | testSvelteEventHandlers({ 40 | onClick(event: MouseEvent) { 41 | console.info(event); 42 | }, 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /docs/reactify.md: -------------------------------------------------------------------------------- 1 | ## reactify 2 | 3 | Convert a Svelte component into an React component. 4 | 5 | ### Usage: 6 | 7 | ```ts 8 | import { reactify } from "svelte-preprocess-react"; 9 | import Button from "$lib/components/Button.svelte"; 10 | 11 | const svelte = reactify({ Button }); 12 | 13 | type Props = { 14 | onClose: () => void; 15 | }; 16 | const Dialog: React.FC = ({ onClose }) => ( 17 |
    18 |

    Thanks for subscribing!

    19 | onClose()}> 20 | Close 21 | 22 |
    23 | ); 24 | ``` 25 | 26 | ## When React starts the rendering, rendering the children is delayed 27 | 28 | This is because we want to extract the context from the Svelte component and provide that to the Svelte child components 29 | 30 | ## Svelte components missing CSS? 31 | 32 | This happens when a Svelte component is only used in the React server render and the "external" CSS from the compile step is not injected into the page. 33 | 34 | When you're also loading the Svelte components on the page, the bundler will also include the CSS into the page. 35 | Another option is to set the **compilerOptions.css** to "injected". 36 | 37 | ```js 38 | // svelte.config.js 39 | export default { 40 | preprocess: [preprocessReact()], 41 | compilerOptions: { 42 | css: "injected", 43 | }, 44 | }; 45 | ``` 46 | -------------------------------------------------------------------------------- /src/routes/react-ssr/+server.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from "react"; 2 | import App from "../../demo/react-components/App"; 3 | import renderToStringAsync from "svelte-preprocess-react/internal/renderToStringAsync"; 4 | 5 | const cssScript = ``; 6 | const hydrateScript = ``; 10 | 11 | export async function GET({ url }) { 12 | const html = await renderToStringAsync(createElement(App)); 13 | let script = ""; 14 | if (url.searchParams.get("css")) { 15 | script += cssScript; 16 | } 17 | if (url.searchParams.get("hydrate")) { 18 | script = hydrateScript; 19 | } 20 | 21 | return new Response( 22 | ` 23 | 24 | 25 | 26 | 27 | React SSR 28 | 29 | 30 | 31 | ${html} 32 | ${script} 33 | 34 | 35 | `, 36 | { 37 | status: 200, 38 | headers: { "Content-Type": "text/html; charset=utf-8" }, 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/tests/reactify.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { expectType } from "tsd"; 3 | import * as React from "react"; 4 | import reactify from "svelte-preprocess-react/reactify"; 5 | import Dog from "./fixtures/Dog.svelte"; 6 | import Children from "./fixtures/Children.svelte"; 7 | import renderToStringAsync from "svelte-preprocess-react/internal/renderToStringAsync"; 8 | 9 | describe("reactify-ts", () => { 10 | const svelte = reactify({ Dog, Children }); 11 | 12 | it("converts Svelte props into React props", async () => { 13 | expectType< 14 | React.FC<{ 15 | name: string; 16 | onbark?: ((sound: string) => void) | undefined; 17 | }> 18 | >(svelte.Dog); 19 | expectType>(svelte.Children); 20 | 21 | const html = await renderToStringAsync( 22 | React.createElement(svelte.Dog, { name: "Fido" }), 23 | ); 24 | expect(html).toMatchInlineSnapshot( 25 | `"Fido"`, 26 | ); 27 | }); 28 | 29 | it("reactify() on the same component returns a identical (cached) react component", () => { 30 | const DogReact = reactify(Dog); 31 | expect(svelte.Dog === DogReact).toBeTruthy(); 32 | expectType(DogReact); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/internal/reactifySSR.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRawSnippet, type Component, type Snippet } from "svelte"; 3 | import renderToStringAsync from "./renderToStringAsync.js"; 4 | import { render } from "svelte/server"; 5 | import ReactifiedSSR from "./ReactifiedSSR.svelte"; 6 | 7 | /** 8 | * Convert a Svelte SSR component into a React SSR component. 9 | */ 10 | export default async function reactifySSR( 11 | SvelteComponent: Component, 12 | props: Record, 13 | reactChildren: unknown, 14 | ) { 15 | let children: Snippet | undefined = undefined; 16 | if (typeof reactChildren !== "undefined" && reactChildren !== null) { 17 | // @TODO: Use a nested context 18 | const nested = await renderToStringAsync( 19 | React.createElement( 20 | "reactified-ssr-fragment", 21 | null, 22 | reactChildren as React.ReactNode, 23 | ), 24 | ); 25 | children = createRawSnippet(() => ({ 26 | render: () => { 27 | return nested.substring(25, nested.length - 26); 28 | }, 29 | })); 30 | } 31 | 32 | const { body, head } = await render(ReactifiedSSR, { 33 | props: { SvelteComponent, props, reactChildren, children }, 34 | }); 35 | // @TODO: Improve handling of head content 36 | return React.createElement("reactified", { 37 | style: { display: "contents" }, 38 | dangerouslySetInnerHTML: { __html: head + body }, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /playwright/tests/sveltify.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.use({ viewport: { width: 480, height: 360 } }); 4 | test.describe("sveltify", () => { 5 | test("props", async ({ page }) => { 6 | await page.goto("/playwright"); 7 | await expect(page.locator("text=Ready")).toBeVisible(); 8 | await page.evaluate(() => { 9 | const win = window as any; 10 | const target = document.getElementById("playground"); 11 | if (win.app) { 12 | win.svelteUnmount(win.app); 13 | } 14 | win.app = win.svelteMount(win.StatefulClicker, { 15 | target, 16 | props: {}, 17 | }); 18 | }); 19 | const message = page.getByTestId("message"); 20 | await expect(message).toContainText("You clicked 0 times"); 21 | await page.click("text=+"); 22 | await expect(message).toContainText("You clicked 1 times"); 23 | await page.click("text=+"); 24 | await page.click("text=+"); 25 | await expect(message).toContainText("You clicked 3 times"); 26 | }); 27 | 28 | test("react context", async ({ page }) => { 29 | await page.goto("/context-react"); 30 | await expect( 31 | page.getByRole("heading", { name: "Hello from react context" }), 32 | ).toBeVisible(); 33 | }); 34 | 35 | test("svelte context", async ({ page }) => { 36 | await page.goto("/context-svelte"); 37 | await expect( 38 | page.locator('text=/.*"Hello from svelte route".*/'), 39 | ).toBeVisible(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/lib/react-router/internal/locationToUrl.ts: -------------------------------------------------------------------------------- 1 | import type { To } from "../types"; 2 | 3 | /** 4 | * Convert a react-router location to an URL. 5 | */ 6 | export default function locationToUrl(to: To, base?: URL): URL { 7 | const pathname = typeof to === "string" ? to : to.pathname; 8 | let url: URL; 9 | const baseUrl = new URL( 10 | base ?? 11 | (typeof window === "undefined" 12 | ? "http://localhost" 13 | : window.location.href), 14 | ); 15 | 16 | if (typeof to === "string" && /^[a-z]+:\/\//.test(to)) { 17 | // Absolute URL incl domain 18 | url = new URL(to, baseUrl); 19 | } else if (pathname === ".") { 20 | // react-router uses "." to represent the current location 21 | url = new URL(baseUrl); 22 | } else if (pathname.startsWith("/")) { 23 | // Absolute path (same domain) 24 | url = new URL(pathname, baseUrl); 25 | } else if (pathname === "..") { 26 | baseUrl.pathname += "/"; 27 | url = new URL(pathname, baseUrl); 28 | url.pathname = url.pathname.substring(0, url.pathname.length - 1); 29 | } else { 30 | // relative path 31 | url = new URL(pathname, baseUrl); 32 | } 33 | if (typeof to === "object") { 34 | url.search = to.search; 35 | url.hash = to.hash; 36 | } 37 | if (url.origin === baseUrl.origin) { 38 | url.toString = function toString() { 39 | // Strip the origin from the URL 40 | return URL.prototype.toString.call(url).substring(this.origin.length); 41 | }; 42 | } 43 | return url; 44 | } 45 | -------------------------------------------------------------------------------- /docs/migration-to-2.0.md: -------------------------------------------------------------------------------- 1 | # Migration to 2.0 2 | 3 | In the tag change the `:` double colon into a `.` dot, instead of: 4 | 5 | `` ❌ old 6 | 7 | write: 8 | 9 | `` ✅ new 10 | 11 | ## Type safety 12 | 13 | When using Typescript, in the ` 35 | 36 | {#if react$children !== undefined} 37 | 38 | 39 | {#if children} 40 | {@render children()} 41 | {:else if slot} 42 | { 44 | const child = slot; 45 | if (child) { 46 | const parent = child.parentElement; 47 | el.appendChild(child); 48 | return () => { 49 | parent?.appendChild(child); 50 | }; 51 | } 52 | }} 53 | > 54 | {/if} 55 | 56 | {:else} 57 | 58 | {/if} 59 | -------------------------------------------------------------------------------- /playwright/tests/react-first.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, type Page } from "@playwright/test"; 2 | import svelteConfig from "../../svelte.config.js"; 3 | 4 | test.use({ viewport: { width: 480, height: 360 } }); 5 | test.describe("react-first", () => { 6 | test("render reactified Svelte component inside a React SPA", async ({ 7 | page, 8 | }) => { 9 | await page.goto("/react-spa.html"); 10 | await testDog(page); 11 | await testChildren(page); 12 | await testContext(page); 13 | }); 14 | 15 | test("Hydrate reactified Svelte component inside React server rendered page", async ({ 16 | page, 17 | }) => { 18 | await page.goto("/react-ssr?hydrate=1"); 19 | await testDog(page); 20 | await testChildren(page); 21 | await testContext(page); 22 | }); 23 | }); 24 | 25 | test.describe("react-first no-js", () => { 26 | test.use({ javaScriptEnabled: false }); 27 | if (svelteConfig?.compilerOptions?.css !== "injected") { 28 | test.skip(); 29 | } 30 | test("Server render reactified Svelte component inside React server", async ({ 31 | page, 32 | }) => { 33 | await page.goto("/react-ssr?css=1"); 34 | // Only leaf nodes work (children are not supported) 35 | await testDog(page); 36 | }); 37 | }); 38 | 39 | async function testDog(page: Page) { 40 | await expect(page.getByText("Scooby")).toBeVisible(); 41 | await expect(page.locator("svelte-dog")).toHaveScreenshot(); 42 | } 43 | 44 | async function testChildren(page: Page) { 45 | await expect( 46 | page.locator(".wrapper", { 47 | hasText: "React element inside a reactified Svelte component", 48 | }), 49 | ).toHaveScreenshot(); 50 | } 51 | async function testContext(page: Page) { 52 | await expect( 53 | page.locator("pre", { hasText: '"Svelte context value"' }), 54 | ).toHaveScreenshot(); 55 | } 56 | -------------------------------------------------------------------------------- /docs/react-router.md: -------------------------------------------------------------------------------- 1 | # react-router 2 | 3 | Using multiple routers in one app is a problem, as they are not built with that use-case in mind. 4 | To make migrate React components that using react-router to Svelte easier, we created `svelte-preprocess-react/react-router` 5 | 6 | This is **not** a drop-in replacement for react-router, as it lacks many features, but it eases the migration process. 7 | 8 | ## What does it do? 9 | 10 | It offers Hooks and Components that are used in the leaf nodes of the component tree, like: 11 | 12 | - `` 13 | - `` 14 | - useLocation() 15 | - useHistory() 16 | - useParams() 17 | 18 | This allows the basic things like reading info about the current url, rendering links and programmatic navigation to work. 19 | 20 | ## What it does NOT do? 21 | 22 | Complex things like route matching, rendering routes, data-loading are out of scope. 23 | This becomes the job of the (svelte) router you're migrating to. 24 | 25 | ## How to use it? 26 | 27 | replace: 28 | `import { Link } from "react-router-dom"` 29 | with: 30 | `import { Link } from "svelte-preprocess-react/react-router"` 31 | 32 | To use hooks or the NavLink component, you need to wrap your component in a Router component. 33 | 34 | [svelte-preprocess-react/react-router/Router.svelte](../src/lib/react-router/Router.svelte) 35 | 36 | ## Usage in @sveltejs/kit 37 | 38 | In src/routes/+layout.svelte 39 | 40 | ```svelte 41 | 49 | 50 | 51 | 52 | 53 | ``` 54 | 55 | the actual navigation and url updates are done by the SvelteKit router. The `` exposed the current url and push & replace actions 56 | -------------------------------------------------------------------------------- /src/lib/sveltify.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "svelte"; 2 | import type { 3 | IntrinsicElementComponents, 4 | StaticPropComponents, 5 | Sveltified, 6 | } from "./internal/types.js"; 7 | import SveltifiedUniversal from "./internal/SveltifiedUniversal.svelte"; 8 | 9 | const cache = new WeakMap(); 10 | const intrinsicElementCache: Record = {}; 11 | 12 | /** 13 | * Convert a React components into Svelte components. 14 | */ 15 | export default function sveltify< 16 | T extends Record< 17 | string, 18 | keyof React.JSX.IntrinsicElements | React.JSXElementConstructor 19 | >, 20 | >( 21 | components: T, 22 | ): { 23 | [K in keyof T]: Sveltified & StaticPropComponents; 24 | } & IntrinsicElementComponents { 25 | return Object.fromEntries( 26 | Object.entries(components).map(([key, reactComponent]) => { 27 | if (reactComponent === undefined) { 28 | return [key, undefined]; 29 | } 30 | return [key, single(reactComponent)]; 31 | }), 32 | ) as any; 33 | } 34 | 35 | function single(ReactComponent: any) { 36 | if ( 37 | typeof ReactComponent !== "function" && 38 | typeof ReactComponent === "object" && 39 | ReactComponent !== null && 40 | "default" in ReactComponent && 41 | typeof ReactComponent.default === "function" 42 | ) { 43 | // Fix SSR import issue where node doesn't import the esm version. 'react-youtube' 44 | ReactComponent = ReactComponent.default; 45 | } 46 | const hit = 47 | typeof ReactComponent === "string" 48 | ? intrinsicElementCache[ReactComponent] 49 | : cache.get(ReactComponent); 50 | if (hit) { 51 | return hit; 52 | } 53 | const name = ReactComponent.displayName ?? ReactComponent.name ?? "anonymous"; 54 | const named = { 55 | [name](this: any, $$renderer: any, $$props: any, ...args: any[]) { 56 | $$props.react$component = ReactComponent; 57 | // @ts-ignore 58 | return SveltifiedUniversal.call(this, $$renderer, $$props, ...args); 59 | }, 60 | }; 61 | if (typeof ReactComponent === "string") { 62 | intrinsicElementCache[ReactComponent] = named[name]; 63 | } else { 64 | cache.set(ReactComponent, named[name]); 65 | } 66 | return named[name] as any as Component; 67 | } 68 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | This document describes design-decisions and implementation details of the preprocessor. 4 | 5 | **Principles:** 6 | 7 | - Compatibility first, Ease-of-use second and Performance last. 8 | 9 | ## Context 10 | 11 | ```jsx 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ``` 20 | 21 | Both Svelte and React have component trees, for context to work in both, Svelte needs to act as if the tree is: 22 | 23 | ```jsx 24 | 25 | 26 | 27 | ``` 28 | 29 | and React needs to act as if the tree is: 30 | 31 | ```jsx 32 | 33 | 34 | 35 | ``` 36 | 37 | ### Client mode 38 | 39 | `sveltify()` creates a single React Root and based on the Hierarchy of the ReactWrapper components we're able to built the React tree: 40 | 41 | ```jsx 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ``` 50 | 51 | The ``s use React Portals to render the components into the DOM of the ReactWrapper Svelte component. 52 | 53 | This is why the children prop passed to your React component is an array, even when you manually pass a children prop. 54 | This array allows svelte-preprocess-react to inject the slotted content into the correct place in the React tree. 55 | 56 | ### Server mode 57 | 58 | Server detection is done at runtime, so the client will also ship with the renderToStringYou server code. 59 | For smaller bundle size you can disable this feature by passing `ssr: false` to the preprocess function. 60 | 61 | Svelte is rendered first, while building the vdom for React. 62 | Using string based methods: 63 | `` from the Svelte is injected into the React's `` 64 | `` from the React is then injected into the Svelte's `` 65 | 66 | This allows React to maintain it's component trees (needed for context) 67 | Note: Components created with `reactify` have trouble preserving the Svelte context 68 | 69 | # Wrappers elements 70 | 71 | `` are wrappers rendered by Svelte 72 | `` are wrappers rendered by React 73 | -------------------------------------------------------------------------------- /src/lib/internal/types.ts: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import type { Component, Snippet } from "svelte"; 3 | 4 | export type HandlerName = `on${Capitalize}`; 5 | export type EventName = T extends `on${infer N}` 6 | ? Uncapitalize 7 | : never; 8 | 9 | export type SvelteEventHandlers = 10 | T extends Record 11 | ? Partial, (e: Value) => void | boolean>> 12 | : never; 13 | 14 | type Uppercase = 15 | | "A" 16 | | "B" 17 | | "C" 18 | | "D" 19 | | "E" 20 | | "F" 21 | | "G" 22 | | "H" 23 | | "I" 24 | | "J" 25 | | "K" 26 | | "L" 27 | | "M" 28 | | "N" 29 | | "O" 30 | | "P" 31 | | "Q" 32 | | "R" 33 | | "S" 34 | | "T" 35 | | "U" 36 | | "V" 37 | | "W" 38 | | "X" 39 | | "Y" 40 | | "Z"; 41 | 42 | type ReactEventProp = `on${Uppercase}${string}`; 43 | type ExcludeProps = T extends ReactEventProp ? T : never; 44 | type ExcludeEvents = T extends ReactEventProp ? never : T; 45 | 46 | export type EventProps = Omit< 47 | ReactProps, 48 | ExcludeEvents 49 | >; 50 | export type OmitEventProps = Omit< 51 | ReactProps, 52 | ExcludeProps 53 | >; 54 | 55 | export type ReactifiedSync = ( 56 | props: Record, 57 | children: unknown, 58 | slot: HTMLElement | null, 59 | ) => void; 60 | 61 | export type ChildrenPropsAsSnippet = T extends { children: unknown } 62 | ? Omit & { children: Snippet | T["children"] } 63 | : T extends { children?: unknown } 64 | ? Omit & { children?: Snippet | T["children"] } 65 | : T; 66 | 67 | export type Sveltified< 68 | T extends 69 | | keyof React.JSX.IntrinsicElements 70 | | React.JSXElementConstructor, 71 | > = Component>>; 72 | 73 | export type IntrinsicElementComponents = { 74 | [K in keyof React.JSX.IntrinsicElements]: Component< 75 | ChildrenPropsAsSnippet> 76 | >; 77 | }; 78 | 79 | /* Primitive typing of `Component.Item` components */ 80 | export type StaticPropComponents = Record< 81 | string, 82 | Component & Record> 83 | >; 84 | 85 | export type ChildrenPropsAsReactNode> = { 86 | [K in keyof T]: K extends "children" ? React.ReactNode : T[K]; 87 | }; 88 | 89 | export type Prettify = { 90 | [K in keyof T]: T[K]; 91 | } & {}; 92 | -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { copyFile, readdir, stat } from "fs/promises"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | test.describe.configure({ mode: "serial" }); 7 | test.use({ viewport: { width: 480, height: 360 } }); 8 | test.describe("ssr", () => { 9 | const urls = [ 10 | "/context-svelte", 11 | "/context-react", 12 | "/preprocessor", 13 | "/hooks", 14 | "/dynamic", 15 | "/input", 16 | "/react-router/home", 17 | "/react-router/about", 18 | "/fixtures/RestProps", 19 | "/fixtures/List", 20 | "/intrinsic-elements", 21 | "/fixtures/Dog", 22 | "/context-react-nested", 23 | "/fixtures/RestProps", 24 | ]; 25 | 26 | test("client-rendered", async ({ page }) => { 27 | // Create Screenshots using Client Side Rendering. 28 | for (const url of urls) { 29 | await test.step(url, async () => { 30 | await page.goto(url); 31 | await expect(page.locator('body[data-ssr="spa"]')).toBeAttached({ 32 | timeout: 10_000, 33 | }); 34 | await expect(page.locator("body")).toHaveScreenshot(); 35 | }); 36 | } 37 | }); 38 | 39 | test("sync screenshots", async () => { 40 | // Copy client-rendered screenshots to be used as reference for server-rendered snapshots. 41 | const snapshotsPath = path.join( 42 | path.dirname(fileURLToPath(import.meta.url)), 43 | "ssr.test.ts-snapshots", 44 | ); 45 | const files = await readdir(snapshotsPath); 46 | let checked = 0; 47 | for (const file of files) { 48 | if (/client-rendered/.exec(file)) { 49 | const clientFile = path.join(snapshotsPath, file); 50 | const serverFile = path.join( 51 | snapshotsPath, 52 | file.replace("client-rendered", "no-js-server-rendered"), 53 | ); 54 | const serverInfo = await stat(serverFile).catch(() => false as const); 55 | if (!serverInfo) { 56 | await copyFile(clientFile, serverFile); 57 | } else { 58 | const clientInfo = await stat(clientFile); 59 | if (clientInfo.size !== serverInfo.size) { 60 | await copyFile(clientFile, serverFile); 61 | } 62 | } 63 | checked++; 64 | } 65 | } 66 | expect(checked).toBe(urls.length); 67 | }); 68 | test.describe("no-js", () => { 69 | test.use({ javaScriptEnabled: false }); 70 | test("server-rendered", async ({ page }) => { 71 | for (const url of urls) { 72 | await test.step(url, async () => { 73 | await page.goto(url); 74 | await expect(page.locator("body")).toHaveScreenshot(); 75 | }); 76 | } 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/lib/internal/SveltifiedCSR.svelte: -------------------------------------------------------------------------------- 1 | 69 | 70 | {#if children} 71 | 95 | {/if} 96 | 97 | -------------------------------------------------------------------------------- /docs/caveats.md: -------------------------------------------------------------------------------- 1 | # Caveats 2 | 3 | We try to maximize compatibility between React & Svelte. 4 | However, there are some inherent limitations to be aware of. 5 | 6 | ## Binding 7 | 8 | React components are one directional by design, therefor you can't two-way bind to a prop: 9 | 10 | ```svelte 11 | 12 | ❌ 13 | ``` 14 | 15 | Listen to events instead: 16 | 17 | ```svelte 18 | { 21 | value = e.currentTarget.value; 22 | }} 23 | /> 24 | ✅ 25 | ``` 26 | 27 | ## No JSX 28 | 29 | JSX is not supported inside \*.svelte files: 30 | 31 | ```svelte 32 | } 34 | label="Books" 35 | /> 36 | ❌ 37 | ``` 38 | 39 | Use the non-jsx syntax: 40 | 41 | ```svelte 42 | 45 | 46 | 47 | ✅ 48 | ``` 49 | 50 | ## Children incompatibility 51 | 52 | React children and Svelte children are fundamentally different. 53 | 54 | In Svelte children are a `Snippet` or `undefined`. You can't do much besides ` {@render children?.()}`. 55 | This improved encapsulation and predictability, but is less flexible than React. 56 | 57 | In React, children can be any type and can be inspected, modified or used in multiple ways. 58 | 59 | Svelte/React input: 60 | 61 | ```svelte 62 | 63 | Content 64 | 65 | ``` 66 | 67 | React vDOM output: 68 | 69 | ```jsx 70 | 71 |

    72 | 73 | <- When merging the render trees the Content is injected into this element 74 | 75 |

    76 |
    77 | ``` 78 | 79 | HTML output: 80 | 81 | ```html 82 | 83 |

    84 | 85 | 86 | Content 87 | 88 | 89 |

    90 |
    91 | ``` 92 | 93 | There are cases where the React virtual DOM or this real DOM can cause problems. 94 | The workaround is to write a react component that creates the wanted structure and use that component inside Svelte. 95 | 96 | ### Render props 97 | 98 | ```svelte 99 | {async (query) => fetchResults(query))} 100 | ❌ 101 | ``` 102 | 103 | Use the `children` prop instead: 104 | 105 | ```svelte 106 | fetchResults(query)) } /> 107 | ✅ 108 | ``` 109 | 110 | ## Asynchronous rendering 111 | 112 | We render 2 trees, one for Svelte and one for React and then interleave their output. 113 | This requires both renderers to be asynchronous and wait on each other. 114 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import "eslint-plugin-only-warn"; 2 | import js from "@eslint/js"; 3 | import ts from "typescript-eslint"; 4 | import prettier from "eslint-config-prettier"; 5 | import svelte from "eslint-plugin-svelte"; 6 | import globals from "globals"; 7 | 8 | export default ts.config( 9 | { 10 | ignores: [ 11 | ".svelte-kit", 12 | ".vercel", 13 | "build", 14 | "node_modules", 15 | "package", 16 | "vite.config.ts.timestamp-*.mjs", 17 | ], 18 | }, 19 | { 20 | languageOptions: { 21 | ecmaVersion: "latest", 22 | sourceType: "module", 23 | globals: { 24 | ...globals.browser, 25 | sveltify: true, 26 | hooks: true, 27 | react: true, 28 | }, 29 | parserOptions: { 30 | parser: ts.parser, 31 | extraFileExtensions: [".svelte"], 32 | project: "tsconfig.eslint.json", 33 | }, 34 | }, 35 | }, 36 | js.configs.recommended, 37 | ts.configs.eslintRecommended, 38 | ...ts.configs.recommendedTypeChecked, 39 | ...ts.configs.stylisticTypeChecked, 40 | ...svelte.configs["flat/recommended"], 41 | prettier, 42 | ...svelte.configs["flat/prettier"], 43 | { 44 | rules: { 45 | "@typescript-eslint/consistent-type-definitions": ["warn", "type"], 46 | "@typescript-eslint/ban-ts-comment": "off", 47 | "@typescript-eslint/no-explicit-any": "off", 48 | "@typescript-eslint/no-shadow": "warn", 49 | "@typescript-eslint/no-unsafe-assignment": "off", 50 | "@typescript-eslint/no-unsafe-member-access": "off", 51 | "@typescript-eslint/no-unused-vars": [ 52 | "warn", 53 | { ignoreRestSiblings: true, argsIgnorePattern: "^_+$" }, 54 | ], 55 | curly: "warn", 56 | eqeqeq: "warn", 57 | "no-console": ["warn", { allow: ["info", "warn", "error"] }], 58 | "no-useless-rename": "warn", 59 | "object-shorthand": "warn", 60 | "prefer-const": "off", 61 | "prefer-template": "warn", 62 | "svelte/block-lang": ["warn", { script: "ts" }], 63 | "svelte/no-at-html-tags": "off", 64 | "svelte/prefer-const": ["warn", { destructuring: "all" }], 65 | "svelte/require-each-key": "off", 66 | "@typescript-eslint/no-unsafe-call": 0, 67 | "@typescript-eslint/no-unsafe-return": 0, 68 | "@typescript-eslint/no-unsafe-argument": 0, 69 | }, 70 | }, 71 | { 72 | files: ["src/routes/**/*.ts", "src/routes/**/*.svelte"], 73 | rules: { 74 | // ESLint is not aware of the generated ./$types and reports false positives 75 | "@typescript-eslint/no-unsafe-argument": 0, 76 | "@typescript-eslint/no-unsafe-call": 0, 77 | }, 78 | }, 79 | { 80 | files: ["**/*.cjs"], 81 | rules: { 82 | // Allow require() in CommonJS modules. 83 | "@typescript-eslint/no-require-imports": "off", 84 | }, 85 | }, 86 | { 87 | files: ["**/*.cjs", "**/*.js", "**/*.server.ts"], 88 | languageOptions: { globals: globals.node }, 89 | }, 90 | ); 91 | -------------------------------------------------------------------------------- /src/lib/reactify.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createElement, 3 | Fragment, 4 | use, 5 | useLayoutEffect, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | import { mount, unmount, type Component } from "svelte"; 10 | import type { 11 | ChildrenPropsAsReactNode, 12 | ReactifiedSync, 13 | } from "./internal/types.js"; 14 | import ReactifiedCSR from "./internal/ReactifiedCSR.svelte"; 15 | import ReactContext from "./internal/ReactContext.js"; 16 | 17 | const cache = new WeakMap, React.FunctionComponent>(); 18 | 19 | /** 20 | * Convert a Svelte components into React components. 21 | */ 22 | export default function reactify< 23 | T extends Component | Record>, 24 | >( 25 | svelteComponents: T, 26 | ): T extends Component 27 | ? React.FunctionComponent> 28 | : { 29 | [K in keyof T]: T[K] extends Component 30 | ? React.FunctionComponent> 31 | : React.FunctionComponent; 32 | } { 33 | if (typeof svelteComponents === "function") { 34 | return single(svelteComponents) as any; 35 | } 36 | const reactComponents: Record> = {}; 37 | for (const key in svelteComponents) { 38 | reactComponents[key] = single((svelteComponents as any)[key], key); 39 | } 40 | return reactComponents as any; 41 | } 42 | 43 | function single(SvelteComponent: Component, key?: string): React.FC { 44 | const hit = cache.get(SvelteComponent); 45 | if (hit) { 46 | return hit; 47 | } 48 | const name = key ?? SvelteComponent.name ?? "anonymous"; 49 | const named = { 50 | [name]({ children, ...props }: any) { 51 | if (typeof document === "undefined") { 52 | // Server-side rendering 53 | return import("./internal/reactifySSR.js").then((module) => 54 | module.default(SvelteComponent, props, children), 55 | ); 56 | } 57 | // Client-side rendering 58 | const targetRef = useRef(null); 59 | const childrenRef = useRef(null); 60 | const syncRef = useRef(null); 61 | const ctx = use(ReactContext); 62 | const [contexts, setContexts] = useState>(); 63 | 64 | useLayoutEffect(() => { 65 | syncRef.current?.(props, children, childrenRef.current); 66 | }); 67 | useLayoutEffect(() => { 68 | const app = mount(ReactifiedCSR, { 69 | target: targetRef.current!, 70 | props: { 71 | SvelteComponent, 72 | init: (sync: ReactifiedSync) => (syncRef.current = sync), 73 | setContexts, 74 | react$children: children, 75 | slot: childrenRef.current, 76 | props, 77 | }, 78 | context: ctx?.context, 79 | }); 80 | return () => { 81 | syncRef.current = null; 82 | void unmount(app); 83 | }; 84 | }, []); 85 | return createElement(Fragment, null, [ 86 | createElement("reactify-svelte-mount", { 87 | ref: targetRef, 88 | key: "component", 89 | style: { display: "contents" }, 90 | }), 91 | 92 | children && 93 | contexts && 94 | createElement( 95 | "reactify-react-child", 96 | { 97 | key: "children", 98 | ref: childrenRef, 99 | style: { display: "contents" }, 100 | }, 101 | createElement( 102 | ReactContext, 103 | { value: { context: contexts, suffix: "reactify" } }, 104 | children, 105 | ), 106 | ), 107 | ]); 108 | }, 109 | }; 110 | cache.set(SvelteComponent, named[name]); 111 | return named[name]; 112 | } 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-preprocess-react", 3 | "description": "Seamlessly use React components inside a Svelte app", 4 | "keywords": [ 5 | "svelte", 6 | "react", 7 | "interoperability" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/bfanger/svelte-preprocess-react.git" 12 | }, 13 | "version": "3.0.0-beta.0", 14 | "license": "MIT", 15 | "type": "module", 16 | "scripts": { 17 | "dev": "vite dev", 18 | "dev:preprocess": "echo 'src/lib/preprocessReact.js' | entr -rn npm run dev", 19 | "build": "vite build", 20 | "preview": "vite preview", 21 | "package": "svelte-package", 22 | "lint": "concurrently -c \"#c596c7\",\"#676778\",\"#7c7cea\" --kill-others-on-fail \"npm:lint:*\"", 23 | "lint:prettier": "prettier --check \"src/**/*.svelte\"", 24 | "lint:svelte-check": "svelte-check --fail-on-warnings", 25 | "lint:eslint": "eslint --max-warnings=0 src", 26 | "format": "eslint --fix src && prettier --write .", 27 | "test": "concurrently -c \"#fcc72a\",\"#45ba4b\" --kill-others-on-fail \"npm:test:*\"", 28 | "test:vitest": "vitest run", 29 | "test:playwright": "playwright test", 30 | "vitest:watch": "vitest watch", 31 | "prepublishOnly": "npm run package" 32 | }, 33 | "lint-staged": { 34 | "*.{ts,svelte}": [ 35 | "svelte-check --fail-on-warnings" 36 | ], 37 | "*.{ts,svelte,js,cjs,mjs}": [ 38 | "eslint --max-warnings 0 --no-ignore" 39 | ], 40 | "*.{ts,js,svelte,css,scss,json,html}": [ 41 | "prettier --check" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "@eslint/js": "^9.38.0", 46 | "@playwright/test": "^1.56.1", 47 | "@sveltejs/adapter-static": "^3.0.10", 48 | "@sveltejs/kit": "^2.47.2", 49 | "@sveltejs/package": "^2.5.4", 50 | "@sveltejs/vite-plugin-svelte": "^6.2.1", 51 | "@testing-library/react": "^16.3.0", 52 | "@testing-library/svelte": "^5.2.8", 53 | "@types/node": "^24.9.1", 54 | "@types/react": "^19.2.2", 55 | "@types/react-dom": "^19.2.2", 56 | "autoprefixer": "^10.4.21", 57 | "concurrently": "^9.2.1", 58 | "eslint": "9.38.0", 59 | "eslint-config-prettier": "^10.1.8", 60 | "eslint-plugin-only-warn": "^1.1.0", 61 | "eslint-plugin-react": "^7.37.5", 62 | "eslint-plugin-svelte": "^3.12.5", 63 | "globals": "^16.4.0", 64 | "happy-dom": "^20.0.8", 65 | "husky": "^9.1.7", 66 | "lint-staged": "^16.2.5", 67 | "postcss": "^8.5.6", 68 | "prettier": "^3.6.2", 69 | "prettier-plugin-svelte": "^3.4.0", 70 | "react": "^19.2.0", 71 | "react-dom": "^19.2.0", 72 | "react-youtube": "^10.1.0", 73 | "svelte": "~5.41.1", 74 | "svelte-check": "^4.3.3", 75 | "svelte-youtube-lite": "~1.2.1", 76 | "svelte2tsx": "^0.7.45", 77 | "tsd": "^0.33.0", 78 | "typescript": "^5.9.3", 79 | "typescript-eslint": "^8.46.2", 80 | "vite": "^7.1.11", 81 | "vite-tsconfig-paths": "^5.1.4", 82 | "vitest": "^3.2.4" 83 | }, 84 | "dependencies": { 85 | "estree-walker": "^3.0.3", 86 | "magic-string": "^0.30.19" 87 | }, 88 | "peerDependencies": { 89 | "react": ">=19.0.0", 90 | "react-dom": ">=19.0.0", 91 | "svelte": ">=5.39.0" 92 | }, 93 | "svelte": "./dist/index.js", 94 | "files": [ 95 | "./dist" 96 | ], 97 | "types": "./dist/index.d.ts", 98 | "exports": { 99 | ".": { 100 | "types": "./dist/index.d.ts", 101 | "svelte": "./dist/index.js", 102 | "default": "./dist/index.js" 103 | }, 104 | "./preprocessReact": { 105 | "types": "./dist/preprocessReact.d.ts", 106 | "default": "./dist/preprocessReact.js" 107 | }, 108 | "./react-router": { 109 | "types": "./dist/react-router/index.d.ts", 110 | "svelte": "./dist/react-router/index.js", 111 | "default": "./dist/react-router/index.js" 112 | } 113 | }, 114 | "typesVersions": { 115 | ">4.0": { 116 | "index.d.ts": [ 117 | "./dist/index.d.ts" 118 | ], 119 | "preprocessReact": [ 120 | "./dist/preprocessReact.d.ts" 121 | ], 122 | "react-router": [ 123 | "./dist/react-router/index.d.ts" 124 | ] 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![svelte-preprocess-react](./static/svelte-preprocess-react.svg)](https://www.npmjs.com/package/svelte-preprocess-react) 2 | 3 | See [v2.x Readme](https://github.com/bfanger/svelte-preprocess-react/blob/2.1.4/README.md) for the stable version. 4 | 5 | # Svelte Preprocess React - 3.0-beta 6 | 7 | Seamlessly use React components inside a Svelte app 8 | 9 | Supports: 10 | 11 | - Nesting (Slot & Children) 12 | - Contexts 13 | - SSR 14 | - Hooks ([useStore](./docs/useStore.md) & [hooks](./docs/hooks.md)) 15 | 16 | This project was featured at the [Svelte London - November 2022 Meetup](https://www.youtube.com/live/DXQl1G54DJY?feature=share&t=2569) 17 | 18 | > "Embrace, extend and extinguish" 19 | 20 | This preprocessor is intended as solution using third-party React components or for migrating an existing React codebase. 21 | 22 | ## Using React inside Svelte components 23 | 24 | Inside the Svelte template prepend the name of the component with `react.` prefix. 25 | 26 | Instead of `