vite icon indicating copy to clipboard operation
vite copied to clipboard

Allow to specify filename of emitted CSS when `build.cssCodeSplit: false`

Open vwkd opened this issue 3 years ago • 19 comments

Clear and concise description of the problem

When building in library mode for a .js file that imports styles, Vite generates a style.css file in the target directory alongside the .js file.

Using build.lib.fileName we can already specify the filename for the .js file. Currently there seems to be no way to specify the filename for the .css file, as it always defaults to style.css.

Suggested solution

Maybe a build.lib.fileNameCSS option?

Alternative

A workaround is renaming the style.css in a postbuild script. But this isn't ideal since it involves yet another build step, and doing it in Vite itself would be more coherent.

Additional context

No response

Validations

vwkd avatar Sep 06 '21 15:09 vwkd

Finally found this issue after several attempts to configure the CSS filename.

Thought this would work...

	css: { postcss: { to: "custom.css" } },

...but nope.

uipoet avatar Nov 09 '21 02:11 uipoet

Me as a vite's newbie. Following config works but not elegant

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          if (assetInfo.name === 'style.css') return 'custom.css';
          return assetInfo.name;
        },
      },
    },
  },
});

koooge avatar Jan 05 '22 07:01 koooge

rollupOptions: {
  output: [
    assetFileNames: (assetInfo) => {
      const { source } = assetInfo
      return `[name][hash][extname]`
    }
  ]
}

You can try these two methods

  • rename ${somewhere}/${cssFilename}.[hash][extname]

  • get the compiled css text from assetInfo.source, an then fs.writeFileSync('somewhere',cssString)

17px avatar Oct 11 '22 08:10 17px

neither solutions work for me at the moment. it's as if the css file is never seen by assetFileNames. going to log...

johnnyshankman avatar Oct 31 '22 20:10 johnnyshankman

node_modules/vite/dist/node/chunks/dep-67e7f8ab.js

const cssBundleName = 'style.css';

