Skip to content

Commit 0e5864e

Browse files
committed
deduplication added for components based reducing tokens by 45% almost
1 parent 990dfc6 commit 0e5864e

7 files changed

Lines changed: 923 additions & 33 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ node_modules/
22
.env
33
package-lock.json
44
dist/
5-
output/
5+
output/
6+
reports/
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// src/extractors/components/deduplicated-extractor.mts
2+
3+
import type { FigmaNode } from '../../types/figma.mjs';
4+
import type { FlutterStyleDefinition } from '../flutter/style-library.mjs';
5+
import { FlutterStyleLibrary } from '../flutter/style-library.mjs';
6+
import {
7+
extractStylingInfo,
8+
extractLayoutInfo,
9+
extractMetadata,
10+
extractTextInfo,
11+
createNestedComponentInfo
12+
} from './extractor.mjs';
13+
import type {
14+
ComponentMetadata,
15+
LayoutInfo,
16+
StylingInfo,
17+
NestedComponentInfo,
18+
TextInfo
19+
} from './types.mjs';
20+
21+
export interface DeduplicatedComponentAnalysis {
22+
metadata: ComponentMetadata;
23+
styleRefs: Record<string, string>;
24+
children: DeduplicatedComponentChild[];
25+
nestedComponents: NestedComponentInfo[];
26+
newStyleDefinitions?: Record<string, FlutterStyleDefinition>;
27+
}
28+
29+
export interface DeduplicatedComponentChild {
30+
nodeId: string;
31+
name: string;
32+
type: string;
33+
styleRefs: string[];
34+
semanticType?: string;
35+
textContent?: string;
36+
}
37+
38+
export class DeduplicatedComponentExtractor {
39+
private styleLibrary = FlutterStyleLibrary.getInstance();
40+
41+
async analyzeComponent(node: FigmaNode, trackNewStyles = false): Promise<DeduplicatedComponentAnalysis> {
42+
const styling = extractStylingInfo(node);
43+
const layout = extractLayoutInfo(node);
44+
const metadata = extractMetadata(node, false); // assuming not user-defined unless specified
45+
46+
const styleRefs: Record<string, string> = {};
47+
const newStyles = new Set<string>();
48+
49+
// Process decoration styles
50+
if (this.hasDecorationProperties(styling)) {
51+
const beforeCount = this.styleLibrary.getAllStyles().length;
52+
styleRefs.decoration = this.styleLibrary.addStyle('decoration', {
53+
fills: styling.fills,
54+
cornerRadius: styling.cornerRadius,
55+
effects: styling.effects
56+
});
57+
if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
58+
newStyles.add(styleRefs.decoration);
59+
}
60+
}
61+
62+
// Process padding styles
63+
if (layout.padding) {
64+
const beforeCount = this.styleLibrary.getAllStyles().length;
65+
styleRefs.padding = this.styleLibrary.addStyle('padding', { padding: layout.padding });
66+
if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
67+
newStyles.add(styleRefs.padding);
68+
}
69+
}
70+
71+
// Process children with deduplication
72+
const children = await this.analyzeChildren(node);
73+
const nestedComponents = this.extractNestedComponents(node);
74+
75+
const result: DeduplicatedComponentAnalysis = {
76+
metadata,
77+
styleRefs,
78+
children,
79+
nestedComponents
80+
};
81+
82+
if (trackNewStyles && newStyles.size > 0) {
83+
result.newStyleDefinitions = this.getStyleDefinitions(Array.from(newStyles));
84+
}
85+
86+
return result;
87+
}
88+
89+
private async analyzeChildren(node: FigmaNode): Promise<DeduplicatedComponentChild[]> {
90+
if (!node.children) return [];
91+
92+
const children: DeduplicatedComponentChild[] = [];
93+
94+
for (const child of node.children) {
95+
if (!child.visible) continue;
96+
97+
const childStyleRefs: string[] = [];
98+
99+
// Extract child styling
100+
const childStyling = extractStylingInfo(child);
101+
if (this.hasDecorationProperties(childStyling)) {
102+
const decorationRef = this.styleLibrary.addStyle('decoration', {
103+
fills: childStyling.fills,
104+
cornerRadius: childStyling.cornerRadius,
105+
effects: childStyling.effects
106+
});
107+
childStyleRefs.push(decorationRef);
108+
}
109+
110+
// Extract text styling for text nodes
111+
let textContent: string | undefined;
112+
if (child.type === 'TEXT') {
113+
const textInfo = extractTextInfo(child);
114+
if (textInfo) {
115+
textContent = textInfo.content;
116+
117+
// Add text style to library
118+
if (child.style) {
119+
const textStyleRef = this.styleLibrary.addStyle('text', {
120+
fontFamily: child.style.fontFamily,
121+
fontSize: child.style.fontSize,
122+
fontWeight: child.style.fontWeight
123+
});
124+
childStyleRefs.push(textStyleRef);
125+
}
126+
}
127+
}
128+
129+
children.push({
130+
nodeId: child.id,
131+
name: child.name,
132+
type: child.type,
133+
styleRefs: childStyleRefs,
134+
semanticType: this.detectSemanticType(child),
135+
textContent
136+
});
137+
}
138+
139+
return children;
140+
}
141+
142+
private hasDecorationProperties(styling: StylingInfo): boolean {
143+
return !!(styling.fills?.length || styling.cornerRadius !== undefined || styling.effects?.dropShadows?.length);
144+
}
145+
146+
private extractTextContent(node: any): string {
147+
return node.characters || node.name || '';
148+
}
149+
150+
private detectSemanticType(node: any): string | undefined {
151+
// Simplified semantic detection
152+
if (node.type === 'TEXT') {
153+
const content = this.extractTextContent(node).toLowerCase();
154+
if (['click', 'submit', 'save', 'cancel'].some(word => content.includes(word))) {
155+
return 'button';
156+
}
157+
return 'text';
158+
}
159+
return undefined;
160+
}
161+
162+
private extractNestedComponents(node: FigmaNode): NestedComponentInfo[] {
163+
if (!node.children) return [];
164+
165+
const nestedComponents: NestedComponentInfo[] = [];
166+
167+
for (const child of node.children) {
168+
if (child.type === 'COMPONENT' || child.type === 'INSTANCE' || child.type === 'COMPONENT_SET') {
169+
nestedComponents.push(createNestedComponentInfo(child));
170+
}
171+
}
172+
173+
return nestedComponents;
174+
}
175+
176+
private getStyleDefinitions(styleIds: string[]): Record<string, FlutterStyleDefinition> {
177+
const definitions: Record<string, FlutterStyleDefinition> = {};
178+
styleIds.forEach(id => {
179+
const definition = this.styleLibrary.getStyle(id);
180+
if (definition) {
181+
definitions[id] = definition;
182+
}
183+
});
184+
return definitions;
185+
}
186+
}

