Skip to content

Commit 138eff7

Browse files
committed
fix(tasty): finalize injector
1 parent 375c2f8 commit 138eff7

File tree

5 files changed

+108
-20
lines changed

5 files changed

+108
-20
lines changed

src/stories/StyleInjector.docs.mdx

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,45 @@ const globalResult = injectGlobal([
9393
globalResult.dispose();
9494
```
9595

96+
### `createGlobalStyle<Props>(strings, ...interpolations): ComponentType`
97+
98+
Creates a React component that injects global styles when mounted (similar to styled-components).
99+
100+
```typescript
101+
import { createGlobalStyle } from '@cube-dev/ui-kit/tasty/injector';
102+
103+
const GlobalReset = createGlobalStyle<{ theme: 'dark' | 'light' }>`
104+
body {
105+
margin: 0;
106+
background: ${props => props.theme === 'dark' ? '#000' : '#fff'};
107+
color: ${props => props.theme === 'dark' ? '#fff' : '#000'};
108+
}
109+
110+
* {
111+
box-sizing: border-box;
112+
}
113+
`;
114+
115+
// Usage in React
116+
<GlobalReset theme="dark" />
117+
```
118+
119+
### `createInjector(config?): StyleInjector`
120+
121+
Creates an isolated injector instance with custom configuration.
122+
123+
```typescript
124+
import { createInjector } from '@cube-dev/ui-kit/tasty/injector';
125+
126+
// Create isolated instance for testing
127+
const testInjector = createInjector({
128+
devMode: true,
129+
forceTextInjection: true,
130+
});
131+
132+
const result = testInjector.inject(rules);
133+
```
134+
96135
### `keyframes(steps, nameOrOptions?): KeyframesResult`
97136

98137
Injects CSS keyframes with automatic deduplication.
@@ -127,17 +166,27 @@ Configures the global injector instance.
127166

128167
```typescript
129168
configure({
130-
devMode: true, // Enable development features
131-
maxRulesPerSheet: 8000, // Cap rules per stylesheet
169+
devMode: true, // Enable development features (auto-detected)
170+
maxRulesPerSheet: 8192, // Cap rules per stylesheet (default: 8192)
132171
unusedStylesThreshold: 500, // Trigger cleanup threshold (CSS rules only)
133-
bulkCleanupDelay: 5000, // Cleanup delay (ms)
134-
idleCleanup: true, // Use requestIdleCallback
172+
bulkCleanupDelay: 5000, // Cleanup delay (ms) - ignored if idleCleanup is true
173+
idleCleanup: true, // Use requestIdleCallback for cleanup
135174
bulkCleanupBatchRatio: 0.5, // Clean up oldest 50% per batch
136-
forceTextInjection: false, // Force textContent insertion
175+
unusedStylesMinAgeMs: 10000, // Minimum age before cleanup (ms)
176+
forceTextInjection: false, // Force textContent insertion (auto-detected for tests)
137177
nonce: 'csp-nonce', // CSP nonce for security
138178
});
139179
```
140180

181+
**Auto-Detection Features:**
182+
- `devMode`: Automatically enabled in development environments (detected via `isDevEnv()`)
183+
- `forceTextInjection`: Automatically enabled in test environments (Jest, Vitest, Mocha, jsdom)
184+
185+
**Configuration Notes:**
186+
- Most options have sensible defaults and auto-detection
187+
- `configure()` is optional - the injector works with defaults
188+
- `unusedStylesMinAgeMs`: Minimum time (ms) a style must remain unused before being eligible for cleanup. Helps prevent removal of styles that might be quickly reactivated.
189+
141190
---
142191

143192
## 🔧 Advanced Features
@@ -240,7 +289,9 @@ const shadowAnimation = keyframes({
240289
### Server-Side Rendering
241290

242291
```typescript
243-
// Extract CSS for SSR
292+
import { getCssText, getCssTextForNode } from '@cube-dev/ui-kit/tasty/injector';
293+
294+
// Extract all CSS for SSR
244295
const cssText = getCssText();
245296

246297
// Extract CSS for specific DOM subtree (like jest-styled-components)
@@ -253,10 +304,12 @@ const componentCSS = getCssTextForNode(container);
253304
```typescript
254305
// Automatically detected test environments:
255306
// - NODE_ENV === 'test'
256-
// - Jest globals (describe, it, expect)
307+
// - Jest globals (jest, describe, it, expect)
257308
// - jsdom user agent
258-
// - Vitest, Mocha globals
309+
// - Vitest globals (vitest)
310+
// - Mocha globals (mocha)
259311

312+
import { getIsTestEnvironment } from '@cube-dev/ui-kit/tasty/injector';
260313
const isTest = getIsTestEnvironment();
261314

262315
// Test-specific optimizations:
@@ -297,10 +350,13 @@ const metrics = injector.instance.getMetrics();
297350
console.log({
298351
cacheHits: metrics.hits, // Successful cache hits
299352
cacheMisses: metrics.misses, // New styles injected
300-
unusedHits: metrics.unusedHits, // Current unused styles (calculated)
301-
bulkCleanups: metrics.bulkCleanups, // CSS cleanup operations (keyframes disposed immediately)
302-
stylesCleanedUp: metrics.stylesCleanedUp, // Total styles removed
353+
unusedHits: metrics.unusedHits, // Current unused styles (calculated on demand)
354+
bulkCleanups: metrics.bulkCleanups, // Number of bulk cleanup operations
355+
stylesCleanedUp: metrics.stylesCleanedUp, // Total styles removed in bulk cleanups
303356
totalInsertions: metrics.totalInsertions, // Lifetime insertions
357+
totalUnused: metrics.totalUnused, // Total styles marked as unused (refCount = 0)
358+
startTime: metrics.startTime, // Metrics collection start timestamp
359+
cleanupHistory: metrics.cleanupHistory, // Detailed cleanup operation history
304360
});
305361
```
306362

@@ -329,7 +385,7 @@ metrics.cleanupHistory.forEach(cleanup => {
329385
timestamp: new Date(cleanup.timestamp),
330386
classesDeleted: cleanup.classesDeleted,
331387
rulesDeleted: cleanup.rulesDeleted,
332-
cssSize: cleanup.cssSize,
388+
cssSize: cleanup.cssSize, // Total CSS size removed (bytes)
333389
});
334390
});
335391
```
@@ -356,8 +412,9 @@ injectGlobal([
356412

357413
// ✅ Configure appropriate thresholds for your app
358414
configure({
359-
unusedStylesThreshold: 200, // Adjust based on component count
360-
bulkCleanupBatchRatio: 0.4, // Balance cleanup frequency vs performance
415+
unusedStylesThreshold: 500, // Default threshold (adjust based on app size)
416+
bulkCleanupBatchRatio: 0.5, // Default: clean oldest 50% per batch
417+
unusedStylesMinAgeMs: 10000, // Wait 10s before cleanup eligibility
361418
});
362419
```
363420