function cssPostPlugin(config) {

    return {
        name: 'vite:css-post',

            let extractedCss = outputToExtractedCSSMap.get(opts);
            if (extractedCss && !hasEmitted) {
                hasEmitted = true;
                extractedCss = await finalizeCss(extractedCss, true, config);
                this.emitFile({
                    name: cssBundleName,
                    type: 'asset',
                    source: extractedCss
                });
            }

https://github.com/vitejs/vite/blob/4fb7ad0e133311b55016698ece01fad9fb59ce11/packages/vite/src/node/plugins/css.ts#L684

emitFile is part of rollup

node_modules/rollup/dist/es/shared/rollup.js

        this.emitFile = (emittedFile) => {
            if (!hasValidType(emittedFile)) {
                return error(errFailedValidation(`Emitted files must be of type "asset" or "chunk", received "${emittedFile && emittedFile.type}".`));
            }
            if (!hasValidName(emittedFile)) {
                return error(errFailedValidation(`The "fileName" or "name" properties of emitted files must be strings that are neither absolute nor relative paths, received "${emittedFile.fileName || emittedFile.name}".`));
            }
            if (emittedFile.type === 'chunk') {
                return this.emitChunk(emittedFile);
            }
            return this.emitAsset(emittedFile);
        };

this calls emitAsset

this calls finalizeAsset

this sets

        bundle[fileName] = {
            fileName,
            get isAsset() {
                warnDeprecation('Accessing "isAsset" on files in the bundle is deprecated, please use "type === \'asset\'" instead', true, options);
                return true;
            },
            name: consumedFile.name,
            source,
            type: 'asset'
        };

with fileName == "style.css"

bundle[fileName] is done at this point ... add proxy for tracing read access

        if (fileName == "style.css") {
            bundle[fileName] = new Proxy(bundle[fileName], {
                get(obj, key) {
                    if (key == "source") {
                        console.log(`trace style bundle: get bundle[${fileName}].${key}`, new Error().stack);
                    }
                    return obj[key]
                }
            })
        }

rollup writes to style.css

async function writeOutputFile(outputFile, outputOptions) {
    const fileName = resolve(outputOptions.dir || dirname(outputOptions.file), outputFile.fileName);
    // 'recursive: true' does not throw if the folder structure, or parts of it, already exist
    await promises.mkdir(dirname(fileName), { recursive: true });
    let writeSourceMapPromise;
    let source;
    if (outputFile.type === 'asset') {
        source = outputFile.source;
    }
    else {
        source = outputFile.code;
        if (outputOptions.sourcemap && outputFile.map) {
            let url;
            if (outputOptions.sourcemap === 'inline') {
                url = outputFile.map.toUrl();
            }
            else {
                const { sourcemapBaseUrl } = outputOptions;
                const sourcemapFileName = `${basename(outputFile.fileName)}.map`;
                url = sourcemapBaseUrl
                    ? new URL(sourcemapFileName, sourcemapBaseUrl).toString()
                    : sourcemapFileName;
                writeSourceMapPromise = promises.writeFile(`${fileName}.map`, outputFile.map.toString());
            }
            if (outputOptions.sourcemap !== 'hidden') {
                source += `//# ${SOURCEMAPPING_URL}=${url}\n`;
            }
        }
    }
    return Promise.all([promises.writeFile(fileName, source), writeSourceMapPromise]);
}

milahu avatar Nov 25 '22 20:11 milahu

long story short

-  config.build.lib.rollupOptions.output.assetFileNames = (assetInfo) => { ... }
+  config.build    .rollupOptions.output.assetFileNames = (assetInfo) => { ... }

so this works

https://github.com/vitejs/vite/issues/4863#issuecomment-1005451468

milahu avatar Nov 25 '22 20:11 milahu

How can we use the entry name to construct two files with the name L.css and X.css when specifying an entry like this:

export default {
    build : {
        rollupOptions : {
            input : {
                L : 'login/index.less',
                X : 'X/index.less'
            }
        }
    }
};

Inside the assetFileNames hook, we cannot distinguish two assets with the same name index.css.

aleen42 avatar Dec 30 '22 07:12 aleen42

Having the same issue with library-mode of multi-entry. each entry has css (LESS actually) inclusion along the "import" lane. Yet i get a single "styles.css" file at the end.

tried using "config.build.rollupOptions.output.assetFileNames" but assetInfo is always: { name: 'style.css', type: 'asset', source: 'vite internal call, ignore' }

amirgalor-gong avatar Jan 10 '23 21:01 amirgalor-gong

How can we use the entry name to construct two files with the name L.css and X.css when specifying an entry like this:

export default {
    build : {
        rollupOptions : {
            input : {
                L : 'login/index.less',
                X : 'X/index.less'
            }
        }
    }
};

Inside the assetFileNames hook, we cannot distinguish two assets with the same name index.css.

I'm quite amazed at the amount of bullshit code i had to write to make this possible, when old solutions like grunt where just 1 line. I'm leaving it here in case someone else still needs it:

import { defineConfig } from "vite";

const inputs = {
    "css/built.css": "./htdocs/css/src/main.scss",
    "unlogged/css/main.css": "./htdocs/unlogged/css/main.scss",
    "js/built": "./htdocs/js/index.js",
};
let i = 0;

export default defineConfig({
    build: {
        emptyOutDir: false,
        outDir: "htdocs/",
        sourcemap: true,
        cssCodeSplit: true,
        rollupOptions: {
            input: inputs,
            output: {
                assetFileNames: (file) => {
                    // those are input file names, we don't care about them
                    if (Object.values(inputs).includes(file.name) || Object.values(inputs).includes("./" + file.name)) {
                        return `[name].[ext]`;
                    }

                    const endPath = Object.keys(inputs)[i];
                    i++;

                    return endPath;
                },
                entryFileNames: `[name].js`, // otherwise it will add the hash
                chunkFileNames: `[name].js`, // otherwise it will add the hash
            },
        },
    },
});

psociety avatar Jun 14 '23 08:06 psociety

This worked for me:

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, "src/main.js"),
      name: "libraryName",
      fileName: "libraryName",
    },
    rollupOptions: {
      output: {
        assetFileNames: "libraryName.[ext]",
      },
    },
  },
});

LuisSevillano avatar Jun 16 '23 10:06 LuisSevillano

Here is one potentially dangerous way to solve for multiple entrypoint apps (instead of auto-incrementing index.css files. Ex: index1.css, index2.css) it if you're into potentially dangerous solves. If you have a better way to do this (and there has to be one, right?) please share it.

You can see it working as intended here.

import { resolve } from "path"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react-swc"

const inputNames = ["door_one", "door_two", "door_three"]

