vite
vite copied to clipboard
Allow to specify filename of emitted CSS when `build.cssCodeSplit: false`
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
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
Finally found this issue after several attempts to configure the CSS filename.
Thought this would work...
css: { postcss: { to: "custom.css" } },
...but nope.
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;
},
},
},
},
});
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 thenfs.writeFileSync('somewhere',cssString)
neither solutions work for me at the moment. it's as if the css file is never seen by assetFileNames
. going to log...
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]);
}
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
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
.
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' }
How can we use the entry name to construct two files with the name
L.css
andX.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 nameindex.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
},
},
},
});
This worked for me:
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, "src/main.js"),
name: "libraryName",
fileName: "libraryName",
},
rollupOptions: {
output: {
assetFileNames: "libraryName.[ext]",
},
},
},
});
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
})
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.
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 Would you create a new issue with reproduction?
Would you create a new issue with reproduction?
Will do later today 👌
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
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!
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 inassetFileNames
function
Tried it with Vite 5.2.9 and it worked fine.
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 🤦 )
这个问题, 将我这边终结,不可不说 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
}
}
}
what a heavy thread