Skip to content

Commit

Permalink
feat(lib)!: use package name for css output file name (#18488)
Browse files Browse the repository at this point in the history
Co-authored-by: 翠 / green <green@sapphi.red>
  • Loading branch information
bluwy and sapphi-red authored Oct 30, 2024
1 parent d951310 commit 61cbf6f
Show file tree
Hide file tree
Showing 17 changed files with 290 additions and 14 deletions.
22 changes: 20 additions & 2 deletions docs/config/build-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,28 @@ Options to pass on to [@rollup/plugin-dynamic-import-vars](https://github.com/ro

## build.lib

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

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

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

```js twoslash [vite.config.js]
import { defineConfig } from 'vite'

export default defineConfig({
build: {
lib: {
entry: ['src/main.js'],
fileName: (format, entryName) => `my-lib-${entryName}.${format}.js`,
cssFileName: 'my-lib-style',
},
},
})
```

## build.manifest

Expand Down
30 changes: 29 additions & 1 deletion docs/guide/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,12 @@ import Bar from './Bar.vue'
export { Foo, Bar }
```

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`):
Running `vite build` with this config uses a Rollup preset that is oriented towards shipping libraries and produces two bundle formats:

- `es` and `umd` (for single entry)
- `es` and `cjs` (for multiple entries)

The formats can be configured with the [`build.lib.formats`](/config/build-options.md#build-lib) option.

```
$ vite build
Expand Down Expand Up @@ -251,6 +256,29 @@ Recommended `package.json` for your lib:

:::

### CSS support

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

You can export the CSS file in your `package.json` to be imported by users:

```json {12}
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.umd.cjs",
"module": "./dist/my-lib.js",
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs"
},
"./style.css": "./dist/my-lib.css"
}
}
```

::: tip File Extensions
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`.
:::
Expand Down
20 changes: 20 additions & 0 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ From Vite 6, the modern API is used by default for Sass. If you wish to still us

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

### Customize CSS output file name in library mode

In Vite 5, the CSS output file name in library mode was always `style.css` and cannot be easily changed through the Vite config.

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.

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:

```json [package.json]
{
"name": "my-lib",
"exports": {
"./style.css": "./dist/style.css" // [!code --]
"./style.css": "./dist/my-lib.css" // [!code ++]
}
}
```

If you prefer to stick with `style.css` like in Vite 5, you can set `build.lib.cssFileName: 'style'` instead.

## Advanced

There are other breaking changes which only affect few users.
Expand Down
49 changes: 49 additions & 0 deletions packages/vite/src/node/__tests__/plugins/css.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, test } from 'vitest'
import { resolveConfig } from '../../config'
import type { InlineConfig } from '../../config'
Expand All @@ -9,9 +10,12 @@ import {
getEmptyChunkReplacer,
hoistAtRules,
preprocessCSS,
resolveLibCssFilename,
} from '../../plugins/css'
import { PartialEnvironment } from '../../baseEnvironment'

const __dirname = path.resolve(fileURLToPath(import.meta.url), '..')

