Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can css be styleInjected in library mode? #1579

Open
gwsbhqt opened this issue Jan 18, 2021 · 71 comments
Open

Can css be styleInjected in library mode? #1579

gwsbhqt opened this issue Jan 18, 2021 · 71 comments
Labels
enhancement New feature or request feat: library mode

Comments

@gwsbhqt
Copy link

gwsbhqt commented Jan 18, 2021

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 gwsbhqt changed the title can css styles be appended into the single target js file in library mode? Can css styles be appended into the single target js file in library mode? Jan 18, 2021
@gwsbhqt gwsbhqt changed the title Can css styles be appended into the single target js file in library mode? Can css be styleInjected in library mode? Jan 19, 2021
@yyx990803 yyx990803 added the enhancement New feature or request label Jan 19, 2021
@yyx990803
Copy link
Member

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 yyx990803 reopened this Jan 20, 2021
@gwsbhqt
Copy link
Author

gwsbhqt commented Jan 20, 2021

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.

@eikooc
Copy link

eikooc commented Jan 26, 2021

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?

@shanlh
Copy link

shanlh commented Feb 3, 2021

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

@richardtallent
Copy link

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?

@fuxingloh
Copy link

fuxingloh commented Feb 8, 2021

@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

@richardtallent
Copy link

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!

@ziping-li
Copy link

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

@hiendv
Copy link

hiendv commented May 14, 2021

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?

@aekasitt

This comment was marked as spam.

@gwsbhqt
Copy link
Author

gwsbhqt commented Aug 23, 2021

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

@phantomlsh

This comment was marked as spam.

@xinyao27
Copy link

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

harmendv added a commit to libvue/core that referenced this issue Sep 19, 2021
@ghost
Copy link

ghost commented Sep 28, 2021

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

@tylerrrkd
Copy link

tylerrrkd commented Oct 3, 2021

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

@7zf001
Copy link

7zf001 commented Nov 3, 2021

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.

@fnick851
Copy link

fnick851 commented Nov 26, 2021

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

@Shinigami92 Shinigami92 added YAO Yet another option... and removed YAO Yet another option... labels Nov 26, 2021
@Shinigami92
Copy link
Member

Sorry, that was Kami (my cat)

@wxsms
Copy link

wxsms commented Dec 8, 2021

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.

snqb added a commit to logicalclocks/quartz that referenced this issue Apr 28, 2023
in Library mode vite just doesn't include `.css` files.
It's a known issue. vitejs/vite#1579
This PR includes a plugin that includes CSS output via JS.
We need it to load `react-date-picker`'s styles.
snqb added a commit to logicalclocks/quartz that referenced this issue Apr 28, 2023
in Library mode vite just doesn't include `.css` files.
It's a known issue. vitejs/vite#1579
This PR includes a plugin that includes CSS output via JS.
We need it to load `react-date-picker`'s styles.
@marcelobotega
Copy link

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

@DamonCais
Copy link

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.

@receter
Copy link

receter commented Aug 14, 2023

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

@nicefan
Copy link

nicefan commented Sep 7, 2023

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

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

@emosheeep
Copy link
Contributor

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

@receter
Copy link

receter commented Oct 10, 2023

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

miguelgrc added a commit to cern-sis/react-formule that referenced this issue Oct 31, 2023
Without this, when building, Vite would generate a style.css file that would need to be imported by users in the host app, which is not an ideal DX

See vitejs/vite#1579
@TerkaPrzemyslaw1897
Copy link

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?

@kalnode
Copy link

kalnode commented Jan 19, 2024

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.

@eguzman-tc
Copy link

eguzman-tc commented Apr 10, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request feat: library mode
Projects
None yet
Development

No branches or pull requests