├── .browserslistrc ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENCE ├── README.md ├── dev-preprocess.sh ├── docs ├── architecture.md ├── caveats.md ├── hooks.md ├── migration-to-2.0.md ├── react-router.md ├── reactify.md ├── useStore.md └── utilities.md ├── eslint.config.js ├── package.json ├── playwright.config.ts ├── playwright └── tests │ ├── hooks.spec.ts │ ├── react-first.test.ts │ ├── react-first.test.ts-snapshots │ ├── 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 │ ├── react-first-no-js-Server-render-reactified-Svelte-component-inside-React-server-1-darwin.png │ ├── 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 │ ├── ssr.test.ts │ ├── 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-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-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 │ └── sveltify.spec.ts ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src ├── app.html ├── demo │ ├── components │ │ ├── Button.svelte │ │ ├── DebugContext.svelte │ │ ├── DebugContextProvider.svelte │ │ └── Examples.svelte │ └── react-components │ │ ├── Alert.module.css │ │ ├── Alert.tsx │ │ ├── App.tsx │ │ └── Counter.tsx ├── global.d.ts ├── lib │ ├── global.d.ts │ ├── hooks.svelte.ts │ ├── index.ts │ ├── internal │ │ ├── Bridge.svelte.ts │ │ ├── Child.ts │ │ ├── ExtractContexts.svelte │ │ ├── ReactFirstContext.ts │ │ ├── ReactWrapper.svelte │ │ ├── Slot.ts │ │ ├── SvelteFirstContext.ts │ │ ├── SvelteWrapper.svelte │ │ ├── detectReactVersion.js │ │ ├── portalTag.ts │ │ └── types.ts │ ├── preprocessReact.js │ ├── react-router │ │ ├── Link.ts │ │ ├── NavLink.ts │ │ ├── index.ts │ │ ├── internal │ │ │ ├── RouterContext.ts │ │ │ ├── locationToUrl.ts │ │ │ └── useRouterContext.ts │ │ ├── types.ts │ │ ├── useHistory.ts │ │ ├── useLocation.ts │ │ └── useParams.ts │ ├── reactify.ts │ ├── sveltify.svelte.ts │ ├── useStore.ts │ └── used.ts ├── routes │ ├── +layout.svelte │ ├── +page.svelte │ ├── api │ │ └── react-version.json │ │ │ └── +server.ts │ ├── broken │ │ └── +page.svelte │ ├── context-react │ │ └── +page.svelte │ ├── context-svelte │ │ └── +page.svelte │ ├── dynamic │ │ └── +page.svelte │ ├── fixtures │ │ ├── +page.svelte │ │ └── [fixture] │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ ├── hooks │ │ ├── +page.svelte │ │ ├── HookWithContext.svelte │ │ └── react-auth.ts │ ├── input │ │ └── +page.svelte │ ├── intrinsic-elements │ │ └── +page.svelte │ ├── lazy │ │ └── +page.svelte │ ├── mini │ │ └── +page.svelte │ ├── playwright │ │ ├── +page.svelte │ │ ├── +page.ts │ │ └── StatefulClicker.svelte │ ├── preprocessor │ │ └── +page.svelte │ ├── react-router │ │ ├── +layout.svelte │ │ ├── Menu.tsx │ │ └── [slug] │ │ │ └── +page.svelte │ ├── react-ssr │ │ └── +server.ts │ ├── render-prop │ │ ├── +page.svelte │ │ └── Search.tsx │ ├── suspense │ │ └── +page.svelte │ ├── sveltify-react │ │ └── +server.ts │ ├── sveltify-react17 │ │ └── +page.svelte │ ├── sveltify-react18 │ │ └── +page.svelte │ ├── typesafe │ │ └── +page.svelte │ └── youtube │ │ ├── react │ │ └── +page.svelte │ │ └── svelte │ │ ├── +page.svelte │ │ └── YouTubeWrapper.tsx ├── server.js └── tests │ ├── __snapshots__ │ └── preprocess.spec.ts.snap │ ├── fixtures │ ├── Binding.svelte │ ├── Blocks.svelte │ ├── Children.svelte │ ├── Clicker.tsx │ ├── Container.svelte │ ├── Dog.svelte │ ├── Element.svelte │ ├── Forwarding.svelte │ ├── List.svelte │ ├── List.tsx │ ├── Multiple.svelte │ ├── NoScript.svelte │ ├── Provider.svelte │ ├── RestProps.svelte │ ├── Slots.svelte │ ├── SlottedText.svelte │ └── Typescript.svelte │ ├── preprocess.spec.ts │ ├── react-router │ ├── Link.spec.tsx │ ├── NavLink.spec.tsx │ ├── TestRouter.tsx │ └── locationToUrl.spec.ts │ ├── reactify-tsx.spec.tsx │ ├── reactify.spec.ts │ └── types.spec.ts ├── static ├── favicon.ico ├── react-spa.html └── svelte-preprocess-react.svg ├── svelte.config.js ├── tsconfig.eslint.json ├── tsconfig.json └── vite.config.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run test 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.svelte-kit 2 | /build 3 | /dist 4 | /node_modules 5 | /pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-svelte"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Visit https://github.com/bfanger/svelte-preprocess-react/releases for the release notes. 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![svelte-preprocess-react](./static/svelte-preprocess-react.svg)](https://www.npmjs.com/package/svelte-preprocess-react) 2 | 3 | # Svelte Preprocess React 4 | 5 | Seamlessly use React components inside a Svelte app 6 | 7 | Supports: 8 | 9 | - Nesting (Slot & Children) 10 | - Contexts 11 | - SSR 12 | - Hooks ([useStore](./docs/useStore.md) & [hooks](./docs/hooks.md)) 13 | 14 | This project was featured at the [Svelte London - November 2022 Meetup](https://www.youtube.com/live/DXQl1G54DJY?feature=share&t=2569) 15 | 16 | > "Embrace, extend and extinguish" 17 | 18 | This preprocessor is intended as solution using third-party React components or for migrating an existing React codebase. 19 | 20 | ## Using React inside Svelte components 21 | 22 | Inside the Svelte template prepend the name of the component with `react.` prefix. 23 | 24 | Instead of ` 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 | 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 | -------------------------------------------------------------------------------- /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 ` 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import "eslint-plugin-only-warn"; 2 | // @ts-ignore 3 | import js from "@eslint/js"; 4 | import ts from "typescript-eslint"; 5 | import react from "eslint-plugin-react"; 6 | // @ts-ignore 7 | import prettier from "eslint-config-prettier"; 8 | import svelte from "eslint-plugin-svelte"; 9 | import globals from "globals"; 10 | import svelteParser from "svelte-eslint-parser"; 11 | 12 | export default ts.config( 13 | js.configs.recommended, 14 | ...ts.configs.recommendedTypeChecked, 15 | // @ts-ignore 16 | ...svelte.configs["flat/recommended"], 17 | prettier, 18 | ...svelte.configs["flat/prettier"], 19 | // react.configs.flat.recommended, 20 | { 21 | files: ["**/*.{jsx,tsx}"], 22 | plugins: { react }, 23 | languageOptions: { 24 | parserOptions: { ecmaFeatures: { jsx: true } }, 25 | globals: { ...globals.browser }, 26 | }, 27 | }, 28 | { 29 | languageOptions: { 30 | ecmaVersion: 2022, 31 | sourceType: "module", 32 | globals: { 33 | ...globals.node, 34 | ...globals.browser, 35 | sveltify: true, 36 | hooks: true, 37 | react: true, 38 | }, 39 | parser: svelteParser, 40 | parserOptions: { 41 | parser: ts.parser, 42 | extraFileExtensions: [".svelte"], 43 | project: `tsconfig.eslint.json`, 44 | ecmaFeatures: {}, 45 | }, 46 | }, 47 | }, 48 | { 49 | rules: { 50 | "@typescript-eslint/ban-ts-comment": "off", 51 | "@typescript-eslint/no-explicit-any": "off", 52 | "@typescript-eslint/no-unsafe-assignment": "off", 53 | "@typescript-eslint/no-unsafe-member-access": "off", 54 | "@typescript-eslint/no-unused-vars": [ 55 | "warn", 56 | { ignoreRestSiblings: true, argsIgnorePattern: "^_+$" }, 57 | ], 58 | curly: "warn", 59 | eqeqeq: "warn", 60 | "no-console": ["warn", { allow: ["info", "warn", "error"] }], 61 | "no-useless-rename": "warn", 62 | "object-shorthand": "warn", 63 | "prefer-template": "warn", 64 | "svelte/block-lang": ["warn", { script: "ts" }], 65 | "svelte/no-at-html-tags": "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 | ignores: [ 88 | ".svelte-kit", 89 | ".vercel", 90 | "build", 91 | "node_modules", 92 | "package", 93 | "vite.config.ts.timestamp-*.mjs", 94 | "src/global.d.ts", 95 | "src/tests/reactify.spec.tsx", 96 | ], 97 | }, 98 | ); 99 | -------------------------------------------------------------------------------- /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": "2.1.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 | "preinstall": "npx only-allow pnpm", 32 | "prepublishOnly": "npm run package", 33 | "prepare": "husky || true" 34 | }, 35 | "lint-staged": { 36 | "*.{ts,svelte}": [ 37 | "svelte-check --fail-on-warnings" 38 | ], 39 | "*.{ts,svelte,js,cjs,mjs}": [ 40 | "eslint --max-warnings 0 --no-ignore" 41 | ], 42 | "*.{ts,js,svelte,css,scss,json,html}": [ 43 | "prettier --check" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@playwright/test": "^1.50.1", 48 | "@sveltejs/adapter-static": "^3.0.8", 49 | "@sveltejs/kit": "^2.16.1", 50 | "@sveltejs/package": "^2.3.9", 51 | "@sveltejs/vite-plugin-svelte": "5.0.3", 52 | "@testing-library/react": "^16.2.0", 53 | "@testing-library/svelte": "^5.2.6", 54 | "@types/node": "^22.13.0", 55 | "@types/react": "^19.0.8", 56 | "@types/react-dom": "^19.0.3", 57 | "autoprefixer": "^10.4.20", 58 | "concurrently": "^9.1.2", 59 | "eslint": "9.19.0", 60 | "eslint-config-prettier": "^10.0.1", 61 | "eslint-plugin-only-warn": "^1.1.0", 62 | "eslint-plugin-react": "^7.37.4", 63 | "eslint-plugin-svelte": "^2.46.1", 64 | "globals": "^15.14.0", 65 | "happy-dom": "^16.8.1", 66 | "husky": "^9.1.7", 67 | "lint-staged": "^15.4.3", 68 | "postcss": "^8.5.1", 69 | "prettier": "^3.4.2", 70 | "prettier-plugin-svelte": "^3.3.3", 71 | "react": "19.0.0", 72 | "react-dom": "19.0.0", 73 | "react-youtube": "^10.1.0", 74 | "svelte": "5.19.6", 75 | "svelte-check": "^4.1.4", 76 | "svelte-youtube-lite": "~1.2.1", 77 | "svelte2tsx": "^0.7.34", 78 | "tsd": "^0.31.2", 79 | "typescript": "^5.7.3", 80 | "typescript-eslint": "^8.22.0", 81 | "vite": "^6.0.11", 82 | "vite-tsconfig-paths": "^5.1.4", 83 | "vitest": "^3.0.4" 84 | }, 85 | "dependencies": { 86 | "estree-walker": "^3.0.3", 87 | "magic-string": "^0.30.17" 88 | }, 89 | "peerDependencies": { 90 | "react": ">=16.8.0", 91 | "react-dom": ">=16.8.0", 92 | "svelte": ">=5.0.0" 93 | }, 94 | "svelte": "./dist/index.js", 95 | "files": [ 96 | "./dist" 97 | ], 98 | "types": "./dist/index.d.ts", 99 | "exports": { 100 | ".": { 101 | "types": "./dist/index.d.ts", 102 | "svelte": "./dist/index.js", 103 | "default": "./dist/index.js" 104 | }, 105 | "./preprocessReact": { 106 | "types": "./dist/preprocessReact.d.ts", 107 | "default": "./dist/preprocessReact.js" 108 | }, 109 | "./react-router": { 110 | "types": "./dist/react-router/index.d.ts", 111 | "svelte": "./dist/react-router/index.js", 112 | "default": "./dist/react-router/index.js" 113 | } 114 | }, 115 | "typesVersions": { 116 | ">4.0": { 117 | "index.d.ts": [ 118 | "./dist/index.d.ts" 119 | ], 120 | "preprocessReact": [ 121 | "./dist/preprocessReact.d.ts" 122 | ], 123 | "react-router": [ 124 | "./dist/react-router/index.d.ts" 125 | ] 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/playwright/tests/react-first.test.ts-snapshots/react-first-Hydrate-reactified-Svelte-component-inside-React-server-rendered-page-3-darwin.png -------------------------------------------------------------------------------- /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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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-render-reactified-Svelte-component-inside-a-React-SPA-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/playwright/tests/react-first.test.ts-snapshots/react-first-render-reactified-Svelte-component-inside-a-React-SPA-3-darwin.png -------------------------------------------------------------------------------- /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 | ]; 23 | 24 | test("client-rendered", async ({ page }) => { 25 | // Create Screenshots using Client Side Rendering. 26 | for (const url of urls) { 27 | await page.goto(url); 28 | await expect(page.locator('body[data-ssr="spa"]')).toBeAttached({ 29 | timeout: 10_000, 30 | }); 31 | await expect(page.locator("body")).toHaveScreenshot(); 32 | } 33 | }); 34 | 35 | test("sync screenshots", async () => { 36 | // Copy client-rendered screenshots to be used as reference for server-rendered snapshots. 37 | const snapshotsPath = path.join( 38 | path.dirname(fileURLToPath(import.meta.url)), 39 | "ssr.test.ts-snapshots", 40 | ); 41 | const files = await readdir(snapshotsPath); 42 | let checked = 0; 43 | for (const file of files) { 44 | if (file.match(/client-rendered/)) { 45 | const clientFile = path.join(snapshotsPath, file); 46 | const serverFile = path.join( 47 | snapshotsPath, 48 | file.replace("client-rendered", "no-js-server-rendered"), 49 | ); 50 | const serverInfo = await stat(serverFile).catch(() => false as const); 51 | if (!serverInfo) { 52 | await copyFile(clientFile, serverFile); 53 | } else { 54 | const clientInfo = await stat(clientFile); 55 | if (clientInfo.size !== serverInfo.size) { 56 | await copyFile(clientFile, serverFile); 57 | } 58 | } 59 | checked++; 60 | } 61 | } 62 | expect(checked).toBe(urls.length); 63 | }); 64 | test.describe("no-js", () => { 65 | test.use({ javaScriptEnabled: false }); 66 | test("server-rendered", async ({ page }) => { 67 | for (const url of urls) { 68 | await page.goto(url); 69 | await expect(page.locator("body")).toHaveScreenshot(); 70 | } 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-12-darwin.png -------------------------------------------------------------------------------- /playwright/tests/ssr.test.ts-snapshots/ssr-client-rendered-2-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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-2-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/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/d8649ec01e7cc444d935cb0656dbcfea55bf2651/playwright/tests/ssr.test.ts-snapshots/ssr-no-js-server-rendered-9-darwin.png -------------------------------------------------------------------------------- /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: { ReactDOM: win.ReactDOM }, 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 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | %sveltekit.body% 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/demo/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/demo/components/DebugContext.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
{text}
14 | 15 | 23 | -------------------------------------------------------------------------------- /src/demo/components/DebugContextProvider.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {@render children?.()} 15 | -------------------------------------------------------------------------------- /src/demo/components/Examples.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | { 26 | count = Math.random(); 27 | }} 28 | /> 29 | 30 | A simple alert 31 | -------------------------------------------------------------------------------- /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/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/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/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/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /src/lib/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "svelte"; 2 | import type { Readable } from "svelte/store"; 3 | import type { 4 | IntrinsicElementComponents, 5 | StaticPropComponents, 6 | Sveltified, 7 | } from "./internal/types.js"; 8 | 9 | declare global { 10 | function sveltify< 11 | T extends { 12 | [key: string]: 13 | | keyof JSX.IntrinsicElements 14 | | React.JSXElementConstructor; 15 | }, 16 | >( 17 | reactComponents: T, 18 | ): { 19 | [K in keyof T]: Sveltified & StaticPropComponents; 20 | } & IntrinsicElementComponents; 21 | 22 | function hooks(callback: () => T): (() => T) & Readable; 23 | 24 | const react: IntrinsicElementComponents & { 25 | [component: string]: Component & StaticPropComponents; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/hooks.svelte.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { getContext, onDestroy } from "svelte"; 3 | import { type Readable, writable } from "svelte/store"; 4 | import type { ReactDependencies, TreeNode } from "./internal/types"; 5 | import type { Root } from "react-dom/client"; 6 | 7 | type Dependencies = Omit; 8 | 9 | export default function hooks( 10 | callback: () => T, 11 | dependencies?: Dependencies, 12 | ): (() => T) & Readable { 13 | let state = $state(); 14 | const store = writable(); 15 | 16 | const parent = getContext("ReactWrapper"); 17 | 18 | function Hook() { 19 | state = callback(); 20 | store.set(state); 21 | return null; 22 | } 23 | 24 | if (!dependencies || !dependencies.ReactDOM || !dependencies.flushSync) { 25 | throw new Error( 26 | "{ ReactDOM, flushSync } are not injected, check svelte.config.js for: `preprocess: [preprocessReact()],`", 27 | ); 28 | } 29 | if (parent) { 30 | const hook = { Hook, key: autoKey(parent) }; 31 | parent.hooks.push(hook); 32 | 33 | dependencies.flushSync(() => { 34 | parent.rerender?.("hooks"); 35 | }); 36 | 37 | onDestroy(() => { 38 | const index = parent.hooks.findIndex((h) => h.key === hook.key); 39 | if (index !== -1) { 40 | parent.hooks.splice(index, 1); 41 | } 42 | }); 43 | } else { 44 | onDestroy(standalone(Hook, dependencies)); 45 | } 46 | let subscribe = (fn: (value: T | undefined) => void) => { 47 | console.warn( 48 | "Using a hooks() as store is deprecated, instead use $derived.by: `let [state, setState] = $derived.by(hooks(() => useState(1)));`", 49 | ); 50 | subscribe = store.subscribe; 51 | return store.subscribe(fn); 52 | }; 53 | function signal() { 54 | return state; 55 | } 56 | signal.subscribe = (fn: (value: T | undefined) => void) => { 57 | return subscribe(fn); 58 | }; 59 | return signal as any; 60 | } 61 | 62 | function standalone(Hook: React.FC, dependencies: Dependencies) { 63 | const { renderToString, ReactDOM, flushSync } = dependencies; 64 | if (typeof document === "undefined") { 65 | if (!renderToString) { 66 | throw new Error("renderToString parameter is required for SSR"); 67 | } 68 | renderToString(React.createElement(Hook)); 69 | return () => {}; 70 | } 71 | const el = document.createElement("react-hooks"); 72 | let root: Root | undefined; 73 | if ("createRoot" in ReactDOM) { 74 | root = ReactDOM.createRoot?.(el); 75 | flushSync(() => { 76 | root?.render(React.createElement(Hook)); 77 | }); 78 | } else { 79 | ReactDOM.render(React.createElement(Hook), el); 80 | } 81 | return () => { 82 | if (root) { 83 | root.unmount(); 84 | } else if ("unmountComponentAtNode" in ReactDOM) { 85 | ReactDOM.unmountComponentAtNode(el); 86 | } 87 | }; 88 | } 89 | 90 | const keys = new WeakMap(); 91 | /** 92 | * Get incrementing number per node. 93 | */ 94 | function autoKey(node: TreeNode | undefined) { 95 | if (!node) { 96 | return -1; 97 | } 98 | let key: number | undefined = keys.get(node); 99 | if (key === undefined) { 100 | key = 0; 101 | } else { 102 | key += 1; 103 | } 104 | keys.set(node, key); 105 | return key; 106 | } 107 | -------------------------------------------------------------------------------- /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.svelte.js"; 6 | export { default as used } from "./used.js"; 7 | export { default as useStore } from "./useStore.js"; 8 | -------------------------------------------------------------------------------- /src/lib/internal/Bridge.svelte.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useEffect, createElement } from "react"; 2 | import type { ReactDependencies, TreeNode } from "./types.js"; 3 | import Child from "./Child.js"; 4 | import SvelteFirstContext from "./SvelteFirstContext.js"; 5 | import portalTag from "svelte-preprocess-react/internal/portalTag.js"; 6 | 7 | type BridgeProps = { 8 | node: TreeNode; 9 | createPortal: ReactDependencies["createPortal"]; 10 | source?: "hooks"; 11 | }; 12 | const Bridge: React.FC = ({ node, createPortal, source }) => { 13 | const fresh = useRef(false); 14 | const mounted = useRef(false); 15 | const [result, setResult] = useState(() => { 16 | return renderBridge(node, createPortal, mounted, source); 17 | }); 18 | useEffect( 19 | () => 20 | $effect.root(() => { 21 | $effect(() => { 22 | fresh.current = true; 23 | setResult(renderBridge(node, createPortal, mounted, source)); 24 | }); 25 | }), 26 | [], 27 | ); 28 | if (fresh.current) { 29 | fresh.current = false; 30 | return result; 31 | } 32 | return renderBridge(node, createPortal, mounted, source); 33 | }; 34 | 35 | function renderBridge( 36 | node: TreeNode, 37 | createPortal: BridgeProps["createPortal"], 38 | mounted: { current: boolean }, 39 | source?: "hooks", 40 | ) { 41 | let { children } = node.props; 42 | const props = { ...node.props.reactProps }; 43 | delete props.children; 44 | const { portalTarget, svelteChildren, childrenSource, hooks } = node; 45 | 46 | if (svelteChildren) { 47 | if (!children) { 48 | children = createElement(Child, { 49 | nodeKey: node.key, 50 | key: "svelte$Children", 51 | el: childrenSource, 52 | }); 53 | } else { 54 | console.warn("Can't have both React & Svelte children"); 55 | } 56 | } 57 | if (node.nodes.length !== 0 || hooks.length !== 0) { 58 | children = [ 59 | children, 60 | ...node.nodes.map((subnode) => 61 | createElement(Bridge, { 62 | key: `bridge${subnode.key}`, 63 | createPortal, 64 | node: subnode, 65 | }), 66 | ), 67 | ...hooks.map(({ Hook, key }) => 68 | createElement(Hook, { key: `hook${key}` }), 69 | ), 70 | ]; 71 | } 72 | 73 | const vdom = createElement( 74 | SvelteFirstContext.Provider, 75 | { value: node }, 76 | children === undefined 77 | ? createElement(node.reactComponent, props) 78 | : createElement(node.reactComponent, props, children), 79 | ); 80 | if (portalTarget) { 81 | if (source !== "hooks" && mounted.current === false) { 82 | portalTarget.innerHTML = ""; // Remove injected SSR content 83 | mounted.current = true; 84 | } 85 | return createPortal(vdom, portalTarget); 86 | } 87 | return createElement( 88 | portalTag("react", "portal", "source", node.key), 89 | { style: { display: "none" } }, 90 | vdom, 91 | ); 92 | } 93 | export default Bridge; 94 | -------------------------------------------------------------------------------- /src/lib/internal/Child.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import portalTag from "svelte-preprocess-react/internal/portalTag"; 3 | 4 | type Props = { 5 | nodeKey: string; 6 | el: HTMLElement | undefined; 7 | }; 8 | const Child: React.FC = ({ nodeKey, el }) => { 9 | const ref = React.useRef(undefined); 10 | React.useEffect(() => { 11 | if (!ref.current) { 12 | return; 13 | } 14 | if (el) { 15 | el.style.display = "contents"; 16 | ref.current.appendChild(el); 17 | } 18 | }, [ref, el]); 19 | return React.createElement( 20 | portalTag("react", "children", "target", nodeKey), 21 | { 22 | ref, 23 | style: { display: "contents" }, 24 | }, 25 | ); 26 | }; 27 | export default Child; 28 | -------------------------------------------------------------------------------- /src/lib/internal/ExtractContexts.svelte: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/lib/internal/ReactFirstContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * Context from a reactified parent component. 5 | */ 6 | const ReactFirstContext = React.createContext< 7 | | { 8 | /** Resolves, when the parent svelte component was mounted & contexts are extracted */ 9 | promise: Promise; 10 | contexts?: Map; 11 | } 12 | | undefined 13 | >(undefined); 14 | ReactFirstContext.displayName = "ReactToReactContext"; 15 | export default ReactFirstContext; 16 | -------------------------------------------------------------------------------- /src/lib/internal/ReactWrapper.svelte: -------------------------------------------------------------------------------- 1 | 93 | 94 | 99 | 100 | {#if children} 101 | {@render children()} 106 | {/if} 107 | 108 | {#each slots as slot, i} 109 | {@render slot()} 114 | {/each} 115 | -------------------------------------------------------------------------------- /src/lib/internal/Slot.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import portalTag from "./portalTag"; 3 | import SvelteFirstContext from "./SvelteFirstContext"; 4 | 5 | type Props = { 6 | slot: number; 7 | }; 8 | const Slot: React.FC = ({ slot }) => { 9 | const node = React.useContext(SvelteFirstContext); 10 | const ref = React.useRef(undefined); 11 | const el = node?.slotSources[slot]; 12 | React.useEffect(() => { 13 | if (!ref.current || ref.current.children.length > 0) { 14 | return; 15 | } 16 | if (el) { 17 | el.style.display = "contents"; 18 | ref.current.appendChild(el); 19 | } 20 | }, [ref, node]); 21 | if (!node) { 22 | return null; 23 | } 24 | return React.createElement( 25 | portalTag("react", `slot${slot}`, "target", node.key), 26 | { 27 | ref, 28 | style: { display: "contents" }, 29 | }, 30 | ); 31 | }; 32 | export default Slot; 33 | -------------------------------------------------------------------------------- /src/lib/internal/SvelteFirstContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { TreeNode } from "./types"; 3 | 4 | /** 5 | * Context from a sveltified parent component. 6 | */ 7 | const SvelteFirstContext = React.createContext( 8 | undefined as TreeNode | undefined, 9 | ); 10 | SvelteFirstContext.displayName = "SvelteFirstContext"; 11 | export default SvelteFirstContext; 12 | -------------------------------------------------------------------------------- /src/lib/internal/SvelteWrapper.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | {#if typeof react$children === "undefined"} 45 | 46 | {:else} 47 | {#if typeof react$children === "string"}{react$children}{:else}{/if}{#if setContexts}{/if} 56 | {/if} 58 | -------------------------------------------------------------------------------- /src/lib/internal/detectReactVersion.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | export default async function detectReactVersion() { 5 | try { 6 | const pkg = await fs.readFile(path.resolve(process.cwd(), "package.json")); 7 | const json = JSON.parse(pkg.toString()); 8 | const semver = json.devDependencies.react || json.dependencies.react; 9 | const match = `${semver}`.match(/[^0-9]*([0-9]+)/); 10 | if (match) { 11 | return parseInt(match[1], 10); 12 | } 13 | throw new Error("No react in dependencies"); 14 | } catch { 15 | console.warn('Could not detect React version. Assuming "react@18"'); 16 | return 18; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/internal/portalTag.ts: -------------------------------------------------------------------------------- 1 | export default function portalTag( 2 | framework: "svelte" | "react", 3 | section: "portal" | "children" | `slot${number}`, 4 | direction: "target" | "source", 5 | nodeKey: string, 6 | ): string { 7 | return `${framework}-${section}${nodeKey.replace(/\//g, "-")}${direction}`; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/internal/types.ts: -------------------------------------------------------------------------------- 1 | import type ReactDOMServer from "react-dom/server"; 2 | import type React from "react"; 3 | import type { Root } from "react-dom/client"; 4 | import type { Component, Snippet } from "svelte"; 5 | 6 | export type HandlerName = `on${Capitalize}`; 7 | export type EventName = T extends `on${infer N}` 8 | ? Uncapitalize 9 | : never; 10 | 11 | export type SvelteEventHandlers = 12 | T extends Record 13 | ? Partial, (e: Value) => void | boolean>> 14 | : never; 15 | 16 | type Uppercase = 17 | | "A" 18 | | "B" 19 | | "C" 20 | | "D" 21 | | "E" 22 | | "F" 23 | | "G" 24 | | "H" 25 | | "I" 26 | | "J" 27 | | "K" 28 | | "L" 29 | | "M" 30 | | "N" 31 | | "O" 32 | | "P" 33 | | "Q" 34 | | "R" 35 | | "S" 36 | | "T" 37 | | "U" 38 | | "V" 39 | | "W" 40 | | "X" 41 | | "Y" 42 | | "Z"; 43 | 44 | type ReactEventProp = `on${Uppercase}${string}`; 45 | type ExcludeProps = T extends ReactEventProp ? T : never; 46 | type ExcludeEvents = T extends ReactEventProp ? never : T; 47 | 48 | export type EventProps = Omit< 49 | ReactProps, 50 | ExcludeEvents 51 | >; 52 | export type OmitEventProps = Omit< 53 | ReactProps, 54 | ExcludeProps 55 | >; 56 | 57 | export type TreeNode = SvelteInit & { 58 | reactComponent: React.FunctionComponent | React.ComponentClass; 59 | key: string; 60 | autoKey: number; 61 | nodes: TreeNode[]; 62 | rerender?: (source?: "hooks") => void; 63 | unroot?: () => void; 64 | }; 65 | 66 | export type SvelteInit = { 67 | props: { reactProps: Record; children: React.ReactNode }; // The react props 68 | portalTarget: HTMLElement | undefined; // An element to portal the React component into 69 | childrenSource: HTMLElement | undefined; // An element containing the children from Svelte, inject as children into the React component 70 | slotSources: HTMLElement[]; // An array of elements containing the slots from Svelte, inject as partials into the React component 71 | svelteChildren: Snippet | undefined; // The svelte children prop (snippet/slot) 72 | context: Map; // The full Svelte context 73 | hooks: { Hook: React.FunctionComponent; key: number }[]; 74 | parent?: TreeNode; 75 | }; 76 | 77 | export type ChildrenPropsAsSnippet = T extends { children: unknown } 78 | ? Omit & { children: Snippet | T["children"] } 79 | : T extends { children?: unknown } 80 | ? Omit & { children?: Snippet | T["children"] } 81 | : T; 82 | 83 | export type Sveltified< 84 | T extends 85 | | keyof React.JSX.IntrinsicElements 86 | | React.JSXElementConstructor, 87 | > = Component>>; 88 | 89 | export type IntrinsicElementComponents = { 90 | [K in keyof React.JSX.IntrinsicElements]: Component< 91 | ChildrenPropsAsSnippet> 92 | >; 93 | }; 94 | 95 | /* Primitive typing of `Component.Item` components */ 96 | export type StaticPropComponents = { 97 | [key: string]: Component & { 98 | [key: string]: Component & { 99 | [key: string]: Component; 100 | }; 101 | }; 102 | }; 103 | 104 | export type ReactDependencies = { 105 | ReactDOM: 106 | | { 107 | createRoot: (container: Element) => Root; // React 18 and above 108 | } 109 | | { 110 | render(component: React.ReactNode, container: Element): void; // React 17 and below 111 | unmountComponentAtNode(container: Element): void; 112 | }; 113 | createPortal: ( 114 | children: React.ReactNode, 115 | container: Element | DocumentFragment, 116 | key?: null | string, 117 | ) => React.ReactPortal; 118 | flushSync: (cb: () => void) => void; 119 | renderToString?: typeof ReactDOMServer.renderToString; 120 | }; 121 | 122 | export type ChildrenPropsAsReactNode> = { 123 | [K in keyof T]: K extends "children" ? React.ReactNode : T[K]; 124 | }; 125 | 126 | export type Prettify = { 127 | [K in keyof T]: T[K]; 128 | } & {}; 129 | -------------------------------------------------------------------------------- /src/lib/preprocessReact.js: -------------------------------------------------------------------------------- 1 | import MagicString from "magic-string"; 2 | import { parse, preprocess } from "svelte/compiler"; 3 | import detectReactVersion from "./internal/detectReactVersion.js"; 4 | import { walk } from "estree-walker"; 5 | 6 | /** 7 | * @typedef {import("svelte/compiler").PreprocessorGroup} PreprocessorGroup 8 | * @typedef {import("svelte/compiler").Processed} Processed 9 | */ 10 | 11 | const defaults = /** @type {const} */ ({ 12 | react: "auto", 13 | ssr: true, 14 | }); 15 | 16 | /** 17 | * Svelte preprocessor to use convert tags into Sveltified React components. 18 | * 19 | * Imports renderToString from 'react-dom/server' unless the `ssr` option is set to false. 20 | * 21 | * @param {{ 22 | * ssr?: boolean; 23 | * react?: number | "auto"; 24 | * preprocess?: PreprocessorGroup | PreprocessorGroup[]; 25 | * }} options 26 | * @returns {PreprocessorGroup} 27 | */ 28 | export default function preprocessReact(options = {}) { 29 | let react = options.react ?? defaults.react; 30 | const ssr = options.ssr ?? defaults.ssr; 31 | 32 | return { 33 | async markup({ content, filename }) { 34 | /** @type {Processed | undefined} */ 35 | let preprocessed; 36 | if (options.preprocess) { 37 | preprocessed = await preprocess(content, options.preprocess, { 38 | filename, 39 | }); 40 | 41 | content = preprocessed.code; 42 | } 43 | 44 | if (react === "auto") { 45 | react = await detectReactVersion(); 46 | } 47 | const processed = transform(content, { 48 | filename, 49 | react, 50 | ssr, 51 | }); 52 | if (!preprocessed) { 53 | return processed; 54 | } 55 | if (!processed.map) { 56 | return preprocessed; 57 | } 58 | return { 59 | code: processed.code, 60 | map: preprocessed.map ?? processed.map, // @todo apply sourcemaps 61 | dependencies: preprocessed.dependencies, 62 | }; 63 | }, 64 | }; 65 | } 66 | const prefix = "inject$$"; 67 | 68 | /** 69 | * @param {string} content 70 | * @param {{ 71 | * filename?: string; 72 | * react: number; 73 | * ssr: boolean; 74 | * }} options 75 | * @returns 76 | */ 77 | function transform(content, options) { 78 | /** @type {string} */ 79 | 80 | const packageName = "svelte-preprocess-react"; 81 | const imports = []; 82 | const sveltifyDeps = [`ReactDOM: ${prefix}ReactDOM`]; 83 | const hooksDeps = [`ReactDOM: ${prefix}ReactDOM`]; 84 | if (options.react >= 18) { 85 | imports.push(`import ${prefix}ReactDOM from "react-dom/client";`); 86 | sveltifyDeps.push(`createPortal: ${prefix}createPortal`); 87 | hooksDeps.push(`flushSync: ${prefix}flushSync`); 88 | } else { 89 | imports.push(`import ${prefix}ReactDOM from "react-dom";`); 90 | sveltifyDeps.push(`createPortal: ${prefix}ReactDOM.createPortal`); 91 | hooksDeps.push(`flushSync: ${prefix}ReactDOM.flushSync`); 92 | } 93 | 94 | if (options.ssr) { 95 | imports.push( 96 | `import { renderToString as ${prefix}renderToString } from "react-dom/server";`, 97 | ); 98 | sveltifyDeps.push(`renderToString: ${prefix}renderToString`); 99 | hooksDeps.push(`renderToString: ${prefix}renderToString`); 100 | } 101 | 102 | const ast = parse(content, { 103 | filename: options.filename, 104 | modern: false, 105 | }); 106 | const s = new MagicString(content, { filename: options.filename }); 107 | const components = replaceReactTags(ast.html, s, options.filename); 108 | const aliases = Object.entries(components); 109 | 110 | /** @type {Set<'sveltify' | 'hooks'>} */ 111 | let imported = new Set(); 112 | /** @type {Set<'sveltify' | 'hooks'>} */ 113 | let used = new Set(); 114 | let defined = false; 115 | /** @type {false|Set} */ 116 | let aliased = false; 117 | 118 | /** 119 | * Detect sveltify import and usage 120 | * 121 | * @param {import('estree-walker').Node} node 122 | * @param {import('estree-walker').Node|null} parent 123 | */ 124 | function enter(node, parent) { 125 | if (node.type === "Identifier" && node.name === "sveltify" && parent) { 126 | if ( 127 | parent.type === "ImportSpecifier" || 128 | parent.type === "ImportDeclaration" 129 | ) { 130 | imported.add("sveltify"); 131 | } else if (parent.type === "CallExpression") { 132 | if ( 133 | parent?.arguments.length === 1 && 134 | "end" in parent.arguments[0] && 135 | typeof parent.arguments[0].end === "number" 136 | ) { 137 | s.appendRight( 138 | parent.arguments[0].end, 139 | `, { ${sveltifyDeps.join(", ")} }`, 140 | ); 141 | } 142 | 143 | const componentsArg = parent.arguments[0]; 144 | if (!aliased && componentsArg.type === "ObjectExpression") { 145 | aliased = new Set(); 146 | for (const property of componentsArg.properties) { 147 | if (property.type === "Property") { 148 | if (property.key.type === "Identifier") { 149 | aliased.add(property.key.name); 150 | } 151 | } 152 | } 153 | if ("end" in componentsArg && typeof componentsArg.end === "number") { 154 | for (const [alias, { expression }] of aliases) { 155 | if (!aliased.has(alias)) { 156 | s.appendRight( 157 | componentsArg.end - 1, 158 | `, ${alias}: ${expression === expression.toLowerCase() ? JSON.stringify(expression) : expression} `, 159 | ); 160 | } 161 | } 162 | } else { 163 | console.warn("missing end in Node"); 164 | } 165 | } 166 | used.add("sveltify"); 167 | } 168 | } 169 | 170 | if (node.type === "Identifier" && node.name === "hooks" && parent) { 171 | if ( 172 | parent.type === "ImportSpecifier" || 173 | parent.type === "ImportDeclaration" 174 | ) { 175 | imported.add("hooks"); 176 | } else if (parent.type === "CallExpression") { 177 | if ( 178 | parent?.arguments.length === 1 && 179 | "end" in parent.arguments[0] && 180 | typeof parent.arguments[0].end === "number" 181 | ) { 182 | s.appendRight( 183 | parent.arguments[0].end, 184 | `, { ${hooksDeps.join(", ")} }`, 185 | ); 186 | } 187 | used.add("hooks"); 188 | } 189 | } 190 | if ( 191 | node.type === "Identifier" && 192 | node.name === "react" && 193 | parent?.type === "VariableDeclarator" 194 | ) { 195 | defined = true; 196 | } 197 | } 198 | if (ast.module) { 199 | walk(ast.module, { enter }); 200 | } 201 | if (ast.instance) { 202 | walk(ast.instance, { enter }); 203 | } 204 | if (used.size === 0 && aliases.length === 0) { 205 | return { code: content }; 206 | } 207 | let declarators = []; 208 | if ( 209 | !imported.has("sveltify") && 210 | (used.has("sveltify") || aliases.length > 0) 211 | ) { 212 | declarators.push("sveltify"); 213 | } 214 | if (!imported.has("hooks") && used.has("hooks")) { 215 | declarators.push("hooks"); 216 | } 217 | if (declarators.length > 0) { 218 | imports.push(`import { ${declarators.join(", ")} } from "${packageName}";`); 219 | } 220 | 221 | const script = ast.instance || ast.module; 222 | let wrappers = []; 223 | if (!defined && aliases.length > 0) { 224 | wrappers.push( 225 | `const react = sveltify({ ${Object.entries(components) 226 | .map(([alias, { expression }]) => { 227 | if (expression !== alias) { 228 | return `${alias}: ${expression}`; 229 | } 230 | if (expression.toLowerCase() === expression) { 231 | return `${expression.match(/^[a-z]+$/) ? expression : JSON.stringify(expression)}: ${JSON.stringify(expression)}`; 232 | } 233 | return expression; 234 | }) 235 | .join(", ")} }, { ${sveltifyDeps.join(", ")} });`, 236 | ); 237 | used.add("sveltify"); 238 | } 239 | if (used.has("sveltify") && used.has("hooks")) { 240 | imports.push( 241 | `import { createPortal as ${prefix}createPortal, flushSync as ${prefix}flushSync } from "react-dom";`, 242 | ); 243 | } else if (used.has("sveltify")) { 244 | imports.push( 245 | `import { createPortal as ${prefix}createPortal } from "react-dom";`, 246 | ); 247 | } else if (used.has("hooks")) { 248 | imports.push( 249 | `import { flushSync as ${prefix}flushSync } from "react-dom";`, 250 | ); 251 | } 252 | 253 | if (Object.values(components).find((c) => c.dispatcher)) { 254 | imports.push( 255 | 'import { createEventDispatcher as React$$createEventDispatcher } from "svelte";', 256 | ); 257 | wrappers.push("const React$$dispatch = React$$createEventDispatcher();"); 258 | } 259 | if (!script) { 260 | s.prepend( 261 | `\n\n`, 262 | ); 263 | } else { 264 | /** @type {any} */ 265 | const program = script.content; 266 | s.appendRight(program.end, `;${wrappers.join(" ")}`); 267 | s.appendRight(program.start, imports.join(" ")); 268 | } 269 | return { 270 | code: s.toString(), 271 | map: s.generateMap(), 272 | }; 273 | } 274 | 275 | /** 276 | * Replace react:* tags by injecting Sveltified versions of the React components. 277 | * 278 | * @param {any} node 279 | * @param {MagicString} content 280 | * @param {string | undefined} filename 281 | * @param {Record} components 282 | */ 283 | function replaceReactTags(node, content, filename, components = {}) { 284 | if ( 285 | (node.type === "Element" && node.name.startsWith("react:")) || 286 | (node.type === "InlineComponent" && node.name.startsWith("react.")) 287 | ) { 288 | let legacy = node.name.startsWith("react:"); 289 | 290 | if (legacy) { 291 | let location = ""; 292 | if (filename) { 293 | location += ` in ${filename}`; 294 | } 295 | if (node.start) { 296 | location += ` on line ${content.original.substring(0, node.start).split("\n").length}`; 297 | } 298 | console.warn( 299 | `'<${node.name}' syntax is deprecated, use '`) { 317 | // Replace closing tag with alias 318 | const fullTag = content.slice(node.start, node.end - 1); 319 | const whitespaceLength = fullTag.length - fullTag.trimEnd().length; 320 | const tagEnd = node.end - whitespaceLength - node.name.length - 1; 321 | if (content.slice(tagEnd - 2, tagEnd + 6) !== ``, 324 | ); 325 | } else { 326 | content.overwrite( 327 | tagEnd, 328 | tagEnd + node.name.length, 329 | `react.${alias}`, 330 | ); 331 | } 332 | } 333 | } 334 | 335 | if (!components[alias]) { 336 | components[alias] = { dispatcher: false, expression }; 337 | } 338 | /** @type {string[]|false}*/ 339 | let spread = 340 | node.attributes.find( 341 | (/** @type {any}*/ node) => node.type === "Spread", 342 | ) === undefined 343 | ? false 344 | : []; 345 | 346 | node.attributes.forEach((/** @type {any} */ attr) => { 347 | if (attr.type === "Binding") { 348 | let location = ""; 349 | if (filename) { 350 | location += ` in ${filename}`; 351 | } 352 | if (node.start) { 353 | location += ` on line ${content.original.substring(0, node.start).split("\n").length}`; 354 | } 355 | console.warn( 356 | `Two-way binding is not compatible with React components: 357 | ${content.slice( 358 | node.start, 359 | node.attributes[0].start, 360 | )}${content.slice(attr.start, attr.end)}>${location}`, 361 | ); 362 | } 363 | if (attr.type === "EventHandler") { 364 | const event = attr; 365 | const eventStart = event.start; 366 | if (event.modifiers.length > 0) { 367 | throw new Error(`event modifiers are not supported`); 368 | } 369 | if (event.expression !== null) { 370 | content.overwrite( 371 | eventStart, 372 | eventStart + 4, 373 | `on${event.name[0].toUpperCase()}`, 374 | ); 375 | } else { 376 | content.overwrite( 377 | eventStart, 378 | eventStart + 3 + event.name.length, 379 | `on${ 380 | event.name[0].toUpperCase() + event.name.substring(1) 381 | }={(e) => React$$dispatch(${JSON.stringify(event.name)}, e)}`, 382 | ); 383 | components[alias].dispatcher = true; 384 | } 385 | } else if (spread) { 386 | if (attr.type === "Spread") { 387 | spread.push( 388 | `...${content.slice(attr.expression.start, attr.expression.end)}`, 389 | ); 390 | } else if (attr.name !== "children") { 391 | let prop = 392 | attr.name.indexOf("-") === -1 393 | ? attr.name 394 | : JSON.stringify(attr.name); 395 | let value = ""; 396 | if (typeof attr.value === "boolean") { 397 | value = JSON.stringify(attr.value); 398 | } else if (Array.isArray(attr.value) && attr.value.length === 1) { 399 | if (attr.value[0].type === "Text") { 400 | value = JSON.stringify( 401 | content.slice(attr.value[0].start, attr.value[0].end), 402 | ); 403 | } else if (attr.value[0].type === "MustacheTag") { 404 | value = content.slice( 405 | attr.value[0].start + 1, 406 | attr.value[0].end - 1, 407 | ); 408 | } else { 409 | console.warn( 410 | `Unexpected attribute type: ${attr.value[0].type}`, 411 | content.slice(attr.start, attr.end), 412 | ); 413 | } 414 | } else { 415 | console.warn( 416 | "Unexpected attribute syntax:", 417 | content.slice(attr.start, attr.end), 418 | ); 419 | } 420 | spread.push(`${prop}: ${value}`); 421 | } 422 | } 423 | }); 424 | if (spread) { 425 | content.overwrite( 426 | node.attributes[0].start, 427 | node.attributes[node.attributes.length - 1].end, 428 | `react$props={{ ${spread.join(", ")} }}`, 429 | ); 430 | } 431 | if (node.children && !legacy) { 432 | if (node.children.length === 0) { 433 | const childrenProp = 434 | Array.isArray(node.attributes) && 435 | node.attributes.find( 436 | (/** @type {any} */ attr) => attr.name === "children", 437 | ); 438 | if (childrenProp) { 439 | // If children are passed as attribute, pass the value as-is to the react component. 440 | content.appendLeft(childrenProp.start, "react$"); // renames "children" to "react$children" 441 | } 442 | } else { 443 | const isTextContent = 444 | node.children.filter( 445 | (/** @type {any} */ child) => 446 | ["Text", "MustacheTag"].includes(child.type) === false, 447 | ).length === 0; 448 | /** @type {string[]} */ 449 | const escaped = []; 450 | if (isTextContent) { 451 | // Convert text & expressions into a children prop. 452 | escaped.push('"'); 453 | node.children.forEach((/** @type {any} */ child) => { 454 | if (child.type === "Text") { 455 | escaped.push( 456 | child.data.replace(/"/g, `{'"'}`).replace(/\n/g, `{'\\n'}`), 457 | ); 458 | } else if (child.type === "MustacheTag") { 459 | const expression = content.original.slice(child.start, child.end); 460 | escaped.push(expression); 461 | } else { 462 | throw new Error(`Unexpected node type:${child.type}`); 463 | } 464 | }); 465 | escaped.push('"'); 466 | // slot was converted to children prop 467 | content.appendRight( 468 | node.children[0].start - 1, 469 | ` react$children=${escaped.join("")} /`, 470 | ); 471 | content.remove(node.children[0].start, node.end); 472 | return components; 473 | } 474 | } 475 | } 476 | } 477 | /** 478 | * @param {any} child 479 | */ 480 | function processChild(child) { 481 | replaceReactTags(child, content, filename, components); 482 | } 483 | // traverse children & branching blocks 484 | node.children?.forEach(processChild); 485 | node.else?.children?.forEach(processChild); 486 | node.pending?.children?.forEach(processChild); 487 | node.then?.children?.forEach(processChild); 488 | node.catch?.children?.forEach(processChild); 489 | return components; 490 | } 491 | -------------------------------------------------------------------------------- /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/react-router/NavLink.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { RouteCondition } from "./types.js"; 3 | import locationToUrl from "./internal/locationToUrl.js"; 4 | import useRouterContext from "./internal/useRouterContext.js"; 5 | import Link, { type LinkProps } from "./Link.js"; 6 | 7 | export type NavLinkProps = Omit< 8 | LinkProps, 9 | "className" | "style" | "children" 10 | > & { 11 | children?: React.ReactNode | ((condition: RouteCondition) => React.ReactNode); 12 | className?: string | ((condition: RouteCondition) => string | undefined); 13 | style?: 14 | | React.CSSProperties 15 | | ((condition: RouteCondition) => React.CSSProperties | undefined); 16 | }; 17 | const NavLink: React.FC = ({ 18 | className, 19 | style, 20 | children, 21 | ...rest 22 | }) => { 23 | const context = useRouterContext(); 24 | const attrs: LinkProps = rest; 25 | const target = locationToUrl(attrs.to, context.url).toString(); 26 | const current = locationToUrl(context.url, context.url).toString(); 27 | const isActive = target === current; 28 | const condition: RouteCondition = { isActive }; 29 | if (typeof className === "function") { 30 | attrs.className = className(condition); 31 | } else if (isActive) { 32 | attrs.className = className ? `${className} active` : "active"; 33 | } else { 34 | attrs.className = className; 35 | } 36 | 37 | attrs.style = typeof style === "function" ? style(condition) : style; 38 | 39 | return React.createElement( 40 | Link, 41 | attrs, 42 | typeof children === "function" ? children(condition) : children, 43 | ); 44 | }; 45 | export default NavLink; 46 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/lib/reactify.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRawSnippet, mount, unmount, type Component } from "svelte"; 3 | import { render } from "svelte/server"; 4 | import SvelteFirstContext from "./internal/SvelteFirstContext.js"; 5 | import ReactFirstContext from "./internal/ReactFirstContext.js"; 6 | import SvelteWrapper from "./internal/SvelteWrapper.svelte"; 7 | import type { ChildrenPropsAsReactNode } from "svelte-preprocess-react/internal/types.js"; 8 | 9 | const server = typeof document === "undefined"; 10 | const cache = new WeakMap, React.FunctionComponent>(); 11 | 12 | let $$payload: any; 13 | 14 | /** 15 | * Convert a Svelte components into React components. 16 | */ 17 | export default function reactify< 18 | T extends Component | Record>, 19 | >( 20 | svelteComponents: T, 21 | ): T extends Component 22 | ? React.FunctionComponent> 23 | : { 24 | [K in keyof T]: T[K] extends Component 25 | ? React.FunctionComponent> 26 | : React.FunctionComponent; 27 | } { 28 | if (typeof svelteComponents === "function") { 29 | return single(svelteComponents) as any; 30 | } 31 | const reactComponents: Record> = {}; 32 | for (const key in svelteComponents) { 33 | reactComponents[key] = single((svelteComponents as any)[key], key); 34 | } 35 | return reactComponents as any; 36 | } 37 | 38 | function single

>( 39 | SvelteComponent: Component

, 40 | name?: string, 41 | ): React.FunctionComponent { 42 | const hit = cache.get(SvelteComponent); 43 | if (hit) { 44 | return hit; 45 | } 46 | if (!name) { 47 | name = SvelteComponent.name; 48 | } 49 | const named = { 50 | [name](options: any) { 51 | const { children, ...props } = options; 52 | const wrapperRef = React.useRef(undefined); 53 | const sveltePropsRef = React.useRef<(props: P) => void>(undefined); 54 | const svelteChildrenRef = React.useRef(undefined); 55 | const reactChildrenRef = React.useRef(undefined); 56 | const node = React.useContext(SvelteFirstContext); 57 | 58 | const nestedContext = React.useContext(ReactFirstContext); 59 | const nestedRef = React.useRef( 60 | {} as { 61 | promise: Promise; 62 | contexts?: Map; 63 | resolve: () => void; 64 | }, 65 | ); 66 | if (typeof nestedRef.current.promise === "undefined") { 67 | nestedRef.current.promise = new Promise((resolve) => { 68 | nestedRef.current.resolve = resolve; 69 | }); 70 | } 71 | const nodeKey = node?.key ?? "/island/"; 72 | 73 | const mountComponent = React.useCallback((target: HTMLElement) => { 74 | return mount(SvelteWrapper, { 75 | target, 76 | props: { 77 | SvelteComponent: SvelteComponent as any, 78 | nodeKey: node?.key ?? "/island/", 79 | react$children: node ? children : children ? [] : undefined, 80 | setContexts: node 81 | ? undefined 82 | : (value: Map) => { 83 | nestedRef.current.contexts = value; 84 | }, 85 | props, 86 | setSlot: (el: HTMLElement | undefined) => { 87 | if (el && reactChildrenRef.current) { 88 | el.appendChild(reactChildrenRef.current); 89 | } 90 | svelteChildrenRef.current = el; 91 | }, 92 | }, 93 | context: node?.context ?? nestedContext?.contexts, 94 | }); 95 | }, []); 96 | 97 | // Mount the Svelte component 98 | React.useEffect(() => { 99 | const target = wrapperRef.current; 100 | if (!target) { 101 | return undefined; 102 | } 103 | let component: ReturnType; 104 | if (nestedContext) { 105 | void nestedContext.promise.then(() => { 106 | component = mountComponent(target); 107 | nestedRef.current.resolve(); 108 | }); 109 | } else { 110 | component = mountComponent(target); 111 | nestedRef.current.resolve(); 112 | } 113 | 114 | sveltePropsRef.current = (globalThis as any).$$reactifySetProps; 115 | return () => { 116 | void (async () => { 117 | await nestedContext?.promise; 118 | await unmount(component); 119 | })(); 120 | }; 121 | }, [wrapperRef]); 122 | 123 | // Sync props & events 124 | React.useEffect(() => { 125 | if (sveltePropsRef.current) { 126 | sveltePropsRef.current(props); 127 | } 128 | }, [props, sveltePropsRef]); 129 | 130 | // Sync children/slot 131 | React.useEffect(() => { 132 | if (reactChildrenRef.current) { 133 | if ( 134 | svelteChildrenRef.current && 135 | reactChildrenRef.current.parentElement !== svelteChildrenRef.current 136 | ) { 137 | svelteChildrenRef.current.appendChild(reactChildrenRef.current); 138 | } else if (node === undefined) { 139 | reactChildrenRef.current.style.display = "contents"; 140 | } 141 | } else if (svelteChildrenRef.current) { 142 | svelteChildrenRef.current.innerHTML = ""; 143 | } 144 | }, [reactChildrenRef]); 145 | 146 | if (server) { 147 | let html = ""; 148 | if ($$payload) { 149 | const len = $$payload.out.length; 150 | (SvelteWrapper as any)($$payload, { 151 | SvelteComponent, 152 | nodeKey, 153 | props, 154 | react$children: children, 155 | }); 156 | html = $$payload.out.slice(len); 157 | $$payload.out = $$payload.out.slice(0, len); 158 | } else { 159 | if (children && !props.children) { 160 | props.children = createRawSnippet(() => ({ 161 | render() { 162 | return ""; 163 | }, 164 | })); 165 | } 166 | const result = render(SvelteComponent as any, { 167 | props, 168 | context: node?.context, 169 | }); 170 | if (typeof result.head === "string") { 171 | html += result.head; 172 | } 173 | if (typeof result.body === "string") { 174 | html += result.body; 175 | } 176 | } 177 | if (!node) { 178 | return React.createElement("reactify-svelte", { 179 | key: "reactify", 180 | ref: wrapperRef, 181 | node: undefined, 182 | style: { display: "contents" }, 183 | suppressHydrationWarning: true, 184 | dangerouslySetInnerHTML: { __html: html }, 185 | }); 186 | } 187 | return [ 188 | React.createElement("reactify-svelte", { 189 | key: "reactify", 190 | ref: wrapperRef, 191 | node: nodeKey, 192 | style: { display: "contents" }, 193 | suppressHydrationWarning: true, 194 | dangerouslySetInnerHTML: { __html: html }, 195 | }), 196 | ...(children 197 | ? [ 198 | React.createElement( 199 | "react-children", 200 | { 201 | key: "react-children", 202 | node: nodeKey, 203 | style: { display: "none" }, 204 | }, 205 | children, 206 | ), 207 | ] 208 | : []), 209 | ]; 210 | } 211 | 212 | return React.createElement( 213 | "reactify-svelte", 214 | { 215 | key: "reactify", 216 | ref: wrapperRef, 217 | node: node?.key, 218 | style: { display: "contents" }, 219 | suppressHydrationWarning: true, 220 | }, 221 | children 222 | ? React.createElement( 223 | "react-children", 224 | { 225 | key: "react-children", 226 | ref: reactChildrenRef, 227 | node: node?.key, 228 | style: { display: "none" }, 229 | }, 230 | node 231 | ? children 232 | : React.createElement( 233 | ReactFirstContext.Provider, 234 | { value: nestedRef.current }, 235 | children, 236 | ), 237 | ) 238 | : undefined, 239 | ); 240 | }, 241 | }; 242 | if (name) { 243 | (named[name] as React.FC).displayName = name; 244 | } 245 | cache.set(SvelteComponent, named[name]); 246 | return named[name]; 247 | } 248 | 249 | export function setPayload(payload: any) { 250 | $$payload = payload; 251 | } 252 | -------------------------------------------------------------------------------- /src/lib/sveltify.svelte.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { 3 | ChildrenPropsAsSnippet, 4 | IntrinsicElementComponents, 5 | ReactDependencies, 6 | StaticPropComponents, 7 | SvelteInit, 8 | Sveltified, 9 | TreeNode, 10 | } from "./internal/types.js"; 11 | import Bridge from "./internal/Bridge.svelte.js"; 12 | import ReactWrapper from "./internal/ReactWrapper.svelte"; 13 | import { setPayload } from "./reactify.js"; 14 | import type { Component } from "svelte"; 15 | import portalTag from "svelte-preprocess-react/internal/portalTag.js"; 16 | 17 | let sharedRoot: TreeNode | undefined; 18 | 19 | export default sveltify; 20 | 21 | type Dependencies = Omit; 22 | 23 | function sveltify< 24 | T extends { 25 | [key: string]: 26 | | keyof React.JSX.IntrinsicElements 27 | | React.JSXElementConstructor; 28 | }, 29 | >( 30 | components: T, 31 | dependencies?: Dependencies, 32 | ): { 33 | [K in keyof T]: Sveltified & StaticPropComponents; 34 | } & IntrinsicElementComponents; 35 | /** 36 | * Convert a React components into Svelte components. 37 | */ 38 | function sveltify< 39 | T extends React.FC | React.ComponentClass | React.JSXElementConstructor, 40 | >(components: T, dependencies?: Dependencies): Sveltified; 41 | function sveltify(components: any, dependencies?: Dependencies): any { 42 | if (!dependencies) { 43 | throw new Error( 44 | "{ createPortal, ReactDOM } are not injected, check svelte.config.js for: `preprocess: [preprocessReact()],`", 45 | ); 46 | } 47 | if ( 48 | typeof components !== "object" || // React.FC or JSXIntrinsicElements 49 | ("render" in components && typeof components.render === "function") || // React.ComponentClass 50 | "_context" in components || // a Context.Provider 51 | ("Provider" in components && components.Provider === components) // a React 19 Context.Provider 52 | ) { 53 | return single(components as React.FC, dependencies); 54 | } 55 | return multiple(components, dependencies); 56 | } 57 | 58 | type CacheEntry = Dependencies & { Sveltified: unknown }; 59 | const cache = new WeakMap(); 60 | const intrinsicElementCache: Record = {}; 61 | 62 | function multiple< 63 | T extends { 64 | [key: string]: React.FC | React.ComponentClass; 65 | }, 66 | >( 67 | reactComponents: T, 68 | dependencies: Dependencies, 69 | ): { 70 | [K in keyof T]: Component>>; 71 | } { 72 | return Object.fromEntries( 73 | Object.entries(reactComponents).map(([key, reactComponent]) => { 74 | if (reactComponent === undefined) { 75 | return [key, undefined]; 76 | } 77 | const hit = 78 | typeof reactComponent === "string" 79 | ? intrinsicElementCache[reactComponent] 80 | : cache.get(reactComponent); 81 | if ( 82 | hit && 83 | hit.createPortal === dependencies.createPortal && 84 | hit.ReactDOM === dependencies.ReactDOM && 85 | hit.renderToString === dependencies.renderToString 86 | ) { 87 | return [key, hit.Sveltified]; 88 | } 89 | const entry = { 90 | ...dependencies, 91 | Sveltified: single(reactComponent, dependencies), 92 | }; 93 | if (typeof reactComponent === "string") { 94 | intrinsicElementCache[reactComponent] = entry; 95 | } else if (typeof reactComponent === "function") { 96 | cache.set(reactComponent, entry); 97 | } 98 | return [key, entry.Sveltified]; 99 | }), 100 | ) as any; 101 | } 102 | 103 | function single( 104 | reactComponent: T, 105 | dependencies: Dependencies, 106 | ): Component>> { 107 | const client = typeof document !== "undefined"; 108 | const { createPortal, ReactDOM, renderToString } = dependencies; 109 | if ( 110 | typeof reactComponent !== "function" && 111 | typeof reactComponent === "object" && 112 | reactComponent !== null && 113 | "default" in reactComponent && 114 | typeof (reactComponent as any).default === "function" 115 | ) { 116 | // Fix SSR import issue where node doesn't import the esm version. 'react-youtube' 117 | reactComponent = (reactComponent as any).default; 118 | } 119 | 120 | function Sveltified(anchorOrPayload: any, $$props: any) { 121 | let standalone = !sharedRoot; 122 | 123 | $$props.svelteInit = (init: SvelteInit) => { 124 | let unroot: undefined | (() => void); 125 | if (!init.parent && !sharedRoot) { 126 | let portalTarget = $state(); 127 | const hooks = $state<{ Hook: React.FunctionComponent; key: number }[]>( 128 | [], 129 | ); 130 | const rootNode: TreeNode = { 131 | key: 132 | typeof anchorOrPayload.anchor === "undefined" 133 | ? "/" 134 | : `${anchorOrPayload.anchor}-${anchorOrPayload.out.length}/}`, 135 | autoKey: 0, 136 | reactComponent: ({ children }: any) => children as React.ReactNode, 137 | get portalTarget() { 138 | return portalTarget; 139 | }, 140 | props: { reactProps: {}, children: null }, 141 | childrenSource: undefined, 142 | slotSources: [], 143 | svelteChildren: undefined, 144 | nodes: [], 145 | context: new Map(), 146 | get hooks() { 147 | return hooks; 148 | }, 149 | }; 150 | sharedRoot = rootNode; 151 | if (!client) { 152 | unroot = () => { 153 | sharedRoot = undefined; 154 | }; 155 | void Promise.resolve().then(() => { 156 | if (sharedRoot === rootNode) { 157 | console.warn("unroot() was not called, did rendering fail?"); 158 | sharedRoot = undefined; 159 | } 160 | }); 161 | } 162 | if (client) { 163 | const rootEl = document.createElement("react-root"); 164 | const root = 165 | "createRoot" in ReactDOM ? ReactDOM.createRoot(rootEl) : undefined; 166 | portalTarget = document.createElement("bridge-root"); 167 | document.head.appendChild(rootEl); 168 | document.head.appendChild(portalTarget); 169 | 170 | if (root) { 171 | rootNode.rerender = (source) => { 172 | root.render( 173 | React.createElement(Bridge, { 174 | node: rootNode, 175 | createPortal, 176 | source, 177 | }), 178 | ); 179 | }; 180 | } else { 181 | if (!("render" in ReactDOM)) { 182 | throw new Error("ReactDOM.render is required to use sveltify"); 183 | } 184 | rootNode.rerender = (source) => { 185 | ReactDOM.render( 186 | React.createElement(Bridge, { 187 | node: rootNode, 188 | createPortal, 189 | source, 190 | }), 191 | rootEl, 192 | ); 193 | }; 194 | } 195 | } 196 | } 197 | let parent = init.parent; 198 | if (!parent) { 199 | parent = sharedRoot as TreeNode; 200 | standalone = true; 201 | } 202 | parent.autoKey += 1; 203 | const key = `${parent.key}${parent.autoKey}/`; 204 | const node: TreeNode = Object.assign(init, { 205 | key, 206 | autoKey: 0, 207 | reactComponent, 208 | nodes: [], 209 | rerender: parent.rerender, 210 | unroot, 211 | }); 212 | parent.nodes.push(node); 213 | if (client) { 214 | parent.rerender?.(); 215 | } 216 | return node; 217 | }; 218 | 219 | (ReactWrapper as any)(anchorOrPayload, $$props); 220 | 221 | if (standalone && !client) { 222 | let renderError: Error | undefined; 223 | if (renderToString && sharedRoot) { 224 | setPayload(anchorOrPayload); 225 | try { 226 | const html = renderToString( 227 | React.createElement(Bridge, { node: sharedRoot, createPortal }), 228 | ); 229 | const source = { html }; 230 | applyPortals(anchorOrPayload, sharedRoot, source); 231 | } catch (err) { 232 | renderError = (err as Error) ?? new Error("Unknown error"); 233 | sharedRoot = undefined; 234 | } 235 | setPayload(undefined); 236 | if (renderError) { 237 | throw renderError; 238 | } 239 | } 240 | } 241 | } 242 | 243 | return Sveltified as any; 244 | } 245 | 246 | /** 247 | * Merge output rendered by React into the correct place of the html. 248 | * (Mutates target and source objects) 249 | */ 250 | function applyPortals( 251 | $$payload: { out: string }, 252 | node: TreeNode, 253 | source: { html: string }, 254 | ) { 255 | node.nodes.forEach((subnode) => applyPortals($$payload, subnode, source)); 256 | if (node === sharedRoot) { 257 | return; 258 | } 259 | applyPortal($$payload, node, source); 260 | } 261 | 262 | function applyPortal( 263 | $$payload: { out: string }, 264 | node: TreeNode, 265 | source: { html: string }, 266 | ) { 267 | if (node.svelteChildren !== undefined) { 268 | const child = extract( 269 | portalTag("svelte", "children", "source", node.key), 270 | $$payload.out, 271 | ); 272 | try { 273 | source.html = inject( 274 | portalTag("react", "children", "target", node.key), 275 | child.innerHtml, 276 | source.html, 277 | ); 278 | } catch { 279 | // The React component didn't render the children, the rendering of children can be conditional. 280 | } 281 | } 282 | try { 283 | const portal = extract( 284 | portalTag("react", "portal", "source", node.key), 285 | source.html, 286 | ); 287 | 288 | source.html = portal.outerRemoved; 289 | $$payload.out = inject( 290 | portalTag("svelte", "portal", "target", node.key), 291 | portal.innerHtml, 292 | $$payload.out, 293 | ); 294 | } catch (err) { 295 | if (!node.parent) { 296 | throw err; 297 | } 298 | // Nested component didn't render, could be suspense or conditional rendering. 299 | } 300 | } 301 | 302 | function extract(tag: string, html: string) { 303 | const open = `<${tag}`; 304 | const close = ``; 305 | const position = html.indexOf(open); 306 | if (position === -1) { 307 | throw new Error(`Couldn't find '${open}'`); 308 | } 309 | const start = html.indexOf(">", position + open.length) + 1; 310 | const end = html.indexOf(close, start); 311 | if (end === -1) { 312 | throw new Error(`Couldn't find '${close}'`); 313 | } 314 | const innerHtml = html.substring(start, end); 315 | const outerRemoved = 316 | html.substring(0, start) + html.substring(end + close.length); 317 | 318 | return { 319 | innerHtml, 320 | outerRemoved, 321 | }; 322 | } 323 | 324 | function inject(tag: string, content: string, target: string) { 325 | const open = `<${tag}`; 326 | const close = ``; 327 | const position = target.indexOf(open); 328 | if (position === -1) { 329 | throw new Error(`Couldn't find ${open}`); 330 | } 331 | const start = target.indexOf(">", position + open.length) + 1; 332 | 333 | const end = target.indexOf(close, start); 334 | if (position === -1) { 335 | throw new Error(`Couldn't find ${close}`); 336 | } 337 | 338 | return target.substring(0, start) + content + target.substring(end); 339 | } 340 | -------------------------------------------------------------------------------- /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/used.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | /** 4 | * @deprecated 5 | * Stop using used() to silence linting & typescript errors, instead: 6 | * ```ts 7 | * const react = sveltity({ MyComponent }); 8 | * ``` 9 | * and use the component as: 10 | * ```svelte 11 | * 12 | * ``` 13 | * for improved type-safety & autocompletion. 14 | */ 15 | export default function used(...args: any[]) {} 16 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {@render children()} 11 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | Svelte logo in react colors and the React logo in svelte colors 5 | 19 | -------------------------------------------------------------------------------- /src/routes/api/react-version.json/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "@sveltejs/kit"; 2 | import detectReactVersion from "../../../lib/internal/detectReactVersion"; 3 | 4 | export const GET: RequestHandler = async () => 5 | new Response(`${await detectReactVersion()}`, { 6 | headers: { "Content-Type": "application/json; charset=utf-8" }, 7 | }); 8 | -------------------------------------------------------------------------------- /src/routes/broken/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/routes/context-react/+page.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/routes/context-svelte/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/routes/dynamic/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes/fixtures/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
    11 | {#each getFiles() as file} 12 |
  • {file}
  • 13 | {/each} 14 |
15 | -------------------------------------------------------------------------------- /src/routes/fixtures/[fixture]/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

{data.title}

7 |
8 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /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]) => path.match(/([^/]+)\.svelte$/)?.[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/routes/hooks/+page.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
Count: {count}
21 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | {#if auth.authenticated} 34 | 35 | {:else} 36 | 37 | {/if} 38 | -------------------------------------------------------------------------------- /src/routes/hooks/HookWithContext.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if auth?.authenticated} 9 |
Authenticated
10 | {:else} 11 |
Not authenticated
12 | {/if} 13 | -------------------------------------------------------------------------------- /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/routes/input/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | {value} 11 | -------------------------------------------------------------------------------- /src/routes/intrinsic-elements/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | Heading 3 | 4 | Subheading 5 | -------------------------------------------------------------------------------- /src/routes/lazy/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if browser} 10 | 11 | {/if} 12 | -------------------------------------------------------------------------------- /src/routes/mini/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/routes/playwright/+page.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 |

Using React {reactVersion}

25 | {#if loading} 26 |

Loading...

27 | {:else} 28 |

Ready

29 | {/if} 30 |
31 | 32 |
33 | 34 | 55 | -------------------------------------------------------------------------------- /src/routes/playwright/+page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This page exposes variables on the window object that can be used by the Playwright tests. 3 | */ 4 | 5 | import type { PageLoad } from "./$types"; 6 | 7 | export const load: PageLoad = async ({ fetch }) => { 8 | const reactVersion = await (await fetch("/api/react-version.json")).json(); 9 | const reactDomModule = 10 | reactVersion <= 17 11 | ? () => import("react-dom") 12 | : () => import("react-dom/client"); 13 | return { 14 | reactVersion, 15 | ReactDOM: (await reactDomModule()).default, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/routes/playwright/StatefulClicker.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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/react-router/+layout.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes/react-router/Menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { NavLink } from "svelte-preprocess-react/react-router"; 3 | 4 | type Props = { 5 | to: string; 6 | title: string; 7 | }; 8 | const MenuItem: React.FC = ({ to, title }) => ( 9 |
  • 10 | ({ fontWeight: isActive ? "bold" : "normal" })} 13 | > 14 | {title} 15 | 16 |
  • 17 | ); 18 | 19 | const Menu: React.FC = () => ( 20 |
      21 | 22 | 23 | 24 |
    25 | ); 26 | export default Menu; 27 | -------------------------------------------------------------------------------- /src/routes/react-router/[slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

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

    9 | 10 | Back to home 11 | -------------------------------------------------------------------------------- /src/routes/react-ssr/+server.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from "react"; 2 | import { renderToString } from "react-dom/server"; 3 | import App from "../../demo/react-components/App"; 4 | 5 | const cssScript = ``; 6 | const hydrateScript = ``; 10 | 11 | export function GET({ url }) { 12 | const html = renderToString(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/routes/render-prop/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 14 | results.map((result, i) => createElement("div", { key: i }, result))} 15 | /> 16 | -------------------------------------------------------------------------------- /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/suspense/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/routes/sveltify-react/+server.ts: -------------------------------------------------------------------------------- 1 | import { redirect, type RequestHandler } from "@sveltejs/kit"; 2 | import detectReactVersion from "../../lib/internal/detectReactVersion"; 3 | 4 | export const GET: RequestHandler = async () => { 5 | const version = await detectReactVersion(); 6 | const location = version <= 17 ? "/sveltify-react17" : "/sveltify-react18"; 7 | redirect(302, location); 8 | }; 9 | -------------------------------------------------------------------------------- /src/routes/sveltify-react17/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/routes/sveltify-react18/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/routes/youtube/react/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /src/routes/youtube/svelte/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/server.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/svelte-preprocess-react/d8649ec01e7cc444d935cb0656dbcfea55bf2651/src/server.js -------------------------------------------------------------------------------- /src/tests/__snapshots__/preprocess.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`svelte-preprocess-react > should convert text content to react children prop 1`] = ` 4 | " 11 | 12 | 13 | 14 | 15 | 16 | 17 | " 18 | `; 19 | 20 | exports[`svelte-preprocess-react > should import 'react-dom/server' when ssr is enabled 1`] = ` 21 | " 28 | 29 | { 32 | count = next; 33 | }} 34 | > 35 | " 36 | `; 37 | 38 | exports[`svelte-preprocess-react > should inject a script tag 1`] = ` 39 | " 44 | 45 | 46 | 47 | " 48 | `; 49 | 50 | exports[`svelte-preprocess-react > should not import 'react-dom/server' when ssr is disabled 1`] = ` 51 | " 58 | 59 | { 62 | count = next; 63 | }} 64 | > 65 | " 66 | `; 67 | 68 | exports[`svelte-preprocess-react > should portal slotted content as children 1`] = ` 69 | " 74 | 75 | 76 | " 77 | `; 78 | 79 | exports[`svelte-preprocess-react > should process tags 1`] = ` 80 | " 85 | 86 | 87 | 88 | 89 | Item 3 90 | 93 | 94 | " 95 | `; 96 | 97 | exports[`svelte-preprocess-react > should process tags 1`] = ` 98 | " 108 | 109 | 110 | " 111 | `; 112 | 113 | exports[`svelte-preprocess-react > should process tags 1`] = ` 114 | " 121 | 122 | { 125 | count = next; 126 | }} 127 | > 128 | " 129 | `; 130 | 131 | exports[`svelte-preprocess-react > should process tags 2`] = ` 132 | " 140 | 141 |

    prop and event

    142 | { 145 | count = next; 146 | }} 147 | > 148 |

    prop and Prop event

    149 | { 152 | console.info("count"); 153 | }} 154 | > 155 |

    prop

    156 | 157 |

    .

    158 | 159 | " 160 | `; 161 | 162 | exports[`svelte-preprocess-react > should process (lowercase) tags 1`] = ` 163 | " 168 | 169 | console.info("clicked")}> 170 | 171 | 172 | " 173 | `; 174 | 175 | exports[`svelte-preprocess-react > should process {...rest} props 1`] = ` 176 | " 184 | 185 | 186 | Hi 187 | 188 | " 189 | `; 190 | 191 | exports[`svelte-preprocess-react > should process {:else} {:then} and {:catch} sections 1`] = ` 192 | " 200 | 201 | 202 | {#if number === 1} 203 | 204 | 205 | {:else if number === 2} 206 | 207 | {:else} 208 | 209 | {/if} 210 | 211 | {#each [] as _} 212 | 213 | {:else} 214 | 215 | {/each} 216 | 217 | {#await Promise.resolve()} 218 | 219 | {:then} 220 | 221 | {:catch} 222 | 223 | {/await} 224 | " 225 | `; 226 | 227 | exports[`svelte-preprocess-react > should process on:event forwarding 1`] = ` 228 | " 238 | 239 | 240 | " 241 | `; 242 | 243 | exports[`svelte-preprocess-react > should support typescript when using preprocess 1`] = ` 244 | " 247 | 248 |

    {title}

    249 | " 250 | `; 251 | -------------------------------------------------------------------------------- /src/tests/fixtures/Binding.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | console.info(update)} /> 10 | -------------------------------------------------------------------------------- /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/tests/fixtures/Children.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
    11 | {@render children()} 12 |
    13 | 14 | 24 | -------------------------------------------------------------------------------- /src/tests/fixtures/Clicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Props = { 4 | count: number; 5 | onCount: (count: number) => void; 6 | }; 7 | const Clicker: React.FC = ({ count, onCount }) => ( 8 |
    9 |

    You clicked {count} times

    10 | 11 |
    12 | ); 13 | 14 | export default Clicker; 15 | -------------------------------------------------------------------------------- /src/tests/fixtures/Container.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | { 12 | count = next; 13 | }} 14 | > 15 | -------------------------------------------------------------------------------- /src/tests/fixtures/Dog.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {name} 16 | 17 | 27 | -------------------------------------------------------------------------------- /src/tests/fixtures/Element.svelte: -------------------------------------------------------------------------------- 1 | console.info("clicked")}> 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/tests/fixtures/Forwarding.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/tests/fixtures/List.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | Item 2 10 | Item 3 11 | 14 | 15 | -------------------------------------------------------------------------------- /src/tests/fixtures/List.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | }; 6 | export default function List({ children }: Props) { 7 | return
      {children}
    ; 8 | } 9 | 10 | type ItemProps = { 11 | label: string; 12 | children: React.ReactNode; 13 | }; 14 | 15 | function ListItem({ label, children }: ItemProps) { 16 | return ( 17 |
  • 18 | {label} 19 | {children} 20 |
  • 21 | ); 22 | } 23 | type IconProps = { 24 | icon: string; 25 | }; 26 | ListItem.Icon = ({ icon }: IconProps) => { 27 | return {icon}; 28 | }; 29 | 30 | List.Item = ListItem; 31 | -------------------------------------------------------------------------------- /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/tests/fixtures/NoScript.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/tests/fixtures/Provider.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | content 13 | -------------------------------------------------------------------------------- /src/tests/fixtures/RestProps.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Hi 12 | 13 | -------------------------------------------------------------------------------- /src/tests/fixtures/Slots.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | A simple primary alert. Check it out! 8 | -------------------------------------------------------------------------------- /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/tests/fixtures/Typescript.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

    {title}

    6 | -------------------------------------------------------------------------------- /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 "../lib/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(" 8 | 9 | 10 | 11 |

    Loading...

    12 |
    13 | 14 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /static/svelte-preprocess-react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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: { css: "injected" }, 9 | kit: { 10 | alias: { 11 | "svelte-preprocess-react": "src/lib", 12 | }, 13 | adapter: adapter({ 14 | fallback: "index.html", 15 | }), 16 | }, 17 | vitePlugin: { 18 | inspector: true, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------