@rakeyshgidwani/roger-ui-bank-theme-harvey 0.2.52 ā 0.3.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 +1 -1
- package/dist/components/ui/button.d.ts +3 -1
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.esm.js +3 -2
- package/dist/components/ui/button.js +3 -2
- package/dist/components/ui/layout/container.d.ts +57 -0
- package/dist/components/ui/layout/container.d.ts.map +1 -0
- package/dist/components/ui/layout/container.esm.js +173 -0
- package/dist/components/ui/layout/container.js +173 -0
- package/dist/components/ui/layout/index.d.ts +9 -0
- package/dist/components/ui/layout/index.d.ts.map +1 -0
- package/dist/components/ui/layout/index.esm.js +6 -0
- package/dist/components/ui/layout/index.js +6 -0
- package/dist/components/ui/layout/responsive-grid.d.ts +93 -0
- package/dist/components/ui/layout/responsive-grid.d.ts.map +1 -0
- package/dist/components/ui/layout/responsive-grid.esm.js +124 -0
- package/dist/components/ui/layout/responsive-grid.js +124 -0
- package/dist/components/ui/navigation/index.d.ts +2 -1
- package/dist/components/ui/navigation/index.d.ts.map +1 -1
- package/dist/components/ui/navigation/index.esm.js +1 -0
- package/dist/components/ui/navigation/index.js +1 -0
- package/dist/components/ui/navigation/progressive-navigation.d.ts +37 -0
- package/dist/components/ui/navigation/progressive-navigation.d.ts.map +1 -0
- package/dist/components/ui/navigation/progressive-navigation.esm.js +145 -0
- package/dist/components/ui/navigation/progressive-navigation.js +145 -0
- package/dist/components/ui/navigation/types.d.ts +21 -0
- package/dist/components/ui/navigation/types.d.ts.map +1 -1
- package/dist/hooks/use-adaptive-layout.d.ts +2 -1
- package/dist/hooks/use-adaptive-layout.d.ts.map +1 -1
- package/dist/hooks/use-adaptive-layout.esm.js +13 -8
- package/dist/hooks/use-adaptive-layout.js +13 -8
- package/dist/hooks/use-device.d.ts +3 -1
- package/dist/hooks/use-device.d.ts.map +1 -1
- package/dist/hooks/use-device.esm.js +14 -7
- package/dist/hooks/use-device.js +14 -7
- package/dist/index.d.ts +19 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +9 -4
- package/dist/index.js +9 -4
- package/dist/plugins/css-purge-optimizer.d.ts +25 -0
- package/dist/plugins/css-purge-optimizer.d.ts.map +1 -0
- package/dist/plugins/css-purge-optimizer.esm.js +414 -0
- package/dist/plugins/css-purge-optimizer.js +414 -0
- package/dist/plugins/performance-monitor.d.ts +29 -0
- package/dist/plugins/performance-monitor.d.ts.map +1 -0
- package/dist/plugins/performance-monitor.esm.js +221 -0
- package/dist/plugins/performance-monitor.js +221 -0
- package/dist/plugins/progressive-css-loader.d.ts +21 -0
- package/dist/plugins/progressive-css-loader.d.ts.map +1 -0
- package/dist/plugins/progressive-css-loader.esm.js +227 -0
- package/dist/plugins/progressive-css-loader.js +227 -0
- package/dist/plugins/theme-css-generator.d.ts.map +1 -1
- package/dist/plugins/theme-css-generator.esm.js +19 -6
- package/dist/plugins/theme-css-generator.js +19 -6
- package/dist/styles.css +1025 -110
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.esm.js +4 -1
- package/dist/theme.js +4 -1
- package/dist/themes/phase1-constants.d.ts +23 -0
- package/dist/themes/phase1-constants.d.ts.map +1 -0
- package/dist/themes/phase1-constants.esm.js +180 -0
- package/dist/themes/phase1-constants.js +180 -0
- package/dist/themes/themes/default.d.ts.map +1 -1
- package/dist/themes/themes/default.esm.js +4 -1
- package/dist/themes/themes/default.js +4 -1
- package/dist/themes/themes/harvey.d.ts.map +1 -1
- package/dist/themes/themes/harvey.esm.js +4 -1
- package/dist/themes/themes/harvey.js +4 -1
- package/dist/themes/types.d.ts +62 -0
- package/dist/themes/types.d.ts.map +1 -1
- package/dist/themes/validation.d.ts +17 -0
- package/dist/themes/validation.d.ts.map +1 -1
- package/dist/themes/validation.esm.js +218 -0
- package/dist/themes/validation.js +218 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/progressive-css-injector.d.ts +80 -0
- package/dist/utils/progressive-css-injector.d.ts.map +1 -0
- package/dist/utils/progressive-css-injector.esm.js +217 -0
- package/dist/utils/progressive-css-injector.js +217 -0
- package/package.json +1 -1
- package/src/components/ui/button.tsx +9 -6
- package/src/components/ui/layout/container.tsx +312 -0
- package/src/components/ui/layout/index.ts +10 -0
- package/src/components/ui/layout/responsive-grid.tsx +286 -0
- package/src/components/ui/navigation/index.ts +2 -0
- package/src/components/ui/navigation/progressive-navigation.tsx +453 -0
- package/src/components/ui/navigation/types.ts +41 -0
- package/src/hooks/use-adaptive-layout.ts +13 -9
- package/src/hooks/use-device.tsx +17 -10
- package/src/index.ts +19 -4
- package/src/plugins/css-purge-optimizer.ts +491 -0
- package/src/plugins/performance-monitor.ts +292 -0
- package/src/plugins/progressive-css-loader.ts +269 -0
- package/src/plugins/theme-css-generator.ts +22 -6
- package/src/styles/components/base/badge.css +2 -2
- package/src/styles/components/base/button.css +238 -35
- package/src/styles/components/base/card.css +2 -2
- package/src/styles/components/base/checkbox.css +3 -3
- package/src/styles/components/base/label.css +3 -3
- package/src/styles/components/feedback/skeleton.css +1 -1
- package/src/styles/components/feedback/toast.css +1 -1
- package/src/styles/components/index.css +3 -0
- package/src/styles/components/layout/container.css +466 -0
- package/src/styles/components/layout/index.css +5 -0
- package/src/styles/components/layout/responsive-grid.css +422 -0
- package/src/styles/components/navigation/breadcrumb.css +1 -1
- package/src/styles/components/navigation/index.css +1 -0
- package/src/styles/components/navigation/menu.css +2 -2
- package/src/styles/components/navigation/pagination.css +4 -4
- package/src/styles/components/navigation/progressive-navigation.css +633 -0
- package/src/styles/components/navigation/sidebar.css +4 -4
- package/src/styles/components/navigation/stepper.css +2 -2
- package/src/styles/components/navigation/tabs.css +1 -1
- package/src/styles/progressive.css +17 -0
- package/src/styles/themes/harvey.css +103 -19
- package/src/styles/utilities/semantic-input-system.css +7 -13
- package/src/theme.ts +5 -1
- package/src/themes/phase1-constants.ts +189 -0
- package/src/themes/themes/default.ts +5 -1
- package/src/themes/themes/harvey.ts +5 -1
- package/src/themes/types.ts +77 -1
- package/src/themes/validation.ts +249 -0
- package/src/types.ts +77 -1
- package/src/utils/progressive-css-injector.ts +254 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Purge Optimizer Plugin
|
|
3
|
+
* Implements Phase 3 Performance Optimization: CSS Bundle Optimization
|
|
4
|
+
*
|
|
5
|
+
* Optimizations:
|
|
6
|
+
* - Removes unused responsive breakpoint styles
|
|
7
|
+
* - Purges unused CSS custom properties
|
|
8
|
+
* - Optimizes CSS layer ordering
|
|
9
|
+
* - Minifies and compresses output
|
|
10
|
+
*/
|
|
11
|
+
import postcss from 'postcss';
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
breakpoints: ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'],
|
|
14
|
+
preserveCustomProperties: [
|
|
15
|
+
// Core design system variables
|
|
16
|
+
'--cs-*',
|
|
17
|
+
'--tw-*', // Preserve all Tailwind CSS variables
|
|
18
|
+
// Component design tokens (critical for responsive system)
|
|
19
|
+
'--button-*',
|
|
20
|
+
'--badge-*',
|
|
21
|
+
'--card-*',
|
|
22
|
+
'--input-*',
|
|
23
|
+
'--label-*',
|
|
24
|
+
'--checkbox-*',
|
|
25
|
+
'--menu-*',
|
|
26
|
+
'--sidebar-*',
|
|
27
|
+
'--breadcrumb-*',
|
|
28
|
+
'--pagination-*',
|
|
29
|
+
'--stepper-*',
|
|
30
|
+
'--tabs-*',
|
|
31
|
+
'--progressive-nav-*',
|
|
32
|
+
'--skeleton-*',
|
|
33
|
+
'--toast-*',
|
|
34
|
+
'--popover-*',
|
|
35
|
+
'--tooltip-*',
|
|
36
|
+
'--enterprise-*',
|
|
37
|
+
'--font-*',
|
|
38
|
+
'--user-menu-*',
|
|
39
|
+
'--subscription-*',
|
|
40
|
+
'--list-*',
|
|
41
|
+
'--tree-*',
|
|
42
|
+
// Layout and utility tokens
|
|
43
|
+
'--container-*',
|
|
44
|
+
'--grid-*',
|
|
45
|
+
'--spacing-*',
|
|
46
|
+
'--breakpoint-*'
|
|
47
|
+
],
|
|
48
|
+
preserveSelectors: [
|
|
49
|
+
'.sr-only',
|
|
50
|
+
'.focus\\:*',
|
|
51
|
+
'.hover\\:*',
|
|
52
|
+
'.active\\:*',
|
|
53
|
+
'[data-*]'
|
|
54
|
+
],
|
|
55
|
+
aggressive: false,
|
|
56
|
+
minify: true
|
|
57
|
+
};
|
|
58
|
+
export default function cssPurgeOptimizer(config = {}) {
|
|
59
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
60
|
+
return {
|
|
61
|
+
name: 'css-purge-optimizer',
|
|
62
|
+
apply: 'build',
|
|
63
|
+
generateBundle(_options, bundle) {
|
|
64
|
+
// Find CSS assets in bundle
|
|
65
|
+
const cssAssets = Object.keys(bundle).filter(key => key.endsWith('.css'));
|
|
66
|
+
cssAssets.forEach(assetKey => {
|
|
67
|
+
const asset = bundle[assetKey];
|
|
68
|
+
if (asset.type === 'asset' && typeof asset.source === 'string') {
|
|
69
|
+
const optimizedCSS = optimizeCSS(asset.source, finalConfig);
|
|
70
|
+
// Calculate optimization savings
|
|
71
|
+
const originalSize = Buffer.byteLength(asset.source, 'utf8');
|
|
72
|
+
const optimizedSize = Buffer.byteLength(optimizedCSS, 'utf8');
|
|
73
|
+
const savings = originalSize - optimizedSize;
|
|
74
|
+
const savingsPercent = Math.round((savings / originalSize) * 100);
|
|
75
|
+
console.log(`š§ CSS Optimization: ${assetKey}`);
|
|
76
|
+
console.log(` Original: ${Math.round(originalSize / 1024 * 100) / 100}KB`);
|
|
77
|
+
console.log(` Optimized: ${Math.round(optimizedSize / 1024 * 100) / 100}KB`);
|
|
78
|
+
console.log(` Savings: ${Math.round(savings / 1024 * 100) / 100}KB (${savingsPercent}%)`);
|
|
79
|
+
// Update the asset with optimized CSS
|
|
80
|
+
asset.source = optimizedCSS;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Main CSS optimization function
|
|
88
|
+
*/
|
|
89
|
+
function optimizeCSS(cssContent, config) {
|
|
90
|
+
let optimizedCSS = cssContent;
|
|
91
|
+
// Step 1: Analyze usage patterns
|
|
92
|
+
const usage = analyzeUsage(cssContent, config);
|
|
93
|
+
// Step 2: Remove unused responsive breakpoint styles
|
|
94
|
+
optimizedCSS = removeUnusedBreakpoints(optimizedCSS, usage, config);
|
|
95
|
+
// Step 3: Remove unused custom properties
|
|
96
|
+
optimizedCSS = removeUnusedCustomProperties(optimizedCSS, usage, config);
|
|
97
|
+
// Step 4: Optimize CSS layers and ordering
|
|
98
|
+
optimizedCSS = optimizeCSSLayers(optimizedCSS);
|
|
99
|
+
// Step 5: Remove duplicate rules
|
|
100
|
+
optimizedCSS = removeDuplicateRules(optimizedCSS);
|
|
101
|
+
// Step 6: Minify if enabled
|
|
102
|
+
if (config.minify) {
|
|
103
|
+
optimizedCSS = minifyCSS(optimizedCSS);
|
|
104
|
+
}
|
|
105
|
+
return optimizedCSS;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Analyzes CSS usage patterns using PostCSS
|
|
109
|
+
*/
|
|
110
|
+
function analyzeUsage(cssContent, config) {
|
|
111
|
+
const usage = {
|
|
112
|
+
usedBreakpoints: new Set(),
|
|
113
|
+
usedCustomProperties: new Set(),
|
|
114
|
+
usedSelectors: new Set(),
|
|
115
|
+
unusedRules: []
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const root = postcss.parse(cssContent);
|
|
119
|
+
// Walk through all nodes to analyze usage
|
|
120
|
+
root.walkAtRules('media', (rule) => {
|
|
121
|
+
// Analyze media query breakpoints
|
|
122
|
+
const mediaQuery = rule.params;
|
|
123
|
+
config.breakpoints.forEach((breakpoint) => {
|
|
124
|
+
const breakpointPixels = getBreakpointPixels(breakpoint);
|
|
125
|
+
// Check for pixel-based media queries
|
|
126
|
+
if (mediaQuery.includes(`${breakpointPixels}px`)) {
|
|
127
|
+
usage.usedBreakpoints.add(breakpoint);
|
|
128
|
+
}
|
|
129
|
+
// Check for CSS variable usage
|
|
130
|
+
if (mediaQuery.includes(`--cs-breakpoints-${breakpoint}`)) {
|
|
131
|
+
usage.usedBreakpoints.add(breakpoint);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// Analyze custom property declarations and usage
|
|
136
|
+
root.walkDecls((decl) => {
|
|
137
|
+
// Find custom property definitions
|
|
138
|
+
if (decl.prop.startsWith('--')) {
|
|
139
|
+
// Check if this property is used elsewhere in the CSS
|
|
140
|
+
const propertyUsage = cssContent.includes(`var(${decl.prop})`);
|
|
141
|
+
if (propertyUsage) {
|
|
142
|
+
usage.usedCustomProperties.add(decl.prop);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Find custom property usage in values
|
|
146
|
+
const varMatches = decl.value.match(/var\(--[^)]+\)/g);
|
|
147
|
+
if (varMatches) {
|
|
148
|
+
varMatches.forEach(match => {
|
|
149
|
+
const propertyName = match.match(/--[^)]+/)?.[0];
|
|
150
|
+
if (propertyName) {
|
|
151
|
+
usage.usedCustomProperties.add(propertyName);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// Analyze selectors
|
|
157
|
+
root.walkRules((rule) => {
|
|
158
|
+
const selectors = rule.selector.split(',');
|
|
159
|
+
selectors.forEach(selector => {
|
|
160
|
+
const trimmedSelector = selector.trim();
|
|
161
|
+
usage.usedSelectors.add(trimmedSelector);
|
|
162
|
+
// Extract class names
|
|
163
|
+
const classMatches = trimmedSelector.match(/\.[a-zA-Z0-9_-]+/g);
|
|
164
|
+
if (classMatches) {
|
|
165
|
+
classMatches.forEach(className => {
|
|
166
|
+
usage.usedSelectors.add(className);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
// For safety, always mark critical breakpoints as used
|
|
172
|
+
const criticalBreakpoints = ['xs', 'sm', 'md'];
|
|
173
|
+
criticalBreakpoints.forEach((bp) => usage.usedBreakpoints.add(bp));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.warn('PostCSS parsing failed in usage analysis, falling back to regex:', error);
|
|
177
|
+
// Fallback to basic regex analysis
|
|
178
|
+
return analyzeUsageFallback(cssContent, config);
|
|
179
|
+
}
|
|
180
|
+
return usage;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Fallback regex-based usage analysis
|
|
184
|
+
*/
|
|
185
|
+
function analyzeUsageFallback(cssContent, config) {
|
|
186
|
+
const usage = {
|
|
187
|
+
usedBreakpoints: new Set(),
|
|
188
|
+
usedCustomProperties: new Set(),
|
|
189
|
+
usedSelectors: new Set(),
|
|
190
|
+
unusedRules: []
|
|
191
|
+
};
|
|
192
|
+
// Analyze breakpoint usage in media queries
|
|
193
|
+
config.breakpoints.forEach((breakpoint) => {
|
|
194
|
+
const varPattern = new RegExp(`var\\(--cs-breakpoints-${breakpoint}\\)`, 'g');
|
|
195
|
+
const breakpointPixels = getBreakpointPixels(breakpoint);
|
|
196
|
+
const pixelPattern = new RegExp(`(min-width|max-width):\\s*${breakpointPixels}px`, 'g');
|
|
197
|
+
const literalPattern = new RegExp(`\\b${breakpoint}\\b`, 'g');
|
|
198
|
+
if (varPattern.test(cssContent) ||
|
|
199
|
+
pixelPattern.test(cssContent) ||
|
|
200
|
+
literalPattern.test(cssContent)) {
|
|
201
|
+
usage.usedBreakpoints.add(breakpoint);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// For safety, always mark critical breakpoints as used
|
|
205
|
+
const criticalBreakpoints = ['xs', 'sm', 'md'];
|
|
206
|
+
criticalBreakpoints.forEach((bp) => usage.usedBreakpoints.add(bp));
|
|
207
|
+
// Analyze custom property usage
|
|
208
|
+
const customPropertyMatches = cssContent.match(/var\(--[^)]+\)/g) || [];
|
|
209
|
+
customPropertyMatches.forEach(match => {
|
|
210
|
+
const propertyName = match.match(/--[^)]+/)?.[0];
|
|
211
|
+
if (propertyName) {
|
|
212
|
+
usage.usedCustomProperties.add(propertyName);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Analyze selector usage
|
|
216
|
+
const selectorMatches = cssContent.match(/\.[a-zA-Z0-9_-]+/g) || [];
|
|
217
|
+
selectorMatches.forEach(selector => {
|
|
218
|
+
usage.usedSelectors.add(selector);
|
|
219
|
+
});
|
|
220
|
+
return usage;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Maps breakpoint names to their pixel values
|
|
224
|
+
*/
|
|
225
|
+
function getBreakpointPixels(breakpoint) {
|
|
226
|
+
const breakpointMap = {
|
|
227
|
+
xs: 0, // mobile-first, no min-width
|
|
228
|
+
sm: 640,
|
|
229
|
+
md: 768,
|
|
230
|
+
lg: 1024,
|
|
231
|
+
xl: 1280,
|
|
232
|
+
'2xl': 1536,
|
|
233
|
+
'3xl': 1792
|
|
234
|
+
};
|
|
235
|
+
return breakpointMap[breakpoint] || 0;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Removes unused responsive breakpoint styles using PostCSS
|
|
239
|
+
*/
|
|
240
|
+
function removeUnusedBreakpoints(cssContent, usage, config) {
|
|
241
|
+
if (!config.aggressive) {
|
|
242
|
+
console.log(` Skipping breakpoint removal (non-aggressive mode)`);
|
|
243
|
+
return cssContent;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const root = postcss.parse(cssContent);
|
|
247
|
+
let removedCount = 0;
|
|
248
|
+
root.walkAtRules('media', (rule) => {
|
|
249
|
+
const mediaQuery = rule.params;
|
|
250
|
+
// Check if this media query uses an unused breakpoint
|
|
251
|
+
const usesUnusedBreakpoint = config.breakpoints.some(breakpoint => {
|
|
252
|
+
// Skip critical breakpoints
|
|
253
|
+
if (['xs', 'sm', 'md'].includes(breakpoint)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
// Check if breakpoint is unused and referenced in this media query
|
|
257
|
+
if (!usage.usedBreakpoints.has(breakpoint)) {
|
|
258
|
+
const breakpointPixels = getBreakpointPixels(breakpoint);
|
|
259
|
+
return mediaQuery.includes(`${breakpointPixels}px`) ||
|
|
260
|
+
mediaQuery.includes(`--cs-breakpoints-${breakpoint}`);
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
});
|
|
264
|
+
if (usesUnusedBreakpoint) {
|
|
265
|
+
console.log(` Removed unused breakpoint media query: ${mediaQuery}`);
|
|
266
|
+
rule.remove();
|
|
267
|
+
removedCount++;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
return root.toString();
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.warn('PostCSS parsing failed in breakpoint removal, using original CSS:', error);
|
|
274
|
+
return cssContent;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Removes unused CSS custom properties using PostCSS
|
|
279
|
+
*/
|
|
280
|
+
function removeUnusedCustomProperties(cssContent, usage, config) {
|
|
281
|
+
try {
|
|
282
|
+
const root = postcss.parse(cssContent);
|
|
283
|
+
let removedCount = 0;
|
|
284
|
+
root.walkDecls((decl) => {
|
|
285
|
+
// Only process custom property declarations
|
|
286
|
+
if (decl.prop.startsWith('--')) {
|
|
287
|
+
// Check if property should be preserved (check this first for Tailwind variables)
|
|
288
|
+
const shouldPreserve = config.preserveCustomProperties.some(pattern => {
|
|
289
|
+
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
290
|
+
return regex.test(decl.prop);
|
|
291
|
+
});
|
|
292
|
+
// Always preserve whitelisted properties (like --tw-*), otherwise check usage
|
|
293
|
+
if (!shouldPreserve && !usage.usedCustomProperties.has(decl.prop)) {
|
|
294
|
+
console.log(` Removed unused custom property: ${decl.prop}`);
|
|
295
|
+
decl.remove();
|
|
296
|
+
removedCount++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
return root.toString();
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
console.warn('PostCSS parsing failed in custom property removal, using original CSS:', error);
|
|
304
|
+
return cssContent;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Optimizes CSS layer ordering using PostCSS
|
|
309
|
+
*/
|
|
310
|
+
function optimizeCSSLayers(cssContent) {
|
|
311
|
+
try {
|
|
312
|
+
const root = postcss.parse(cssContent);
|
|
313
|
+
const layerOrder = ['reset', 'base', 'themes', 'components', 'utilities'];
|
|
314
|
+
let hasLayers = false;
|
|
315
|
+
// Remove existing @layer declarations
|
|
316
|
+
root.walkAtRules('layer', (rule) => {
|
|
317
|
+
if (!rule.nodes || rule.nodes.length === 0) {
|
|
318
|
+
// This is a layer order declaration, not a layer block
|
|
319
|
+
hasLayers = true;
|
|
320
|
+
rule.remove();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
// Add optimized layer order at the beginning if we found layers
|
|
324
|
+
if (hasLayers) {
|
|
325
|
+
const layerDeclaration = postcss.atRule({
|
|
326
|
+
name: 'layer',
|
|
327
|
+
params: layerOrder.join(', ')
|
|
328
|
+
});
|
|
329
|
+
root.prepend(layerDeclaration);
|
|
330
|
+
root.prepend(postcss.comment({ text: ' Optimized CSS layer order ' }));
|
|
331
|
+
}
|
|
332
|
+
return root.toString();
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
console.warn('PostCSS parsing failed in layer optimization, using original CSS:', error);
|
|
336
|
+
return cssContent;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Removes duplicate CSS rules using PostCSS
|
|
341
|
+
*/
|
|
342
|
+
function removeDuplicateRules(cssContent) {
|
|
343
|
+
try {
|
|
344
|
+
const root = postcss.parse(cssContent);
|
|
345
|
+
const ruleMap = new Map();
|
|
346
|
+
let removedCount = 0;
|
|
347
|
+
root.walkRules((rule) => {
|
|
348
|
+
const selector = rule.selector;
|
|
349
|
+
const declarations = rule.nodes?.map(node => {
|
|
350
|
+
if (node.type === 'decl') {
|
|
351
|
+
return `${node.prop}: ${node.value}`;
|
|
352
|
+
}
|
|
353
|
+
return '';
|
|
354
|
+
}).filter(Boolean).sort().join('; ');
|
|
355
|
+
const ruleKey = `${selector}|${declarations}`;
|
|
356
|
+
if (ruleMap.has(ruleKey)) {
|
|
357
|
+
console.log(` Removed duplicate rule: ${selector}`);
|
|
358
|
+
rule.remove();
|
|
359
|
+
removedCount++;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
ruleMap.set(ruleKey, selector);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return root.toString();
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
console.warn('PostCSS parsing failed in duplicate rule removal, using original CSS:', error);
|
|
369
|
+
return cssContent;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Basic CSS minification
|
|
374
|
+
*/
|
|
375
|
+
function minifyCSS(cssContent) {
|
|
376
|
+
return cssContent
|
|
377
|
+
// Remove comments
|
|
378
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
379
|
+
// Remove unnecessary whitespace
|
|
380
|
+
.replace(/\s+/g, ' ')
|
|
381
|
+
// Remove whitespace around braces and semicolons
|
|
382
|
+
.replace(/\s*{\s*/g, '{')
|
|
383
|
+
.replace(/\s*}\s*/g, '}')
|
|
384
|
+
.replace(/\s*;\s*/g, ';')
|
|
385
|
+
.replace(/\s*:\s*/g, ':')
|
|
386
|
+
.replace(/\s*,\s*/g, ',')
|
|
387
|
+
// Remove trailing semicolons
|
|
388
|
+
.replace(/;}/g, '}')
|
|
389
|
+
// Remove leading/trailing whitespace
|
|
390
|
+
.trim();
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Generates optimization report
|
|
394
|
+
*/
|
|
395
|
+
export function generateOptimizationReport(originalCSS, optimizedCSS) {
|
|
396
|
+
const originalSize = Buffer.byteLength(originalCSS, 'utf8');
|
|
397
|
+
const optimizedSize = Buffer.byteLength(optimizedCSS, 'utf8');
|
|
398
|
+
const savings = originalSize - optimizedSize;
|
|
399
|
+
const savingsPercent = Math.round((savings / originalSize) * 100);
|
|
400
|
+
return `
|
|
401
|
+
CSS Optimization Report
|
|
402
|
+
======================
|
|
403
|
+
Original Size: ${Math.round(originalSize / 1024 * 100) / 100}KB
|
|
404
|
+
Optimized Size: ${Math.round(optimizedSize / 1024 * 100) / 100}KB
|
|
405
|
+
Savings: ${Math.round(savings / 1024 * 100) / 100}KB (${savingsPercent}%)
|
|
406
|
+
|
|
407
|
+
Optimizations Applied:
|
|
408
|
+
- Removed unused responsive breakpoint styles
|
|
409
|
+
- Purged unused CSS custom properties
|
|
410
|
+
- Optimized CSS layer ordering
|
|
411
|
+
- Removed duplicate rules
|
|
412
|
+
- Applied minification
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Monitor Plugin
|
|
3
|
+
* Implements Phase 3 Performance Optimization: Bundle Size Monitoring
|
|
4
|
+
*
|
|
5
|
+
* Tracks:
|
|
6
|
+
* - CSS bundle sizes (critical vs progressive)
|
|
7
|
+
* - JavaScript bundle sizes
|
|
8
|
+
* - Performance metrics and alerts
|
|
9
|
+
* - Loading performance analysis
|
|
10
|
+
*/
|
|
11
|
+
import type { Plugin } from 'vite';
|
|
12
|
+
interface PerformanceConfig {
|
|
13
|
+
thresholds: {
|
|
14
|
+
criticalCSS: number;
|
|
15
|
+
progressiveCSS: number;
|
|
16
|
+
totalCSS: number;
|
|
17
|
+
javascript: number;
|
|
18
|
+
};
|
|
19
|
+
reportPath: string;
|
|
20
|
+
enableAlerts: boolean;
|
|
21
|
+
baselinePath?: string;
|
|
22
|
+
}
|
|
23
|
+
export default function performanceMonitor(config?: Partial<PerformanceConfig>): Plugin;
|
|
24
|
+
/**
|
|
25
|
+
* Creates performance comparison utility
|
|
26
|
+
*/
|
|
27
|
+
export declare function comparePerformance(currentReport: string, baselineReport?: string): void;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=performance-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-monitor.d.ts","sourceRoot":"","sources":["../../src/plugins/performance-monitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAIlC,UAAU,iBAAiB;IAEzB,UAAU,EAAE;QACV,WAAW,EAAE,MAAM,CAAA;QACnB,cAAc,EAAE,MAAM,CAAA;QACtB,QAAQ,EAAE,MAAM,CAAA;QAChB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAA;IAED,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAsCD,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,MAAM,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,MAAM,CAwB1F;AAwKD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CA+BvF"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Monitor Plugin
|
|
3
|
+
* Implements Phase 3 Performance Optimization: Bundle Size Monitoring
|
|
4
|
+
*
|
|
5
|
+
* Tracks:
|
|
6
|
+
* - CSS bundle sizes (critical vs progressive)
|
|
7
|
+
* - JavaScript bundle sizes
|
|
8
|
+
* - Performance metrics and alerts
|
|
9
|
+
* - Loading performance analysis
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
thresholds: {
|
|
15
|
+
criticalCSS: 50, // KB - Mobile-critical styles should be minimal
|
|
16
|
+
progressiveCSS: 150, // KB - Desktop enhancement styles
|
|
17
|
+
totalCSS: 200, // KB - Total CSS budget
|
|
18
|
+
javascript: 500 // KB - JS bundle threshold
|
|
19
|
+
},
|
|
20
|
+
reportPath: 'performance-report.json',
|
|
21
|
+
enableAlerts: true
|
|
22
|
+
};
|
|
23
|
+
export default function performanceMonitor(config = {}) {
|
|
24
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
return {
|
|
27
|
+
name: 'performance-monitor',
|
|
28
|
+
apply: 'build',
|
|
29
|
+
generateBundle(_options, bundle) {
|
|
30
|
+
const analysis = analyzeBundle(bundle, startTime, finalConfig);
|
|
31
|
+
// Write performance report
|
|
32
|
+
const reportPath = path.resolve(finalConfig.reportPath);
|
|
33
|
+
fs.writeFileSync(reportPath, JSON.stringify(analysis, null, 2));
|
|
34
|
+
// Console output
|
|
35
|
+
logPerformanceReport(analysis, finalConfig);
|
|
36
|
+
// Alerts
|
|
37
|
+
if (finalConfig.enableAlerts && analysis.alerts.length > 0) {
|
|
38
|
+
console.warn('\nā ļø Performance Alerts:');
|
|
39
|
+
analysis.alerts.forEach(alert => console.warn(` ${alert}`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Analyzes bundle contents and generates performance report
|
|
46
|
+
*/
|
|
47
|
+
function analyzeBundle(bundle, startTime, config) {
|
|
48
|
+
const analysis = {
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
files: {},
|
|
51
|
+
totals: {
|
|
52
|
+
criticalCSS: 0,
|
|
53
|
+
progressiveCSS: 0,
|
|
54
|
+
totalCSS: 0,
|
|
55
|
+
javascript: 0,
|
|
56
|
+
assets: 0,
|
|
57
|
+
total: 0
|
|
58
|
+
},
|
|
59
|
+
performance: {
|
|
60
|
+
bundleTime: Date.now() - startTime,
|
|
61
|
+
compressionRatio: 0
|
|
62
|
+
},
|
|
63
|
+
alerts: []
|
|
64
|
+
};
|
|
65
|
+
// Analyze each file in the bundle
|
|
66
|
+
Object.entries(bundle).forEach(([fileName, asset]) => {
|
|
67
|
+
if (asset.type === 'asset' || asset.type === 'chunk') {
|
|
68
|
+
const size = getAssetSize(asset);
|
|
69
|
+
const type = getAssetType(fileName);
|
|
70
|
+
const category = getAssetCategory(fileName);
|
|
71
|
+
analysis.files[fileName] = {
|
|
72
|
+
size,
|
|
73
|
+
type,
|
|
74
|
+
category
|
|
75
|
+
};
|
|
76
|
+
// Update totals
|
|
77
|
+
if (type === 'css') {
|
|
78
|
+
if (category === 'critical') {
|
|
79
|
+
analysis.totals.criticalCSS += size;
|
|
80
|
+
}
|
|
81
|
+
else if (category === 'progressive') {
|
|
82
|
+
analysis.totals.progressiveCSS += size;
|
|
83
|
+
}
|
|
84
|
+
analysis.totals.totalCSS += size;
|
|
85
|
+
}
|
|
86
|
+
else if (type === 'js') {
|
|
87
|
+
analysis.totals.javascript += size;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
analysis.totals.assets += size;
|
|
91
|
+
}
|
|
92
|
+
analysis.totals.total += size;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// Calculate content-aware estimated gzip compression ratio
|
|
96
|
+
const calculateEstimatedGzipRatio = () => {
|
|
97
|
+
// Realistic gzip compression ratios by content type (compressed size / original size)
|
|
98
|
+
const cssGzipRatio = 0.15; // CSS compresses to ~15% of original size
|
|
99
|
+
const jsGzipRatio = 0.25; // JavaScript compresses to ~25% of original size
|
|
100
|
+
const cssSize = analysis.totals.totalCSS;
|
|
101
|
+
const jsSize = analysis.totals.javascript;
|
|
102
|
+
// Only calculate gzip for CSS and JS (assets like fonts/images typically aren't gzipped by CDNs)
|
|
103
|
+
const compressibleSize = cssSize + jsSize;
|
|
104
|
+
if (compressibleSize === 0)
|
|
105
|
+
return 0;
|
|
106
|
+
// Calculate estimated gzipped size for compressible content only
|
|
107
|
+
const estimatedGzipSize = (cssSize * cssGzipRatio) +
|
|
108
|
+
(jsSize * jsGzipRatio);
|
|
109
|
+
// Return ratio of estimated gzipped size to total compressible size
|
|
110
|
+
return Math.round((estimatedGzipSize / compressibleSize) * 100) / 100;
|
|
111
|
+
};
|
|
112
|
+
analysis.performance.compressionRatio = calculateEstimatedGzipRatio();
|
|
113
|
+
// Generate alerts
|
|
114
|
+
analysis.alerts = generateAlerts(analysis.totals, config.thresholds);
|
|
115
|
+
return analysis;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Determines asset size in KB
|
|
119
|
+
*/
|
|
120
|
+
function getAssetSize(asset) {
|
|
121
|
+
const source = asset.source || asset.code || '';
|
|
122
|
+
const bytes = typeof source === 'string' ? Buffer.byteLength(source, 'utf8') : source.length;
|
|
123
|
+
return Math.round(bytes / 1024 * 100) / 100; // KB with 2 decimal precision
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Determines asset type from filename
|
|
127
|
+
*/
|
|
128
|
+
function getAssetType(fileName) {
|
|
129
|
+
if (fileName.endsWith('.css'))
|
|
130
|
+
return 'css';
|
|
131
|
+
if (fileName.endsWith('.js') || fileName.endsWith('.mjs'))
|
|
132
|
+
return 'js';
|
|
133
|
+
return 'asset';
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Categorizes assets for performance analysis
|
|
137
|
+
*/
|
|
138
|
+
function getAssetCategory(fileName) {
|
|
139
|
+
if (fileName.includes('critical'))
|
|
140
|
+
return 'critical';
|
|
141
|
+
if (fileName.includes('progressive'))
|
|
142
|
+
return 'progressive';
|
|
143
|
+
if (fileName.includes('vendor') || fileName.includes('node_modules'))
|
|
144
|
+
return 'vendor';
|
|
145
|
+
if (fileName.includes('component'))
|
|
146
|
+
return 'component';
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Generates performance alerts based on thresholds
|
|
151
|
+
*/
|
|
152
|
+
function generateAlerts(totals, thresholds) {
|
|
153
|
+
const alerts = [];
|
|
154
|
+
if (totals.criticalCSS > thresholds.criticalCSS) {
|
|
155
|
+
alerts.push(`Critical CSS size (${totals.criticalCSS}KB) exceeds threshold (${thresholds.criticalCSS}KB)`);
|
|
156
|
+
}
|
|
157
|
+
if (totals.progressiveCSS > thresholds.progressiveCSS) {
|
|
158
|
+
alerts.push(`Progressive CSS size (${totals.progressiveCSS}KB) exceeds threshold (${thresholds.progressiveCSS}KB)`);
|
|
159
|
+
}
|
|
160
|
+
if (totals.totalCSS > thresholds.totalCSS) {
|
|
161
|
+
alerts.push(`Total CSS size (${totals.totalCSS}KB) exceeds threshold (${thresholds.totalCSS}KB)`);
|
|
162
|
+
}
|
|
163
|
+
if (totals.javascript > thresholds.javascript) {
|
|
164
|
+
alerts.push(`JavaScript bundle size (${totals.javascript}KB) exceeds threshold (${thresholds.javascript}KB)`);
|
|
165
|
+
}
|
|
166
|
+
return alerts;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Logs performance report to console
|
|
170
|
+
*/
|
|
171
|
+
function logPerformanceReport(analysis, config) {
|
|
172
|
+
console.log('\nš Performance Analysis Report');
|
|
173
|
+
console.log('================================');
|
|
174
|
+
console.log('\nš¦ Bundle Sizes:');
|
|
175
|
+
console.log(` Critical CSS: ${analysis.totals.criticalCSS}KB (threshold: ${config.thresholds.criticalCSS}KB)`);
|
|
176
|
+
console.log(` Progressive CSS: ${analysis.totals.progressiveCSS}KB (threshold: ${config.thresholds.progressiveCSS}KB)`);
|
|
177
|
+
console.log(` Total CSS: ${analysis.totals.totalCSS}KB (threshold: ${config.thresholds.totalCSS}KB)`);
|
|
178
|
+
console.log(` JavaScript: ${analysis.totals.javascript}KB (threshold: ${config.thresholds.javascript}KB)`);
|
|
179
|
+
console.log(` Assets: ${analysis.totals.assets}KB`);
|
|
180
|
+
console.log(` Total Bundle: ${analysis.totals.total}KB`);
|
|
181
|
+
console.log('\nā” Performance Metrics:');
|
|
182
|
+
console.log(` Build Time: ${analysis.performance.bundleTime}ms`);
|
|
183
|
+
const compressibleSize = analysis.totals.totalCSS + analysis.totals.javascript;
|
|
184
|
+
console.log(` Gzip Ratio: ${analysis.performance.compressionRatio} (CSS+JS compression)`);
|
|
185
|
+
console.log(` Est. Gzip Size: ~${Math.round(compressibleSize * analysis.performance.compressionRatio)}KB (CSS+JS only)`);
|
|
186
|
+
console.log(`\nš Report saved to: ${config.reportPath}`);
|
|
187
|
+
if (analysis.alerts.length === 0) {
|
|
188
|
+
console.log('\nā
All performance thresholds met!');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Creates performance comparison utility
|
|
193
|
+
*/
|
|
194
|
+
export function comparePerformance(currentReport, baselineReport) {
|
|
195
|
+
if (!baselineReport || !fs.existsSync(baselineReport)) {
|
|
196
|
+
console.log('\nš Baseline report not found. Current report will serve as baseline.');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const current = JSON.parse(fs.readFileSync(currentReport, 'utf8'));
|
|
201
|
+
const baseline = JSON.parse(fs.readFileSync(baselineReport, 'utf8'));
|
|
202
|
+
console.log('\nš Performance Comparison (vs baseline)');
|
|
203
|
+
console.log('======================================');
|
|
204
|
+
const metrics = [
|
|
205
|
+
{ name: 'Critical CSS', current: current.totals.criticalCSS, baseline: baseline.totals.criticalCSS },
|
|
206
|
+
{ name: 'Progressive CSS', current: current.totals.progressiveCSS, baseline: baseline.totals.progressiveCSS },
|
|
207
|
+
{ name: 'Total CSS', current: current.totals.totalCSS, baseline: baseline.totals.totalCSS },
|
|
208
|
+
{ name: 'JavaScript', current: current.totals.javascript, baseline: baseline.totals.javascript },
|
|
209
|
+
{ name: 'Total Bundle', current: current.totals.total, baseline: baseline.totals.total }
|
|
210
|
+
];
|
|
211
|
+
metrics.forEach(metric => {
|
|
212
|
+
const diff = metric.current - metric.baseline;
|
|
213
|
+
const diffPercent = metric.baseline > 0 ? Math.round((diff / metric.baseline) * 100) : 0;
|
|
214
|
+
const indicator = diff > 0 ? 'š' : diff < 0 ? 'š' : 'ā”ļø';
|
|
215
|
+
console.log(` ${metric.name.padEnd(15)} ${indicator} ${metric.current}KB (${diff >= 0 ? '+' : ''}${diff}KB, ${diffPercent >= 0 ? '+' : ''}${diffPercent}%)`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error('Failed to compare performance reports:', error);
|
|
220
|
+
}
|
|
221
|
+
}
|