Skip to content

feat: add vite-plugin to resolve CSS automatically#6055

Open
nmerget wants to merge 23 commits intomainfrom
feat-vite-plugin
Open

feat: add vite-plugin to resolve CSS automatically#6055
nmerget wants to merge 23 commits intomainfrom
feat-vite-plugin

Conversation

@nmerget
Copy link
Copy Markdown
Collaborator

@nmerget nmerget commented Feb 13, 2026

Proposed changes

add vite-plugin to resolve CSS automatically based on detected components and styles

Types of changes

  • Bugfix (non-breaking change that fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactoring (improvements to existing components or architectural decisions)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update (if none of the other choices apply)

Further comments

🔭🐙🐈 Test this branch here: https://design-system.deutschebahn.com/core-web/review/feat-vite-plugin

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 13, 2026

🦋 Changeset detected

Latest commit: 95b044b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@db-ux/core-vite-plugin Minor
@db-ux/core-foundations Minor
@db-ux/core-components Minor
@db-ux/react-core-components Minor
@db-ux/ngx-core-components Minor
@db-ux/v-core-components Minor
@db-ux/wc-core-components Minor
@db-ux/core-stylelint Minor
@db-ux/core-migration Minor
@db-ux/agent-cli Minor
@db-ux/core-eslint-plugin Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added 📕documentation Improvements or additions to documentation 🚢📀cicd Changes inside .github folder 🛠️configuration 🏗foundations labels Feb 13, 2026
@mfranzke mfranzke changed the title feat: add vite-plugin to resolve CSS automatically based on detected … feat: add vite-plugin to resolve CSS automatically Feb 14, 2026
@nmerget nmerget marked this pull request as ready for review February 23, 2026 08:28
@nmerget nmerget moved this from 🏗 In progress to 🎁 Ready for review in UX Engineering Team Backlog Feb 23, 2026
@mfranzke mfranzke requested a review from Copilot February 26, 2026 16:28
@michaelmkraus michaelmkraus moved this from 🎁 Ready for review to 👀 Actively In Review in UX Engineering Team Backlog Mar 4, 2026
@michaelmkraus michaelmkraus self-requested a review March 4, 2026 14:54
const importMatches = code.matchAll(IMPORT_PATTERN);
for (const match of importMatches) {
const componentName = match[1]
.toLowerCase()
Copy link
Copy Markdown
Contributor

@michaelmkraus michaelmkraus Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.toLowerCase()

we have to remove the first .toLowerCase(), otherwise the regex will fail.

const usageMatches = code.matchAll(COMPONENT_USAGE_PATTERN);
for (const match of usageMatches) {
const componentName = match[1]
.toLowerCase()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.toLowerCase()

we have to remove the first .toLowerCase(), otherwise the regex will fail.


const IMPORT_PATTERN =
/import\s+\{[^}]*\bDB(\w+)\b[^}]*\}\s+from\s+['"]@db-ux\/(?:react|ngx|v|wc)-core-components['"]/g;
const COMPONENT_USAGE_PATTERN = /<DB(\w+)[\s>]/g;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works for JSX but won't work for e.g. <db-button>

forceInclude: string[]
): Promise<Set<string>> {
const components = new Set<string>(forceInclude);
const moduleIds = Array.from(context.getModuleIds());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using context.getModuleIds() here might cause issues with the dev-server. Since Vite loads files on-demand, the modules will be mostly empty when the initial CSS file is requested.
To fix this, we could scan the file system directly (e.g., using fast-glob). This is the same approach Tailwind CSS uses.

import fg from 'fast-glob';
const files = await fg(['src/**/*.{vue,jsx,tsx,html}', 'index.html'], { absolute: true });
for (const file of files) {
    const code = readFileSync(file, 'utf-8');
    // run detection regex...
}


const distDir = resolve(process.cwd(), outDir);
try {
const files = await readdir(distDir, { recursive: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

closeBundle is not meant to read and write directly to the dist folder. Changing files on disk behind Vite's back can break source maps or other plugins.
We should use the generateBundle hook instead. It allows to iterate over the bundle object and modify the CSS directly before Vite writes it to the disk.

Comment on lines +140 to +143
const fullPath =
key === 'animations'
? `build/styles/${path}`
: `build/styles/${path}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const fullPath =
key === 'animations'
? `build/styles/${path}`
: `build/styles/${path}`;
const fullPath = `build/styles/${path}`;

Both cases for fullPath return the exact same string.

Comment on lines +52 to +54
css = css.replace(
new RegExp(`\\[data-color=${color}\\],\\.db-color-${color}`, 'g'),
`.db-color-${color}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern searches for the exact string [data-color=blue] (without quotes).
If selectors with quotes are used (e.g. [data-color="blue"]), this regex will fail and won't remove the unused styles.

const mod = server.moduleGraph.getModuleById(cssModuleId);
if (mod) {
server.moduleGraph.invalidateModule(mod);
server.ws.send({ type: 'full-reload' });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
server.ws.send({ type: 'full-reload' });

Sending a full-reload event here breaks Vite's hot module replacement. It forces a full page refresh in the browser every time a developer saves a .vue or .tsx file, which disturbs the fast developer experience Vite is known for.
Using server.moduleGraph.invalidateModule(mod) should be enough for Vite to update CSS without refreshing the page.

];

const COLOR_PATTERNS = [
/\.db-color-(neutral|brand|blue|burgundy|critical|cyan|green|informational|light-green|orange|pink|red|successful|turquoise|violet|warning|yellow)/g,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first pattern in each const array looks for a dot (e.g., /\.db-color-...). In the relevant files, developers will write class="db-color-blue". The regex will fail to match and all class-based usages will be ignored and falsely removed.

import { readFileSync } from 'fs';
import type { PluginContext } from 'rollup';

const COMPONENT_PATTERNS = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every time a new component is created or a color/token is renamed/added, developers must remember to manually update these regex strings, the VALID_COMPONENTS array and the hardcoded lists in optimizer.ts and types.ts. Otherwise, the Vite plugin will strip the CSS for the new component.

Instead of maintaining manual lists, we can use a generic regex to capture anything relevant and dynamically check if it's a valid component by looking them up in the file system.

/["']data-font-size["']:\s*["'](body|headline)-(3xs|2xs|xs|sm|md|lg|xl|2xl|3xl)["']/g
];

const IMPORT_PATTERN =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user imports multiple components in a single statement (e.g. import { DBButton, DBCard } from '@db-ux/react-core-components'), \bDB(\w+)\b only catches the first one (Button). DBCard will be ignored.

Comment on lines +97 to +98
const componentMatch =
match[0].match(/db-(\S+?)(?:[\s"]|$)/);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class detection misses components if multiple db- classes are used in the same attribute (e.g., <div class="db-navigation-item db-infotext">).

The regex only returns the first hit, meaning db-infotext will be ignored. I am not sure if this really occurs (two components sharing the same wrapper)...

''
);
css = css.replace(
new RegExp(`--db-${color}-[a-z0-9-]+:[^;]+;`, 'g'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vite strips the trailing semicolon of the last property in a rule block (.class { color: red; --db-blue: #000 }). Because the regex strictly requires a semicolon, it will fail to remove any unused variables that happen to be at the end of a block.
We should make the semicolon optional and ensure we don't match past the closing }

);
// Remove entire rule block
css = css.replace(
new RegExp(`\\.db-color-${color}\\{[^}]+\\}`, 'g'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex for removing entire unused rule blocks is too rigid. It assumes the opening { comes exactly after the class name.
Therefore, it completely ignores pseudo-classes (like .db-color-blue:hover { ... }), leaving those as dead code in the bundle.

Comment on lines +11 to +15
"exports": {
".": {
"types": "./build/src/index.d.ts",
"default": "./build/src/index.js"
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the tsconfig.json uses "outDir": "build" and the code is inside src, TypeScript will handle src as the root directory. This means src/index.ts will be compiled directly to build/index.js, not build/src/index.js. If we publish it like this, users will get an error.


describe('generateCSS', () => {
it('should include default theme when no theme is detected', () => {
const css = generateCSS({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests are using an outdated signature for generateCSS. The tests pass the configuration as a flat object, but generateCSS expects them to be inside include and exclude objects (see GenerateOptions type)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested the unit tests. When I change the call to this, the tests are working.

const css = generateCSS({
    include: {
        foundations: ['icons']
    },
    exclude: {},
    hasTailwind: false
});

@michaelmkraus michaelmkraus modified the milestones: 4.6.0, 4.7.0 Mar 5, 2026
Comment on lines +16 to +19
"@components": ["../../../../output/react/src"],
"@components/src/*": ["../../../../output/react/src/*"],
"@components/components/*": [
"../../../../output/react/src/components/*"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@components": ["../../../../output/react/src"],
"@components/src/*": ["../../../../output/react/src/*"],
"@components/components/*": [
"../../../../output/react/src/components/*"
"@components": ["../../../../../output/react/src"],
"@components/src/*": ["../../../../../output/react/src/*"],
"@components/components/*": [
"../../../../../output/react/src/components/*"

I think we have to go up one more level. Here, we go up only 4 levels to the output directory, but in the vite.config.ts 5 (which is correct).

Comment on lines +16 to +19
"@components": ["../../../../output/vue/src"],
"@components/src/*": ["../../../../output/vue/src/*"],
"@components/components/*": [
"../../../../output/vue/src/components/*"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@components": ["../../../../output/vue/src"],
"@components/src/*": ["../../../../output/vue/src/*"],
"@components/components/*": [
"../../../../output/vue/src/components/*"
"@components": ["../../../../../output/vue/src"],
"@components/src/*": ["../../../../../output/vue/src/*"],
"@components/components/*": [
"../../../../../output/vue/src/components/*"

I think we have to go up one more level. Here, we go up only 4 levels to the output directory, but in the vite.config.ts 5 (which is correct).

(d) => !detectedDensities.has(d)
);

const allFontSizes = [
Copy link
Copy Markdown
Contributor

@michaelmkraus michaelmkraus Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type FontSize in the types.ts has more entries than allFontSizes.
If a user doesn't use headline-md in their code, the optimizer looks for it in the allFontSizes array. Since it is missing there, it never gets added to unusedFontSizes. As a result, the plugin will never remove the unused CSS variables for the missing font sizes.

PluginConfig
} from './types.js';

export default function dbUxPlugin(config: PluginConfig = {}): Plugin {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user passes a custom config to the plugin

dbUxPlugin({ include: { components: ['button'] } })

the default value for the include object is completely overridden because the objec exists and the foundations array inside the include object becomes undefined. Users will lose their default icons, helper etc. styles because they explicitly wanted to include a single component.

@michaelmkraus michaelmkraus moved this from 👀 Actively In Review to 🎶 Waiting for feedback in UX Engineering Team Backlog Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚢📀cicd Changes inside .github folder 🏘components 🛠️configuration 📕documentation Improvements or additions to documentation 🏗foundations

Projects

Status: No status
Status: 🎶 Waiting for feedback

Development

Successfully merging this pull request may close these issues.

6 participants