describe('search css url function', () => {
test('some spaces before it', () => {
expect(
Expand Down Expand Up @@ -369,3 +373,48 @@ describe('preprocessCSS', () => {
`)
})
})

describe('resolveLibCssFilename', () => {
test('use name from package.json', () => {
const filename = resolveLibCssFilename(
{
entry: 'mylib.js',
},
path.resolve(__dirname, '../packages/name'),
)
expect(filename).toBe('mylib.css')
})

test('set cssFileName', () => {
const filename = resolveLibCssFilename(
{
entry: 'mylib.js',
cssFileName: 'style',
},
path.resolve(__dirname, '../packages/noname'),
)
expect(filename).toBe('style.css')
})

test('use fileName if set', () => {
const filename = resolveLibCssFilename(
{
entry: 'mylib.js',
fileName: 'custom-name',
},
path.resolve(__dirname, '../packages/name'),
)
expect(filename).toBe('custom-name.css')
})

test('use fileName if set and has array entry', () => {
const filename = resolveLibCssFilename(
{
entry: ['mylib.js', 'mylib2.js'],
fileName: 'custom-name',
},
path.resolve(__dirname, '../packages/name'),
)
expect(filename).toBe('custom-name.css')
})
})
11 changes: 7 additions & 4 deletions packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
copyDir,
displayTime,
emptyDir,
getPkgName,
joinUrlSegments,
normalizePath,
partialEncodeURIPath,
Expand Down Expand Up @@ -296,6 +297,12 @@ export interface LibraryOptions {
* format as an argument.
*/
fileName?: string | ((format: ModuleFormat, entryName: string) => string)
/**
* The name of the CSS file output if the library imports CSS. Defaults to the
* same value as `build.lib.fileName` if it's set a string, otherwise it falls
* back to the name option of the project package.json.
*/
cssFileName?: string
}

export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' | 'system'
Expand Down Expand Up @@ -879,10 +886,6 @@ function prepareOutDir(
}
}

function getPkgName(name: string) {
return name?.[0] === '@' ? name.split('/')[1] : name
}

type JsExt = 'js' | 'cjs' | 'mjs'

function resolveOutputJsExtension(
Expand Down
44 changes: 42 additions & 2 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
toOutputFilePathInCss,
toOutputFilePathInJS,
} from '../build'
import type { LibraryOptions } from '../build'
import {
CLIENT_PUBLIC_PATH,
CSS_LANGS_RE,
Expand All @@ -60,6 +61,7 @@ import {
generateCodeFrame,
getHash,
getPackageManagerCommand,
getPkgName,
injectQuery,
isDataUrl,
isExternalUrl,
Expand All @@ -81,6 +83,8 @@ import { PartialEnvironment } from '../baseEnvironment'
import type { TransformPluginContext } from '../server/pluginContainer'
import { searchForWorkspaceRoot } from '../server/searchRoot'
import { type DevEnvironment } from '..'
import type { PackageCache } from '../packages'
import { findNearestPackageData } from '../packages'
import { addToHTMLProxyTransformResult } from './html'
import {
assetUrlRE,
Expand Down Expand Up @@ -213,7 +217,7 @@ const functionCallRE = /^[A-Z_][.\w-]*\(/i
const transformOnlyRE = /[?&]transform-only\b/
const nonEscapedDoubleQuoteRe = /(?<!\\)"/g

const cssBundleName = 'style.css'
const defaultCssBundleName = 'style.css'

const enum PreprocessLang {
less = 'less',
Expand Down Expand Up @@ -256,6 +260,9 @@ export const removedPureCssFilesCache = new WeakMap<
Map<string, RenderedChunk>
>()

// Used only if the config doesn't code-split CSS (builds a single CSS file)
export const cssBundleNameCache = new WeakMap<ResolvedConfig, string>()

const postcssConfigCache = new WeakMap<
ResolvedConfig,
PostCSSConfigResult | null | Promise<PostCSSConfigResult | null>
Expand Down Expand Up @@ -428,6 +435,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
// since output formats have no effect on the generated CSS.
let hasEmitted = false
let chunkCSSMap: Map<string, string>
let cssBundleName: string

const rollupOptionsOutput = config.build.rollupOptions.output
const assetFileNames = (
Expand Down Expand Up @@ -464,6 +472,14 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
hasEmitted = false
chunkCSSMap = new Map()
codeSplitEmitQueue = createSerialPromiseQueue()
cssBundleName = config.build.lib
? resolveLibCssFilename(
config.build.lib,
config.root,
config.packageCache,
)
: defaultCssBundleName
cssBundleNameCache.set(config, cssBundleName)
},

async transform(css, id) {
Expand Down Expand Up @@ -1851,7 +1867,9 @@ async function minifyCSS(
...config.css?.lightningcss,
targets: convertTargets(config.build.cssTarget),
cssModules: undefined,
filename: cssBundleName,
// TODO: Pass actual filename here, which can also be passed to esbuild's
// `sourcefile` option below to improve error messages
filename: defaultCssBundleName,
code: Buffer.from(css),
minify: true,
})
Expand Down Expand Up @@ -3255,3 +3273,25 @@ export const convertTargets = (
convertTargetsCache.set(esbuildTarget, targets)
return targets
}

export function resolveLibCssFilename(
libOptions: LibraryOptions,
root: string,
packageCache?: PackageCache,
): string {
if (typeof libOptions.cssFileName === 'string') {
return `${libOptions.cssFileName}.css`
} else if (typeof libOptions.fileName === 'string') {
return `${libOptions.fileName}.css`
}

const packageJson = findNearestPackageData(root, packageCache)?.data
const name = packageJson ? getPkgName(packageJson.name) : undefined

if (!name)
throw new Error(
'Name in package.json is required if option "build.lib.cssFileName" is not provided.',
)

return `${name}.css`
}
13 changes: 8 additions & 5 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
publicAssetUrlRE,
urlToBuiltUrl,
} from './asset'
import { isCSSRequest } from './css'
import { cssBundleNameCache, isCSSRequest } from './css'
import { modulePreloadPolyfillId } from './modulePreloadPolyfill'

interface ScriptAssetsUrl {
Expand Down Expand Up @@ -909,10 +909,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {

// inject css link when cssCodeSplit is false
if (!this.environment.config.build.cssCodeSplit) {
const cssChunk = Object.values(bundle).find(
(chunk) =>
chunk.type === 'asset' && chunk.names.includes('style.css'),
) as OutputAsset | undefined
const cssBundleName = cssBundleNameCache.get(config)
const cssChunk =
cssBundleName &&
(Object.values(bundle).find(
(chunk) =>
chunk.type === 'asset' && chunk.names.includes(cssBundleName),
) as OutputAsset | undefined)
if (cssChunk) {
result = injectToHead(result, [
{
Expand Down
4 changes: 4 additions & 0 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,10 @@ export function getNpmPackageName(importPath: string): string | null {
}
}

export function getPkgName(name: string): string | undefined {
return name?.[0] === '@' ? name.split('/')[1] : name
}

const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g
export function escapeRegex(str: string): string {
return str.replace(escapeRegexRE, '\\$&')
Expand Down
38 changes: 38 additions & 0 deletions playground/lib/__tests__/lib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,44 @@ describe.runIf(isBuild)('build', () => {
expect(iife).toMatch('process.env.NODE_ENV')
expect(umd).toMatch('process.env.NODE_ENV')
})

test('single entry with css', () => {
const css = readFile('dist/css-single-entry/test-my-lib.css')
const js = readFile('dist/css-single-entry/test-my-lib.js')
const umd = readFile('dist/css-single-entry/test-my-lib.umd.cjs')
expect(css).toMatch('entry-1.css')
expect(js).toMatch('css-entry-1')
expect(umd).toContain('css-entry-1')
})

test('multi entry with css', () => {
const css = readFile('dist/css-multi-entry/test-my-lib.css')
const js1 = readFile('dist/css-multi-entry/css-entry-1.js')
const js2 = readFile('dist/css-multi-entry/css-entry-2.js')
const cjs1 = readFile('dist/css-multi-entry/css-entry-1.cjs')
const cjs2 = readFile('dist/css-multi-entry/css-entry-2.cjs')
expect(css).toMatch('entry-1.css')
expect(css).toMatch('entry-2.css')
expect(js1).toMatch('css-entry-1')
expect(js2).toMatch('css-entry-2')
expect(cjs1).toContain('css-entry-1')
expect(cjs2).toContain('css-entry-2')
})

test('multi entry with css and code split', () => {
const css1 = readFile('dist/css-code-split/css-entry-1.css')
const css2 = readFile('dist/css-code-split/css-entry-2.css')
const js1 = readFile('dist/css-code-split/css-entry-1.js')
const js2 = readFile('dist/css-code-split/css-entry-2.js')
const cjs1 = readFile('dist/css-code-split/css-entry-1.cjs')
const cjs2 = readFile('dist/css-code-split/css-entry-2.cjs')
expect(css1).toMatch('entry-1.css')
expect(css2).toMatch('entry-2.css')
expect(js1).toMatch('css-entry-1')
expect(js2).toMatch('css-entry-2')
expect(cjs1).toContain('css-entry-1')
expect(cjs2).toContain('css-entry-2')
})
})

test.runIf(isServe)('dev', async () => {
Expand Down
Loading

0 comments on commit 61cbf6f

Please sign in to comment.