vite
vite copied to clipboard
Support `additionalData` for the lightningcss css transformer
Description
The additionalData option for other preprocessors is extremely valuable in sharing variables and other items to other files without explicit inclusion. While this largely isn't necessary for most things with Lightning CSS, it's still really useful for sharing @custom-media directives throughout your CSS.
Suggested solution
Offer the ability to prepend a string to all CSS files/segments when using the Lightning CSS transformer.
Alternative
No response
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.
I've been tinkering with this issue and it seems like CSS plugin skips preprocessing steps if the transformer is lightningCSS(1), thus it can't benefit from the additionalData option. additionalData might be better as a separate option rather than a sub-option under css.preprocessorOptions, since it isn’t originally a preprocessing option. It really is an additional field.
Would it make sense to move it up as css.additionalData so that it could also be available to non-preprocessed CSS?
(1): https://github.com/vitejs/vite/blob/bb2f8bb55fdd64e4f16831ff98921c221a5e734a/packages/vite/src/node/plugins/css.ts#L1215-L1223
As it's called out in doc https://vitejs.dev/config/shared-options.html#css-preprocessoroptions-extension-additionaldata, one of the intended usage is to inject a pre-processor specific variable since otherwise it would cause a duplication:
Note that if you include actual styles and not just variables, those styles will be duplicated in the final bundle.
For this reason, using additionalData for raw css files might not be recommended.
@soluml I'm not familiar with @custom-media. Can you elaborate how you intend to use additionalData and whether it would cause style duplication (or is it something lightningcss would transpile it away)?
While it seems reasonable to support additionalData for lightningcss, I'm wondering if this can be easily implemented as Vite plugin.
I made an example here https://github.com/hi-ogawa/reproductions/tree/main/vite-17724-lightningcss-custom-media
import { defineConfig } from "vite";
export default defineConfig({
css: {
transformer: "lightningcss",
lightningcss: {
drafts: {
customMedia: true,
},
},
},
plugins: [
{
name: "css-additional-data",
enforce: "pre",
transform(code, id) {
if (id.endsWith(".css")) {
// probably shouldn't inject `@custom-media` directly since it can cause an error such as:
// @import rules must precede all rules aside from @charset and @layer statements
return `@import "/src/custom-media.css";` + code;
}
},
},
],
});
@hi-ogawa, thanks for the code snippet! I tested it and it works great. According to the LightningCSS documentation, imports should be relative only so I'm surprised this works. To handle the case of precedence for @charset and @layer which has to be before @import, I modified the snippet a little:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
cssMinify: 'lightningcss',
},
css: {
transformer: 'lightningcss',
lightningcss: {
targets,
drafts: {
customMedia: true,
},
},
},
plugins: [
{
name: 'css-additional-data',
enforce: 'pre',
transform(code, id) {
if (id.endsWith('.css')) {
const parts = code.split('\n');
let insertIndex = 0;
// Skip @charset and @layer statements
for (const part of parts) {
const trimmedPart = part.trim();
if (
trimmedPart.startsWith('@charset') ||
trimmedPart.startsWith('@layer')
) {
insertIndex++;
} else {
break;
}
}
// Insert the @import rule
parts.splice(insertIndex, 0, '@import "/app/styles/_media.css";');
return parts.join('\n');
}
},
},
],
});
This approach could be a problem when parsing minified code e.g. third party CSS but typically they shouldn't need @custom-media. On the other hand, splitting on semi-colons has a whole load of edge cases such as semi-colons in url() or content etc. To cover those cases, an actual CSS parser would make sense over simple JS string manipulation.
Alternatively, you can actually just simply import @custom-media directly because it will be transpiled away until the syntax is moved outside draft status and is widely supported. Bit of a ticking time bomb that might break builds in a few years, but imo it's a much safer approach than any of the other approaches above otherwise.
import fs from 'node:fs';
import { defineConfig } from 'vite';
const customMedia = fs.readFileSync('./app/styles/_media.css', 'utf-8');
export default defineConfig({
build: {
cssMinify: 'lightningcss',
},
css: {
transformer: 'lightningcss',
lightningcss: {
targets,
drafts: {
customMedia: true,
},
},
},
plugins: [
{
name: 'css-custom-media',
enforce: 'pre',
transform(code, id) {
if (id.endsWith('.css')) {
return `${customMedia}\n${code}`;
}
},
},
],
});
I iterated a little on the above also adding hot reload and using as a module
export function customMedia(path) {
return {
name: 'css-additional-data',
enforce: 'pre',
handleHotUpdate({ file, server }) {
const tidyPath = path.replace(/^(\.\/|\.\.\/)+/, '')
if (file.includes(tidyPath)) server.restart()
},
transform(code, id) {
if (id.endsWith('.css')) {
const parts = code.split('\n')
let insertIndex = 0
// Skip @charset and @layer statements
for (const part of parts) {
const trimmedPart = part.trim()
if (trimmedPart.startsWith('@charset') || trimmedPart.startsWith('@layer')) {
insertIndex++
} else {
break
}
}
// Insert the @import rule
parts.splice(insertIndex, 0, `@import '${path}';`)
return parts.join('\n')
}
}
}
}
Usage
import { defineConfig } from 'vite'
import { customMedia } from './src/library/customMedia'
export default defineConfig({
build: { cssMinify: 'lightningcss' },
css: {
transformer: 'lightningcss',
lightningcss: {
drafts: { customMedia: true }
}
},
plugins: [customMedia('./src/library/media.css')]
})
Would be cool if i could set that lightningcss custommedia flag from the plugin code but dont think you can
@mattpilott thanks for the snippet, it works just fine for CSS files. However, a similar strategy doesn't work with styles declared inside the <style> block of Astro and Vue components. Is there a solution?
Hey @ShayanTheNerd im not sure why it wouldnt work in those frameworks, i use svelte and its working in component