vitejs / vite

Next generation frontend tooling. It's fast!

Home Page:http://vitejs.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Can css be styleInjected in library mode?

gwsbhqt opened this issue · comments

Is your feature request related to a problem? Please describe.
I'm using vite to build a library, and sometimes the library requires a dozen lines of simple styles. After building, a style.css file will be generated. But I think it is not necessary to generate two files, it may be enough to generate only one file. Is there any way to add style.css to the target js file?

The styleInject helper function is used in rollup and babel plug-ins. In iife/umd mode, the style is automatically injected when library loaded.

image

The problem is injecting the style assumes a DOM environment which will make the library SSR-incompatible.

If you only have minimal css, the easiest way is to simply use inline styles.

Generally, existing style libraries are used in a dom environment.
Now I am using ts/vite/preact/module less to build a library. If I use the inline style, then there is no way to use modular less.
Maybe you are right. It may be a better choice to directly use inline styles, but still hope to provide more library mode options.
Thanks for your reply.

I know you already answered that assuming a DOM environment will make the library SSR-incompatible, but maybe it would be possible to provide an option for enabling inlining of the styles?

I have made a reproduction project showing a component being created in the ui-components project then after being built and installed in the separate project not including any styles. vite-library-style-repro

@yyx990803 how would you suggest making a library of components that can be included without importing a global styles.css, possibly including styles that are unneeded or clashing with other styles?

In pass, I use rollup build vue2 library, I found that the style will auto inject to "<head></head>"

I've been running into this for the past week and pulling my hair out, as this is not a documented behavior. I was about to file an issue (repro: https://github.com/richardtallent/vite-bug-cssnotimported) and just came across this one.

Requiring non-SSR users to manually import a CSS file to get a component's default style to work is suboptimal DX, and an unfamiliar surprise for anyone accustomed to importing components under Vue 2.

Inline CSS is not a viable alternative, at least for a non-trivial component. Inline CSS also creates additional challenges for a user who wants to override the default style.

Is there a middle ground here to help developers who want to support the SSR use case, without making non-SSR users jump through an extra hoop to style each component they use?

@richardtallent Sharing my experience here, I moved to inline all CSS and moved all <template> into render function.

I was following this issue for over 2 weeks now, I was stumbled upon how to effectively inject style into the head. Effectively for DX, requiring the user to import style was suboptimal for my use case. By comparison Vue 2 was much easier to install for the users than Vue 3, so this was a no go for me.

My trivial library target SSR/SSG/SPA and is e2e tested on all 3 modes which makes it an extra headache to move to Vue 3 in Vite. There was also some quirks with head injected css for my use case so inlining style was not something I was entirely against. (In fact I wanted to for consistency sake.)

For inline CSS overriding, what I did to allow default style override is to explicitly state how to with ::v-deep and !important in the documentation. reference

.some-component ::v-deep(.v-hl-btn) {
  width: 8px !important;
}

Not all css can be inlined directly (pseduo, media query, '>' and many more), which require me to rewrite my entire component into render function. reference

export default defineComponent({
  render() {
    return h('div', {
      class: 'vue-horizontal',
      style: {
        position: 'relative',
        display: 'flex',
      }
    }, [...])
  }
})

I hope this helps anyone down this road. For reference: fuxingloh/vue-horizontal#87

Thanks @fuxingloh! This sounds like an interesting workaround, but I don't want to give up on writing idiomatic SFCs (with template and style blocks) and allowing users to override styles with standard CSS. However, I have bookmarked your component since I like how you've done your testing and I hope to learn from it!

I am using the style-inject now. I wonder if this problem has been solved? Eventually a css file will be generated, and the style code is also included in js.

import { computed, defineComponent, ref } from 'vue';
import { queryMedia } from '@convue-lib/utils';
import styleInject from 'style-inject';
import css from './index.less';

styleInject(css);

export default defineComponent({
  name: 'Container',
  props: {
    fuild: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, { slots }) {
    const media = ref('');
    queryMedia((data: string) => (media.value = data));
    const className = computed(() => ({
      'convue-container': true,
      fuild: props.fuild,
      [media.value]: true,
    }));

    return () => <div class={className.value}>{slots.default?.()}</div>;
  },
});

https://github.com/ziping-li/convue-lib/blob/master/packages/container/src/index.tsx

Inlining style won't work with the transition also. Requiring users to explicitly import the style is not really an option. Is there anything we can do to settle down this problem?

Any update on this issue?

@ziping-li @hiendv @aekasitt
I used a method similar to @ziping-li to solve this thorny problem.

// index.ts entry point

import styleInject from 'style-inject'

styleInject('__STYLE_CSS__') // __STYLE_CSS__ is a placeholder pseudo code
// gulpfile.js build script file

const { readFileSync, rmSync, writeFileSync } = require('fs')
const { series } = require('gulp')
const { exec } = require('shelljs')

function readWriteFileSync(path, callback) {
    writeFileSync(path, callback(readFileSync(path, { encoding: 'utf8' })), { encoding: 'utf8' })
}

function readRemoveFileSync(path, callback) {
    callback(readFileSync(path, { encoding: 'utf8' }))
    rmSync(path, { force: true })
}

async function clean() {
    exec('rimraf ./dist ./coverage ./.eslintcache ./.stylelintcache')
}

async function build_library() {
    exec('tsc')
    exec('copyfiles -u 1 ./src/**/* ./dist') // copy style to tsc dist
    exec('vite build')

    readRemoveFileSync('./dist/style.css', css => {
        // __STYLE_CSS__ replace by style.css
        readWriteFileSync('./dist/library.umd.js', js => replace(js, '__STYLE_CSS__', css))
    })
}

exports.build = series(clean, build_library)
// package.json

{
    "scripts": {
        "build": "gulp build"
    }
}

