Search code examples
javascripthtmlcompilationsveltesveltekit

Turn a Svelte Component into HTML, CSS, and JS


My ideal end goal:

Dynamically output HTML, CSS, and JS from a Svelte component so I can host it elsewhere (e.g. S3 bucket).

Context:

  1. I have a Svelte app that collects information from users
  2. From this information, I have to generate a website
  3. The user can select layout options and a color palette
  4. These options will be passed down to the component that needs to be converted into HTML, CSS, and JS.

Questions:

  1. Is this possible? I can see Svelt REPL/W3Schools/etc. have something similar (component on the left, dynamically generated web page on the right)
  2. Can I use a CSS framework like Tailwind in the component?
  3. In the best-case scenario, the user will change the options, and the dynamic website will update in near-real-time. Doable?

Example:

// website.svelte

<script lang="ts">
    export let layout: string;
    export let colors: string[];
    const onClick = () => alert(5);
</script>

<button class="text-bold" on:click={onClick}>{text}</button> // Tailwind class
<section class={layout + colors}>...stuff</section>
// builder.svelte
<script lang="ts">
    import Website from './website.svelte';

    let srcdoc: string;

    const generate = async () => {
        // const props = {layout: 'grid', colors: ['blue', 'green']};
        // const {js, html, css} = new Website(...props).decompile();
        // srcdoc = `<style>${css}</style> ${html} <script>${js}</script>`;
    };
</script>

<button on:click={generate}>Generate</button>
<iframe title="Dynamic website" {srcdoc}></iframe>

Possible solutions

  1. new Website({target: domElement}) and then take the innerHTML. This works only for the HTML. The CSS/JS is nowhere to be seen.
  2. Perhaps I can create a separate Svelte project and invoke the iframe with a src instead of a srcdoc and pass the props in the query string. When the user is happy with their configuration, I will save it to a database and deploy the other Svelte project with that configuration for each user.
  3. Make use of web components somehow.

Solution

  • Yes it possible, but not especially trivial.

    You'll need to compile your Svelte component(s) yourself. This will give you the CSS and some JS code to generate your target website.

    For generation, you have 2 options. You can either use the "ssr" mode of Svelte, or the "dom" mode. With SSR, you'll be able to produce the HTML for a fully static site. With DOM, you'll get a client side component that you can render anywhere on a page, with client-side JS.

    You can actually use both, to prerender the HTML to be sent by the server, and then claim the already rendered DOM in the browser, with the client side component (with hydrate option).

    For compilation, you can directly use the Svelte compiler API. For Typescript (or else), you would also need to preprocess your Svelte components. In order to switch between SSR or DOM components, you'd use the generate compiler option.

    You'll probably meet hard hurdles quickly down this road... The Svelte compiler works at individual components level, meaning you'll get ".js" from your ".svelte" components, but if they ever import anything, it will be left to you to make sure these imports actually work, somehow.

    Your best bet is probably to use Rollup that will compile your components AND bundle everything together (that is, resolve imports and all), and give you some nicely usable JS (and CSS) as a result.

    You can check the old Svelte template to get an idea of how this works.

    In your case, you will probably need some kind of config file like this:

    rollup.config.js

    import svelte from "rollup-plugin-svelte";
    import preprocess from "svelte-preprocess";
    import css from "rollup-plugin-css-only";
    import commonjs from "@rollup/plugin-commonjs";
    import resolve from "@rollup/plugin-node-resolve";
    
    const production = !process.env.ROLLUP_WATCH;
    
    export default [
        // DOM
        {
            input: "src/website.svelte",
            output: {
                format: "es",
                file: "src/build/website.js",
                name: "App"
            },
            plugins: [
                svelte({
                    preprocess: [preprocess()],
                    compilerOptions: {
                        dev: !production,
                        hydratable: true
                    }
                }),
                css({ output: "bundle.css" }),
                resolve({
                    browser: true,
                    dedupe: ["svelte"],
                    exportConditions: ["svelte"]
                }),
                commonjs()
            ]
        },
        // SSR
        {
            input: "src/website.svelte",
            output: {
                format: "es",
                file: "src/build/website.ssr.js",
                name: "App"
            },
            plugins: [
                svelte({
                    preprocess: [preprocess()],
                    compilerOptions: {
                        dev: !production,
                        generate: "ssr"
                    }
                }),
                css({ output: "bundle.css" }),
                resolve({
                    browser: true,
                    dedupe: ["svelte"],
                    exportConditions: ["svelte"]
                }),
                commonjs()
            ]
        }
    ];
    

    We're actually using 2 separate configs in the same file, in order to produce SSR and DOM components in one go.

    You'll obviously need the relevant dependencies:

    pnpm add -D rollup rollup-plugin-svelte svelte-preprocess rollup-plugin-css-only @rollup/plugin-commonjs @rollup/plugin-node-resolve
    

    Then you can build by running this command (or equivalent with your package manager):

    pnpm rollup -c
    

    After that, you'll have the 3 files you wanted in the src/build directory:

    • bundle.css
    • website.js
    • website.ssr.js

    You can use those to generate the HTML with the SSR component, or to render the component in a browser with the DOM component, or both (hydration).

    Here's an example Svelte component that illustrates the whole loop (SSR + client side rendering with hydration), using the following pre-built files:

    src/App.svelte

    <script >
      import Website from "./build/website.js"
      import WebsiteSSR from "./build/website.ssr.js"
    
      let target
      let styleTarget
      let cmp
      let css = ""
    
      let props = {
        layout: "tables",
        colors: [],
      }
    
      function update(props) {
        // cleanup
        if (cmp) {
          cmp.$destroy()
        }
    
        // ssr
        const { html, css: _css, head } = WebsiteSSR.render(props)
        css = _css.code
        target.innerHTML = html
        styleTarget.innerText = css
    
        // client-side (with hydration)
        cmp = new Website({ target, hydrate: true, props })
      }
    
      // when target is ready, and props changes, update output
      $: if (target) update(props)
    </script>
    
    <main>
      <div class="in">
        <label>
          Layout
          <input bind:value={props.layout} />
        </label>
      </div>
    
      <div bind:this={target} />
      <style bind:this={styleTarget} />
    </main>
    
    <style>
      main {
        display: flex;
      }
      div {
        width: 50%;
      }
    </style>
    

    (This component was tested in a basic Vite+Svelte app created with pnpm create vite.)

    Notice that we're using the Client-side component API and Server-side component API for our prebuilt component here. The App component is just here for illustration, you don't need Svelte at all to use the components, once they're already compiled. (And avoid trying to use those prebuilt component in an actual Svelte app, the compatibility is anything but guaranteed across different compilation setups.)

    And that's all there is to it.

    You'll obviously need to dig the docs of the different tools mentioned above, to understand how to use them, and probably tweak a few options. But hopefully this answer can give you a complete overview of what you need to search.