src/tasty/debug.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Debug utilities for inspecting tasty-generated CSS at runtime
33
*/
44

5-
import { getCssText, getCssTextForNode, injector } from './injector';
5+
import { getCssTextForNode, injector } from './injector';
66
import { isDevEnv } from './utils/isDevEnv';
77

88
// Type definitions for the new API

src/tasty/injector/injector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export class StyleInjector {
253253

254254
return {
255255
dispose: () => {
256-
if (info) this.sheetManager.deleteRule(registry, info);
256+
if (info) this.sheetManager.deleteGlobalRule(registry, key);
257257
},
258258
};
259259
}

src/tasty/injector/sheet-manager.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class SheetManager {
5656
keyframesCache: new Map(),
5757
keyframesCounter: 0,
5858
injectedProperties: new Set<string>(),
59+
globalRules: new Map(),
5960
} as unknown as RootRegistry;
6061

6162
this.rootRegistries.set(root, registry);
@@ -357,11 +358,34 @@ export class SheetManager {
357358
insertGlobalRule(
358359
registry: RootRegistry,
359360
flattenedRules: StyleRule[],
360-
className: string,
361+
globalKey: string,
361362
root: Document | ShadowRoot,
362363
): RuleInfo | null {
363-
// For now, global rules are handled the same way as regular rules
364-
return this.insertRule(registry, flattenedRules, className, root);
364+
// Insert the rule using the same mechanism as regular rules
365+
const ruleInfo = this.insertRule(registry, flattenedRules, globalKey, root);
366+
367+
// Track global rules for index adjustment
368+
if (ruleInfo) {
369+
registry.globalRules.set(globalKey, ruleInfo);
370+
}
371+
372+
return ruleInfo;
373+
}
374+
375+
/**
376+
* Delete a global CSS rule by key
377+
*/
378+
public deleteGlobalRule(registry: RootRegistry, globalKey: string): void {
379+
const ruleInfo = registry.globalRules.get(globalKey);
380+
if (!ruleInfo) {
381+
return;
382+
}
383+
384+
// Delete the rule using the standard deletion mechanism
385+
this.deleteRule(registry, ruleInfo);
386+
387+
// Remove from global rules tracking
388+
registry.globalRules.delete(globalKey);
365389
}
366390

367391
/**
@@ -432,6 +456,11 @@ export class SheetManager {
432456
adjustRuleInfo(info);
433457
}
434458

459+
// Adjust global rules
460+
for (const info of registry.globalRules.values()) {
461+
adjustRuleInfo(info);
462+
}
463+
435464
// No need to separately adjust unused rules since they're part of the rules Map
436465

437466
// Adjust keyframes indices stored in cache

src/tasty/injector/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export interface CacheMetrics {
6767
startTime: number;
6868

6969
// Calculated getters
70-
unusedHits?: number; // calculated as hits from reused unused styles (not tracked)
70+
unusedHits?: number; // calculated as current unused styles count (refCount = 0)
7171
}
7272

7373
export interface RootRegistry {
@@ -95,6 +95,8 @@ export interface RootRegistry {
9595
keyframesCounter: number;
9696
/** Set of injected @property names for tracking */
9797
injectedProperties: Set<string>;
98+
/** Global rules tracking for index adjustment */
99+
globalRules: Map<string, RuleInfo>; // globalKey -> rule info
98100
}
99101

100102
// StyleRule is now just an alias for StyleResult from renderStyles

0 commit comments

Comments
 (0)