+1 for this feature.

I encountered the same problem, css cannot be loaded correctly when building lib mode.

I created a plugin to temporarily solve this problem.

import fs from 'fs'
import { resolve } from 'path'
import type { ResolvedConfig, PluginOption } from 'vite'

const fileRegex = /\.(css)$/

const injectCode = (code: string) =>
  `function styleInject(css,ref){if(ref===void 0){ref={}}var insertAt=ref.insertAt;if(!css||typeof document==="undefined"){return}var head=document.head||document.getElementsByTagName("head")[0];var style=document.createElement("style");style.type="text/css";if(insertAt==="top"){if(head.firstChild){head.insertBefore(style,head.firstChild)}else{head.appendChild(style)}}else{head.appendChild(style)}if(style.styleSheet){style.styleSheet.cssText=css}else{style.appendChild(document.createTextNode(css))}};styleInject(\`${code}\`)`
const template = `console.warn("__INJECT__")`

let viteConfig: ResolvedConfig
const css: string[] = []

export default function libInjectCss(): PluginOption {
  return {
    name: 'lib-inject-css',

    apply: 'build',

    configResolved(resolvedConfig: ResolvedConfig) {
      viteConfig = resolvedConfig
    },

    transform(code: string, id: string) {
      if (fileRegex.test(id)) {
        css.push(code)
        return {
          code: '',
        }
      }
      if (
        // @ts-ignore
        id.includes(viteConfig.build.lib.entry)
      ) {
        return {
          code: `${code}
          ${template}`,
        }
      }
      return null
    },

    async writeBundle(_: any, bundle: any) {
      for (const file of Object.entries(bundle)) {
        const { root } = viteConfig
        const outDir: string = viteConfig.build.outDir || 'dist'
        const fileName: string = file[0]
        const filePath: string = resolve(root, outDir, fileName)

        try {
          let data: string = fs.readFileSync(filePath, {
            encoding: 'utf8',
          })

          if (data.includes(template)) {
            data = data.replace(template, injectCode(css.join('\n')))
          }

          fs.writeFileSync(filePath, data)
        } catch (e) {
          console.error(e)
        }
      }
    },
  }
}

https://github.com/ohbug-org/ohbug-extension-feedback/blob/main/libInjectCss.ts

@chenyueban This completely solved my problem, thanks! Much more elegant than crawling the dependency graph 👍
Have you run into any shortcomings with this approach yet?

@chenyueban Hi, friend. I use your plugin in my lib code, and it doesn't work. And I found difference is that I turned on css.modules and scss. Then I change the Reg to /\.(scss)/, and amazing, vite does not compile css file, but does not inject to js bundle either. Do you have any suggestion? Thank you my friend.

I encountered the same problem, css cannot be loaded correctly when building lib mode.

I created a plugin to temporarily solve this problem.

import fs from 'fs'
import { resolve } from 'path'
import type { ResolvedConfig, PluginOption } from 'vite'

const fileRegex = /\.(css)$/

const injectCode = (code: string) =>
  `function styleInject(css,ref){if(ref===void 0){ref={}}var insertAt=ref.insertAt;if(!css||typeof document==="undefined"){return}var head=document.head||document.getElementsByTagName("head")[0];var style=document.createElement("style");style.type="text/css";if(insertAt==="top"){if(head.firstChild){head.insertBefore(style,head.firstChild)}else{head.appendChild(style)}}else{head.appendChild(style)}if(style.styleSheet){style.styleSheet.cssText=css}else{style.appendChild(document.createTextNode(css))}};styleInject(\`${code}\`)`
const template = `console.warn("__INJECT__")`

let viteConfig: ResolvedConfig
const css: string[] = []

export default function libInjectCss(): PluginOption {
  return {
    name: 'lib-inject-css',

    apply: 'build',

    configResolved(resolvedConfig: ResolvedConfig) {
      viteConfig = resolvedConfig
    },

    transform(code: string, id: string) {
      if (fileRegex.test(id)) {
        css.push(code)
        return {
          code: '',
        }
      }
      if (
        // @ts-ignore
        id.includes(viteConfig.build.lib.entry)
      ) {
        return {
          code: `${code}
          ${template}`,
        }
      }
      return null
    },

    async writeBundle(_: any, bundle: any) {
      for (const file of Object.entries(bundle)) {
        const { root } = viteConfig
        const outDir: string = viteConfig.build.outDir || 'dist'
        const fileName: string = file[0]
        const filePath: string = resolve(root, outDir, fileName)

        try {
          let data: string = fs.readFileSync(filePath, {
            encoding: 'utf8',
          })

          if (data.includes(template)) {
            data = data.replace(template, injectCode(css.join('\n')))
          }

          fs.writeFileSync(filePath, data)
        } catch (e) {
          console.error(e)
        }
      }
    },
  }
}

https://github.com/ohbug-org/ohbug-extension-feedback/blob/main/libInjectCss.ts

Is there any other way, please? I see that rollup can directly output an esm file and include css.

I faced the same issue. I wrote a Vue composition function that injects scoped CSS from a string at runtime that uses the same approach as Kremling for React.

It is not a perfect solution, since I imagine the majority of Vue users would prefer to use the standard SFC <style> block, and the solution requires writing a little extra syntax. But it does solve the problem of including CSS in JS. Also it cleans up its CSS when a component is unmounted.

https://github.com/fnick851/vue-use-css

Sorry, that was Kami (my cat)

commented

The problem is injecting the style assumes a DOM environment which will make the library SSR-incompatible.

If you only have minimal css, the easiest way is to simply use inline styles.

What if we just provide a option to import the css file into the bundled js file like this, instead of inject into DOM, will this be more SSR friendly?