export const mapCSSFileNames = {
  name: 'mapCSSFileNames',
  generateBundle(opts, bundle) {
    const cssNamesMap = {}

    // This is dumb... why do I have to do this... oh golly im so clueless..
    Object.entries(bundle).forEach(([fileName, fileInfo]) => {
      if (inputNames.includes(fileInfo.name)) {        
        const ambiguous_css_name = [...fileInfo.viteMetadata.importedCss][0]
        const mapped_css_name = `${fileInfo.name}.css`
        bundle[ambiguous_css_name].fileName = mapped_css_name
        bundle[ambiguous_css_name].name = mapped_css_name
        cssNamesMap[ambiguous_css_name] = mapped_css_name
      }
    })
  },
  writeBundle(opts, bundle) {
    const cssNamesMap = {}

    Object.keys(bundle).forEach((key) => {
      const file = bundle[key]
      if(file.fileName.endsWith(".css")) {
        cssNamesMap[key] = file.fileName
      }
    })

    Object.entries(bundle).forEach(([fileName, fileInfo]) => {
      const cssFileNames = Object.keys(cssNamesMap)
      if(fileName.endsWith(".html")) {
        cssFileNames.forEach((cssFileName) => {

          if(fileInfo.source.includes(cssFileName)) {
            console.log(`Pattern: ${cssFileName} found in ${fileName} replacing it with: ${cssNamesMap[cssFileName]}`);
            fileInfo.source = fileInfo.source.replace(cssFileName, cssNamesMap[cssFileName])
          }
        })
      }
    })
  },
}

export default defineConfig(({ mode }) => {
  const config = {
    root: "src",
    build: {
      outDir: "../static/",
      emptyOutDir: true,
      rollupOptions: {
        maxParallelFileOps: 100,
        input: {
          door_one: resolve(__dirname, "src", "DoorOne", "index.html"),
          door_two: resolve(__dirname, "src", "DoorTwo", "index.html"),
          door_three: resolve(__dirname, "src", "DoorThree", "index.html"),
        },
        output: {
          entryFileNames: "[name].js",
        },
      },
      commonjsOptions: { include: [] },
    },
    plugins: [
      react({
        include: [/\.jsx?$/, /\.js$/],
      }),
      mapCSSFileNames,
    ],
    resolve: {
      alias: {
        "~": resolve(__dirname, "src/"),
      },
    },
    optimizeDeps: {
      disabled: false,
    },
  }
  return config
})

matty-at-rdc avatar Oct 25 '23 21:10 matty-at-rdc

We've discussed this in the last meeting. We think deriving the CSS file name from build.lib.fileName would be good. But unfortunately this will be a breaking change and we'll need to wait for Vite 6 to change it.

For now, you can use the workaround in https://github.com/vitejs/vite/issues/4863#issuecomment-1005451468.


Maybe we can have a experimental.enableCssFilenameDerivation flag that opt-in the behavior above.

sapphi-red avatar Nov 15 '23 12:11 sapphi-red

In Vite 5, the build step now prints the CSS filename for each JS format:

$ vite build

vite v5.0.0 building for production...
✓ 3 modules transformed.
dist/libraryName.css  17.22 kB │ gzip: 6.70 kB
dist/libraryName.js    4.22 kB │ gzip: 1.29 kB
dist/libraryName.css     17.22 kB │ gzip: 6.70 kB
dist/libraryName.umd.js   3.48 kB │ gzip: 1.33 kB
dist/libraryName.css      17.22 kB │ gzip: 6.70 kB
dist/libraryName.iife.js   3.31 kB │ gzip: 1.24 kB
✓ built in 220ms

I hope it doesn't mean that the same file is being build three times... 😅 (edit: it does 😕)

kytta avatar Nov 20 '23 08:11 kytta

@kytta Would you create a new issue with reproduction?

sapphi-red avatar Nov 21 '23 12:11 sapphi-red

Would you create a new issue with reproduction?

Will do later today 👌

kytta avatar Nov 21 '23 12:11 kytta

Me as a vite's newbie. Following config works but not elegant

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          if (assetInfo.name === 'style.css') return 'custom.css';
          return assetInfo.name;
        },
      },
    },
  },
});

Doesn't work for me anymore in Vite 4.5.2: asset keeps it's name doesn't matter what I return in assetFileNames function

styfrombrest avatar Jan 31 '24 15:01 styfrombrest

if someone is not injecting an index.css file, and use the default name generated by the styleX plugin, here a plugin i created for my case to rename the file.

import { rename, readdirSync } from "fs";

const renameCss = ({
  folder,
  filename,
}: {
  folder: string;
  filename: string;
}) => ({
  name: "vite-plugin-rename-css",
  writeBundle() {
    const files = readdirSync(folder);
    const file = files[0];
    rename(`${folder}/${file}`, `${folder}/${filename}`, () => {
      console.log("renamed css!");
    });
  },
});

then use it as a normal plugin after the styleX() plugin call passing the build folder the css it's in and the new filename you want.

plugins: [
    react(),
    styleX(),
    renameCss({
      folder: "./lib/assets",
      filename: "style.css",
    }),
    dts(),
  ]

