Skip to content

Commit 61cbf6f

Browse files
bluwysapphi-red
andauthoredOct 30, 2024··
feat(lib)!: use package name for css output file name (#18488)
Co-authored-by: 翠 / green <green@sapphi.red>
1 parent d951310 commit 61cbf6f

17 files changed

+290
-14
lines changed
 

‎docs/config/build-options.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,28 @@ Options to pass on to [@rollup/plugin-dynamic-import-vars](https://github.com/ro
162162

163163
## build.lib
164164

165-
- **Type:** `{ entry: string | string[] | { [entryAlias: string]: string }, name?: string, formats?: ('es' | 'cjs' | 'umd' | 'iife')[], fileName?: string | ((format: ModuleFormat, entryName: string) => string) }`
165+
- **Type:** `{ entry: string | string[] | { [entryAlias: string]: string }, name?: string, formats?: ('es' | 'cjs' | 'umd' | 'iife')[], fileName?: string | ((format: ModuleFormat, entryName: string) => string), cssFileName?: string }`
166166
- **Related:** [Library Mode](/guide/build#library-mode)
167167

168-
Build as a library. `entry` is required since the library cannot use HTML as entry. `name` is the exposed global variable and is required when `formats` includes `'umd'` or `'iife'`. Default `formats` are `['es', 'umd']`, or `['es', 'cjs']`, if multiple entries are used. `fileName` is the name of the package file output, default `fileName` is the name option of package.json, it can also be defined as function taking the `format` and `entryName` as arguments.
168+
Build as a library. `entry` is required since the library cannot use HTML as entry. `name` is the exposed global variable and is required when `formats` includes `'umd'` or `'iife'`. Default `formats` are `['es', 'umd']`, or `['es', 'cjs']`, if multiple entries are used.
169+
170+
`fileName` is the name of the package file output, which defaults to the `"name"` in `package.json`. It can also be defined as a function taking the `format` and `entryName` as arguments, and returning the file name.
171+
172+
If your package imports CSS, `cssFileName` can be used to specify the name of the CSS file output. It defaults to the same value as `fileName` if it's set a string, otherwise it also falls back to the `"name"` in `package.json`.
173+
174+
```js twoslash [vite.config.js]
175+
import { defineConfig } from 'vite'
176+
177+
export default defineConfig({
178+
build: {
179+
lib: {
180+
entry: ['src/main.js'],
181+
fileName: (format, entryName) => `my-lib-${entryName}.${format}.js`,
182+
cssFileName: 'my-lib-style',
183+
},
184+
},
185+
})
186+
```
169187

170188
## build.manifest
171189

‎docs/guide/build.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,12 @@ import Bar from './Bar.vue'
200200
export { Foo, Bar }
201201
```
202202

203-
Running `vite build` with this config uses a Rollup preset that is oriented towards shipping libraries and produces two bundle formats: `es` and `umd` (configurable via `build.lib`):
203+
Running `vite build` with this config uses a Rollup preset that is oriented towards shipping libraries and produces two bundle formats:
204+
205+
- `es` and `umd` (for single entry)
206+
- `es` and `cjs` (for multiple entries)
207+
208+
The formats can be configured with the [`build.lib.formats`](/config/build-options.md#build-lib) option.
204209

205210
```
206211
$ vite build
@@ -251,6 +256,29 @@ Recommended `package.json` for your lib:
251256

252257
:::
253258

259+
### CSS support
260+
261+
If your library imports any CSS, it will be bundled as a single CSS file besides the built JS files, e.g. `dist/my-lib.css`. The name defaults to `build.lib.fileName`, but can also be changed with [`build.lib.cssFileName`](/config/build-options.md#build-lib).
262+
263+
You can export the CSS file in your `package.json` to be imported by users:
264+
265+
```json {12}
266+
{
267+
"name": "my-lib",
268+
"type": "module",
269+
"files": ["dist"],
270+
"main": "./dist/my-lib.umd.cjs",
271+
"module": "./dist/my-lib.js",
272+
"exports": {
273+
".": {
274+
"import": "./dist/my-lib.js",
275+
"require": "./dist/my-lib.umd.cjs"
276+
},
277+
"./style.css": "./dist/my-lib.css"
278+
}
279+
}
280+
```
281+
254282
::: tip File Extensions
255283
If the `package.json` does not contain `"type": "module"`, Vite will generate different file extensions for Node.js compatibility. `.js` will become `.mjs` and `.cjs` will become `.js`.
256284
:::

‎docs/guide/migration.md

+20
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,26 @@ From Vite 6, the modern API is used by default for Sass. If you wish to still us
3232

3333
To migrate to the modern API, see [the Sass documentation](https://sass-lang.com/documentation/breaking-changes/legacy-js-api/).
3434

35+
### Customize CSS output file name in library mode
36+
37+
In Vite 5, the CSS output file name in library mode was always `style.css` and cannot be easily changed through the Vite config.
38+
39+
From Vite 6, the default file name now uses `"name"` in `package.json` similar to the JS output files. If [`build.lib.fileName`](/config/build-options.md#build-lib) is set with a string, the value will also be used for the CSS output file name. To explicitly set a different CSS file name, you can use the new [`build.lib.cssFileName`](/config/build-options.md#build-lib) to configure it.
40+
41+
To migrate, if you had relied on the `style.css` file name, you should update references to it to the new name based on your package name. For example:
42+
43+
```json [package.json]
44+
{
45+
"name": "my-lib",
46+
"exports": {
47+
"./style.css": "./dist/style.css" // [!code --]
48+
"./style.css": "./dist/my-lib.css" // [!code ++]
49+
}
50+
}
51+
```
52+
53+
If you prefer to stick with `style.css` like in Vite 5, you can set `build.lib.cssFileName: 'style'` instead.
54+
3555
## Advanced
3656

3757
There are other breaking changes which only affect few users.

‎packages/vite/src/node/__tests__/plugins/css.spec.ts

+49
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'node:path'
2+
import { fileURLToPath } from 'node:url'
23
import { describe, expect, test } from 'vitest'
34
import { resolveConfig } from '../../config'
45
import type { InlineConfig } from '../../config'
@@ -9,9 +10,12 @@ import {
910
getEmptyChunkReplacer,
1011
hoistAtRules,
1112
preprocessCSS,
13+
resolveLibCssFilename,
1214
} from '../../plugins/css'
1315
import { PartialEnvironment } from '../../baseEnvironment'
1416

17+
const __dirname = path.resolve(fileURLToPath(import.meta.url), '..')
18+
1519
describe('search css url function', () => {
1620
test('some spaces before it', () => {
1721
expect(
@@ -369,3 +373,48 @@ describe('preprocessCSS', () => {
369373
`)
370374
})
371375
})
376+
377+
describe('resolveLibCssFilename', () => {
378+
test('use name from package.json', () => {
379+
const filename = resolveLibCssFilename(
380+
{
381+
entry: 'mylib.js',
382+
},
383+
path.resolve(__dirname, '../packages/name'),
384+
)
385+
expect(filename).toBe('mylib.css')
386+
})
387+
388+
test('set cssFileName', () => {
389+
const filename = resolveLibCssFilename(
390+
{
391+
entry: 'mylib.js',
392+
cssFileName: 'style',
393+
},
394+
path.resolve(__dirname, '../packages/noname'),
395+
)
396+
expect(filename).toBe('style.css')
397+
})
398+
399+
test('use fileName if set', () => {
400+
const filename = resolveLibCssFilename(
401+
{
402+
entry: 'mylib.js',
403+
fileName: 'custom-name',
404+
},
405+
path.resolve(__dirname, '../packages/name'),
406+
)
407+
expect(filename).toBe('custom-name.css')
408+
})
409+
410+
test('use fileName if set and has array entry', () => {
411+
const filename = resolveLibCssFilename(
412+
{
413+
entry: ['mylib.js', 'mylib2.js'],
414+
fileName: 'custom-name',
415+
},
416+
path.resolve(__dirname, '../packages/name'),
417+
)
418+
expect(filename).toBe('custom-name.css')
419+
})
420+
})

‎packages/vite/src/node/build.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
copyDir,
4545
displayTime,
4646
emptyDir,
47+
getPkgName,
4748
joinUrlSegments,
4849
normalizePath,
4950
partialEncodeURIPath,
@@ -296,6 +297,12 @@ export interface LibraryOptions {
296297
* format as an argument.
297298
*/
298299
fileName?: string | ((format: ModuleFormat, entryName: string) => string)
300+
/**
301+
* The name of the CSS file output if the library imports CSS. Defaults to the
302+
* same value as `build.lib.fileName` if it's set a string, otherwise it falls
303+
* back to the name option of the project package.json.
304+
*/
305+
cssFileName?: string
299306
}
300307

301308
export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' | 'system'
@@ -879,10 +886,6 @@ function prepareOutDir(
879886
}
880887
}
881888

882-
function getPkgName(name: string) {
883-
return name?.[0] === '@' ? name.split('/')[1] : name
884-
}
885-
886889
type JsExt = 'js' | 'cjs' | 'mjs'
887890

888891
function resolveOutputJsExtension(

‎packages/vite/src/node/plugins/css.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
toOutputFilePathInCss,
4242
toOutputFilePathInJS,
4343
} from '../build'
44+
import type { LibraryOptions } from '../build'
4445
import {
4546
CLIENT_PUBLIC_PATH,
4647
CSS_LANGS_RE,
@@ -60,6 +61,7 @@ import {
6061
generateCodeFrame,
6162
getHash,
6263
getPackageManagerCommand,
64+
getPkgName,
6365
injectQuery,
6466
isDataUrl,
6567
isExternalUrl,
@@ -81,6 +83,8 @@ import { PartialEnvironment } from '../baseEnvironment'
8183
import type { TransformPluginContext } from '../server/pluginContainer'
8284
import { searchForWorkspaceRoot } from '../server/searchRoot'
8385
import { type DevEnvironment } from '..'
86+
import type { PackageCache } from '../packages'
87+
import { findNearestPackageData } from '../packages'
8488
import { addToHTMLProxyTransformResult } from './html'
8589
import {
8690
assetUrlRE,
@@ -213,7 +217,7 @@ const functionCallRE = /^[A-Z_][.\w-]*\(/i
213217
const transformOnlyRE = /[?&]transform-only\b/
214218
const nonEscapedDoubleQuoteRe = /(?<!\\)"/g
215219

216-
const cssBundleName = 'style.css'
220+
const defaultCssBundleName = 'style.css'
217221

218222
const enum PreprocessLang {
219223
less = 'less',
@@ -256,6 +260,9 @@ export const removedPureCssFilesCache = new WeakMap<
256260
Map<string, RenderedChunk>
257261
>()
258262

263+
// Used only if the config doesn't code-split CSS (builds a single CSS file)
264+
export const cssBundleNameCache = new WeakMap<ResolvedConfig, string>()
265+
259266
const postcssConfigCache = new WeakMap<
260267
ResolvedConfig,
261268
PostCSSConfigResult | null | Promise<PostCSSConfigResult | null>
@@ -428,6 +435,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
428435
// since output formats have no effect on the generated CSS.
429436
let hasEmitted = false
430437
let chunkCSSMap: Map<string, string>
438+
let cssBundleName: string
431439

432440
const rollupOptionsOutput = config.build.rollupOptions.output
433441
const assetFileNames = (
@@ -464,6 +472,14 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
464472
hasEmitted = false
465473
chunkCSSMap = new Map()
466474
codeSplitEmitQueue = createSerialPromiseQueue()
475+
cssBundleName = config.build.lib
476+
? resolveLibCssFilename(
477+
config.build.lib,
478+
config.root,
479+
config.packageCache,
480+
)
481+
: defaultCssBundleName
482+
cssBundleNameCache.set(config, cssBundleName)
467483
},
468484

469485
async transform(css, id) {
@@ -1851,7 +1867,9 @@ async function minifyCSS(
18511867
...config.css?.lightningcss,
18521868
targets: convertTargets(config.build.cssTarget),
18531869
cssModules: undefined,
1854-
filename: cssBundleName,
1870+
// TODO: Pass actual filename here, which can also be passed to esbuild's
1871+
// `sourcefile` option below to improve error messages
1872+
filename: defaultCssBundleName,
18551873
code: Buffer.from(css),
18561874
minify: true,
18571875
})
@@ -3255,3 +3273,25 @@ export const convertTargets = (
32553273
convertTargetsCache.set(esbuildTarget, targets)
32563274
return targets
32573275
}
3276+
3277+
export function resolveLibCssFilename(
3278+
libOptions: LibraryOptions,
3279+
root: string,
3280+
packageCache?: PackageCache,
3281+
): string {
3282+
if (typeof libOptions.cssFileName === 'string') {
3283+
return `${libOptions.cssFileName}.css`
3284+
} else if (typeof libOptions.fileName === 'string') {
3285+
return `${libOptions.fileName}.css`
3286+
}
3287+
3288+
const packageJson = findNearestPackageData(root, packageCache)?.data
3289+
const name = packageJson ? getPkgName(packageJson.name) : undefined
3290+
3291+
if (!name)
3292+
throw new Error(
3293+
'Name in package.json is required if option "build.lib.cssFileName" is not provided.',
3294+
)
3295+
3296+
return `${name}.css`
3297+
}

‎packages/vite/src/node/plugins/html.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
publicAssetUrlRE,
4040
urlToBuiltUrl,
4141
} from './asset'
42-
import { isCSSRequest } from './css'
42+
import { cssBundleNameCache, isCSSRequest } from './css'
4343
import { modulePreloadPolyfillId } from './modulePreloadPolyfill'
4444

4545
interface ScriptAssetsUrl {
@@ -909,10 +909,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
909909

910910
// inject css link when cssCodeSplit is false
911911
if (!this.environment.config.build.cssCodeSplit) {
912-
const cssChunk = Object.values(bundle).find(
913-
(chunk) =>
914-
chunk.type === 'asset' && chunk.names.includes('style.css'),
915-
) as OutputAsset | undefined
912+
const cssBundleName = cssBundleNameCache.get(config)
913+
const cssChunk =
914+
cssBundleName &&
915+
(Object.values(bundle).find(
916+
(chunk) =>
917+
chunk.type === 'asset' && chunk.names.includes(cssBundleName),
918+
) as OutputAsset | undefined)
916919
if (cssChunk) {
917920
result = injectToHead(result, [
918921
{

‎packages/vite/src/node/utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,10 @@ export function getNpmPackageName(importPath: string): string | null {
13141314
}
13151315
}
13161316

1317+
export function getPkgName(name: string): string | undefined {
1318+
return name?.[0] === '@' ? name.split('/')[1] : name
1319+
}
1320+
13171321
const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g
13181322
export function escapeRegex(str: string): string {
13191323
return str.replace(escapeRegexRE, '\\$&')

‎playground/lib/__tests__/lib.spec.ts

+38
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,44 @@ describe.runIf(isBuild)('build', () => {
9090
expect(iife).toMatch('process.env.NODE_ENV')
9191
expect(umd).toMatch('process.env.NODE_ENV')
9292
})
93+
94+
test('single entry with css', () => {
95+
const css = readFile('dist/css-single-entry/test-my-lib.css')
96+
const js = readFile('dist/css-single-entry/test-my-lib.js')
97+
const umd = readFile('dist/css-single-entry/test-my-lib.umd.cjs')
98+
expect(css).toMatch('entry-1.css')
99+
expect(js).toMatch('css-entry-1')
100+
expect(umd).toContain('css-entry-1')
101+
})
102+
103+
test('multi entry with css', () => {
104+
const css = readFile('dist/css-multi-entry/test-my-lib.css')
105+
const js1 = readFile('dist/css-multi-entry/css-entry-1.js')
106+
const js2 = readFile('dist/css-multi-entry/css-entry-2.js')
107+
const cjs1 = readFile('dist/css-multi-entry/css-entry-1.cjs')
108+
const cjs2 = readFile('dist/css-multi-entry/css-entry-2.cjs')
109+
expect(css).toMatch('entry-1.css')
110+
expect(css).toMatch('entry-2.css')
111+
expect(js1).toMatch('css-entry-1')
112+
expect(js2).toMatch('css-entry-2')
113+
expect(cjs1).toContain('css-entry-1')
114+
expect(cjs2).toContain('css-entry-2')
115+
})
116+
117+
test('multi entry with css and code split', () => {
118+
const css1 = readFile('dist/css-code-split/css-entry-1.css')
119+
const css2 = readFile('dist/css-code-split/css-entry-2.css')
120+
const js1 = readFile('dist/css-code-split/css-entry-1.js')
121+
const js2 = readFile('dist/css-code-split/css-entry-2.js')
122+
const cjs1 = readFile('dist/css-code-split/css-entry-1.cjs')
123+
const cjs2 = readFile('dist/css-code-split/css-entry-2.cjs')
124+
expect(css1).toMatch('entry-1.css')
125+
expect(css2).toMatch('entry-2.css')
126+
expect(js1).toMatch('css-entry-1')
127+
expect(js2).toMatch('css-entry-2')
128+
expect(cjs1).toContain('css-entry-1')
129+
expect(cjs2).toContain('css-entry-2')
130+
})
93131
})
94132

95133
test.runIf(isServe)('dev', async () => {

‎playground/lib/__tests__/serve.ts

+18
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ export async function serve(): Promise<{ close(): Promise<void> }> {
9090
configFile: path.resolve(__dirname, '../vite.named-exports.config.js'),
9191
})
9292

93+
await build({
94+
root: rootDir,
95+
logLevel: 'warn', // output esbuild warns
96+
configFile: path.resolve(__dirname, '../vite.css-single-entry.config.js'),
97+
})
98+
99+
await build({
100+
root: rootDir,
101+
logLevel: 'warn', // output esbuild warns
102+
configFile: path.resolve(__dirname, '../vite.css-multi-entry.config.js'),
103+
})
104+
105+
await build({
106+
root: rootDir,
107+
logLevel: 'warn', // output esbuild warns
108+
configFile: path.resolve(__dirname, '../vite.css-code-split.config.js'),
109+
})
110+
93111
// start static file server
94112
const serve = sirv(path.resolve(rootDir, 'dist'))
95113
const httpServer = http.createServer((req, res) => {

‎playground/lib/src/css-entry-1.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './entry-1.css'
2+
3+
export default 'css-entry-1'

‎playground/lib/src/css-entry-2.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './entry-2.css'
2+
3+
export default 'css-entry-2'

‎playground/lib/src/entry-1.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h1 {
2+
content: 'entry-1.css';
3+
}

‎playground/lib/src/entry-2.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h2 {
2+
content: 'entry-2.css';
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { defineConfig } from 'vite'
3+
4+
export default defineConfig({
5+
build: {
6+
cssCodeSplit: true,
7+
lib: {
8+
entry: [
9+
fileURLToPath(new URL('src/css-entry-1.js', import.meta.url)),
10+
fileURLToPath(new URL('src/css-entry-2.js', import.meta.url)),
11+
],
12+
name: 'css-code-split',
13+
},
14+
outDir: 'dist/css-code-split',
15+
},
16+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { defineConfig } from 'vite'
3+
4+
export default defineConfig({
5+
build: {
6+
lib: {
7+
entry: [
8+
fileURLToPath(new URL('src/css-entry-1.js', import.meta.url)),
9+
fileURLToPath(new URL('src/css-entry-2.js', import.meta.url)),
10+
],
11+
name: 'css-multi-entry',
12+
},
13+
outDir: 'dist/css-multi-entry',
14+
},
15+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { defineConfig } from 'vite'
3+
4+
export default defineConfig({
5+
build: {
6+
lib: {
7+
entry: fileURLToPath(new URL('src/css-entry-1.js', import.meta.url)),
8+
name: 'css-single-entry',
9+
},
10+
outDir: 'dist/css-single-entry',
11+
},
12+
})

0 commit comments

Comments
 (0)
Please sign in to comment.