// bundled js file (in es format only)
import './style.css';
import { defineComponent, openBlock, createBlock, renderSlot, withScopeId } from "vue";

// ...

And the css import will be handled by the downstream bundler like, webpack, rollup, vite, whatever.

I encountered the same problem, css cannot be loaded correctly when building lib mode.

I created a plugin to temporarily solve this problem.

@chenyueban do you have an example repo with how/where you use this?

btw, I have an NPM7 monorepo with several packages that build multiple components with Vite, but it's tricky to see where to best add and execute this script.

commented

The problem is injecting the style assumes a DOM environment which will make the library SSR-incompatible.
If you only have minimal css, the easiest way is to simply use inline styles.

What if we just provide a option to import the css file into the bundled js file like this, instead of inject into DOM, will this be more SSR friendly?

// bundled js file
import './style.css';
import { defineComponent, openBlock, createBlock, renderSlot, withScopeId } from "vue";

// ...

And the css import will be handled by the downstream bundler like, webpack, rollup, vite, whatever.

Base on chenyueban's solution, I created a plugin for this idea: https://github.com/wxsms/vite-plugin-libcss

Not an expert to vite, but works in my case.

@chenyueban, I followed your plugin code with Vite 2.7.3 and the resulting css inside bundle is not minified at all. Is that an intended feature / side effect or I'm missing something.

@ludmiloff you should never minify stuff in library mode in my opinion. Library mode means your package will be consumed by another dev's bundler like Vite, Rollup, Esbuild or Webpack.

So those bundlers will be the ones responsible minifying the entire project at the end. But any libraries in between shouldn't be minified imo. : )

(it also makes it harder for developers when something goes wrong and they jump to the source code and only get minified code : S )

@mesqueeb, I forgot to mention I'm using libinjectcss for a non-lib setup, despite this issue is for library mode only. Just found the plugin might be useful for my setup. My fault, apologies. Anyway, I slightly modified the plugin and I'm able to to minimise the css with the help of esbuild. Here is my code, based on original work of @chenyueban


/* eslint-disable import/no-extraneous-dependencies */
import fs from 'fs'
import esbuild from 'esbuild'
import { resolve } from 'path'

const fileRegex = /\.(css).*$/
const injectCode = (code) =>
  `function styleInject(css,ref){if(ref===void 0){ref={}}var insertAt=ref.insertAt;if(!css||typeof document==="undefined"){return}var head=document.head||document.getElementsByTagName("head")[0];var style=document.createElement("style");style.type="text/css";if(insertAt==="top"){if(head.firstChild){head.insertBefore(style,head.firstChild)}else{head.appendChild(style)}}else{head.appendChild(style)}if(style.styleSheet){style.styleSheet.cssText=css}else{style.appendChild(document.createTextNode(css))}};styleInject(\`${code}\`)`
const template = `console.warn("__INJECT__")`

let viteConfig
const css = []

async function minifyCSS(css, config) {
  const { code, warnings } = await esbuild.transform(css, {
      loader: 'css',
      minify: true,
      target: config.build.cssTarget || undefined
  });
  if (warnings.length) {
      const msgs = await esbuild.formatMessages(warnings, { kind: 'warning' });
      config.logger.warn(source.yellow(`warnings when minifying css:\n${msgs.join('\n')}`));
  }
  return code;
}

export default function libInjectCss(){
  return {
    name: 'lib-inject-css',
    apply: 'build',

    configResolved(resolvedConfig) {
      viteConfig = resolvedConfig
    },

    async transform(code, id) {
      if (fileRegex.test(id)) {
        const minified = await minifyCSS(code, viteConfig)
        css.push(minified.trim())
        return {
          code: '',
        }
      }
      if (
        // @ts-ignore
        // true ||
        id.includes(viteConfig.build.lib.entry) ||
        id.includes(viteConfig.build.rollupOptions.input)
      ) {
        return {
          code: `${code};
          ${template}`,
        }
      }
      return null
    },

    async writeBundle(_, bundle) {
      for (const file of Object.entries(bundle)) {
        const { root } = viteConfig
        const outDir = viteConfig.build.outDir || 'dist'
        const fileName = file[0]
        const filePath = resolve(root, outDir, fileName)

        try {
          let data = fs.readFileSync(filePath, {
            encoding: 'utf8',
          })

          if (data.includes(template)) {
            data = data.replace(template, injectCode(css.join('\n')))
          }

          fs.writeFileSync(filePath, data)
        } catch (e) {
          console.error(e)
        }
      }
    },
  }
}

I built on the work of others and came up with a temporary solution. I do hope they provide some kind of flag, in the future, to get the native Rollup functionality for this.

This is in JS

libInjectCss.js

import fs from 'fs';
import { resolve } from 'path';
import createHash from 'hash-generator';
const minify = require('@node-minify/core');
const cleanCSS = require('@node-minify/clean-css');

const fileRegex = /\.module\.(scss|less|css)(\?used)?$/;

const injector = `function styleInject(css, ref) {
  if ( ref === void 0 ) ref = {};
  var insertAt = ref.insertAt;
  if (!css || typeof document === 'undefined') { return; }
  var head = document.head || document.getElementsByTagName('head')[0];
  var style = document.createElement('style');
  style.type = 'text/css';
  if (insertAt === 'top') {
    if (head.firstChild) {
      head.insertBefore(style, head.firstChild);
    } else {
      head.appendChild(style);
    }
  } else {
    head.appendChild(style);
  }
  if (style.styleSheet) {
    style.styleSheet.cssText = css;
  } else {
    style.appendChild(document.createTextNode(css));
  }
}`;

const injectCode = (value) => {
  const codeId = createHash(5);
  return `const css_${codeId} = "${value}";

styleInject(css_${codeId});

`;
};

const template = `console.warn("__INJECT__")`;