src/extractors/components/index.mts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export {
3131

3232
export {VariantAnalyzer} from './variant-analyzer.mjs';
3333

34+
// Deduplicated extractor
35+
export {
36+
DeduplicatedComponentExtractor,
37+
type DeduplicatedComponentAnalysis,
38+
type DeduplicatedComponentChild
39+
} from './deduplicated-extractor.mjs';
40+
3441
// Types
3542
export type {
3643
ComponentAnalysis,

src/extractors/flutter/index.mts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// src/extractors/flutter/index.mts
2+
3+
export {
4+
FlutterStyleLibrary,
5+
FlutterCodeGenerator,
6+
type FlutterStyleDefinition
7+
} from './style-library.mjs';
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// src/extractors/flutter/style-library.mts
2+
3+
export interface FlutterStyleDefinition {
4+
id: string;
5+
category: 'decoration' | 'text' | 'layout' | 'padding';
6+
properties: Record<string, any>;
7+
flutterCode: string;
8+
hash: string;
9+
usageCount: number;
10+
}
11+
12+
export class FlutterStyleLibrary {
13+
private static instance: FlutterStyleLibrary;
14+
private styles = new Map<string, FlutterStyleDefinition>();
15+
private hashToId = new Map<string, string>();
16+
17+
static getInstance(): FlutterStyleLibrary {
18+
if (!this.instance) {
19+
this.instance = new FlutterStyleLibrary();
20+
}
21+
return this.instance;
22+
}
23+
24+
addStyle(category: string, properties: any): string {
25+
const hash = this.generateHash(properties);
26+
27+
if (this.hashToId.has(hash)) {
28+
const existingId = this.hashToId.get(hash)!;
29+
const style = this.styles.get(existingId)!;
30+
style.usageCount++;
31+
return existingId;
32+
}
33+
34+
const generatedId = this.generateId();
35+
const styleId = `${category}${generatedId.charAt(0).toUpperCase()}${generatedId.slice(1)}`;
36+
const definition: FlutterStyleDefinition = {
37+
id: styleId,
38+
category: category as any,
39+
properties,
40+
flutterCode: this.generateFlutterCode(category, properties),
41+
hash,
42+
usageCount: 1
43+
};
44+
45+
this.styles.set(styleId, definition);
46+
this.hashToId.set(hash, styleId);
47+
return styleId;
48+
}
49+
50+
getStyle(id: string): FlutterStyleDefinition | undefined {
51+
return this.styles.get(id);
52+
}
53+
54+
getAllStyles(): FlutterStyleDefinition[] {
55+
return Array.from(this.styles.values());
56+
}
57+
58+
reset(): void {
59+
this.styles.clear();
60+
this.hashToId.clear();
61+
}
62+
63+
private generateHash(properties: any): string {
64+
return JSON.stringify(properties, Object.keys(properties).sort());
65+
}
66+
67+
private generateId(): string {
68+
return Date.now().toString(36) + Math.random().toString(36).substr(2, 4);
69+
}
70+
71+
private generateFlutterCode(category: string, properties: any): string {
72+
switch (category) {
73+
case 'decoration':
74+
return FlutterCodeGenerator.generateDecoration(properties);
75+
case 'text':
76+
return FlutterCodeGenerator.generateTextStyle(properties);
77+
case 'padding':
78+
return FlutterCodeGenerator.generatePadding(properties);
79+
case 'layout':
80+
// Layout code generation can be added later
81+
return `// ${category} implementation`;
82+
default:
83+
return `// ${category} implementation`;
84+
}
85+
}
86+
}
87+
88+
export class FlutterCodeGenerator {
89+
static generateDecoration(properties: any): string {
90+
let code = 'BoxDecoration(\n';
91+
92+
if (properties.fills?.length > 0) {
93+
const fill = properties.fills[0];
94+
if (fill.hex) {
95+
code += ` color: Color(0xFF${fill.hex.substring(1)}),\n`;
96+
}
97+
}
98+
99+
if (properties.cornerRadius !== undefined) {
100+
if (typeof properties.cornerRadius === 'number') {
101+
code += ` borderRadius: BorderRadius.circular(${properties.cornerRadius}),\n`;
102+
} else {
103+
const r = properties.cornerRadius;
104+
code += ` borderRadius: BorderRadius.only(\n`;
105+
code += ` topLeft: Radius.circular(${r.topLeft}),\n`;
106+
code += ` topRight: Radius.circular(${r.topRight}),\n`;
107+
code += ` bottomLeft: Radius.circular(${r.bottomLeft}),\n`;
108+
code += ` bottomRight: Radius.circular(${r.bottomRight}),\n`;
109+
code += ` ),\n`;
110+
}
111+
}
112+
113+
if (properties.effects?.dropShadows?.length > 0) {
114+
code += ` boxShadow: [\n`;
115+
properties.effects.dropShadows.forEach((shadow: any) => {
116+
code += ` BoxShadow(\n`;
117+
code += ` color: Color(0xFF${shadow.hex.substring(1)}).withOpacity(${shadow.opacity}),\n`;
118+
code += ` offset: Offset(${shadow.offset.x}, ${shadow.offset.y}),\n`;
119+
code += ` blurRadius: ${shadow.radius},\n`;
120+
if (shadow.spread) {
121+
code += ` spreadRadius: ${shadow.spread},\n`;
122+
}
123+
code += ` ),\n`;
124+
});
125+
code += ` ],\n`;
126+
}
127+
128+
code += ')';
129+
return code;
130+
}
131+
132+
static generatePadding(properties: any): string {
133+
const p = properties.padding;
134+
if (!p) return 'EdgeInsets.zero';
135+
136+
if (p.isUniform) {
137+
return `EdgeInsets.all(${p.top})`;
138+
}
139+
return `EdgeInsets.fromLTRB(${p.left}, ${p.top}, ${p.right}, ${p.bottom})`;
140+
}
141+
142+
static generateTextStyle(properties: any): string {
143+
const parts: string[] = [];
144+
145+
if (properties.fontFamily) {
146+
parts.push(`fontFamily: '${properties.fontFamily}'`);
147+
}
148+
if (properties.fontSize) {
149+
parts.push(`fontSize: ${properties.fontSize}`);
150+
}
151+
if (properties.fontWeight && properties.fontWeight !== 400) {
152+
const weight = properties.fontWeight >= 700 ? 'FontWeight.bold' :
153+
properties.fontWeight >= 600 ? 'FontWeight.w600' :
154+
properties.fontWeight >= 500 ? 'FontWeight.w500' :
155+
'FontWeight.normal';
156+
parts.push(`fontWeight: ${weight}`);
157+
}
158+
159+
return `TextStyle(${parts.join(', ')})`;
160+
}
161+
}

0 commit comments

Comments
 (0)