0

I've managed to create a Svelte web component that can be used on external websites running on different frameworks.

The final problem I'm having is that Tailwind doesn't seem to be included in the final build that lands in the dist_js folder, nor are the images in static/images.

I'm not sure what I'm missing here and would appreciate some help.

Command to run the build: npm run build -- --mode=development && npm run webComp

package.json

{
    "name": "thor",
    "version": "0.0.1",
    "scripts": {
        "dev": "vite dev",
        "build": "vite build && npm run package",
        "preview": "vite preview",
        "webComp": "vite -c vite.webcomponent.config.js build",
        "package": "svelte-kit sync && svelte-package && publint",
        "prepublishOnly": "npm run package",
        "lint": "prettier --check . && eslint .",
        "format": "prettier --write ."
    },
    "exports": {
        ".": {
            "types": "./dist/index.d.ts",
            "svelte": "./dist/index.js"
        }
    },
    "files": [
        "dist",
        "!dist/**/*.test.*",
        "!dist/**/*.spec.*"
    ],
    "peerDependencies": {
        "svelte": "^4.0.0"
    },
    "devDependencies": {
        "@sveltejs/adapter-auto": "^3.0.0",
        "@sveltejs/kit": "^2.0.0",
        "@sveltejs/package": "^2.0.0",
        "@sveltejs/vite-plugin-svelte": "^3.0.0",
        "@types/eslint": "^8.56.0",
        "autoprefixer": "^10.4.16",
        "eslint": "^8.56.0",
        "eslint-config-prettier": "^9.1.0",
        "eslint-plugin-svelte": "^2.35.1",
        "flowbite": "^2.3.0",
        "flowbite-svelte": "^0.44.24",
        "flowbite-svelte-icons": "^1.5.0",
        "postcss": "^8.4.32",
        "postcss-load-config": "^5.0.2",
        "prettier": "^3.1.1",
        "prettier-plugin-svelte": "^3.1.2",
        "prettier-plugin-tailwindcss": "^0.5.9",
        "publint": "^0.1.9",
        "svelte": "^4.2.7",
        "tailwindcss": "^3.3.6",
        "tslib": "^2.4.1",
        "typescript": "^5.3.2",
        "vite": "^5.0.11"
    },
    "dependencies": {
        "ably": "^2.0.1",
        "svelte-preprocess": "^5.1.3",
        "uuidv4": "^6.2.13"
    },
    "svelte": "./dist/index.js",
    "types": "./dist/index.d.ts",
    "type": "module"
}

vite.webcomponent.config.js

import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
    build: {
        lib: {
            entry: resolve(__dirname, 'dist/index.js'),
            name: 'Components',
            fileName: 'components',
        },
        outDir: 'dist_js',
    },
    plugins: [
        svelte(),
    ],
});

src/lib/index.js

// Reexport your entry components here
import Main from './components/Main.svelte';
export { Main };

svelte.config.js

import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import adapter from '@sveltejs/adapter-auto';

/** @type {import('@sveltejs/kit').Config} */
const config = {
    kit: {
        // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
        // If your environment is not supported or you settled on a specific environment, switch out the adapter.
        // See https://kit.svelte.dev/docs/adapters for more information about adapters.
        adapter: adapter()
    },

    preprocess: [vitePreprocess({})],

    compilerOptions: {
        customElement: true
    },

};

export default config;

Folder Structure

enter image description here

2
  • Why are you setting --mode=development? Also, show the svelte.config.js and are there console errors when running the code? How do you run the script and which? (Since you have chunks, you maybe have to use the module script.) Commented Apr 2, 2024 at 19:38
  • Only using --mode=development temporarily for certain ENV variables. No console errors. I'm running the script by importing components.umd.js into a plain HTML, creating the <widget> element and manually appending id which makes the component appear with all the correct data, but with no CSS. The usage is the same as this: docs.boodil.com Commented Apr 2, 2024 at 20:07

2 Answers 2

0

If you set up Tailwind as documented that should work as is, as long as the styles are imported in the component the library is for, not some page. Though the import of the CSS file in the <script> will result in a separate stylesheet that will need to be included along with the script.

There are ways to integrate this into JS:

  1. Use a Vite plugin that injects the styles as part of the JS
  2. Import the styles into the <style> tag and configure Svelte to inject the CSS instead of outputting a file.

There already exist plugins for option #1, you would just need to include them in the build.

Option #2 is a bit involved and the preprocessors don't seem to quite work as intended.