function buildOutput(extracts) {
  const out = [];
  extracts.forEach((value) => {
    out.push(injectCode(value));
  });
  return `
${injector}

${out.join('')}`;
}

let viteConfig;
const css = [];

export default function libInjectCss() {
  const extracted = new Map();

  return {
    name: 'lib-inject-css',

    apply: 'build',

    configResolved(resolvedConfig) {
      viteConfig = resolvedConfig;
    },

    async transform(code, id) {
      if (fileRegex.test(id)) {
        const minified = await minify({
          compressor: cleanCSS,
          content: code,
        });
        extracted.set(id, minified);
        css.push(code);
        return {
          code: '',
        };
      }
      if (id.includes(viteConfig.build.lib.entry)) {
        return {
          code: `${code}
${template}`,
        };
      }
      return null;
    },

    async writeBundle(_, bundle) {
      for (const file of Object.entries(bundle)) {
        const { root } = viteConfig;
        const outDir = viteConfig.build.outDir || 'dist';
        const fileName = file[0];
        const filePath = resolve(root, outDir, fileName);

        try {
          let data = fs.readFileSync(filePath, {
            encoding: 'utf8',
          });

          // eslint-disable-next-line max-depth
          if (data.includes(template)) {
            data = data.replace(template, buildOutput(extracted));
          }

          fs.writeFileSync(filePath, data);
        } catch (e) {
          console.error(e);
        }
      }
    },
  };
}

And my vite.config.js

import path from 'path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import libInjectCss from './configs/libInjectCss';

const isExternal = (id) => !id.startsWith('.') && !path.isAbsolute(id);

export default defineConfig({
  plugins: [react(), libInjectCss()],
  build: {
    sourcemap: true,
    lib: {
      entry: path.resolve(__dirname, 'src/index.js'),
      name: 'myLibrary',
      formats: ['es'],
      fileName: (format) => `my-library.${format}.js`,
    },
    rollupOptions: {
      external: isExternal,
    },
  },
});

It does bark about sourcemaps, but my code works in my app...

While digging through Vite's cssPostPlugin() I noticed, that you need to set

{
	build: {
		cssCodeSplit: true,
		lib: {
			format: ['umd'],
		}
	}
}

in order for the styles to be injected into the chunk, see line 414 to line 456:
image

Unfortunately this currently only works with UMD modules.

@donnikitos the problem is __vite_style__ can pollute the global namespace in the case of an IIFE build.
I wish there was a simple alternative to inject CSS easily to making a DOM aware library with Vite

While that is understandable, not all libraries may choose to even provide an IIFE build, or in that case can ask the consumer to load the CSS manually, while still providing it as part of the main import for ESM and UMD modules.

Anyway, it would be great to have an easier way to supply (scoped) CSS that doesn't require the consumer to either load a massive global CSS file or having to include the CSS by hand by crafting their own wrapping components.

I was hoping to port our existing Vue 2 UI library from rollup to vite, but this problem is stopping me from being able to do so unfortunately.

+1 for this feature.

Agreed, for most this won't be needed.

But we are developing an externally loaded application with Svelte. So consumers will add it on their website to lazy load our app(similar to how messenger is integrated). Since we have very little CSS we thought to inject them with JS script(which can be easily done with rollup by default).

When we heard of Vite, we loved to use it as the main dev tool but this problem stopped us from doing that.

+1 for this

+1 for this feature. I'm migrating a small lib with react components from tsdx to vite, and was surprised that my styles were not injected

+1. I'm working on an UI Lib for Vue 3, then I need to inject css into es.js file.
Really appreciate if you can bring this feature to Vite.

+1

I built a plugin for this, until such time as the Vite folks give us an option. Sure it isn't perfect, but hope it helps. Goes like this:

import path from 'path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import injectCss from '@cxing/vitejs-plugin-inject-css';

const isExternal = (id) => !id.startsWith('.') && !path.isAbsolute(id);

export default defineConfig({
  plugins: [react(), injectCss()],
  build: {
    sourcemap: true,
    lib: {
      entry: path.resolve(__dirname, 'src/index.js'),
      name: 'myLibrary',
      formats: ['es'],
      fileName: (format) => `my-library.${format}.js`,
    },
    rollupOptions: {
      external: isExternal,
    },
  },
});

The fact that umd builds where cssCodeSplit = true causes CSS to be injected makes switching to Vite possible for me. (Albeit, cssCodeSplit is counter-intuitive wording for me.)

The fact that umd builds where cssCodeSplit = true causes CSS to be injected makes switching to Vite possible for me. (Albeit, cssCodeSplit is counter-intuitive wording for me.)

yep! cssCodeSplit sounds like splitting but inject.

While digging through Vite's cssPostPlugin() I noticed, that you need to set

{
	build: {
		cssCodeSplit: true,
		lib: {
			format: ['umd'],
		}
	}
}

in order for the styles to be injected into the chunk, see line 414 to line 456: image

Unfortunately this currently only works with UMD modules.

It's weird that build-csscodesplit says

If you specify build.lib, build.cssCodeSplit will be false as default.

and it does work well.

hope add this feature one day.

Experiencing this issue as well, any idea when this feature will be available?

+1 from me for this feature!

Context: I'm working on a library that provides styles for both "Web Components" (WC) and native HTML elements. That means each WC will have its own isolated styles (not a problem), but I also need a global stylesheet for the native HTML elements (the problem).

A workaround I'm currently using to see if I can actually switch to Vite from Snowpack:

vite.config.ts:

// pretty much default for a "Library Mode" config
import { defineConfig } from 'vite';

export default defineConfig({
    build: {
        lib: {
            entry: 'src/index.ts',
            fileName: () => 'index.js',
            formats: ['es'],
        },
        sourcemap: true,
    },
});

src/index.ts:

