Dally
Dally

Reputation: 1427

Tailwind CSS missing from universal Svelte web component

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

Upvotes: 0

Views: 753

Answers (2)

Dally
Dally

Reputation: 1427

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);
        });
};

Upvotes: 0

brunnerh
brunnerh

Reputation: 185280

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).

Upvotes: 0

Related Questions