Some steps to follow are:

  • Set css mode in svelte.config.js:
    compilerOptions: {
        customElement: true,
        css: "injected",
    },
    
  • Use svelte-preprocess to support global style tags, also adjust svelte.config.js accordingly and enable postcss:
    import sveltePreprocess from "svelte-preprocess";
    
    const config = {
        preprocess: [sveltePreprocess({
            postcss: true,
        })],
        ...
    
  • Create a component that encapsulates the base directives, e.g. tailwind.svelte:
    <style lang="postcss" global>
        @tailwind base;
        @tailwind components;
        @tailwind utilities;
    </style>
    
    (I tried using a :global block instead, but that does not seem to work.)
  • Import and insert the component in your custom element component:
    <svelte:options customElement="..." />
    <script>
        import Tailwind from './tailwind.svelte';
    </script>
    <Tailwind />
    ...
    

If the chunks don't get too large they will automatically be merged into one JS file. There are Vite/Rollup options for managing this to some degree; e.g. build.rollupOptions.output.experimentalMinChunkSize could maybe be set to high value in the Vite build config.


As noted before, the only thing relevant to the component library build is the lib/index.js hence anything in static is disregarded. Images should not be put into static anyway because then they either cannot be properly cached or you can end up with unwanted caching.

Images should generally be imported where they are used so they are either turned into assets files or inlined directly by Vite.

E.g.

<!-- my-element.svelte -->
<script>
  import src from '$lib/images/some-image.jpg';
</script>
<img {src} alt="...">

Images can also be referenced in CSS when using certain paths like $lib/... (example here).

In library mode (build.lib) assets will always be inlined, regardless of size (otherwise there is an option that governs this limit: build.assetsInlineLimit).

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you for this. Did you run into any issue when encapsulating the base directives? It seems @tailwind utilities is causing an issue: :global(...) must contain a single selector. If I remove global I can see a stylesheet added to the component when I load it in my plain HTML file but it's not the entire Tailwind CSS - Probably because global is missing.
This should work if using the listed pre-processor and <style lang="postcss" global>, maybe there are some version issues.
Odd, as I'm using the latest version 5.1.3. With vitePreprocess there are no errors but then it's no longer pulling the CSS.
vitePreprocess can never work, it does not support this. It's not just the pre-processor version that is relevant, also Svelte & SvelteKit.
When you tried the :global block, did you get an error? I'm getting no error with the block but it's not pulling the CSS either.
|
0

Managed to solve my problem using a completely different method. Ending up using ESBuild to build my project which outputs one single JS file and a CSS file.

I can't seem to generate a JS file that contains the CSS using ESBuild - I don't think ESBuild supports this.

I'm hosting that CSS file on a CDN and as I'm using Shadow DOM for the web component, I inject it back in to my main/entry component if the Shadow DOM is present. When running the project locally, it doesn't do the injection as it's not needed.

May not be the slickest solution but it's working.

esbuild.js

import esbuild from "esbuild";
import sveltePlugin from "esbuild-svelte";
import postCssPlugin from 'esbuild-style-plugin';
import tailwindcss from 'tailwindcss';
import tailwindConfig from "./tailwind.config.js";
import autoprefixer from 'autoprefixer';
import 'dotenv/config';

esbuild
    .build({
        entryPoints: ["src/lib/index.js"],
        mainFields: ["svelte", "browser", "module", "main"],
        conditions: ["svelte", "browser"],
        bundle: true,
        minify: true,
        outfile: "build/main.js",
        define: {
            'process.env.BASE_URL': JSON.stringify(process.env.PUBLIC_DEV_BASE_URL),
            'process.env.ABLY_KEY': JSON.stringify(process.env.PUBLIC_ABLY_KEY),
        },
        plugins: [
            sveltePlugin({
                compilerOptions: {
                    customElement: true,
                },
            }),
            postCssPlugin({
                postcss: {
                    plugins: [tailwindcss(tailwindConfig), autoprefixer],
                },
            }),
        ],
        logLevel: "info",
    })
    .catch(() => process.exit(1));

Injection of CSS into Shadow DOM

onMount(async () => {
    // If Shadow DOM exists, inject Tailwind CSS into Shadow DOM
    if (document.getElementsByTagName('widget')?.[0]?.shadowRoot) getCSS();

});

const getCSS = async () => {
    fetch('https://examplecdn.com/main.css')
        .then((response) => response.text())
        .then((data) => {
            let style = document.createElement('style');
            style.textContent = data;
            document.getElementsByTagName('widget')[0].shadowRoot.appendChild(style);
        });
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.