// web components
import './components/index';

// global styles
import globalStyles from './styles/index.scss?inline'; // appending ?inline removes file from /dist

const styleElement = document.createElement('style');
styleElement.title = 'Library Name - Global Styles;
styleElement.innerText = globalStyles;
document.head.appendChild(styleElement)

That way when I import the library using: <script type="module" src="./dist/index.js"></script> a <style> element is injected into the document <head> containing the global styles. Not loving it. Will definitely keep searching...

The problem is injecting the style assumes a DOM environment which will make the library SSR-incompatible.

If you only have minimal css, the easiest way is to simply use inline styles.

I currently met issue using both CSS modules and ?inline

import styles from './style.module.css?inline'

const Component = () => {
  return (
    <div>
      <style>{styles}</style>
      <!-- styles.title undefined -->
      <div className={styles.title}></div>
    </div>
  )
}

I have no method to reference the moduled CSS class name.

commented

I just spent hours converting my Vue library to work with Vite (because the rollup plugin for vue said it wasn't maintained anymore and to move to vite), only to realise at the very end when trying to use the package that no CSS gets included in the library and there's no way to fix it.

I don't think Vite should claim to have a library mode in the docs and talk about being able to build components libraries if it sneakily strips all the CSS.

It's not a nice surprise to arrive at after you've done a whole migration to using vite.

I just spent hours converting my Vue library to work with Vite (because the rollup plugin for vue said it wasn't maintained anymore and to move to vite), only to realise at the very end when trying to use the package that no CSS gets included in the library and there's no way to fix it.

I don't think Vite should claim to have a library mode in the docs and talk about being able to build components libraries if it sneakily strips all the CSS.

It's not a nice surprise to arrive at after you've done a whole migration to using vite.

For me, it was also big surprise, but I can suggest a more or less compromise solution that can cover the main requirements from the UI Kit.

In a nutshell, you should do something like this:

  • use tsc to build your components to compile .ts files into .js with .d.ts;

  • then just copy (not by hand, of course, but by the power of Node JS) .vue sources to dist;

  • you can do the same with the scss global styles (which contains the reset of native browser styles, including fonts, etc.). Although you can build it, here as you wish;

  • debug and develop UI Kit components through Storybook;

  • if the main project is built by ViteJS, then you will face the problem of importing assets (importing fonts in style files, for example). This is solved like this:

import { defineConfig, searchForWorkspaceRoot } from 'vite'

let cwd = process.cwd()
export default defineConfig({
    server: {
        fs: {
            allow:[
                searchForWorkspaceRoot(cwd),
                path.resolve(cwd, '@ui-kit/dist')
            ]
        }
    }
})

I have not yet checked what is there with webpack.

  • then you will face the interesting task of implementing the component if you want to embed icons in the UI Kit with a classic svg sprite. But this is also a solvable (already solved) problem. At the same time, you can make it beautiful and implement both support for icons from a set and custom ones from the project to which you connect your library of components.

PS1. Sorry, that I describe the solution only thesis. When I agree on the issue of publishing the source code, I will put together a small "UI Kit Template" with the solution to all this, which can be downloaded and adapted for yourself.

PS2. In fact, you can not wait for such a publication, but look towards the sources of VitePress. Evan and Kiaking solved this problem in much the same way.

I had some issues with the other plugin mentioned here to solve this issue so I built a simpler one. This is zero dependency and 39 LOC.

https://github.com/itsyjs/vite-css-inject

For minification or any other changes to the CSS just use Vite's built-in PostCSS pipeline.

So, I think I found a workaround. Here is my setup.

ESM tree-shakable vue3 components library with injectable styles.

vite.config.ts

import path from 'path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import eslintPlugin from 'vite-plugin-eslint';
import renameNodeModules from 'rollup-plugin-rename-node-modules';
import { walk } from 'estree-walker';

function wtfPlugin() {
  const isCss = (filename: string) => /\s?css$/.test(filename);

  function injectCss(css: string) {
    const style = document.createElement('style');
    document.head.firstChild
      ? document.head.insertBefore(style, document.head.firstChild)
      : document.head.appendChild(style);
    style.appendChild(document.createTextNode(css));
    return css;
  }

  return {
    name: 'wtf-plugin',
    renderChunk(this: any, content: string, chunk: any) {
      if (isCss(chunk.facadeModuleId)) {
        const ast = this.parse(content, {
          ecmaVersion: 'latest',
          sourceType: 'module'
        });

        let code = chunk.code;

        walk(ast, {
          enter(node) {
            if (node.type === 'Literal') {
              code = `export default injectCss(${JSON.stringify(
                (node as any).value
              )});${injectCss.toString()}`;
            }
          }
        });

        return { code };
      }
    }
  };
}

export default defineConfig({
  build: {
    lib: {
      formats: ['es'],
      entry: path.resolve(__dirname, 'src/index.ts'),
      fileName: () => 'index.es.js'
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        preserveModules: true,
        preserveModulesRoot: 'src',
        entryFileNames(chunk) {
          if (chunk.facadeModuleId && /\s?css$/.test(chunk.facadeModuleId)) {
            return 'styles.css.js';
          }

          return '[name].[format].js';
        }
      }
    }
  },
  plugins: [vue(), wtfPlugin(), eslintPlugin(), renameNodeModules()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/assets/mixins" as *;`
      }
    }
  }
});

package.json

{
  "name": "@whatever/ui-kit",
  "sideEffects": [
    "*.css*.js"
  ],
  "module": "index.es.js",
  ...
}

