diff --git a/.changeset/tokens-resolve-alias-followthrough-566.md b/.changeset/tokens-resolve-alias-followthrough-566.md new file mode 100644 index 000000000..1d33b8d2a --- /dev/null +++ b/.changeset/tokens-resolve-alias-followthrough-566.md @@ -0,0 +1,7 @@ +--- +"@vuetify/v0": patch +--- + +fix(createTokens): resolve aliases reached through a segment path, return directly-passed TokenAlias literals, and cache chained resolutions (#566) + +`resolve()` now follows a `{alias}` that a dotted-segment lookup lands on — previously it returned the raw `'{alias}'` string (visible under `flat: true`, where nested objects are stored whole and addressed by segment). A `TokenAlias` object passed directly to `resolve()` now returns its `$value` (previously a non-alias `$value` was stringified and looked up as an id, yielding `undefined`), and aliased resolutions cache the outer key rather than only the terminal hop. `resolve()` also accepts an optional return-type parameter. diff --git a/packages/0/src/composables/createTokens/index.test.ts b/packages/0/src/composables/createTokens/index.test.ts index 10afafc23..a74634db0 100644 --- a/packages/0/src/composables/createTokens/index.test.ts +++ b/packages/0/src/composables/createTokens/index.test.ts @@ -2217,25 +2217,24 @@ describe('createTokens', () => { expect(result).toBe('#007BFF') }) - it('should handle TokenAlias with non-string $value (converts to string for lookup)', () => { + it('should return a non-string $value literal directly', () => { const aliasObj: TokenAlias = { $value: 42 } const context = createTokens({}) - // When passing TokenAlias with non-string $value, it converts to string "42" - // and tries to look it up in registry, which returns undefined + // A directly-passed TokenAlias carries its own value; a non-alias $value + // is the resolved value, not a registry id to look up. const result = context.resolve(aliasObj) - expect(result).toBeUndefined() + expect(result).toBe(42) }) - it('should handle TokenAlias with object $value (converts to string for lookup)', () => { + it('should return an object $value directly', () => { const aliasObj: TokenAlias = { $value: { a: 1, b: 2 } } const context = createTokens({}) - // Object values get stringified, lookup fails, returns undefined const result = context.resolve(aliasObj) - expect(result).toBeUndefined() + expect(result).toEqual({ a: 1, b: 2 }) }) it('should handle TokenAlias with undefined $value', () => { diff --git a/packages/0/src/composables/createTokens/index.ts b/packages/0/src/composables/createTokens/index.ts index ce69500d0..7162a4499 100644 --- a/packages/0/src/composables/createTokens/index.ts +++ b/packages/0/src/composables/createTokens/index.ts @@ -110,7 +110,7 @@ export interface TokenContext extends RegistryContext * console.log(tokens.resolve('{colors.secondary}')) // '#3b82f6' * ``` */ - resolve: (token: string | TokenAlias) => unknown | undefined + resolve: (token: string | TokenAlias) => T | undefined } export interface TokenOptions extends RegistryOptions { @@ -188,6 +188,14 @@ export function createTokens< const reference: unknown = isTokenAlias(token) ? token.$value : token const isAliasReference = isString(reference) && isAlias(reference) + + // A TokenAlias passed directly carries its own value: only a brace-alias + // $value needs further resolution — a literal or object $value is the value. + if (isTokenAlias(token) && !isAliasReference) { + cache.set(cacheKey, reference) + return reference + } + const clean = isAliasReference ? reference.slice(1, -1) : String(reference) // Detect circular references @@ -225,7 +233,6 @@ export function createTokens< return undefined } - let result: unknown | undefined let current: unknown = found.value if (segments.length > 0) { @@ -247,20 +254,16 @@ export function createTokens< cache.set(cacheKey, undefined) return undefined } - - result = current - } else { - if (isTokenAlias(current)) { - const inner = current.$value - if (isString(inner) && isAlias(inner)) return resolve(inner as string, visited) - result = inner - } else if (isString(current) && isAlias(current)) { - return resolve(current, visited) - } else { - result = current - } + } else if (isTokenAlias(current)) { + current = current.$value } + // A terminal string alias resolves one more hop, whether it came from a + // segment walk or a leaf value; anything else is the resolved result. + const result: unknown = isString(current) && isAlias(current) + ? resolve(current, visited) + : current + cache.set(cacheKey, result) return result