vite icon indicating copy to clipboard operation
vite copied to clipboard

Can css be styleInjected in library mode?

Open gwsbhqt opened this issue 3 years ago • 71 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

gwsbhqt avatar Jan 18 '21 10:01 gwsbhqt

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.

yyx990803 avatar Jan 20 '21 03:01 yyx990803

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.

gwsbhqt avatar Jan 20 '21 09:01 gwsbhqt

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?

eikooc avatar Jan 26 '21 09:01 eikooc

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

shanlh avatar Feb 03 '21 08:02 shanlh

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 avatar Feb 08 '21 01:02 richardtallent

@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: https://github.com/fuxingloh/vue-horizontal/pull/87

fuxingloh avatar Feb 08 '21 01:02 fuxingloh

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!

richardtallent avatar Feb 08 '21 04:02 richardtallent

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

ziping-li avatar Feb 27 '21 13:02 ziping-li

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?

hiendv avatar May 14 '21 09:05 hiendv

Any update on this issue?

aekasitt avatar Aug 17 '21 11:08 aekasitt

@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"
    }
}

gwsbhqt avatar Aug 23 '21 05:08 gwsbhqt

+1 for this feature.

phantomlsh avatar Aug 30 '21 22:08 phantomlsh

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

xinyao27 avatar Sep 15 '21 03:09 xinyao27

@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?

ghost avatar Sep 28 '21 03:09 ghost

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

tylerrrkd avatar Oct 03 '21 10:10 tylerrrkd

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.

7zf001 avatar Nov 03 '21 10:11 7zf001

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

fnick851 avatar Nov 26 '21 01:11 fnick851

Sorry, that was Kami (my cat)

Shinigami92 avatar Nov 26 '21 08:11 Shinigami92

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.

wxsms avatar Dec 08 '21 08:12 wxsms

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.

mesqueeb avatar Dec 18 '21 08:12 mesqueeb

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.

wxsms avatar Dec 22 '21 05:12 wxsms

@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 avatar Jan 02 '22 12:01 ludmiloff

@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 avatar Jan 02 '22 14:01 mesqueeb

@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)
        }
      }
    },
  }
}

ludmiloff avatar Jan 03 '22 09:01 ludmiloff

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

cutterbl avatar Jan 14 '22 21:01 cutterbl

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 avatar Feb 13 '22 05:02 donnikitos

@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

kasvith avatar Feb 18 '22 07:02 kasvith

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.

FreekVR avatar Feb 18 '22 09:02 FreekVR

+1 for this feature.

djaxho avatar Feb 18 '22 09:02 djaxho

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

kasvith avatar Feb 18 '22 10:02 kasvith