There are a few things to notice:

  • entryFileNames. You need to rename chunks with styles so that you can define them in package.json as a sideEffect
  • When there are a few style nodes for single component, vite will emit a couple of chunks, style.css.js and style.css2.js, for example. So you need that asterisk in sideEffects field
  • renderChunk hook in plugin. We are extracting raw css text from iife and replacing file content with 'injectable' css. Theoretically, injecting could be done with rollup-plugin-postss, but currently it is not possible
  • It would be nice to inject styles with style-inject and do not spam that function. But anyway I'm happy that it works somehow!
  • This will not work with cssCodeSplit: true option enabled

I hope this plugin will be useful.

vite-plugin-style-inject

While digging through Vite's cssPostPlugin() I noticed, that you need to set

{
	build: {
		cssCodeSplit: true,
		lib: {
			format: ['umd'],
		}
	}
}

in order for the styles to be injected into the chunk, see line 414 to line 456: image
Unfortunately this currently only works with UMD modules.

It's weird that build-csscodesplit says

If you specify build.lib, build.cssCodeSplit will be false as default.

and it does work well.

This works in iife mode as well

hey, the libinjectcss solution was working for me perfectly, then suddenly it stopped and i have no idea why

In 3.0.8 and before it was possible to use cssCodeSplit: true with UMD library build output and have inlined CSS automatically if a library file had something like import 'my-styles.css'.

After upgrading to 3.1.0 that functionality is gone, CSS is not inlined at all, there's not even a separate CSS file generated.

I expect vite to implement this feature, too.
This plugin supports inserting css into the top of head without outputting additional css files.
I hope it will be helpful to you.
@senojs/rollup-plugin-style-inject

+1 to this issue

I expect vite to implement this feature, too. This plugin supports inserting css into the top of head without outputting additional css files. I hope it will be helpful to you. @senojs/rollup-plugin-style-inject

For those who use Typescript in their project alongside a vite.config.ts:
You might try using this plugin, but you will most likely get the following error:
Error [ERR_REQUIRE_ESM]: require() of ES Module ...

Changing the format of vite.config.ts to vite.config.mts will fix the issue. At least for me it did.

PS: Good issue, I hope the Vite folks will resolve this soon.

I found this plugin https://www.npmjs.com/package/vite-plugin-css-injected-by-js
Looks like it does what we need

I found this plugin https://www.npmjs.com/package/vite-plugin-css-injected-by-js Looks like it does what we need

This plugin worked for me!!

Referring to the inspiration from the above students, I wrote a plug-in to solve this problem,the link address: https://www.npmjs.com/package/vite-plugin-lib-inject-style

commented

I found this plugin https://www.npmjs.com/package/vite-plugin-css-injected-by-js Looks like it does what we need

It works for me too, and even with vite@4 🔥

You can see my config here: https://github.com/LouisMazel/maz-ui/blob/3dba08e7e2ec0953675f23aec220188e2d4168c7/packages/lib/build/vite.config.ts

I build each components separately

commented

+1 add this feature one day

+1 to adding this as feature also

commented

i find a new method, without any plugin!

just define the format as "umd", then vite will automatically inject the css into js file.

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' // 将css打包注入到js中,避免css文件单独加载

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), ],
  build: {
    rollupOptions:{
      input:{
        main: resolve(__dirname, 'src/main.ts'),
      },
      
      output:{
        entryFileNames: '[name].js',
        format: 'umd',
      }
    }
  }
})

As a library, we don't need to manually write code like document.createElement('style') to inject the style into the page, that isn't graceful.

Just inject one line like import './style.css' at the top of each chunk file, and then add '**/*.css' to the 'sideEffects' field in the package.json to avoid unexpected tree-shaking by the user's building tool.

So the main problem becomes to how do we know which style files are involved in one chunk file?, fortunately vite has injected viteMetadata property on each chunk file which help us work it out.

We can do some modifications on every generated chunk file by using plugin hook renderChunk and viteMetadata property as following:

{
  name: 'vite:inline-css',
  config(){
    return {
      build: {
        cssCodeSplit: true, // Don't forget to enable css code split, otherwise there's only one style.css
        lib: {
          formats: ['es'],
          // Multi-entry building configuration.
          entry: [
            'src/component-one/index.ts',
            'src/component-two/index.ts',
          ]
        }
      }
    }
  },
  renderChunk(code, chunk) {
    if (chunk.viteMetadata?.importedCss.size) {
      let result = code;
      // Inject the referenced style files at the top of the chunk.
      for (const cssName of chunk.viteMetadata!.importedCss) {
        let cssFilePath = path.relative(path.dirname(chunk.fileName), cssName);
        cssFilePath = cssFilePath.startsWith('.') ? cssFilePath : `./${cssFilePath}`;
        result = `import '${cssFilePath}';\n${result}`; // style inject
      }
      return result;
    }
  }
}

I made vite-plugin-lib-inject-css to solve this prolem. This plugin inject css at the top of each chunk file in lib mode using import statement like this:

// bundled js file, with import css at top (if any)
import './style.css';
// rest of the file
// ...

It delegates the task of how to handle these css files to the user's build tool. And here is an example shows that how should we config to create a multiple entry point component library, which can auto import styles and has out-of-box tree-shaking functionality:

// vite.config.ts
import { libInjectCss } from 'vite-plugin-lib-inject-css';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    libInjectCss(),
  ],
  build: {
    rollupOptions: {
      output: {
        // Put chunk files at <output>/chunks
        chunkFileNames: 'chunks/[name].[hash].js',
        // Put chunk styles at <output>/styles
        assetFileNames: 'assets/[name][extname]',
      },
    },
    lib: {
      format: ['es'],
       entry: {
        index: 'src/index.ts', // Don't forget the main entry!
        button: 'src/components/button/index.ts',
        select: 'src/components/select/index.ts',
      }
    }
  }
})

I am building a simple design system and was running into this issue. Basically I have a few components I want to use in other applications that also have a bundler setup.

