Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/tokens-resolve-alias-followthrough-566.md
Original file line number Diff line number Diff line change
@@ -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<T = unknown>()` also accepts an optional return-type parameter.
13 changes: 6 additions & 7 deletions packages/0/src/composables/createTokens/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
31 changes: 17 additions & 14 deletions packages/0/src/composables/createTokens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export interface TokenContext<Z extends TokenTicket> extends RegistryContext<Z>
* console.log(tokens.resolve('{colors.secondary}')) // '#3b82f6'
* ```
*/
resolve: (token: string | TokenAlias) => unknown | undefined
resolve: <T = unknown>(token: string | TokenAlias) => T | undefined
}

export interface TokenOptions extends RegistryOptions {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -225,7 +233,6 @@ export function createTokens<
return undefined
}

let result: unknown | undefined
let current: unknown = found.value

if (segments.length > 0) {
Expand All @@ -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
Expand Down
Loading