Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/vite/src/node/__tests__/plugins/css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,4 +793,26 @@ exports.foo = foo;
})();"
`)
})

test('should inject helper for cjs format', async () => {
const result = getInlinedCSSInjectedCode(
`"use strict";

//#region src/index.js
const foo = "foo";

//#endregion
exports.foo = foo;`,
'cjs',
)
expect(result).toMatchInlineSnapshot(`
"injectCSS();"use strict";

//#region src/index.js
const foo = "foo";

//#endregion
exports.foo = foo;"
`)
})
})
9 changes: 9 additions & 0 deletions packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,15 @@ export interface LibraryOptions {
* back to the name option of the project package.json.
*/
cssFileName?: string
/**
* Whether to inject CSS into the JavaScript output by appending it to the
* document head at runtime, instead of emitting a separate CSS file.
* This is useful when you want consumers to import a single JS file
* without needing to separately import CSS.
* Note: this makes the library incompatible with SSR environments.
* @default false
*/
cssInject?: boolean
}

export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' // | 'system'
Expand Down
37 changes: 28 additions & 9 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,14 +1033,33 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
if (extractedCss) {
hasEmitted = true
extractedCss = await finalizeCss(extractedCss, config)
this.emitFile({
name: getCssBundleName(),
type: 'asset',
source: extractedCss,
// this file is an implicit entry point, use defaultCssBundleName as the original file name
// this name is also used as a key in the manifest
originalFileName: defaultCssBundleName,
})

const libOptions = config.build.lib
if (libOptions && libOptions.cssInject) {
// Inject CSS into the JS entry chunk(s) via a styleInject helper
extractedCss = extractedCss
.replace(viteHashUpdateMarkerRE, '')
.trim()
const cssString = JSON.stringify(extractedCss)
const injectCode = `(function(){try{var d=document,s=d.createElement("style");s.appendChild(d.createTextNode(${cssString}));d.head.appendChild(s)}catch(e){console.error("vite-css-inject",e)}})();
`
for (const file of Object.values(bundle)) {
if (file.type === 'chunk' && file.isEntry) {
const s = new MagicString(file.code)
injectInlinedCSS(s, this, file.code, opts.format, injectCode)
file.code = s.toString()
}
}
} else {
this.emitFile({
name: getCssBundleName(),
type: 'asset',
source: extractedCss,
// this file is an implicit entry point, use defaultCssBundleName as the original file name
// this name is also used as a key in the manifest
originalFileName: defaultCssBundleName,
})
}
}
}

Expand Down Expand Up @@ -1150,7 +1169,7 @@ export function injectInlinedCSS(
ctx.error('Injection point for inlined CSS not found')
}
injectionPoint = m.index + m[0].length
} else if (format === 'es') {
} else if (format === 'es' || format === 'cjs') {
// legacy build
if (code.startsWith('#!')) {
let secondLinePos = code.indexOf('\n')
Expand Down
12 changes: 12 additions & 0 deletions playground/lib/__tests__/lib.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ describe.runIf(isBuild)('build', () => {
expect(cjs2).toContain('css-entry-2')
})

test('single entry with css inject', () => {
const js = readFile('dist/css-inject/test-my-lib.js')
const umd = readFile('dist/css-inject/test-my-lib.umd.cjs')
// CSS should be injected into JS, not emitted as a separate file
expect(js).toMatch('entry-1.css')
expect(js).toMatch('createElement')
expect(umd).toMatch('entry-1.css')
expect(umd).toMatch('createElement')
// No separate CSS file should be emitted
expect(() => readFile('dist/css-inject/test-my-lib.css')).toThrow()
})

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')
Expand Down
6 changes: 6 additions & 0 deletions playground/lib/__tests__/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ export async function serve(): Promise<{ close(): Promise<void> }> {
configFile: path.resolve(dirname, '../vite.terser.config.js'),
})

await build({
root: rootDir,
logLevel: 'warn', // output esbuild warns
configFile: path.resolve(dirname, '../vite.css-inject.config.js'),
})

// start static file server
const serve = sirv(path.resolve(rootDir, 'dist'))
const httpServer = http.createServer((req, res) => {
Expand Down
13 changes: 13 additions & 0 deletions playground/lib/vite.css-inject.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'

export default defineConfig({
build: {
lib: {
entry: fileURLToPath(new URL('src/css-entry-1.js', import.meta.url)),
name: 'css-inject',
cssInject: true,
},
outDir: 'dist/css-inject',
},
})