I guess I could just use tsc instead and leave the css module files untouched. But for me compiling the css modules fixes a compatibility issue when the package is consumed in both, environments that support named import for css modules and don't.

Importing one stylesheet to use the package would not be a big issue.

But by doing so every consumer would end up having all styles in the bundle even when only using some of the components.

To split up the css I tried preserveModules with cssCodeSplit but this just leads to a bunch of unassignable css files (#8057).

Ideally I would like to use @emosheeep solution vite-plugin-lib-inject-css together with preserveModules.

When rollupOptions.output.preserveModules is true, the association between a chunk and the css it referenced will lose for some reason, in which case style injection will be skipped.

@emosheeep Are you sure about this, what was the issue? I tried it and it did look like it does work.
I was accidentally using the deprecated rollupOptions.preserveModules instead of rollupOptions.output.preserveModules and saw that the imports your plugin injects were correct in my case.

I now use the plugin in combination with manualChunks to create a chunk for every module that is imported in the main file.

But beware, if one junk would import anything from another junk the css in the other chunk will also be imported.

And in the package.json the css imports might need to be declared as a side effect by setting sideEffect: '**/*.css to not get tree shaken downstream.

import { defineConfig } from 'vite'
import { resolve, dirname, sep } from 'path'
import react from '@vitejs/plugin-react'
import { libInjectCss } from 'vite-plugin-lib-inject-css'

const libEntryFile = resolve(__dirname, 'lib/main.ts');
const libEntryFileDir = dirname(libEntryFile);

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react({ jsxRuntime: 'classic' }),
    libInjectCss(),
  ],
  build: {
    lib: {
      entry: libEntryFile,
      formats: ['es'],
      fileName(format, entryName) {
        return ${entryName}.js
      }
    },
    rollupOptions: {
      external: ['react'],
      output: {
        assetFileNames: 'assets/[name]-[hash][extname]',
        manualChunks(id, { getModuleInfo }) {
          const entryModuleInfo = getModuleInfo(libEntryFile);
          const entryModuleImportedIds = entryModuleInfo.importedIds;
          if (entryModuleImportedIds.includes(id)) {
            return dirname(id).replace(libEntryFileDir + sep, '');
          }
        }
      },
    },
  },
})

Hey, I created a pr for this issue #13565.
Feel free to look into it.

I encountered the same problem, css cannot be loaded correctly when building lib mode.

I created a plugin to temporarily solve this problem.

import fs from 'fs'
import { resolve } from 'path'
import type { ResolvedConfig, PluginOption } from 'vite'

const fileRegex = /\.(css)$/

const injectCode = (code: string) =>
  `function styleInject(css,ref){if(ref===void 0){ref={}}var insertAt=ref.insertAt;if(!css||typeof document==="undefined"){return}var head=document.head||document.getElementsByTagName("head")[0];var style=document.createElement("style");style.type="text/css";if(insertAt==="top"){if(head.firstChild){head.insertBefore(style,head.firstChild)}else{head.appendChild(style)}}else{head.appendChild(style)}if(style.styleSheet){style.styleSheet.cssText=css}else{style.appendChild(document.createTextNode(css))}};styleInject(\`${code}\`)`
const template = `console.warn("__INJECT__")`

let viteConfig: ResolvedConfig
const css: string[] = []

export default function libInjectCss(): PluginOption {
  return {
    name: 'lib-inject-css',

    apply: 'build',

    configResolved(resolvedConfig: ResolvedConfig) {
      viteConfig = resolvedConfig
    },

    transform(code: string, id: string) {
      if (fileRegex.test(id)) {
        css.push(code)
        return {
          code: '',
        }
      }
      if (
        // @ts-ignore
        id.includes(viteConfig.build.lib.entry)
      ) {
        return {
          code: `${code}
          ${template}`,
        }
      }
      return null
    },

    async writeBundle(_: any, bundle: any) {
      for (const file of Object.entries(bundle)) {
        const { root } = viteConfig
        const outDir: string = viteConfig.build.outDir || 'dist'
        const fileName: string = file[0]
        const filePath: string = resolve(root, outDir, fileName)

        try {
          let data: string = fs.readFileSync(filePath, {
            encoding: 'utf8',
          })

          if (data.includes(template)) {
            data = data.replace(template, injectCode(css.join('\n')))
          }

          fs.writeFileSync(filePath, data)
        } catch (e) {
          console.error(e)
        }
      }
    },
  }
}

https://github.com/ohbug-org/ohbug-extension-feedback/blob/main/libInjectCss.ts

the id and the entry was different in windows.
use "resolve(id)" to compare with "resolve(viteConfig.build.lib.entry)" will be great.

For anyone interested, this is the solution I ended up with and it works fine for my purpose:

Assuming all your consumers have a bundler setup that can handle CSS, you can use vite-plugin-lib-inject-css to add import statements for the generated CSS to the output JavaScript files.

Importing the CSS rather than injecting it into the DOM has have the advantage that the consumers can further process the CSS styles (such as doing optimizations eg. grouping selectors).

This solution is great for scenarios where your library uses CSS modules but not all consumers do (or consumers have a different style of importing CSS modules).

To make sure not all CSS styles end up in your application, you can split up the bundle. vite-plugin-lib-inject-css will include an import statement to just the mandatory CSS at the beginning of each chunk's output file.

In the Rollup documentation there is a recommended way of doing this:

📘 If you want to convert a set of files to another format while maintaining the file structure and export signatures, the recommended way—instead of using output.preserveModules that may tree-shake exports as well as emit virtual files created by plugins—is to turn every file into an entry point.

You can find a demo repo here: https://github.com/receter/my-component-library

This is my vite configuration:

⚠️Don't forget to add **/*.css to the sideEffects field in your package.json