Of course you can extend it or change it as you wish, was just a quick one for my case, hope it helps!

baxterw3b avatar Apr 02 '24 16:04 baxterw3b

Me as a vite's newbie. Following config works but not elegant

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          if (assetInfo.name === 'style.css') return 'custom.css';
          return assetInfo.name;
        },
      },
    },
  },
});

Doesn't work for me anymore in Vite 4.5.2: asset keeps it's name doesn't matter what I return in assetFileNames function

Tried it with Vite 5.2.9 and it worked fine.

TheJaredWilcurt avatar Apr 22 '24 01:04 TheJaredWilcurt

Copying my comment from the PR:

In the meeting when we discussed this, we decided to re-use the library mode name as the CSS file name instead so we can avoid an option, but that would be for Vite 6.

(I also just realized this was mentioned before: https://github.com/vitejs/vite/issues/4863#issuecomment-1812450561 🤦 )

bluwy avatar May 20 '24 07:05 bluwy

这个问题, 将我这边终结,不可不说 vite 这个构建配置是恶心到我了。 好了 请看我的解决方案 1 首先要用到为什么要用到插件,在代码注释中会说明 vite-plugin-style-inject

import type { Plugin } from 'vite';

export interface PreRenderedAsset {
  name: string;
  type: string;
  source: string;
}

export interface VitePluginStyleInjectOptions {
  fileName?: string;
  assetFileNames?: (chunkInfo: PreRenderedAsset) =>  string | void;
}
export default function VitePluginStyleInject(options?: VitePluginStyleInjectOptions): Plugin {

  const { fileName = 'style.css', assetFileNames } = options || {}
  
  let viteConfig;

  return {
    name: 'vite-plugin-style-inject',
    apply: 'build',
    enforce: 'post',
    configResolved(resolvedConfig) {
      viteConfig = resolvedConfig;
    },
    generateBundle(config, bundles) {

      // cssCodeSplit 提取出 css 之后,这里将所有的css 进行汇总
      const chunks = Object.values(bundles);

      // 需要处理的全部css
      const cssChunks = chunks.filter(p => p.type === 'asset' &&  /[.](css|less|scss)$/.test(p.fileName));


      const cssChunkGroup = cssChunks.reduce((memo: any, chunk: any) => {
        const newFileName = assetFileNames?.(chunk) || fileName;
        const assetChunks = memo[newFileName] || [];
        assetChunks.push(chunk);
        memo[newFileName] = assetChunks;
        return memo
      }, {})

      const cssChunkGroupKeys = Object.keys(cssChunkGroup);
      // create new asset bundles
      const newAssetBundles = cssChunkGroupKeys.map((name) => {
        const cssContent = cssChunkGroup[name].map(p => p.source || '').join('\n');
        return {
          fileName: name,
          name: name,
          needsCodeReference: false,
          source: cssContent,
          type: 'asset'
        }
      })

      // delete old css bundles
      cssChunks.forEach(chunk => {
        delete bundles[chunk.fileName]
      })

      newAssetBundles.forEach((bundle) => {
        // @ts-ignore
        // inject new asset bundles
        bundles[bundle.fileName] = bundle;
      })

    },
  }
}

2 使用这个插件

plugins: [
      vitePluginStyleInject({
        assetFileNames(chunk){
           return /xxx/.test(chunk.fileName) ? 'custom.css' : 'style.css'
        }
      }),
]

3 开启 cssCodeSplit, 为什么要开启呢?看注释

  build: {
      /** 
       * 如果为false, 会先对css 进行聚合,最终只能获取到多个默认的 chunks name = style.css 无法区分
       * 但是开启为true ,assetFileNames 中的chunks 就可以获取到各自的fileName
       * 
       * 同名的 css name ,会生成同名style2, 因此 需要通过插件 vitePluginStyleInject ,对名称重新分类
       */
      cssCodeSplit: true,
}

4 重要 禁用掉已有的 build.rollupOptions.output.assetFileNames 设置

build: {
  rollupOptions: {
     output: {
          /**
           * 1 为了防止这边的修改 导致 在 vitePluginStyleInject 中的 assetFileNames 出现冲突,建议这里不做处理
           * 2 简单的方式 在这边修改名称 是可以的,其实不用插件。但是: assetFileNames 如果返回两个重名的定义
           * 例如: style1.css style1.css ,最终输出的时候 会生成style1.css 和 style12.css,所以才用到上面插件
           * vitePluginStyleInject 自定义实现
           */
        // assetFileNames
     }
  }
}

jxh150535011 avatar Aug 27 '24 11:08 jxh150535011

what a heavy thread

junaga avatar Sep 08 '24 16:09 junaga