svelte(lang='ts') template to react component converter (PoC)
$ npm install svelte2tsx-component -D
# default css generator is @emotion/css
$ npm install react react-dom @types/react @types/react-dom @emotion/css -DConcepts
- Generate Component Props Type from
script lang="ts", leaving TypeScript type information - Convert svelte's built-in functionality into an idiom on React with similar results
- Import
.sveltetransparently as React Component - Support only svelte template subset
API
import { svelteToTsx } from "svelte2tsx-component"; const code = "<div></div>"; const tsxCode = svelteToTsx(code);
with vite
// vite.config.ts import { defineConfig } from "vite"; import { plugin as svelteToTsx } from "svelte-to-tsx"; import ts from "typescript"; export default defineConfig({ plugins: [svelteToTsx({ extensions: [".svelte"], tsCompilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext, jsx: ts.JsxEmit.ReactJSX, } })], });
Examples
svelte template
<script lang="ts"> import { onMount } from "svelte"; export let foo: number; export let bar: number = 1; const x: number = 1; let mut = 2; onMount(() => { console.log("mounted"); mut = 4; }); const onClick = () => { console.log("clicked"); mut = mut + 1; } </script> <div id="x" class="red"> <h1>Nest</h1> hello, {x} </div> <button on:click={onClick}>click</button> <style> .red { color: red; } </style>
to tsx component (react)
import { useEffect, useState } from "react"; import { css } from "@emotion/css"; export default ({ foo, bar = 1 }: { foo: number; bar?: number }) => { const x: number = 1; const [mut, set$mut] = useState(2); useEffect(() => { console.log("mounted"); set$mut(4); }, []); const onClick = () => { console.log("clicked"); set$mut(mut + 1); }; return ( <> <div id="x" className={selector$red}> <h1>Nest</h1> hello, {x} </div> <button onClick={onClick}>click</button> </> ); }; const selector$red = css` color: red; `;
So you can use like this.
import React from "react"; import App from "./App.svelte"; import { createRoot } from "react-dom/client"; const root = document.getElementById("root")!; createRoot(root).render(<App name="svelte-app" onMessage={(data) => { console.log("message received", data) } } />);
(put App.svelte.d.ts manually yet)
Transform Convensions
PropsType with export let
svelte
<script lang="ts"> export let foo: number; export let bar: number = 1; </script>
tsx
export default ({ foo, bar = 1 }: { foo: number, bar?: number }) => { return <></> }
PropsType with svelte's createEventDispatcher<{}>()
svelte
<script lang="ts"> import {createEventDispatcher} from "svelte"; // Only support ObjectTypeLiteral (TypeReference not supported) const dispatch = createEventDispatcher<{ message: { text: string; }; }>(); const onClick = () => { dispatch('message', { text: 'Hello!' }); } </script> <div on:click={onClick}> hello </div>
tsx
export default ({ onMessage, }: { onMessage?: (data: { text: string }) => void; }) => { const onClick = () => { onMessage?.({ text: "Hello!", }); }; return ( <> <div onClick={onClick}>hello</div> </> ); };
Expression in svelte template
<div id="myid"></div> <div id={expr}></div> <div id="{expr}"></div> <div id="head{expr}tail"></div> <div {id}></div> <div {...params}></div>
Slot (default)
svelte
tsx
import { type ReactNode } from "react"; export default ({children}, { children: ReactNode }) => { return <> {children} </>; }
(Named slot not supported)
onMount / onDestroy / beforeUpdate / afterUpdate
Convert to react's useEffect
style property to object
<span class="red">text</span> <style> .red: { color: red; } </style>
to
// Auto import with style block import { css } from "@emotion/css"; // in tsx <span className={style$red}>text</span> const selector$red = css` color: red; `;
Only support single class selector like .red.
Not Supported these patterns.
/* selector combination */ .foo .bar {} .foo > .bar {} /* element selector */ div {} /* global selector */ :global(div) {}
Unsupported features
-
styleproperty with expression- ex.
<div style="color: {color}"></div> - ex.
<div style={obj}></div>
- ex.
-
classproperty with expression- ex.
<div class="c1 {v}"></div> - ex.
<div class={expr}></div>
- ex.
- Await Block
- Property Bindings
<input bind:value /> <svelte:options />svelte'ssetContext/getContext/tick/getAllContextssvelte/motionsvelte/storesvelte/animationsvelte/transitionsvelte/action<Foo let:prop />- css:
:global()
(Checkboxed item may be supportted latter)
Currently, the scope is not parsed, so unintended variable conflicts may occur.
Basic Features
- Module:
<script context=module> - Props Type:
export let foo: numberto{foo}: {foo: number} - Props Type:
export let bar: number = 1to{bar = 1}: {bar?: number} - svelte:
onMount(() => ...)=>useEffect(() => ..., []) - svelte:
onDestroy(() => ...)=>useEffect(() => { return () => ... }, []) - svelte:
dispatch('foo', data)=>onFoo?.(data) - svelte:
beforeUpdate()=>useEffect - svelte:
afterUpdate()=>useEffect(omit first change) - Let:
let x = 1=>const [x, set$x] = setState(1) - Let:
x = 1=>set$x(1); - Computed:
$: added = v + 1; - Computed:
$: document.title = title=>useEffect(() => {document.title = title}, [title]) - Computed:
$: { document.title = title }=>useEffect(() => {document.title = title}, [title]) - Computed:
$: <expr-or-block>=>useEffect() - Template:
<div>1</div>to<><div>1</div></> - Template:
<div id="x"></div>to<><div id="x"></div></> - Template:
<div id={v}></div>to<><div id={v}></div></> - Template:
<div on:click={onClick}></div>to<div onClick={onClick}></div> - Template:
{#if ...} - Template:
{:else if ...} - Template:
{/else} - Template:
{#each items as item} - Template:
{#each items as item, idx} - Template:
{#key <expr>} - Template: with key
{#each items as item (item.id)} - Template: Shorthand assignment
{id} - Template: Spread
{...v} - SpecialTag: RawMustacheTag
{@html <expr} - SpecialTag: DebugTag
{@debug "message"} - SpecialElements: default slot:
<slot> - SpecialElements:
<svelte:self> - SpecialElements:
<svelte:component this={currentSelection.component} foo={bar} /> - Template: attribute name converter like
class=>className,on:click=>onClick - Style:
<style>tag to@emotion/css - Style: option for
import {css} from "..."importer - Plugin: transparent svelte to react loader for rollup or vite
- Inline style property:
<div style="...">to<div style={{}}>
TODO
- Template: Await block
{#await <expr>} - Computed:
$: ({ name } = person) - Directive:
<div contenteditable="true" bind:innerHTML={html}> - Directive:
<img bind:naturalWidth bind:naturalHeight></img> - Directive:
<div bind:this={element}> - Directive:
class:name - Directive:
style:property - Directive:
use:action - SpecialElements:
<svelte:window /> - SpecialElements:
<svelte:document /> - SpecialElements:
<svelte:body /> - SpecialElements:
<svelte:element this={expr} /> - SpecialTag: ConstTag
{@const v = 1} - Directive:
<div on:click|preventDefault={onClick}></div> - Directive:
<span bind:prop={}> - Directive:
<Foo let:xxx> - Directive: event delegation
<Foo on:trigger> - SpecialElements:
<svelte:fragment> - SpecialElements: named slots:
<slot name="..."> - SpecialElements:
$$slots - Generator:
.d.ts(<name>.sveltewith<name>.svelte.d.ts) - Generator: preact
- Generator: qwik
- Generator: solid
- Generator: vue-tsx
Why?
Svelte templates are not difficult to edit with only HTML and CSS knowledge, but the modern front-end ecosystem revolves around JSX.
However, the modern front-end ecosystem revolves around JSX, and we think we need a converter that transparently treats Svelte templates as React components. I think so.
(This is my personal opinion).
Prior Art
LICENSE
MIT