@db-ux/core-vite-plugin 0.0.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @db-ux/core-vite-plugin
2
+
3
+ ## 4.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - feat: add vite-plugin to resolve CSS automatically based on detected components and styles - [see commit 5b1b2d8](https://github.com/db-ux-design-system/core-web/commit/5b1b2d811245fe132685171cb497bd5b16f5ff56)
package/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # @db-ux/core-vite-plugin
2
+
3
+ Vite plugin for optimized DB UX Design System CSS imports. Automatically includes only the CSS you need based on components and classes used in your application.
4
+
5
+ ## Installation
6
+
7
+ ```shell
8
+ npm install @db-ux/core-vite-plugin --save-dev
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage
14
+
15
+ Add the plugin to your `vite.config.js` or `vite.config.ts`:
16
+
17
+ ```js
18
+ import { defineConfig } from "vite";
19
+ import dbUxPlugin from "@db-ux/core-vite-plugin";
20
+
21
+ export default defineConfig({
22
+ plugins: [dbUxPlugin()]
23
+ });
24
+ ```
25
+
26
+ Then import the plugin in your CSS file:
27
+
28
+ ```css
29
+ /* styles.css or main.css */
30
+ @import "@db-ux/core-vite-plugin/index.css";
31
+ ```
32
+
33
+ The plugin handles theme detection, component CSS, and foundation styles automatically.
34
+
35
+ #### Migration
36
+
37
+ If you're currently using the manual CSS layer imports, you can replace them with the plugin's single import:
38
+
39
+ ```css
40
+ /* Before */
41
+ @layer whitelabel-theme, db-ux;
42
+ @import "@db-ux/core-foundations/build/styles/theme/rollup.css"
43
+ layer(whitelabel-theme);
44
+ @import "@db-ux/core-components/build/styles/bundle.css" layer(db-ux);
45
+ ```
46
+
47
+ ```css
48
+ /* After */
49
+ @import "@db-ux/core-vite-plugin/index.css";
50
+ ```
51
+
52
+ ### Advanced Usage
53
+
54
+ For more control, you can configure the plugin:
55
+
56
+ ```js
57
+ import { defineConfig } from "vite";
58
+ import dbUxPlugin from "@db-ux/core-vite-plugin";
59
+
60
+ export default defineConfig({
61
+ plugins: [
62
+ dbUxPlugin({
63
+ include: {
64
+ components: ["button", "input"], // Force include specific components
65
+ foundations: ["helpers", "animations", "icons"], // Force include foundation features
66
+ colors: ["neutral", "brand"], // Force include color schemes
67
+ densities: ["regular", "functional"], // Force include densities
68
+ fontSizes: ["body-md", "headline-lg"] // Force include font sizes
69
+ },
70
+ exclude: {
71
+ components: ["tooltip"], // Exclude specific components
72
+ foundations: ["elevation"], // Exclude foundation features
73
+ colors: ["critical"], // Exclude color schemes
74
+ densities: ["expressive"], // Exclude densities
75
+ fontSizes: ["body-3xs"] // Exclude font sizes
76
+ },
77
+ optimize: true, // Remove unused CSS variable declarations to reduce bundle size (default: true).
78
+ theme: "db-theme", // Specify preferred theme package name (e.g., "db-theme")
79
+ additionalLayers: { after: ["ri-extension"] }, // Append custom layers to the auto-generated order
80
+ debug: false // Generate detection report for debugging (default: false)
81
+ })
82
+ ]
83
+ });
84
+ ```
85
+
86
+ ## Configuration
87
+
88
+ ### `include`
89
+
90
+ - **Type:** `{ components?: Component[], foundations?: FoundationFeature[], colors?: ColorScheme[], densities?: Density[], fontSizes?: FontSize[] }`
91
+ - Force include specific components, foundation features, color schemes, densities, or font sizes
92
+ - **Foundation features:** `icons`, `helpers`, `elevation`, `animations`, `code`
93
+
94
+ ### `exclude`
95
+
96
+ - **Type:** `{ components?: Component[], foundations?: FoundationFeature[], colors?: ColorScheme[], densities?: Density[], fontSizes?: FontSize[] }`
97
+ - Exclude specific components, foundation features, color schemes, densities, or font sizes
98
+
99
+ ### `optimize`
100
+
101
+ - **Type:** `boolean`
102
+ - **Default:** `true`
103
+ - Remove unused CSS variable declarations to reduce bundle size
104
+
105
+ ### `theme`
106
+
107
+ - **Type:** `string`
108
+ - **Default:** `undefined`
109
+ - Specify a preferred theme package name (e.g., `"db-theme"`). The plugin automatically detects installed theme packages from `@db-ux/*-theme` or `@db-ux-inner-source/*-theme`. Use this option to select a specific theme when multiple are available.
110
+
111
+ ### `additionalLayers`
112
+
113
+ - **Type:** `{ before?: string[], after?: string[] }`
114
+ - **Default:** `undefined`
115
+ - Append custom CSS cascade layers before or after the auto-generated layer order. Useful when your project defines its own layers on top of DB UX.
116
+
117
+ ```js
118
+ // Adds layers around the auto-generated ones
119
+ dbUxPlugin({
120
+ additionalLayers: {
121
+ before: ["reset"],
122
+ after: ["ri-extension"]
123
+ }
124
+ });
125
+ // → @layer reset, db-theme, db-ux, ri-extension;
126
+ ```
127
+
128
+ ### `overrideLayers`
129
+
130
+ - **Type:** `string[]`
131
+ - **Default:** `undefined`
132
+ - Fully replace the auto-generated `@layer` statement with a custom layer order. When set, `additionalLayers` is ignored.
133
+
134
+ ```js
135
+ dbUxPlugin({
136
+ overrideLayers: ["db-theme", "db-ux", "ri-extension"]
137
+ });
138
+ // → @layer db-theme, db-ux, ri-extension;
139
+ ```
140
+
141
+ ### `debug`
142
+
143
+ - **Type:** `boolean`
144
+ - **Default:** `false`
145
+ - Generate a `db-ux-detection-report.json` file in the project root directory for debugging purposes. This report contains all detected components, colors, densities, and font sizes. Useful for troubleshooting optimization issues.
146
+
147
+ ## How it works
148
+
149
+ The plugin analyzes your source files to detect:
150
+
151
+ - DB UX component usage (e.g., `class="db-button"`)
152
+ - Custom classes using design tokens
153
+ - Explicitly configured includes/excludes
154
+
155
+ It then generates an optimized CSS bundle containing only:
156
+
157
+ - Required foundation styles (theme, fonts, icons)
158
+ - Component styles for detected components
159
+ - Optional features based on configuration
160
+
161
+ ## Troubleshooting
162
+
163
+ ### Plugin Order with Tailwind CSS
164
+
165
+ If you're using Tailwind CSS alongside this plugin, **the order matters**. The `@db-ux/core-vite-plugin` must be placed **before** the Tailwind plugin in your Vite configuration:
166
+
167
+ ```js
168
+ import { defineConfig } from "vite";
169
+ import dbUxPlugin from "@db-ux/core-vite-plugin";
170
+ import tailwindcss from "@tailwindcss/vite";
171
+
172
+ export default defineConfig({
173
+ plugins: [
174
+ dbUxPlugin(), // Must come before tailwindcss()
175
+ tailwindcss()
176
+ ]
177
+ });
178
+ ```
179
+
180
+ And in your CSS file:
181
+
182
+ ```css
183
+ @import "tailwindcss";
184
+ @import "@db-ux/core-vite-plugin/index.css";
185
+ ```
186
+
187
+ **Why?** The DB UX plugin needs to process and replace the `@import "@db-ux/core-vite-plugin/index.css"` statement with actual CSS before Tailwind processes the file. Tailwind will strip out unrecognized imports, so the DB UX plugin must run first to transform the import into valid CSS that Tailwind can then process.
188
+
189
+ ### CSS `@property` Warnings
190
+
191
+ You may see warnings about "Unknown at rule: @property" in your build output or IDE. These are informational warnings from CSS parsers that don't yet fully support the CSS Houdini `@property` at-rule. These warnings don't affect functionality - the `@property` rule is valid CSS and works correctly in modern browsers. You can safely ignore these warnings or configure your CSS linter to suppress them.
192
+
193
+ ### Styles Not Loading in Dev Mode with `<link>` Tags
194
+
195
+ If you load your CSS via a `<link>` tag in `index.html`:
196
+
197
+ ```html
198
+ <!-- ❌ This won't work with the plugin in dev mode -->
199
+ <link rel="stylesheet" href="/src/styles.css" />
200
+ ```
201
+
202
+ The plugin's `transform` hook only runs for CSS that is part of Vite's module graph. Static `<link>` tags are served as-is by Vite's dev server, so the `@import "@db-ux/core-vite-plugin/index.css"` inside the CSS file is never processed.
203
+
204
+ **Solution:** Import the CSS from your JavaScript/TypeScript entry file instead:
205
+
206
+ ```ts
207
+ // main.ts or main.tsx
208
+ import "./styles.css";
209
+ ```
210
+
211
+ This pulls the CSS into Vite's module graph where the plugin can transform it. The production build is not affected by this issue.
212
+
213
+ ## License
214
+
215
+ This project is licensed under [Apache-2.0](LICENSE).
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Walk up the directory tree from `root` to locate a package path inside node_modules.
3
+ * Handles monorepo hoisting where dependencies may live in a parent node_modules.
4
+ * Returns the resolved absolute path or null if not found.
5
+ */
6
+ export declare function resolvePackagePath(root: string, packagePath: string): string | null;
7
+ /** Public accessor for the discovery cache. */
8
+ export declare function discoverAll(root: string): {
9
+ components: Set<string>;
10
+ colors: string[];
11
+ densities: string[];
12
+ fontSizes: string[];
13
+ };
14
+ /**
15
+ * Scan detected component CSS files for referenced colors, densities, and font sizes
16
+ * so the optimizer doesn't strip variables that components depend on.
17
+ */
18
+ export declare function scanComponentDependencies(root: string, components: Set<string>, colors: Set<string>, densities: Set<string>, fontSizes: Set<string>): void;
19
+ /**
20
+ * Scan all project source files to detect which DB UX components are used.
21
+ * Supports JSX (<DBButton>), kebab-case (<db-button>), CSS classes (class="db-button"),
22
+ * and named imports (import { DBButton } from '...').
23
+ */
24
+ export declare function detectComponents(root: string, forceInclude: string[]): Promise<Set<string>>;
25
+ /** Detect which color schemes are used in the project (e.g. "cyan", "brand"). */
26
+ export declare function detectColors(root: string, forceInclude: string[]): Promise<Set<string>>;
27
+ /** Detect which density variants are used in the project (e.g. "functional", "expressive"). */
28
+ export declare function detectDensities(root: string, forceInclude: string[]): Promise<Set<string>>;
29
+ /** Detect which font size combinations are used in the project (e.g. "body-md", "headline-lg"). */
30
+ export declare function detectFontSizes(root: string, forceInclude: string[]): Promise<Set<string>>;
@@ -0,0 +1,253 @@
1
+ import fg from 'fast-glob';
2
+ import { readFileSync, readdirSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ /**
5
+ * Walk up the directory tree from `root` to locate a package path inside node_modules.
6
+ * Handles monorepo hoisting where dependencies may live in a parent node_modules.
7
+ * Returns the resolved absolute path or null if not found.
8
+ */
9
+ export function resolvePackagePath(root, packagePath) {
10
+ let currentDir = root;
11
+ for (let i = 0; i < 10; i++) {
12
+ const resolved = resolve(currentDir, 'node_modules', packagePath);
13
+ try {
14
+ readdirSync(resolved);
15
+ return resolved;
16
+ }
17
+ catch {
18
+ // Try parent directory
19
+ }
20
+ const parentDir = resolve(currentDir, '..');
21
+ if (parentDir === currentDir)
22
+ break;
23
+ currentDir = parentDir;
24
+ }
25
+ return null;
26
+ }
27
+ /** Return subdirectory names (folders only) from the given path. */
28
+ function readDirNames(dirPath) {
29
+ try {
30
+ return readdirSync(dirPath, { withFileTypes: true })
31
+ .filter((e) => e.isDirectory())
32
+ .map((e) => e.name);
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ }
38
+ /** Return CSS file stems (without .css extension) from the given path, excluding "all.css". */
39
+ function readCssNames(dirPath) {
40
+ try {
41
+ return readdirSync(dirPath)
42
+ .filter((f) => f.endsWith('.css') && f !== 'all.css')
43
+ .map((f) => f.replace('.css', ''));
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ }
49
+ /** Cache of discovered design system values, keyed by project root. */
50
+ const cache = new Map();
51
+ /**
52
+ * Discover all available components, colors, densities, and font sizes
53
+ * by reading the installed @db-ux packages from the filesystem.
54
+ * Results are cached per project root.
55
+ */
56
+ function discover(root) {
57
+ if (cache.has(root))
58
+ return cache.get(root);
59
+ // Components
60
+ const compDir = resolvePackagePath(root, '@db-ux/core-components/build/components');
61
+ const components = new Set(compDir ? readDirNames(compDir) : []);
62
+ // Colors
63
+ const colorDir = resolvePackagePath(root, '@db-ux/core-foundations/build/styles/colors/classes');
64
+ const colors = colorDir ? readCssNames(colorDir) : [];
65
+ // Densities
66
+ const densityDir = resolvePackagePath(root, '@db-ux/core-foundations/build/styles/density/classes');
67
+ const densities = densityDir ? readCssNames(densityDir) : [];
68
+ // Font sizes: body/<size>.css + headline/<size>.css → "body-sm", "headline-lg"
69
+ const fontDir = resolvePackagePath(root, '@db-ux/core-foundations/build/styles/fonts/classes');
70
+ const fontSizes = [];
71
+ if (fontDir) {
72
+ for (const category of readDirNames(fontDir)) {
73
+ for (const size of readCssNames(resolve(fontDir, category))) {
74
+ fontSizes.push(`${category}-${size}`);
75
+ }
76
+ }
77
+ }
78
+ const result = { components, colors, densities, fontSizes };
79
+ cache.set(root, result);
80
+ return result;
81
+ }
82
+ /** Public accessor for the discovery cache. */
83
+ export function discoverAll(root) {
84
+ return discover(root);
85
+ }
86
+ /**
87
+ * Scan detected component CSS files for referenced colors, densities, and font sizes
88
+ * so the optimizer doesn't strip variables that components depend on.
89
+ */
90
+ export function scanComponentDependencies(root, components, colors, densities, fontSizes) {
91
+ const compDir = resolvePackagePath(root, '@db-ux/core-components/build/components');
92
+ if (!compDir)
93
+ return;
94
+ const { colors: validColors, densities: validDensities, fontSizes: validFontSizes } = discover(root);
95
+ const validFontSizeSet = new Set(validFontSizes);
96
+ for (const component of components) {
97
+ const css = readSource(resolve(compDir, component, `${component}.css`));
98
+ if (!css)
99
+ continue;
100
+ for (const color of validColors) {
101
+ if (css.includes(`--db-${color}-`))
102
+ colors.add(color);
103
+ }
104
+ for (const density of validDensities) {
105
+ if (css.includes(`-${density}-`))
106
+ densities.add(density);
107
+ }
108
+ for (const m of css.matchAll(/--db-type-(body|headline)-(\w+)/g)) {
109
+ const fs = `${m[1]}-${m[2]}`;
110
+ if (validFontSizeSet.has(fs))
111
+ fontSizes.add(fs);
112
+ }
113
+ }
114
+ }
115
+ /** Convert PascalCase to kebab-case: "NavigationItem" → "navigation-item". */
116
+ function toKebabCase(str) {
117
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
118
+ }
119
+ /**
120
+ * Build an array of regex patterns to detect usage of design system values.
121
+ * Covers CSS classes (e.g. db-color-cyan), data attributes (e.g. data-color="cyan"),
122
+ * HTML attributes, and JS object notation.
123
+ */
124
+ function buildPatterns(classPrefix, dataAttr, values) {
125
+ return [
126
+ new RegExp(`${classPrefix}(${values})`, 'g'),
127
+ new RegExp(`\\[${dataAttr}=["']?(${values})["']?\\]`, 'g'),
128
+ new RegExp(`${dataAttr}=["'](${values})["']`, 'g'),
129
+ new RegExp(`["']${dataAttr}["']:\\s*["'](${values})["']`, 'g')
130
+ ];
131
+ }
132
+ /** Matches JSX/TSX component usage: <DBButton>, <DBNavigationItem> */
133
+ const JSX_COMPONENT_PATTERN = /<DB(\w+)[\s>/]/g;
134
+ /** Matches Angular/HTML kebab-case usage: <db-button>, <db-navigation-item> */
135
+ const KEBAB_COMPONENT_PATTERN = /<db-([\w-]+)[\s>/]/g;
136
+ /** Matches CSS class-based usage: class="db-button ...", className="db-card" */
137
+ const CLASS_COMPONENT_PATTERN = /(?:class|className)=(?:"[^"]*|'[^']*|\{[^}]*)db-([\w-]+)/g;
138
+ /** Matches named imports from @db-ux framework packages: import { DBButton, DBCard } from '...' */
139
+ const IMPORT_PATTERN = /import\s+\{([^}]+)}\s+from\s+['"]@db-ux\/(?:react|ngx|v|wc)-core-components['"]/g;
140
+ /** Glob all source files from the project root, excluding node_modules/dist/build. */
141
+ async function scanFiles(root) {
142
+ return fg(['**/*.{vue,jsx,tsx,ts,html}'], {
143
+ cwd: root,
144
+ absolute: true,
145
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
146
+ });
147
+ }
148
+ /** Safely read a file's contents, returning null on failure. */
149
+ function readSource(filePath) {
150
+ try {
151
+ return readFileSync(filePath, 'utf-8');
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ /**
158
+ * Scan all project source files to detect which DB UX components are used.
159
+ * Supports JSX (<DBButton>), kebab-case (<db-button>), CSS classes (class="db-button"),
160
+ * and named imports (import { DBButton } from '...').
161
+ */
162
+ export async function detectComponents(root, forceInclude) {
163
+ const components = new Set(forceInclude);
164
+ const { components: validComponents } = discover(root);
165
+ const files = await scanFiles(root);
166
+ for (const file of files) {
167
+ const code = readSource(file);
168
+ if (!code)
169
+ continue;
170
+ // Detect JSX usage: <DBButton>, <DBNavigationItem>
171
+ for (const match of code.matchAll(JSX_COMPONENT_PATTERN)) {
172
+ const name = toKebabCase(match[1]);
173
+ if (validComponents.has(name)) {
174
+ components.add(name);
175
+ }
176
+ }
177
+ // Detect kebab-case usage: <db-button>, <db-navigation-item>
178
+ for (const match of code.matchAll(KEBAB_COMPONENT_PATTERN)) {
179
+ if (validComponents.has(match[1])) {
180
+ components.add(match[1]);
181
+ }
182
+ }
183
+ // Detect class-based usage: class="db-button ..."
184
+ for (const match of code.matchAll(CLASS_COMPONENT_PATTERN)) {
185
+ // Extract all db-* names from the matched attribute value
186
+ const fragment = match[0];
187
+ for (const inner of fragment.matchAll(/db-([\w-]+)/g)) {
188
+ if (validComponents.has(inner[1])) {
189
+ components.add(inner[1]);
190
+ }
191
+ }
192
+ }
193
+ // Detect from imports: import { DBButton, DBCard } from '...'
194
+ for (const match of code.matchAll(IMPORT_PATTERN)) {
195
+ const importList = match[1];
196
+ for (const nameMatch of importList.matchAll(/\bDB(\w+)\b/g)) {
197
+ const name = toKebabCase(nameMatch[1]);
198
+ if (validComponents.has(name)) {
199
+ components.add(name);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ return components;
205
+ }
206
+ /**
207
+ * Shared detection logic for colors, densities, and font sizes.
208
+ * Scans all project source files for class names and data attributes
209
+ * matching the given patterns, returning the set of detected values.
210
+ */
211
+ async function detectByPatterns(root, forceInclude, classPrefix, dataAttr, validValues, mapMatch) {
212
+ const result = new Set(forceInclude);
213
+ if (validValues.length === 0)
214
+ return result;
215
+ const patterns = buildPatterns(classPrefix, dataAttr, validValues.join('|'));
216
+ const files = await scanFiles(root);
217
+ for (const file of files) {
218
+ const code = readSource(file);
219
+ if (!code)
220
+ continue;
221
+ for (const pattern of patterns) {
222
+ for (const match of code.matchAll(pattern)) {
223
+ const value = mapMatch ? mapMatch(match) : match[1];
224
+ if (value)
225
+ result.add(value);
226
+ }
227
+ }
228
+ }
229
+ return result;
230
+ }
231
+ /** Detect which color schemes are used in the project (e.g. "cyan", "brand"). */
232
+ export async function detectColors(root, forceInclude) {
233
+ const { colors } = discover(root);
234
+ return detectByPatterns(root, forceInclude, 'db-color-', 'data-color', colors);
235
+ }
236
+ /** Detect which density variants are used in the project (e.g. "functional", "expressive"). */
237
+ export async function detectDensities(root, forceInclude) {
238
+ const { densities } = discover(root);
239
+ return detectByPatterns(root, forceInclude, 'db-density-', 'data-density', densities);
240
+ }
241
+ /** Detect which font size combinations are used in the project (e.g. "body-md", "headline-lg"). */
242
+ export async function detectFontSizes(root, forceInclude) {
243
+ const { fontSizes: validFontSizes } = discover(root);
244
+ if (validFontSizes.length === 0)
245
+ return new Set(forceInclude);
246
+ const categories = [...new Set(validFontSizes.map((f) => f.split('-')[0]))];
247
+ const sizes = [...new Set(validFontSizes.map((f) => f.split('-')[1]))];
248
+ const validSet = new Set(validFontSizes);
249
+ return detectByPatterns(root, forceInclude, 'db-font-size-', 'data-font-size', [`(${categories.join('|')})-(${sizes.join('|')})`], (match) => {
250
+ const value = `${match[1]}-${match[2]}`;
251
+ return validSet.has(value) ? value : null;
252
+ });
253
+ }
@@ -0,0 +1,7 @@
1
+ import type { GenerateOptions } from './types.js';
2
+ /**
3
+ * Generate the CSS import statements based on detected/discovered values.
4
+ * Produces @import rules for theme, foundations, colors, densities,
5
+ * font sizes, and component styles, all wrapped in @layer declarations.
6
+ */
7
+ export declare function generateCSS(options: GenerateOptions): string;
@@ -0,0 +1,138 @@
1
+ import { readdirSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ /** Maps foundation feature names to their CSS file paths relative to build/styles/. */
4
+ const FOUNDATION_IMPORTS = {
5
+ helpers: 'helpers/classes/all.css',
6
+ elevation: 'defaults/default-elevation.css',
7
+ animations: 'component-animations.css',
8
+ icons: 'defaults/default-icons.css',
9
+ code: 'defaults/default-code.css'
10
+ };
11
+ /** Theme package scopes to search for installed themes. */
12
+ const THEME_SCOPES = ['@db-ux', '@db-ux-inner-source'];
13
+ /**
14
+ * Auto-detect the installed DB UX theme package (e.g. @db-ux/db-theme).
15
+ * Walks up from `root` checking each node_modules for *-theme packages
16
+ * in both @db-ux/* and @db-ux-inner-source/* scopes.
17
+ * Returns the package specifier or null if no theme is found.
18
+ */
19
+ function detectTheme(root, preferredTheme) {
20
+ let currentDir = root;
21
+ for (let i = 0; i < 10; i++) {
22
+ for (const scope of THEME_SCOPES) {
23
+ try {
24
+ const scopeDir = resolve(currentDir, 'node_modules', scope);
25
+ const packages = readdirSync(scopeDir);
26
+ const themePackages = packages.filter((pkg) => pkg.endsWith('-theme'));
27
+ if (themePackages.length === 0)
28
+ continue;
29
+ const match = preferredTheme && themePackages.includes(preferredTheme)
30
+ ? preferredTheme
31
+ : themePackages[0];
32
+ return `${scope}/${match}`;
33
+ }
34
+ catch {
35
+ // Scope not found at this level
36
+ }
37
+ }
38
+ const parentDir = resolve(currentDir, '..');
39
+ if (parentDir === currentDir)
40
+ break;
41
+ currentDir = parentDir;
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * Generate the CSS import statements based on detected/discovered values.
47
+ * Produces @import rules for theme, foundations, colors, densities,
48
+ * font sizes, and component styles, all wrapped in @layer declarations.
49
+ */
50
+ export function generateCSS(options) {
51
+ const { root, include, exclude, theme: preferredTheme, hasTailwind, overrideLayers, additionalLayers } = options;
52
+ const { components, foundations, colors, densities, fontSizes } = include;
53
+ const imports = [];
54
+ const theme = detectTheme(root, preferredTheme);
55
+ const themeName = theme ? theme.split('/').pop() : null;
56
+ // Layer order declaration
57
+ if (overrideLayers?.length) {
58
+ imports.push(`@layer ${overrideLayers.join(', ')};`);
59
+ }
60
+ else {
61
+ let autoLayers;
62
+ if (hasTailwind) {
63
+ autoLayers = themeName
64
+ ? [
65
+ themeName,
66
+ 'theme',
67
+ 'base',
68
+ 'components',
69
+ 'db-ux',
70
+ 'utilities'
71
+ ]
72
+ : ['theme', 'base', 'components', 'db-ux', 'utilities'];
73
+ }
74
+ else {
75
+ autoLayers = themeName ? [themeName, 'db-ux'] : ['db-ux'];
76
+ }
77
+ if (additionalLayers?.before?.length) {
78
+ autoLayers = [...additionalLayers.before, ...autoLayers];
79
+ }
80
+ if (additionalLayers?.after?.length) {
81
+ autoLayers = [...autoLayers, ...additionalLayers.after];
82
+ }
83
+ imports.push(`@layer ${autoLayers.join(', ')};`);
84
+ }
85
+ // Theme or default fallback
86
+ if (theme) {
87
+ imports.push(`@import "${theme}/build/styles/rollup.css" layer(${themeName});`);
88
+ imports.push(`@import "@db-ux/core-foundations/build/styles/defaults/default-container-properties.css" layer(db-ux);`);
89
+ }
90
+ else {
91
+ imports.push(`@import "@db-ux/core-foundations/build/styles/theme/rollup.css" layer(db-ux);`);
92
+ imports.push(`@import "@db-ux/core-foundations/build/styles/fonts/rollup.css" layer(db-ux);`);
93
+ }
94
+ // Tailwind theme
95
+ if (hasTailwind) {
96
+ imports.push(`@import "@db-ux/core-foundations/build/tailwind/theme/index.css";`);
97
+ }
98
+ // Required foundation styles
99
+ imports.push(`@import "@db-ux/core-foundations/build/styles/defaults/default-required.css" layer(db-ux);`);
100
+ imports.push(`@import "@db-ux/core-foundations/build/styles/defaults/default-root.css" layer(db-ux);`);
101
+ // Optional foundation features
102
+ for (const [key, path] of Object.entries(FOUNDATION_IMPORTS)) {
103
+ const feature = key;
104
+ if (foundations?.includes(feature) &&
105
+ !exclude.foundations?.includes(feature)) {
106
+ const basePath = feature === 'animations'
107
+ ? '@db-ux/core-components'
108
+ : '@db-ux/core-foundations';
109
+ imports.push(`@import "${basePath}/build/styles/${path}" layer(db-ux);`);
110
+ }
111
+ }
112
+ // Color schemes
113
+ for (const color of colors || []) {
114
+ if (!exclude.colors?.includes(color)) {
115
+ imports.push(`@import "@db-ux/core-foundations/build/styles/colors/classes/${color}.css" layer(db-ux);`);
116
+ }
117
+ }
118
+ // Densities
119
+ for (const density of densities || []) {
120
+ if (!exclude.densities?.includes(density)) {
121
+ imports.push(`@import "@db-ux/core-foundations/build/styles/density/classes/${density}.css" layer(db-ux);`);
122
+ }
123
+ }
124
+ // Font sizes
125
+ for (const fontSize of fontSizes || []) {
126
+ if (!exclude.fontSizes?.includes(fontSize)) {
127
+ const [category, size] = fontSize.split('-');
128
+ imports.push(`@import "@db-ux/core-foundations/build/styles/fonts/classes/${category}/${size}.css" layer(db-ux);`);
129
+ }
130
+ }
131
+ // Component styles
132
+ for (const component of components || []) {
133
+ if (!exclude.components?.includes(component)) {
134
+ imports.push(`@import "@db-ux/core-components/build/components/${component}/${component}.css" layer(db-ux);`);
135
+ }
136
+ }
137
+ return imports.join('\n');
138
+ }
@@ -0,0 +1,11 @@
1
+ import type { PluginConfig } from './types.js';
2
+ /**
3
+ * Create the DB UX Vite plugin.
4
+ * Returns two plugins: a pre-transform plugin for CSS generation and
5
+ * a post-build plugin for optimizing the final CSS bundle.
6
+ *
7
+ * During dev, all available styles are included for instant HMR.
8
+ * During build, only detected styles are included and unused ones are stripped.
9
+ */
10
+ export default function dbUxPlugin(config?: PluginConfig): any[];
11
+ export type { PluginConfig };
package/build/index.js ADDED
@@ -0,0 +1,168 @@
1
+ import { writeFile } from 'fs/promises';
2
+ import { resolve } from 'path';
3
+ import { detectColors, detectComponents, detectDensities, detectFontSizes, discoverAll, scanComponentDependencies } from './detector.js';
4
+ import { generateCSS } from './generator.js';
5
+ import { removeUnusedStyles } from './optimizer.js';
6
+ /** Default foundation features included unless overridden by user config. */
7
+ const DEFAULT_FOUNDATIONS = [
8
+ 'helpers',
9
+ 'icons',
10
+ 'animations',
11
+ 'code',
12
+ 'elevation'
13
+ ];
14
+ /**
15
+ * Create the DB UX Vite plugin.
16
+ * Returns two plugins: a pre-transform plugin for CSS generation and
17
+ * a post-build plugin for optimizing the final CSS bundle.
18
+ *
19
+ * During dev, all available styles are included for instant HMR.
20
+ * During build, only detected styles are included and unused ones are stripped.
21
+ */
22
+ export default function dbUxPlugin(config = {}) {
23
+ const { optimize = true, debug = false } = config;
24
+ // Deep-merge include/exclude so partial user config doesn't lose defaults
25
+ const include = {
26
+ foundations: config.include?.foundations ?? [...DEFAULT_FOUNDATIONS],
27
+ components: config.include?.components,
28
+ colors: config.include?.colors,
29
+ densities: config.include?.densities,
30
+ fontSizes: config.include?.fontSizes
31
+ };
32
+ const exclude = config.exclude ?? {};
33
+ let detectedComponents = new Set();
34
+ let detectedColors = new Set();
35
+ let detectedDensities = new Set();
36
+ let detectedFontSizes = new Set();
37
+ let hasDetected = false;
38
+ let detectionPromise = null;
39
+ let cssModuleId = null;
40
+ let hasTailwind = false;
41
+ let root = process.cwd();
42
+ let isBuild = false;
43
+ let generatedImports = [];
44
+ const mainPlugin = {
45
+ name: 'db-ux-vite-plugin',
46
+ enforce: 'pre',
47
+ resolveId(id) {
48
+ if (id === '@db-ux/core-vite-plugin/index.css') {
49
+ return id;
50
+ }
51
+ },
52
+ load(id) {
53
+ if (id === '@db-ux/core-vite-plugin/index.css') {
54
+ return '';
55
+ }
56
+ },
57
+ configResolved(resolvedConfig) {
58
+ root = resolvedConfig.root;
59
+ isBuild = resolvedConfig.command === 'build';
60
+ hasTailwind = resolvedConfig.plugins.some((plugin) => plugin.name.startsWith('@tailwindcss/vite'));
61
+ if (debug) {
62
+ console.log(`[db-ux-vite-plugin] Initialized (mode: ${resolvedConfig.command}, tailwind: ${hasTailwind})`);
63
+ }
64
+ },
65
+ handleHotUpdate({ file, server, modules }) {
66
+ if (/\.(vue|jsx|tsx|ts|html)$/.test(file) && cssModuleId) {
67
+ const mod = server.moduleGraph.getModuleById(cssModuleId);
68
+ if (mod) {
69
+ server.moduleGraph.invalidateModule(mod);
70
+ return [...modules, mod];
71
+ }
72
+ }
73
+ },
74
+ async transform(code, id) {
75
+ if (!id.endsWith('.css'))
76
+ return;
77
+ const hasImport = code.includes('@import "@db-ux/core-vite-plugin/index.css"') ||
78
+ code.includes("@import '@db-ux/core-vite-plugin/index.css'");
79
+ if (hasImport) {
80
+ cssModuleId = id;
81
+ if (isBuild && !hasDetected && !detectionPromise) {
82
+ detectionPromise = (async () => {
83
+ hasDetected = true;
84
+ detectedComponents = await detectComponents(root, include.components || []);
85
+ detectedColors = await detectColors(root, include.colors || []);
86
+ detectedDensities = await detectDensities(root, include.densities || []);
87
+ detectedFontSizes = await detectFontSizes(root, include.fontSizes || []);
88
+ scanComponentDependencies(root, detectedComponents, detectedColors, detectedDensities, detectedFontSizes);
89
+ })();
90
+ }
91
+ if (detectionPromise) {
92
+ await detectionPromise;
93
+ }
94
+ // During dev, include all discovered values for instant HMR.
95
+ // During build, use only detected values for tree-shaking.
96
+ const discovered = discoverAll(root);
97
+ const css = generateCSS({
98
+ root,
99
+ include: {
100
+ components: isBuild
101
+ ? Array.from(detectedComponents)
102
+ : Array.from(discovered.components),
103
+ foundations: include.foundations || [],
104
+ colors: isBuild
105
+ ? Array.from(detectedColors)
106
+ : discovered.colors,
107
+ densities: isBuild
108
+ ? Array.from(detectedDensities)
109
+ : discovered.densities,
110
+ fontSizes: isBuild
111
+ ? Array.from(detectedFontSizes)
112
+ : discovered.fontSizes
113
+ },
114
+ exclude,
115
+ theme: config.theme,
116
+ additionalLayers: config.additionalLayers,
117
+ overrideLayers: config.overrideLayers,
118
+ hasTailwind
119
+ });
120
+ generatedImports = css.split('\n');
121
+ if (debug) {
122
+ console.log('\n[db-ux-vite-plugin] Generated imports:\n' +
123
+ generatedImports
124
+ .map((line) => ` ${line}`)
125
+ .join('\n') +
126
+ '\n');
127
+ }
128
+ code = code.replace(/@import ["']@db-ux\/core-vite-plugin\/index\.css["'];?/g, css);
129
+ }
130
+ return code;
131
+ },
132
+ async buildEnd() {
133
+ if (debug && hasDetected) {
134
+ const reportPath = resolve(root, 'db-ux-detection-report.json');
135
+ await writeFile(reportPath, JSON.stringify({
136
+ components: Array.from(detectedComponents).sort(),
137
+ colors: Array.from(detectedColors).sort(),
138
+ densities: Array.from(detectedDensities).sort(),
139
+ fontSizes: Array.from(detectedFontSizes).sort(),
140
+ generatedImports
141
+ }, null, 2), 'utf-8');
142
+ }
143
+ }
144
+ };
145
+ const optimizerPlugin = {
146
+ name: 'db-ux-vite-plugin:optimizer',
147
+ enforce: 'post',
148
+ generateBundle(_options, bundle) {
149
+ if (!optimize || !hasDetected)
150
+ return;
151
+ const { colors, densities, fontSizes } = discoverAll(root);
152
+ const optimizerContext = {
153
+ allColors: colors,
154
+ allDensities: densities,
155
+ allFontSizes: fontSizes
156
+ };
157
+ for (const [, asset] of Object.entries(bundle)) {
158
+ if (asset.type === 'asset' &&
159
+ typeof asset.fileName === 'string' &&
160
+ asset.fileName.endsWith('.css') &&
161
+ typeof asset.source === 'string') {
162
+ asset.source = removeUnusedStyles(asset.source, detectedColors, detectedDensities, detectedFontSizes, optimizerContext);
163
+ }
164
+ }
165
+ }
166
+ };
167
+ return [mainPlugin, optimizerPlugin];
168
+ }
@@ -0,0 +1,12 @@
1
+ /** All available values discovered from the filesystem, used to determine what is unused. */
2
+ export interface OptimizerContext {
3
+ allColors: string[];
4
+ allDensities: string[];
5
+ allFontSizes: string[];
6
+ }
7
+ /**
8
+ * Remove unused CSS custom properties, @property declarations, and rule blocks
9
+ * for colors, densities, and font sizes that were not detected in the project.
10
+ * Also strips empty @layer declarations and @charset directives.
11
+ */
12
+ export declare function removeUnusedStyles(css: string, detectedColors: Set<string>, detectedDensities: Set<string>, detectedFontSizes: Set<string>, context: OptimizerContext): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Remove unused CSS custom properties, @property declarations, and rule blocks
3
+ * for colors, densities, and font sizes that were not detected in the project.
4
+ * Also strips empty @layer declarations and @charset directives.
5
+ */
6
+ export function removeUnusedStyles(css, detectedColors, detectedDensities, detectedFontSizes, context) {
7
+ const unusedColors = context.allColors.filter((c) => !detectedColors.has(c));
8
+ const unusedDensities = context.allDensities.filter((d) => !detectedDensities.has(d));
9
+ const unusedFontSizes = context.allFontSizes.filter((f) => !detectedFontSizes.has(f));
10
+ for (const color of unusedColors) {
11
+ css = css.replace(new RegExp(`@property --db-${color}-[a-z0-9-]+\\{[^}]+\\}`, 'g'), '');
12
+ // Make semicolon optional to handle last property in a block
13
+ css = css.replace(new RegExp(`--db-${color}-[a-z0-9-]+:[^;}]+;?`, 'g'), '');
14
+ // Normalize selectors — handle both quoted and unquoted data-color
15
+ css = css.replace(new RegExp(`\\[data-color=["']?${color}["']?\\],\\.db-color-${color}`, 'g'), `.db-color-${color}`);
16
+ css = css.replace(new RegExp(`\\.db-color-${color},\\[data-color=["']?${color}["']?\\]`, 'g'), `.db-color-${color}`);
17
+ // Remove entire rule blocks including pseudo-classes
18
+ css = css.replace(new RegExp(`\\.db-color-${color}(?:[^{]*?)\\{[^}]+\\}`, 'g'), '');
19
+ }
20
+ for (const density of unusedDensities) {
21
+ css = css.replace(new RegExp(`@property --db-[a-z-]+-${density}-[a-z0-9-]+\\{[^}]+\\}`, 'g'), '');
22
+ css = css.replace(new RegExp(`--db-[a-z-]+-${density}-[a-z0-9-]+:[^;}]+;?`, 'g'), '');
23
+ }
24
+ for (const fontSize of unusedFontSizes) {
25
+ const [type, size] = fontSize.split('-');
26
+ css = css.replace(new RegExp(`--db-type-${fontSize}:[^;}]+;?`, 'g'), '');
27
+ css = css.replace(new RegExp(`--db-base-${type}-icon-weight-${size}:[^;}]+;?`, 'g'), '');
28
+ css = css.replace(new RegExp(`@property --db-base-icon-weight-[a-z]+-[a-z]+-${type}-${size}\\{[^}]+\\}`, 'g'), '');
29
+ css = css.replace(new RegExp(`@property --db-base-icon-font-size-[a-z]+-[a-z]+-${type}-${size}\\{[^}]+\\}`, 'g'), '');
30
+ css = css.replace(new RegExp(`@property --db-typography-[a-z]+-[a-z]+-${type}-${size}\\{[^}]+\\}`, 'g'), '');
31
+ }
32
+ css = css.replace(/@layer variables;/g, '');
33
+ css = css.replace(/@charset "UTF-8";/g, '');
34
+ return css;
35
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Foundation features that can be included or excluded.
3
+ * - `icons`: Include icon fonts
4
+ * - `helpers`: Include helper classes
5
+ * - `elevation`: Include elevation styles
6
+ * - `animations`: Include component animations
7
+ * - `code`: Include code styling
8
+ */
9
+ export type FoundationFeature = 'icons' | 'helpers' | 'elevation' | 'animations' | 'code';
10
+ /**
11
+ * Available color schemes for the design system.
12
+ */
13
+ export type ColorScheme = 'neutral' | 'brand' | 'blue' | 'burgundy' | 'critical' | 'cyan' | 'green' | 'informational' | 'light-green' | 'orange' | 'pink' | 'red' | 'successful' | 'turquoise' | 'violet' | 'warning' | 'yellow';
14
+ /**
15
+ * Available density options for components.
16
+ */
17
+ export type Density = 'regular' | 'functional' | 'expressive';
18
+ /**
19
+ * Available font size options.
20
+ */
21
+ export type FontSize = 'body-3xs' | 'body-2xs' | 'body-xs' | 'body-sm' | 'body-md' | 'body-lg' | 'body-xl' | 'body-2xl' | 'body-3xl' | 'headline-3xs' | 'headline-2xs' | 'headline-xs' | 'headline-sm' | 'headline-md' | 'headline-lg' | 'headline-xl' | 'headline-2xl' | 'headline-3xl';
22
+ /**
23
+ * Configuration options for the Vite plugin.
24
+ */
25
+ export interface PluginConfig {
26
+ /**
27
+ * Force include specific components, foundation features, color schemes, densities, or font sizes.
28
+ */
29
+ include?: {
30
+ components?: string[];
31
+ foundations?: FoundationFeature[];
32
+ colors?: ColorScheme[];
33
+ densities?: Density[];
34
+ fontSizes?: FontSize[];
35
+ };
36
+ /**
37
+ * Exclude specific components, foundation features, color schemes, densities, or font sizes.
38
+ */
39
+ exclude?: {
40
+ components?: string[];
41
+ foundations?: FoundationFeature[];
42
+ colors?: ColorScheme[];
43
+ densities?: Density[];
44
+ fontSizes?: FontSize[];
45
+ };
46
+ /**
47
+ * Remove unused CSS variable declarations to reduce bundle size (default: true).
48
+ */
49
+ optimize?: boolean;
50
+ /**
51
+ * Specify preferred theme package name (e.g., "db-theme").
52
+ */
53
+ theme?: string;
54
+ /**
55
+ * Append custom layers to the auto-generated `@layer` order.
56
+ * @example { after: ['ri-extension'] } → `@layer db-theme, db-ux, ri-extension;`
57
+ * @example { before: ['reset'] } → `@layer reset, db-theme, db-ux;`
58
+ */
59
+ additionalLayers?: {
60
+ before?: string[];
61
+ after?: string[];
62
+ };
63
+ /**
64
+ * Fully replace the auto-generated `@layer` statement.
65
+ * When set, `additionalLayers` is ignored.
66
+ * @example ['db-theme', 'db-ux', 'ri-extension']
67
+ */
68
+ overrideLayers?: string[];
69
+ /**
70
+ * Generate detection report for debugging (default: false).
71
+ */
72
+ debug?: boolean;
73
+ }
74
+ /**
75
+ * Internal options passed to the CSS generator.
76
+ */
77
+ export interface GenerateOptions extends Pick<Required<PluginConfig>, 'include' | 'exclude'>, Pick<PluginConfig, 'theme' | 'additionalLayers' | 'overrideLayers'> {
78
+ /** Vite project root, used for resolving node_modules. */
79
+ root: string;
80
+ hasTailwind: boolean;
81
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/index.css ADDED
@@ -0,0 +1 @@
1
+ /* This file is processed by the @db-ux/core-vite-plugin */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@db-ux/core-vite-plugin",
3
- "version": "0.0.0",
3
+ "version": "4.6.0",
4
4
  "type": "module",
5
5
  "description": "Vite plugin for optimized DB UX Design System CSS imports",
6
6
  "repository": {
@@ -8,14 +8,52 @@
8
8
  "url": "git+https://github.com/db-ux-design-system/core-web.git"
9
9
  },
10
10
  "license": "Apache-2.0",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./build/index.d.ts",
14
+ "default": "./build/index.js"
15
+ },
16
+ "./index.css": "./index.css",
17
+ "./index": "./index.css"
18
+ },
19
+ "files": [
20
+ "CHANGELOG.md",
21
+ "build",
22
+ "index.css",
23
+ "LICENSE"
24
+ ],
11
25
  "keywords": [
12
26
  "vite",
13
27
  "vite-plugin",
14
28
  "db-ux",
15
29
  "design-system"
16
30
  ],
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "copy-output": "npm-run-all copy:*",
34
+ "copy:changelog": "cpr CHANGELOG.md ../../build-outputs/vite-plugin/CHANGELOG.md --overwrite",
35
+ "copy:index.css": "cpr index.css ../../build-outputs/vite-plugin/index.css -o",
36
+ "copy:outputs": "cpr build ../../build-outputs/vite-plugin/build -o",
37
+ "copy:package.json": "cpr package.json ../../build-outputs/vite-plugin/package.json -o",
38
+ "copy:readme": "cpr README.md ../../build-outputs/vite-plugin/README.md -o",
39
+ "test": "vitest run --config vitest.config.ts",
40
+ "test:update": "npm run test -- --update"
41
+ },
42
+ "peerDependencies": {
43
+ "vite": ">=4.0.0"
44
+ },
45
+ "dependencies": {
46
+ "fast-glob": "^3.3.3"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "22.16.0",
50
+ "typescript": "5.9.3",
51
+ "vite": "8.0.6",
52
+ "vitest": "4.1.3"
53
+ },
17
54
  "publishConfig": {
18
55
  "registry": "https://registry.npmjs.org/",
19
56
  "access": "public"
20
- }
57
+ },
58
+ "style": "index.css"
21
59
  }