svelte2tsx-component Svelte Themes

Svelte2tsx Component

svelte2tsx-component

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 -D

Concepts

  • 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 .svelte transparently 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

<slot />

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

  • style property with expression
    • ex. <div style="color: {color}"></div>
    • ex. <div style={obj}></div>
  • class property with expression
    • ex. <div class="c1 {v}"></div>
    • ex. <div class={expr}></div>
  • Await Block
  • Property Bindings <input bind:value />
  • <svelte:options />
  • svelte 's setContext / getContext / tick / getAllContexts
  • svelte/motion
  • svelte/store
  • svelte/animation
  • svelte/transition
  • svelte/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: number to {foo}: {foo: number}
  • Props Type: export let bar: number = 1 to {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>.svelte with <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

Top categories

Loading Svelte Themes