Skip to content

Commit 4a185b4

Browse files
committed
Feat: theme - Support alias method #270
1 parent 50145b3 commit 4a185b4

File tree

2 files changed

+136
-24
lines changed

2 files changed

+136
-24
lines changed

.changeset/honest-dingos-push.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@mincho-js/css": minor
3+
---
4+
5+
**theme**
6+
- Add `theme()` `this.fallbackVar()` API
7+

packages/css/src/theme/index.ts

Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type WithOptionalLayer<T extends Theme> = T & {
3636
interface ThemeSubFunctions {
3737
fallbackVar: typeof fallbackVar;
3838
raw(varReference: unknown): string;
39+
alias(varReference: unknown): string;
3940
}
4041

4142
type ResolveThemeOutput<T extends Theme> = Resolve<ResolveTheme<T>>;
@@ -125,7 +126,18 @@ function assignTokensWithPrefix<ThemeTokens extends Theme>(
125126
const vars: AssignedVars = {};
126127
const resolvedTokens = {} as ResolveTheme<ThemeTokens>;
127128

128-
// Add fallbackVar utility to the context for use in getters
129+
// Execute two-pass token resolution:
130+
// 1. Assign CSS variables to all tokens
131+
// 2. Resolve semantic token references
132+
const context: TokenProcessingContext = {
133+
prefix,
134+
path: [],
135+
parentPath: prefix,
136+
cssVarMap: {},
137+
aliasMap: new Set()
138+
};
139+
140+
// Add utilities to the context for use in getters
129141
Object.defineProperty(resolvedTokens, "fallbackVar", {
130142
value: fallbackVar,
131143
enumerable: false,
@@ -141,21 +153,28 @@ function assignTokensWithPrefix<ThemeTokens extends Theme>(
141153
writable: false
142154
});
143155

144-
// Execute two-pass token resolution:
145-
// 1. Assign CSS variables to all tokens
146-
// 2. Resolve semantic token references
147-
const context: TokenProcessingContext = {
148-
prefix,
149-
path: [],
150-
parentPath: prefix,
151-
cssVarMap: {}
152-
};
156+
// Add alias utility to create object references instead of CSS variables
157+
Object.defineProperty(resolvedTokens, "alias", {
158+
value: createAliasFunction(context),
159+
enumerable: false,
160+
configurable: false,
161+
writable: false
162+
});
163+
153164
assignTokenVariables(tokens, vars, resolvedTokens, context);
154165
resolveSemanticTokens(tokens, vars, resolvedTokens, context);
155166

156167
return { vars, resolvedTokens };
157168
}
158169

170+
/**
171+
* Removes hash suffixes from CSS variable names and var() references
172+
*/
173+
function stripHash(str: string): string {
174+
// Remove hash suffix from CSS variable names and var() references
175+
return str.replace(/__[a-zA-Z0-9]+/g, "");
176+
}
177+
159178
// == Two-Pass Token Resolution ===============================================
160179
/**
161180
* Context object for token processing functions
@@ -165,6 +184,7 @@ interface TokenProcessingContext {
165184
path: string[]; // Current path in object tree
166185
parentPath: string; // Current variable path
167186
cssVarMap: CSSVarMap; // Cache for CSS variable names
187+
aliasMap: Set<string>; // Track paths that should be aliases (not create CSS vars)
168188
}
169189

170190
/**
@@ -218,7 +238,8 @@ function assignTokenVariables(
218238
prefix: context.prefix,
219239
path: currentPath,
220240
parentPath: varPath,
221-
cssVarMap: context.cssVarMap
241+
cssVarMap: context.cssVarMap,
242+
aliasMap: context.aliasMap
222243
}
223244
);
224245
} else {
@@ -280,7 +301,8 @@ function assignTokenVariables(
280301
prefix: context.prefix,
281302
path: currentPath,
282303
parentPath: varPath,
283-
cssVarMap: context.cssVarMap
304+
cssVarMap: context.cssVarMap,
305+
aliasMap: context.aliasMap
284306
});
285307
continue;
286308
}
@@ -313,17 +335,28 @@ function resolveSemanticTokens(
313335

314336
// Handle getters (semantic tokens)
315337
if (typeof descriptor.get === "function") {
316-
const cssVar = getCSSVarByPath(varPath, context);
317-
338+
// Set the current path for alias() to access
339+
currentProcessingPath = currentPath;
318340
// Call getter with resolvedTokens as this context
319341
// This allows semantic tokens to reference other tokens
320342
const computedValue = descriptor.get.call(resolvedTokens);
343+
// Clear the current path after getter execution
344+
currentProcessingPath = [];
321345

322-
// Store the computed reference (should be a var() reference)
323-
vars[cssVar] = computedValue;
346+
// Check if this path is marked as an alias
347+
const currentPathStr = currentPath.join(".");
324348

325-
// Update resolvedTokens with the actual reference
326-
setByPath(resolvedTokens, currentPath, computedValue);
349+
if (context.aliasMap.has(currentPathStr)) {
350+
// Don't create a CSS variable for aliases
351+
// Just update resolvedTokens with the aliased reference (unhashed)
352+
const unhashedValue = stripHash(computedValue);
353+
setByPath(resolvedTokens, currentPath, unhashedValue);
354+
} else {
355+
// Normal flow: create CSS variable
356+
const cssVar = getCSSVarByPath(varPath, context);
357+
vars[cssVar] = computedValue;
358+
setByPath(resolvedTokens, currentPath, computedValue);
359+
}
327360
continue;
328361
}
329362

@@ -343,7 +376,8 @@ function resolveSemanticTokens(
343376
prefix: context.prefix,
344377
path: currentPath,
345378
parentPath: varPath,
346-
cssVarMap: context.cssVarMap
379+
cssVarMap: context.cssVarMap,
380+
aliasMap: context.aliasMap
347381
}
348382
);
349383
continue;
@@ -355,7 +389,8 @@ function resolveSemanticTokens(
355389
prefix: context.prefix,
356390
path: currentPath,
357391
parentPath: varPath,
358-
cssVarMap: context.cssVarMap
392+
cssVarMap: context.cssVarMap,
393+
aliasMap: context.aliasMap
359394
});
360395
}
361396
}
@@ -503,6 +538,24 @@ function createRawExtractor(vars: AssignedVars) {
503538
};
504539
}
505540

541+
/**
542+
* Creates a function that marks tokens as aliases.
543+
* Aliased tokens don't create their own CSS variables but reference existing ones.
544+
*/
545+
// Store the current processing path for alias() to access
546+
let currentProcessingPath: string[] = [];
547+
548+
function createAliasFunction(context: TokenProcessingContext) {
549+
return function alias(varReference: unknown): string {
550+
// Mark the current processing path as an alias
551+
const currentPathStr = currentProcessingPath.join(".");
552+
553+
context.aliasMap.add(currentPathStr);
554+
// Return the var reference unchanged
555+
return String(varReference);
556+
};
557+
}
558+
506559
// == Token Value Extractors ==================================================
507560
function extractFontFamilyValue(value: TokenFontFamilyValue): CSSVarValue {
508561
if (Array.isArray(value)) {
@@ -652,10 +705,6 @@ if (import.meta.vitest) {
652705
}
653706

654707
// Test utility functions for handling hashed CSS variables
655-
function stripHash(str: string): string {
656-
// Remove hash suffix from CSS variable names and var() references
657-
return str.replace(/__[a-zA-Z0-9]+/g, "");
658-
}
659708

660709
function normalizeVars(vars: AssignedVars): AssignedVars {
661710
const normalized: AssignedVars = {};
@@ -894,6 +943,62 @@ if (import.meta.vitest) {
894943
);
895944
});
896945

946+
it("handles alias references in semantic tokens", () => {
947+
const result = assignTokens(
948+
composedValue({
949+
color: {
950+
base: {
951+
blue: "#0000ff",
952+
red: "#ff0000"
953+
},
954+
semantic: {
955+
get primary(): string {
956+
// Use alias to reference without creating CSS var
957+
return this.alias(this.color.base.blue);
958+
},
959+
get danger(): string {
960+
// Another alias
961+
return this.alias(this.color.base.red);
962+
},
963+
get secondary(): string {
964+
// Normal reference (creates CSS var)
965+
return this.color.base.blue;
966+
}
967+
}
968+
}
969+
})
970+
);
971+
972+
validateHashFormat(result.vars);
973+
974+
const normalizedVars = normalizeVars(result.vars);
975+
// Base tokens should have CSS variables
976+
expect(normalizedVars["--color-base-blue"]).toBe("#0000ff");
977+
expect(normalizedVars["--color-base-red"]).toBe("#ff0000");
978+
979+
// Aliased semantic tokens should NOT have CSS variables
980+
981+
expect(normalizedVars["--color-semantic-primary"]).toBeUndefined();
982+
expect(normalizedVars["--color-semantic-danger"]).toBeUndefined();
983+
984+
// Non-aliased semantic token should have CSS variable
985+
expect(stripHash(normalizedVars["--color-semantic-secondary"])).toBe(
986+
"var(--color-base-blue)"
987+
);
988+
989+
// Resolved tokens should all use var() references
990+
const normalizedTokens = normalizeResolvedTokens(result.resolvedTokens);
991+
expect(normalizedTokens.color.semantic.primary).toBe(
992+
"var(--color-base-blue)"
993+
);
994+
expect(normalizedTokens.color.semantic.danger).toBe(
995+
"var(--color-base-red)"
996+
);
997+
expect(normalizedTokens.color.semantic.secondary).toBe(
998+
"var(--color-base-blue)"
999+
);
1000+
});
1001+
8971002
it("handles raw value extraction in semantic tokens", () => {
8981003
const result = assignTokens(
8991004
composedValue({

0 commit comments

Comments
 (0)