1- import type { AssetDescriptor , GetCodeResult , GetTokenDefsResult } from '@tempad-dev/mcp-shared'
1+ import type {
2+ AssetDescriptor ,
3+ GetCodeResult ,
4+ GetTokenDefsResult ,
5+ TokenEntry
6+ } from '@tempad-dev/mcp-shared'
27
38import { MCP_MAX_PAYLOAD_BYTES } from '@tempad-dev/mcp-shared'
49
5- import type { CodegenConfig } from '@/utils/codegen'
6-
710import { buildSemanticTree } from '@/mcp/semantic-tree'
8- import { activePlugin , options } from '@/ui/state'
11+ import { activePlugin } from '@/ui/state'
912import { stringifyComponent } from '@/utils/component'
13+ import {
14+ extractVarNames ,
15+ normalizeCssVarName ,
16+ replaceVarFunctions ,
17+ stripFallback ,
18+ toVarExpr
19+ } from '@/utils/css'
1020
1121import type { RenderContext , CodeLanguage } from './render'
1222
13- import { collectTokenReferences , resolveVariableTokens } from '../token-defs'
23+ import { currentCodegenConfig } from '../config'
24+ import { collectCandidateVariableIds , resolveTokenDefsByNames } from '../token'
1425import { collectSceneData } from './collect'
1526import { buildGetCodeMessage } from './messages'
1627import { renderSemanticNode } from './render'
@@ -52,17 +63,7 @@ const COMPACT_TAGS = new Set([
5263 'summary'
5364] )
5465
55- function filterTokensByNames ( input : GetTokenDefsResult , names : Set < string > ) : GetTokenDefsResult {
56- const tokens : Record < string , GetTokenDefsResult [ 'tokens' ] [ string ] > = { }
57- Object . entries ( input . tokens ) . forEach ( ( [ name , entry ] ) => {
58- if ( names . has ( name ) ) {
59- tokens [ name ] = entry
60- }
61- } )
62- return { tokens }
63- }
64-
65- function primaryTokenValue ( entry : GetTokenDefsResult [ 'tokens' ] [ string ] ) : string | undefined {
66+ function primaryTokenValue ( entry : TokenEntry ) : string | undefined {
6667 if ( typeof entry . value === 'string' ) return entry . value
6768 if ( typeof entry . resolvedValue === 'string' ) return entry . resolvedValue
6869 if ( entry . activeMode && typeof entry . value [ entry . activeMode ] === 'string' ) {
@@ -72,6 +73,34 @@ function primaryTokenValue(entry: GetTokenDefsResult['tokens'][string]): string
7273 return typeof first === 'string' ? first : undefined
7374}
7475
76+ function resolveConcreteValue (
77+ name : string ,
78+ allTokens : GetTokenDefsResult ,
79+ cache : Map < string , string | undefined > ,
80+ depth = 0
81+ ) : string | undefined {
82+ if ( cache . has ( name ) ) return cache . get ( name )
83+ if ( depth > 20 ) return undefined
84+
85+ const entry = allTokens [ name ]
86+ if ( ! entry ) {
87+ cache . set ( name , undefined )
88+ return undefined
89+ }
90+ const val = primaryTokenValue ( entry )
91+ if ( typeof val !== 'string' ) {
92+ cache . set ( name , undefined )
93+ return undefined
94+ }
95+ if ( val . startsWith ( '--' ) && val !== name ) {
96+ const resolved = resolveConcreteValue ( val , allTokens , cache , depth + 1 ) ?? val
97+ cache . set ( name , resolved )
98+ return resolved
99+ }
100+ cache . set ( name , val )
101+ return val
102+ }
103+
75104export async function handleGetCode (
76105 nodes : SceneNode [ ] ,
77106 preferredLang ?: CodeLanguage ,
@@ -92,13 +121,13 @@ export async function handleGetCode(
92121 throw new Error ( 'No renderable nodes found for the current selection.' )
93122 }
94123
95- const config = codegenConfig ( )
124+ const config = currentCodegenConfig ( )
96125 const pluginCode = activePlugin . value ?. code
97126
98127 const assetRegistry = new Map < string , AssetDescriptor > ( )
99128 const { nodes : nodeMap , styles, svgs } = await collectSceneData ( tree . roots , config , assetRegistry )
100129
101- await applyVariableTransforms ( styles , {
130+ const styleVarNames = await applyVariableTransforms ( styles , {
102131 pluginCode,
103132 config
104133 } )
@@ -131,33 +160,48 @@ export async function handleGetCode(
131160 const MAX_CODE_CHARS = Math . floor ( MCP_MAX_PAYLOAD_BYTES * 0.6 )
132161 const { markup, message } = buildGetCodeMessage ( rawMarkup , MAX_CODE_CHARS , tree . stats )
133162
134- const { variableIds } = collectTokenReferences ( nodes )
135- const allTokens = await resolveVariableTokens ( variableIds , config , pluginCode )
136-
137- const usedTokenNames = new Set < string > ( )
138- // Use a simple regex to capture all var(--name) occurrences, including nested ones.
139- // We don't use CSS_VAR_FUNCTION_RE because it consumes the whole function and might miss nested vars in fallbacks.
140- const regex = / v a r \( - - ( [ a - z A - Z 0 - 9 - _ ] + ) / g
141- let match
142- while ( ( match = regex . exec ( markup ) ) ) {
143- usedTokenNames . add ( `--${ match [ 1 ] } ` )
144- }
163+ // Only include tokens actually referenced in the final output.
164+ const usedTokenNames = new Set < string > ( styleVarNames )
165+ extractVarNames ( markup ) . forEach ( ( n ) => usedTokenNames . add ( n ) )
145166
146- const usedTokens = filterTokensByNames ( allTokens , usedTokenNames )
167+ const usedTokens = await resolveTokenDefsByNames ( usedTokenNames , config , pluginCode , {
168+ candidateIds : ( ) => collectCandidateVariableIds ( nodes ) . variableIds
169+ } )
147170
148171 const codegen = {
149172 preset : activePlugin . value ?. name ?? 'none' ,
150173 config
151174 }
152175
153176 if ( resolveTokens ) {
154- const tokenMap = new Map (
155- Object . entries ( usedTokens . tokens ) . map ( ( [ name , entry ] ) => [ name , primaryTokenValue ( entry ) ] )
156- )
157- const resolvedMarkup = markup . replace ( / v a r \( ( - - [ a - z A - Z 0 - 9 - _ ] + ) \) / g, ( match , name ) => {
158- const val = tokenMap . get ( name )
159- return typeof val === 'string' ? val : match
177+ const tokenCache = new Map < string , string | undefined > ( )
178+ const tokenMap = new Map < string , string | undefined > ( )
179+ Object . keys ( usedTokens ) . forEach ( ( name ) => {
180+ tokenMap . set ( name , resolveConcreteValue ( name , usedTokens , tokenCache ) )
160181 } )
182+
183+ const replaceToken = ( input : string ) : string => {
184+ // Always strip fallbacks (even if token isn't resolved).
185+ let out = stripFallback ( input )
186+
187+ // Replace CSS var() functions first (supports whitespace/nesting).
188+ out = replaceVarFunctions ( out , ( { name, full } ) => {
189+ const trimmed = name . trim ( )
190+ if ( ! trimmed . startsWith ( '--' ) ) return full
191+ const canonical = `--${ normalizeCssVarName ( trimmed . slice ( 2 ) ) } `
192+ const val = tokenMap . get ( canonical )
193+ return typeof val === 'string' ? val : toVarExpr ( canonical )
194+ } )
195+
196+ // replace bare --token (e.g., Tailwind arbitrary value: border-[--foo])
197+ out = out . replace ( / - - [ A - Z a - z 0 - 9 - _ ] + / g, ( m ) => {
198+ const val = tokenMap . get ( m )
199+ return typeof val === 'string' ? val : m
200+ } )
201+ return out
202+ }
203+
204+ const resolvedMarkup = replaceToken ( markup )
161205 // If tokens are resolved, we don't need to return the token definitions
162206 return {
163207 lang : resolvedLang ,
@@ -191,8 +235,3 @@ function normalizeRootString(content: string, fallbackTag: string | undefined, l
191235 }
192236 )
193237}
194-
195- function codegenConfig ( ) : CodegenConfig {
196- const { cssUnit, rootFontSize, scale } = options . value
197- return { cssUnit, rootFontSize, scale }
198- }
0 commit comments