// vite.config.ts
import { defineConfig } from 'vite'
import { extname, relative, resolve } from 'path'
import { fileURLToPath } from 'node:url'
import { glob } from 'glob'
import react from '@vitejs/plugin-react'
import dts from 'vite-plugin-dts'
import { libInjectCss } from 'vite-plugin-lib-inject-css'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    libInjectCss(),
    dts({ include: ['lib'] })
  ],
  build: {
    copyPublicDir: false,
    lib: {
      entry: resolve(__dirname, 'lib/main.ts'),
      formats: ['es']
    },
    rollupOptions: {
      external: ['react', 'react/jsx-runtime'],
      input: Object.fromEntries(
        // https://rollupjs.org/configuration-options/#input
        glob.sync('lib/**/*.{ts,tsx}').map(file => [
          // 1. The name of the entry point
          // lib/nested/foo.js becomes nested/foo
          relative(
            'lib',
            file.slice(0, file.length - extname(file).length)
          ),
          // 2. The absolute path to the entry file
          // lib/nested/foo.ts becomes /project/lib/nested/foo.ts
          fileURLToPath(new URL(file, import.meta.url))
        ])
      ),
      output: {
        assetFileNames: 'assets/[name][extname]',
        entryFileNames: '[name].js',
      }
    }
  }
})

If you only have minimal css, The easiest way to do use rollupOption configuration:

{
  rollupOption: {
    output: {
        intro: 'import "./style.css";',
    }
  }
}

If you only have minimal css, The easiest way to do use rollupOption configuration:


{

  rollupOption: {

    output: {

        intro: 'import "./style.css";',

    }

  }

}

hahahaha,can't believe it could be done in this way

If you only have minimal css, The easiest way to do use rollupOption configuration:

{
  rollupOption: {
    output: {
        intro: 'import "./style.css";',
    }
  }
}

You should probably also add ./style.css to the sideEffects field in your package.json

When building components using rollup in our project, we used https://www.npmjs.com/package/rollup-plugin-postcss - this automatically inject styles from Vue SFC into the head of the page (per component). Unfortunately, the above plugin does not work with Vite. We have several components that load asynchronously, the styles of these components should also load asynchronously.
Our library is used by an external client directly as a JS script on the external site, so importing styles at the top of the chunks is not an option, we have to inject them.
I haven't found any solution in the entire thread that would meet this condition, is it even possible?

Re: Using sideEffects in package.json

There's a lot of references to using sideEffects in package.json, but AFAIK isn't this a Webpack-specific feature? I use Vite everywhere.

I am using vite with Vue to build a component library with multiple entry points (to enable imports like import { MyInput } from '@orgname/repo-name/inputs' as well as a global css import such as import '@orgname/repo-name/styles'; ). The existing solutions in this thread didn't exactly fit my needs, but between the solutions here and some help from ChatGPT, I was able to arrive at a working solution. It relies on the same mechanism as @emosheeep's npm package where css files are imported at the top of the generated .js files, so the consuming project must have a build process that supports such imports. The difference is that this solution keeps each component's CSS with its JS code while also allowing for a global stylesheet to be generated (for things like fonts that your library relies on). Defining the plugin this way also removes the need to rely on an external package. In particular, this is created with Vue SFC's in mind which use <style lang="scss" scoped>, though perhaps it could be adapted to other frameworks.

With this setup, vite / rollup creates folders in the dist/ directory which broadly mimic the source code directory structure. After the bundles and component js files (e.g. MyComponent.vue.js) are generated, rollup will go in and add a line like import './MyComponent.css;. It's not the most elegant solution in the world but it works great for my purposes.

vite.config.js:

import { defineConfig } from 'vite';
import { resolve } from 'path';
import vue from '@vitejs/plugin-vue';

import path from 'path';
import fs from 'fs';
import { promisify } from 'util';

const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
const exists = promisify(fs.exists);

// in my actual code, this is pulled out into a helpers/ directory
async function addCSSImport() {
  return {
    name: 'add-css-import',
    async writeBundle(options, bundle) {
      const outputDir = options.dir || '.';

      for (const [fileName] of Object.entries(bundle)) {
        if (fileName.endsWith('.vue.js')) {
          // Extract the base file name without the directory path
          const baseFileName = path.basename(fileName, '.vue.js');
          // Construct the full path to the JS and CSS files
          const jsFilePath = path.join(outputDir, fileName);
          const cssFilePath = jsFilePath.replace('.vue.js', '.css');

          // Check if the CSS file exists
          if (await exists(cssFilePath)) {
            // Read the existing JS file
            let jsFileContent = await readFile(jsFilePath, 'utf8');
            // Generate the import statement for the CSS file
            const cssImport = `import './${baseFileName}.css';\n`;
            // Prepend the import statement to the JS file's content
            jsFileContent = cssImport + jsFileContent;
            // Write the modified JS file content back to the file
            await writeFile(jsFilePath, jsFileContent, 'utf8');
          }
        }
      }
    }
  };
}

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    // prevent monolithic css file from being generated, preferring one for each component
    cssCodeSplit: true,
    lib: {
      entry: {
        ['global-styles']: 'src/styles/app.scss',
        ['buttons.index']: 'src/components/buttons/index.js',
        ['test.index']: 'src/components/test/index.js'
      },
      name: 'my-package-name',
      formats: ['es'] // only export ES module build
    },
    rollupOptions: {
      // externalize deps that shouldn't be bundled into the library
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        },
        // Preserve the modules and directories in the final build
        preserveModules: true
      },
      plugins: [addCSSImport()]
    }
  }
});

and in my package.json, I have:

  "files": [
    "dist"
  ],
  "type": "module",
  "exports": {
    "./styles": "./dist/global-styles.css",
    "./buttons": "./dist/buttons.index.js",
    "./test": "./dist/test.index.js"
  },