├── 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 |
onCount(count + 1)}>+
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 |
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 |
12 | {#if children}{@render children()}{/if}
13 |
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 | Alert
15 | Counter
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 ;
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 |
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 |
9 |
26 |
--------------------------------------------------------------------------------
/src/routes/hooks/+page.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 | Count: {count}
21 | {
24 | setCount(count + 1);
25 | }}>+
27 |
28 |
29 |
30 |
31 |
32 |
33 | {#if auth.authenticated}
34 | Logout
35 | {:else}
36 | Login
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 | setCount(count + 1)}>+
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 | {
74 | function SveltifiedCSRChild() {
75 | el.hidden = false;
76 | const ref = useRef(null);
77 | useLayoutEffect(() => {
78 | ref.current!.appendChild(el);
79 | }, []);
80 | const vdom = [
81 | createElement("sveltify-csr-react-child", { key: "child", ref }),
82 | ];
83 | for (const [key, nestedApp] of branches.entries()) {
84 | vdom.push(
85 | createElement("sveltify-csr-nested-app", { key }, nestedApp),
86 | );
87 | }
88 | return vdom;
89 | }
90 | Child = SveltifiedCSRChild;
91 | }}
92 | >
93 | {@render children()}
94 |
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 | [](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 ``, you'd write ``
27 |
28 | Use libraries from the React's ecosystem, react-youtube for example:
29 |
30 | ```svelte
31 |
37 |
38 |
39 | ```
40 |
41 | The snippet above would be generate:
42 |
43 | ```svelte
44 |
45 |
54 |
55 |
56 | ```
57 |
58 | ## Setup / Installation
59 |
60 | ```sh
61 | npm install --save-dev svelte-preprocess-react@next react react-dom
62 | ```
63 |
64 | Add `preprocessReact` to your svelte.config.js:
65 |
66 | ```js
67 | // svelte.config.js
68 | import preprocessReact from "svelte-preprocess-react/preprocessReact";
69 |
70 | export default {
71 | preprocess: preprocessReact(),
72 | compilerOptions: {
73 | experimental: { async: true }, // v3.0 relies on async svelte components
74 | },
75 | };
76 | ```
77 |
78 | When using other processors like [@sveltejs/vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/preprocess.md) or [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) add preprocessReact preprocessor as the last processor:
79 |
80 | ```js
81 | // svelte.config.js
82 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
83 | import preprocessReact from "svelte-preprocess-react/preprocessReact";
84 |
85 | export default {
86 | preprocess: [vitePreprocess(), preprocessReact()],
87 | compilerOptions: {
88 | experimental: { async: true },
89 | },
90 | };
91 | ```
92 |
93 | ## Using Svelte inside React components
94 |
95 | Once you've converted a React component to Svelte, you'd want delete that React component, but some if other React components depended on that component you can use `reactify` to use the new Svelte component as a React component.
96 |
97 | ```jsx
98 | import { reactify } from "svelte-preprocess-react";
99 | import Button from "../components/Button.svelte";
100 |
101 | const svelte = reactify({ Button });
102 |
103 | function MyComponent() {
104 | return (
105 | console.log("clicked")}>
106 | Click me
107 |
108 | );
109 | }
110 | ```
111 |
112 | ## Using multiple frameworks is a bad idea
113 |
114 | Using multiple frontend frameworks adds overhead, both in User and Developer experience.
115 |
116 | - Increased download size
117 | - Slower (each framework boundary adds overhead)
118 | - Context switching, keeping the intricacies of both Svelte and React in your head slows down development
119 |
120 | When using third-party React components, keep an eye out for Svelte alternatives, or publish your own.
121 |
122 | When used as migration tool (can be used to migrate _from_ or _to_ React),
123 | the goal should be to stop writing new React components, and to convert existing React components to Svelte components.
124 | Once all components are converted this preprocessor should be uninstalled.
125 |
126 | # More info
127 |
128 | - [reactify()](./docs/reactify.md) Convert a Svelte component into an React component
129 | - [hooks()](./docs/hooks.md) Using React hooks inside Svelte components
130 | - [useStore](./docs/useStore.md) Using a Svelte Store in a React components
131 | - [Caveats](./docs/caveats.md) Limitations and workarounds
132 | - [react-router](./docs/react-router.md) Migrate from react-router to SvelteKit
133 | - [Architecture](./docs/architecture.md) svelte-preprocess-react's API Design-principles and System architecture
134 |
--------------------------------------------------------------------------------
/src/lib/internal/SveltifiedSSR.svelte:
--------------------------------------------------------------------------------
1 |
139 |
140 | {@html html}
141 |
--------------------------------------------------------------------------------
/src/tests/react-router/NavLink.spec.tsx:
--------------------------------------------------------------------------------
1 | // @vitest-environment happy-dom
2 | import { describe, it, expect } from "vitest";
3 | import { render } from "@testing-library/react";
4 | import * as React from "react";
5 | import { NavLink } from "svelte-preprocess-react/react-router";
6 | import TestRouter from "./TestRouter";
7 |
8 | describe("NavLink (react-router v6)", () => {
9 | describe("when it does not match", () => {
10 | it("does not apply an 'active' className to the underlying ", () => {
11 | const { container } = render(
12 |
13 | Somewhere else
14 | ,
15 | );
16 | const anchor = container.querySelector("a");
17 | expect(anchor?.className).not.toMatch("active");
18 | });
19 | it("does not change the content inside the ", () => {
20 | const { container } = render(
21 |
22 |
23 | {({ isActive }) => (isActive ? "Current" : "Somewhere else")}
24 |
25 | ,
26 | );
27 |
28 | const anchor = container.querySelector("a");
29 | expect(anchor?.textContent).toMatch("Somewhere else");
30 | });
31 | it("applies an 'undefined' className to the underlying ", () => {
32 | const { container } = render(
33 |
34 |
37 | isActive ? "some-active-classname" : undefined
38 | }
39 | >
40 | Home
41 |
42 | ,
43 | );
44 | const anchor = container.querySelector("a");
45 | expect(anchor?.className).toBe("");
46 | });
47 | });
48 |
49 | describe("when it matches to the end", () => {
50 | it("applies the default 'active' className to the underlying ", () => {
51 | const { container } = render(
52 |
53 | Home
54 | ,
55 | );
56 | const anchor = container.querySelector("a");
57 | expect(anchor?.className).toMatch("active");
58 | });
59 |
60 | it("applies its className correctly when provided as a function", () => {
61 | const { container } = render(
62 |
63 |
66 | `nav-link${isActive ? " highlighted" : " plain"}`
67 | }
68 | >
69 | Home
70 |
71 | ,
72 | );
73 | const anchor = container.querySelector("a");
74 | expect(anchor?.className.includes("nav-link")).toBe(true);
75 | expect(anchor?.className.includes("highlighted")).toBe(true);
76 | expect(anchor?.className.includes("plain")).toBe(false);
77 | });
78 | it("applies its style correctly when provided as a function", () => {
79 | const { container } = render(
80 |
81 |
84 | isActive ? { textTransform: "uppercase" } : {}
85 | }
86 | >
87 | Home
88 |
89 | ,
90 | );
91 | const anchor = container.querySelector("a");
92 | expect(anchor?.style).toMatchObject({ textTransform: "uppercase" });
93 | });
94 | it("applies its children correctly when provided as a function", () => {
95 | const { container } = render(
96 |
97 |
98 | {({ isActive }) => (isActive ? "Home (current)" : "Home")}
99 |
100 | ,
101 | );
102 | const anchor = container.querySelector("a");
103 | expect(anchor?.textContent).toMatch("Home (current)");
104 | });
105 | });
106 |
107 | describe("when it matches a partial URL segment", () => {
108 | it("does not apply the 'active' className to the underlying ", () => {
109 | const { container } = render(
110 |
111 | Home
112 | ,
113 | );
114 | const anchor = container.querySelector("a");
115 | expect(anchor?.className).not.toMatch("active");
116 | });
117 | it("does not match when path is a subset of the active url", () => {
118 | const { container } = render(
119 |
120 | Go to /user
121 | Go to /user-preferences
122 | ,
123 | );
124 | const anchors = [...container.querySelectorAll("a")];
125 | expect(anchors.map((a) => a.className)).toEqual(["", "active"]);
126 | });
127 | it("does not match when active url is a subset of a segment", () => {
128 | const { container } = render(
129 |
130 |
131 | Go to /user
132 | Go to /user-preferences
133 |
134 | ,
135 | );
136 | const anchors = [...container.querySelectorAll("a")];
137 | expect(anchors.map((a) => a.className)).toEqual(["active", ""]);
138 | });
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/src/tests/react-router/Link.spec.tsx:
--------------------------------------------------------------------------------
1 | // @vitest-environment happy-dom
2 | import * as React from "react";
3 | import { render } from "@testing-library/react";
4 | import { describe, expect, it } from "vitest";
5 | import { Link } from "svelte-preprocess-react/react-router";
6 | import TestRouter from "./TestRouter";
7 |
8 | describe(" href", () => {
9 | describe("in a static route", () => {
10 | it("absolute resolves relative to the root URL", () => {
11 | const { container } = render(
12 |
13 |
14 | ,
15 | );
16 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
17 | "/about",
18 | );
19 | });
20 | it(' resolves relative to the current route', () => {
21 | const { container } = render(
22 |
23 |
24 | ,
25 | );
26 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
27 | "/inbox",
28 | );
29 | });
30 | it(' resolves relative to the parent route', () => {
31 | const { container } = render(
32 |
33 |
34 | ,
35 | );
36 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
37 | "/inbox",
38 | );
39 | });
40 | it(' with more .. segments than parent routes resolves to the root URL', () => {
41 | const { container } = render(
42 |
43 | <>
44 |
45 | {/* traverse past the root */}
46 |
47 | >
48 | ,
49 | );
50 | expect(
51 | [...container.querySelectorAll("a")].map((a) => a.getAttribute("href")),
52 | ).toEqual(["/about", "/about"]);
53 | });
54 | });
55 |
56 | describe("in a dynamic route", () => {
57 | it("absolute resolves relative to the root URL", () => {
58 | const { container } = render(
59 |
60 |
61 | ,
62 | );
63 |
64 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
65 | "/about",
66 | );
67 | });
68 |
69 | it(' resolves relative to the current route', () => {
70 | const { container } = render(
71 |
72 |
73 | ,
74 | );
75 |
76 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
77 | "/inbox/messages/abc",
78 | );
79 | });
80 |
81 | // it(' resolves relative to the parent route', () => {
82 | // const { container } = render(
83 | //
84 | //
85 | //
86 | // );
87 |
88 | // expect(container.querySelector("a")?.getAttribute("href")).toEqual(
89 | // "/inbox"
90 | // );
91 | // });
92 |
93 | // it(' with more .. segments than parent routes resolves to the root URL', () => {
94 | // const { container } = render(
95 | //
96 | //
97 | //
98 | // );
99 |
100 | // expect(container.querySelector("a")?.getAttribute("href")).toEqual(
101 | // "/about"
102 | // );
103 | // });
104 | });
105 |
106 | describe("in an index route", () => {
107 | it("absolute resolves relative to the root URL", () => {
108 | const { container } = render(
109 |
110 |
111 | ,
112 | );
113 |
114 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
115 | "/home",
116 | );
117 | });
118 |
119 | it(' resolves relative to the current route', () => {
120 | const { container } = render(
121 |
122 |
123 | ,
124 | );
125 |
126 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
127 | "/inbox",
128 | );
129 | });
130 |
131 | it(' resolves relative to the parent route (ignoring the index route)', () => {
132 | const { container } = render(
133 |
134 |
135 | ,
136 | );
137 |
138 | expect(container.querySelector("a")?.getAttribute("href")).toEqual("/");
139 | });
140 |
141 | it(' with more .. segments than parent routes resolves to the root URL', () => {
142 | const { container } = render(
143 |
144 | <>
145 |
146 | {/* traverse past the root */}
147 |
148 | >
149 | ,
150 | );
151 |
152 | expect(
153 | [...container.querySelectorAll("a")].map((a) => a.getAttribute("href")),
154 | ).toEqual(["/about", "/about"]);
155 | });
156 | });
157 |
158 | describe("when using a browser router", () => {
159 | it("renders proper for BrowserRouter", () => {
160 | const { container } = render(
161 |
162 |
163 | ,
164 | );
165 | expect(container.querySelector("a")?.getAttribute("href")).toEqual(
166 | "/path?search=value#hash",
167 | );
168 | });
169 | });
170 | });
171 |
--------------------------------------------------------------------------------
/src/tests/preprocess.spec.ts:
--------------------------------------------------------------------------------
1 | import { readFile } from "node:fs/promises";
2 | import { dirname, resolve } from "node:path";
3 | import { fileURLToPath } from "node:url";
4 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
5 | import { preprocess } from "svelte/compiler";
6 | import { describe, expect, it, vi } from "vitest";
7 | import preprocessReact from "svelte-preprocess-react/preprocessReact";
8 |
9 | describe("svelte-preprocess-react", () => {
10 | it("should process tags", async () => {
11 | const filename = resolveFilename("./fixtures/Container.svelte");
12 | const src = await readFile(filename, "utf8");
13 | const output = await preprocess(src, preprocessReact(), { filename });
14 | expect(output.code).toMatchSnapshot();
15 | });
16 |
17 | it("should process tags", async () => {
18 | const filename = resolveFilename("./fixtures/Multiple.svelte");
19 | const src = await readFile(filename, "utf8");
20 | const output = await preprocess(src, preprocessReact(), { filename });
21 | expect(output.code).toMatchSnapshot();
22 | });
23 |
24 | // it("should import 'react-dom/server' when ssr is enabled", async () => {
25 | // const filename = resolveFilename("./fixtures/Container.svelte");
26 | // const src = await readFile(filename, "utf8");
27 | // const output = await preprocess(src, preprocessReact({ ssr: true }), {
28 | // filename,
29 | // });
30 | // expect(output.code).toMatch("react-dom/server");
31 | // expect(output.code).toMatchSnapshot();
32 | // });
33 |
34 | // it("should not import 'react-dom/server' when ssr is disabled", async () => {
35 | // const filename = resolveFilename("./fixtures/Container.svelte");
36 | // const src = await readFile(filename, "utf8");
37 | // const output = await preprocess(src, preprocessReact({ ssr: false }), {
38 | // filename,
39 | // });
40 | // expect(output.code).not.toMatch("react-dom/server");
41 | // expect(output.code).toMatchSnapshot();
42 | // });
43 |
44 | it("should fail on bindings", async () => {
45 | const filename = resolveFilename("./fixtures/Binding.svelte");
46 | const src = await readFile(filename, "utf8");
47 | const consoleMock = vi
48 | .spyOn(console, "warn")
49 | .mockImplementation(() => undefined);
50 | await preprocess(src, preprocessReact(), { filename });
51 | expect(consoleMock).toBeCalledWith(
52 | `Two-way binding is not compatible with React components:
53 | in ${filename} on line 9`,
54 | );
55 | consoleMock.mockReset();
56 | });
57 |
58 | it("should portal slotted content as children", async () => {
59 | const filename = resolveFilename("./fixtures/Slots.svelte");
60 | const src = await readFile(filename, "utf8");
61 | const output = await preprocess(src, preprocessReact(), { filename });
62 | expect(output.code).toMatchSnapshot();
63 | });
64 |
65 | it("should inject a script tag", async () => {
66 | const filename = resolveFilename("./fixtures/NoScript.svelte");
67 | const src = await readFile(filename, "utf8");
68 | const output = await preprocess(src, preprocessReact(), { filename });
69 | expect(output.code).toContain("
11 |
12 |
13 |
14 |
15 |
16 |
17 | "
18 | `;
19 |
20 | exports[`svelte-preprocess-react > should detect trailing comma when adding aliases 1`] = `
21 | "
26 |
27 |
28 |
29 |
30 |
31 | "
32 | `;
33 |
34 | exports[`svelte-preprocess-react > should inject a script tag 1`] = `
35 | "
40 |
41 |
42 |
43 | "
44 | `;
45 |
46 | exports[`svelte-preprocess-react > should portal slotted content as children 1`] = `
47 | "
52 |
53 |
54 | "
55 | `;
56 |
57 | exports[`svelte-preprocess-react > should process tags 1`] = `
58 | "
63 |
64 |
65 |
66 |
67 | Item 3
68 |
71 |
72 | "
73 | `;
74 |
75 | exports[`svelte-preprocess-react > should process tags 1`] = `
76 | "
86 |
87 |
88 | "
89 | `;
90 |
91 | exports[`svelte-preprocess-react > should process tags 1`] = `
92 | "
99 |
100 | {
103 | count = next;
104 | }}
105 | >
106 | "
107 | `;
108 |
109 | exports[`svelte-preprocess-react > should process tags 2`] = `
110 | "
118 |
119 | prop and event
120 | {
123 | count = next;
124 | }}
125 | >
126 | prop and Prop event
127 | {
130 | console.info("count");
131 | }}
132 | >
133 | prop
134 |
135 | .
136 |
137 | "
138 | `;
139 |
140 | exports[`svelte-preprocess-react > should process (lowercase) tags 1`] = `
141 | "
146 |
147 | console.info("clicked")}>
148 |
149 |
150 | "
151 | `;
152 |
153 | exports[`svelte-preprocess-react > should process {...rest} props 1`] = `
154 | "
162 |
163 |
164 | Hi
165 |
166 | "
167 | `;
168 |
169 | exports[`svelte-preprocess-react > should process {:else} {:then} and {:catch} sections 1`] = `
170 | "
178 |
179 |
180 | {#if number === 1}
181 |
182 |
183 | {:else if number === 2}
184 |
185 | {:else}
186 |
187 | {/if}
188 |
189 | {#each [] as _}
190 |
191 | {:else}
192 |
193 | {/each}
194 |
195 | {#await Promise.resolve()}
196 |
197 | {:then}
198 |
199 | {:catch}
200 |
201 | {/await}
202 | "
203 | `;
204 |
205 | exports[`svelte-preprocess-react > should process on:event forwarding 1`] = `
206 | "
216 |
217 |
218 | "
219 | `;
220 |
221 | exports[`svelte-preprocess-react > should support typescript when using preprocess 1`] = `
222 | "
225 |
226 | {title}
227 | "
228 | `;
229 |
--------------------------------------------------------------------------------
/static/svelte-preprocess-react.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/lib/preprocessReact.js:
--------------------------------------------------------------------------------
1 | import MagicString from "magic-string";
2 | import { parse, preprocess } from "svelte/compiler";
3 | import { walk } from "estree-walker";
4 |
5 | /**
6 | * @typedef {import("svelte/compiler").PreprocessorGroup} PreprocessorGroup
7 | * @typedef {import("svelte/compiler").Processed} Processed
8 | */
9 |
10 | /**
11 | * Svelte preprocessor to use convert tags into Sveltified React components.
12 | *
13 | * Imports renderToString from 'react-dom/server' unless the `ssr` option is set to false.
14 | *
15 | * @param {{
16 | * preprocess?: PreprocessorGroup | PreprocessorGroup[];
17 | * }} options
18 | * @returns {PreprocessorGroup}
19 | */
20 | export default function preprocessReact(options = {}) {
21 | return {
22 | async markup({ content, filename }) {
23 | /** @type {Processed | undefined} */
24 | let preprocessed;
25 | if (options.preprocess) {
26 | preprocessed = await preprocess(content, options.preprocess, {
27 | filename,
28 | });
29 |
30 | content = preprocessed.code;
31 | }
32 |
33 | const processed = transform(content, {
34 | filename,
35 | });
36 | if (!preprocessed) {
37 | return processed;
38 | }
39 | if (!processed.map) {
40 | return preprocessed;
41 | }
42 | return {
43 | code: processed.code,
44 | map: preprocessed.map ?? processed.map, // @todo apply sourcemaps
45 | dependencies: preprocessed.dependencies,
46 | };
47 | },
48 | };
49 | }
50 | const prefix = "inject$$";
51 |
52 | /**
53 | * @param {string} content
54 | * @param {{
55 | * filename?: string;
56 | * }} options
57 | * @returns
58 | */
59 | function transform(content, options) {
60 | /** @type {string} */
61 |
62 | const packageName = "svelte-preprocess-react";
63 | const imports = [];
64 |
65 | const ast = parse(content, {
66 | filename: options.filename,
67 | modern: false,
68 | });
69 | const s = new MagicString(content, { filename: options.filename });
70 | const components = replaceReactTags(ast.html, s, options.filename);
71 | const aliases = Object.entries(components);
72 |
73 | /** @type {Set<'sveltify'>} */
74 | const imported = new Set();
75 | /** @type {Set<'sveltify'>} */
76 | const used = new Set();
77 | let defined = false;
78 | /** @type {false|Set} */
79 | let aliased = false;
80 |
81 | /**
82 | * Detect sveltify import and usage
83 | *
84 | * @param {import('estree-walker').Node} node
85 | * @param {import('estree-walker').Node|null} parent
86 | */
87 | function enter(node, parent) {
88 | if (node.type === "Identifier" && node.name === "sveltify" && parent) {
89 | if (
90 | parent.type === "ImportSpecifier" ||
91 | parent.type === "ImportDeclaration"
92 | ) {
93 | imported.add("sveltify");
94 | } else if (parent.type === "CallExpression") {
95 | const componentsArg = parent.arguments[0];
96 | if (!aliased && componentsArg.type === "ObjectExpression") {
97 | aliased = new Set();
98 | for (const property of componentsArg.properties) {
99 | if (property.type === "Property") {
100 | if (property.key.type === "Identifier") {
101 | aliased.add(property.key.name);
102 | }
103 | }
104 | }
105 |
106 | if ("end" in componentsArg && typeof componentsArg.end === "number") {
107 | let tailingComma = false;
108 | const lastProp = componentsArg.properties.at(-1);
109 | if (
110 | lastProp &&
111 | "end" in lastProp &&
112 | typeof lastProp.end === "number"
113 | ) {
114 | tailingComma = s
115 | .slice(lastProp.end, componentsArg.end - 1)
116 | .includes(",");
117 | }
118 | for (const [alias, { expression }] of aliases) {
119 | if (!aliased.has(alias)) {
120 | if (!tailingComma) {
121 | s.appendRight(componentsArg.end - 1, ", ");
122 | tailingComma = true;
123 | }
124 | s.appendRight(
125 | componentsArg.end - 1,
126 | `${alias}: ${expression.substring(0, 1) === expression.substring(0, 1).toLowerCase() ? JSON.stringify(expression) : expression}, `,
127 | );
128 | }
129 | }
130 | } else {
131 | console.warn("missing end in Node");
132 | }
133 | }
134 | used.add("sveltify");
135 | }
136 | }
137 |
138 | if (
139 | node.type === "Identifier" &&
140 | node.name === "react" &&
141 | parent?.type === "VariableDeclarator"
142 | ) {
143 | defined = true;
144 | }
145 | }
146 | if (ast.module) {
147 | walk(ast.module, { enter });
148 | }
149 | if (ast.instance) {
150 | walk(ast.instance, { enter });
151 | }
152 | if (used.size === 0 && aliases.length === 0) {
153 | return { code: content };
154 | }
155 | const declarators = [];
156 | if (
157 | !imported.has("sveltify") &&
158 | (used.has("sveltify") || aliases.length > 0)
159 | ) {
160 | declarators.push("sveltify");
161 | }
162 | if (declarators.length > 0) {
163 | imports.push(`import { ${declarators.join(", ")} } from "${packageName}";`);
164 | }
165 |
166 | const script = ast.instance ?? ast.module;
167 | const wrappers = [];
168 | if (!defined && aliases.length > 0) {
169 | wrappers.push(
170 | `const react = sveltify({ ${Object.entries(components)
171 | .map(([alias, { expression }]) => {
172 | if (expression !== alias) {
173 | return `${alias}: ${expression}`;
174 | }
175 | if (expression.toLowerCase() === expression) {
176 | return `${/^[a-z]+$/.exec(expression) ? expression : JSON.stringify(expression)}: ${JSON.stringify(expression)}`;
177 | }
178 | return expression;
179 | })
180 | .join(", ")} });`,
181 | );
182 | used.add("sveltify");
183 | }
184 |
185 | if (Object.values(components).find((c) => c.dispatcher)) {
186 | imports.push(
187 | 'import { createEventDispatcher as React$$createEventDispatcher } from "svelte";',
188 | );
189 | wrappers.push("const React$$dispatch = React$$createEventDispatcher();");
190 | }
191 | if (!script) {
192 | s.prepend(
193 | `\n\n`,
194 | );
195 | } else {
196 | /** @type {any} */
197 | const program = script.content;
198 | s.appendRight(program.end, `;${wrappers.join(" ")}`);
199 | s.appendRight(program.start, imports.join(" "));
200 | }
201 | return {
202 | code: s.toString(),
203 | map: s.generateMap(),
204 | };
205 | }
206 |
207 | /**
208 | * Replace react:* tags by injecting Sveltified versions of the React components.
209 | *
210 | * @param {any} node
211 | * @param {MagicString} content
212 | * @param {string | undefined} filename
213 | * @param {Record} components
214 | */
215 | function replaceReactTags(node, content, filename, components = {}) {
216 | if (
217 | (node.type === "Element" && node.name.startsWith("react:")) ||
218 | (node.type === "InlineComponent" && node.name.startsWith("react."))
219 | ) {
220 | const legacy = node.name.startsWith("react:");
221 |
222 | if (legacy) {
223 | let location = "";
224 | if (filename) {
225 | location += ` in ${filename}`;
226 | }
227 | if (node.start) {
228 | location += ` on line ${content.original.substring(0, node.start).split("\n").length}`;
229 | }
230 | console.warn(
231 | `'<${node.name}' syntax is deprecated, use ' `) {
249 | // Replace closing tag with alias
250 | const fullTag = content.slice(node.start, node.end - 1);
251 | const whitespaceLength = fullTag.length - fullTag.trimEnd().length;
252 | const tagEnd = node.end - whitespaceLength - node.name.length - 1;
253 | if (content.slice(tagEnd - 2, tagEnd + 6) !== `${tagPrefix}`) {
254 | console.warn(
255 | `Unexpected formatting of the closing tag of <${node.name}>`,
256 | );
257 | } else {
258 | content.overwrite(
259 | tagEnd,
260 | tagEnd + node.name.length,
261 | `react.${alias}`,
262 | );
263 | }
264 | }
265 | }
266 |
267 | if (!components[alias]) {
268 | components[alias] = { dispatcher: false, expression };
269 | }
270 | /** @type {string[]|false}*/
271 | const spread =
272 | node.attributes.find(
273 | (/** @type {any}*/ attribute) => attribute.type === "Spread",
274 | ) === undefined
275 | ? false
276 | : [];
277 |
278 | node.attributes.forEach((/** @type {any} */ attr) => {
279 | if (attr.type === "Binding") {
280 | let location = "";
281 | if (filename) {
282 | location += ` in ${filename}`;
283 | }
284 | if (node.start) {
285 | location += ` on line ${content.original.substring(0, node.start).split("\n").length}`;
286 | }
287 | console.warn(
288 | `Two-way binding is not compatible with React components:
289 | ${content.slice(
290 | node.start,
291 | node.attributes[0].start,
292 | )}${content.slice(attr.start, attr.end)}>${location}`,
293 | );
294 | }
295 | if (attr.type === "EventHandler") {
296 | const event = attr;
297 | const eventStart = event.start;
298 | if (event.modifiers.length > 0) {
299 | throw new Error(`event modifiers are not supported`);
300 | }
301 | if (event.expression !== null) {
302 | content.overwrite(
303 | eventStart,
304 | eventStart + 4,
305 | `on${event.name[0].toUpperCase()}`,
306 | );
307 | } else {
308 | content.overwrite(
309 | eventStart,
310 | eventStart + 3 + event.name.length,
311 | `on${
312 | event.name[0].toUpperCase() + event.name.substring(1)
313 | }={(e) => React$$dispatch(${JSON.stringify(event.name)}, e)}`,
314 | );
315 | components[alias].dispatcher = true;
316 | }
317 | } else if (spread) {
318 | if (attr.type === "Spread") {
319 | spread.push(
320 | `...${content.slice(attr.expression.start, attr.expression.end)}`,
321 | );
322 | } else if (attr.name !== "children") {
323 | const prop =
324 | attr.name.indexOf("-") === -1
325 | ? attr.name
326 | : JSON.stringify(attr.name);
327 | let value = "";
328 | if (typeof attr.value === "boolean") {
329 | value = JSON.stringify(attr.value);
330 | } else if (Array.isArray(attr.value) && attr.value.length === 1) {
331 | if (attr.value[0].type === "Text") {
332 | value = JSON.stringify(
333 | content.slice(attr.value[0].start, attr.value[0].end),
334 | );
335 | } else if (attr.value[0].type === "MustacheTag") {
336 | value = content.slice(
337 | attr.value[0].start + 1,
338 | attr.value[0].end - 1,
339 | );
340 | } else {
341 | console.warn(
342 | `Unexpected attribute type: ${attr.value[0].type}`,
343 | content.slice(attr.start, attr.end),
344 | );
345 | }
346 | } else {
347 | console.warn(
348 | "Unexpected attribute syntax:",
349 | content.slice(attr.start, attr.end),
350 | );
351 | }
352 | spread.push(`${prop}: ${value}`);
353 | }
354 | }
355 | });
356 | if (spread) {
357 | content.overwrite(
358 | node.attributes[0].start,
359 | node.attributes[node.attributes.length - 1].end,
360 | `react$props={{ ${spread.join(", ")} }}`,
361 | );
362 | }
363 | if (node.children && !legacy) {
364 | if (node.children.length === 0) {
365 | const childrenProp =
366 | Array.isArray(node.attributes) &&
367 | node.attributes.find(
368 | (/** @type {any} */ attr) => attr.name === "children",
369 | );
370 | if (childrenProp) {
371 | // If children are passed as attribute, pass the value as-is to the react component.
372 | content.appendLeft(childrenProp.start, "react$"); // renames "children" to "react$children"
373 | }
374 | } else {
375 | const isTextContent =
376 | node.children.filter(
377 | (/** @type {any} */ child) =>
378 | ["Text", "MustacheTag"].includes(child.type) === false,
379 | ).length === 0;
380 | /** @type {string[]} */
381 | const escaped = [];
382 | if (isTextContent) {
383 | // Convert text & expressions into a children prop.
384 | escaped.push('"');
385 | node.children.forEach((/** @type {any} */ child) => {
386 | if (child.type === "Text") {
387 | escaped.push(
388 | child.data.replace(/"/g, `{'"'}`).replace(/\n/g, `{'\\n'}`),
389 | );
390 | } else if (child.type === "MustacheTag") {
391 | const text = content.original.slice(child.start, child.end);
392 | escaped.push(text);
393 | } else {
394 | throw new Error(`Unexpected node type:${child.type}`);
395 | }
396 | });
397 | escaped.push('"');
398 | // slot was converted to children prop
399 | content.appendRight(
400 | node.children[0].start - 1,
401 | ` react$children=${escaped.join("")} /`,
402 | );
403 | content.remove(node.children[0].start, node.end);
404 | return components;
405 | }
406 | }
407 | }
408 | }
409 | /**
410 | * @param {any} child
411 | */
412 | function processChild(child) {
413 | replaceReactTags(child, content, filename, components);
414 | }
415 | // traverse children & branching blocks
416 | node.children?.forEach(processChild);
417 | node.else?.children?.forEach(processChild);
418 | node.pending?.children?.forEach(processChild);
419 | node.then?.children?.forEach(processChild);
420 | node.catch?.children?.forEach(processChild);
421 | return components;
422 | }
423 |
--------------------------------------------------------------------------------