@@ -2,10 +2,13 @@ import type { AssetDescriptor, GetCodeResult } from '@tempad-dev/mcp-shared'
22
33import { MCP_MAX_PAYLOAD_BYTES } from '@tempad-dev/mcp-shared'
44
5+ import type { CodegenConfig } from '@/utils/codegen'
6+
57import { activePlugin } from '@/ui/state'
68import { stringifyComponent } from '@/utils/component'
79import { simplifyColorMixToRgba } from '@/utils/css'
810
11+ import type { VisibleTree } from './model'
912import type { CodeLanguage , RenderContext } from './render'
1013
1114import { currentCodegenConfig } from '../config'
@@ -15,6 +18,7 @@ import { planAssets } from './assets/plan'
1518import { collectNodeData } from './collect'
1619import { buildGetCodeWarnings , truncateCode } from './messages'
1720import { renderTree } from './render'
21+ import { resolvePluginComponent , type PluginComponent } from './render/plugin'
1822import { sanitizeStyles } from './sanitize'
1923import { buildLayoutStyles } from './styles'
2024import {
@@ -71,6 +75,14 @@ export async function handleGetCode(
7175 preferredLang ?: CodeLanguage ,
7276 resolveTokens ?: boolean
7377) : Promise < GetCodeResult > {
78+ const now = ( ) => ( typeof performance !== 'undefined' ? performance . now ( ) : Date . now ( ) )
79+ const startedAt = now ( )
80+ const timings : Array < [ string , number ] > = [ ]
81+ const stamp = ( label : string , start : number ) => {
82+ const elapsed = Math . round ( ( now ( ) - start ) * 10 ) / 10
83+ timings . push ( [ label , elapsed ] )
84+ }
85+
7486 if ( nodes . length !== 1 ) {
7587 throw new Error ( 'Select exactly one node or provide a single root node id.' )
7688 }
@@ -80,7 +92,9 @@ export async function handleGetCode(
8092 throw new Error ( 'The selected node is not visible.' )
8193 }
8294
95+ let t = now ( )
8396 const tree = buildVisibleTree ( nodes )
97+ stamp ( 'tree' , t )
8498 const rootId = tree . rootIds [ 0 ]
8599 if ( ! rootId ) {
86100 throw new Error ( 'No renderable nodes found for the current selection.' )
@@ -94,20 +108,52 @@ export async function handleGetCode(
94108 const config = currentCodegenConfig ( )
95109 const pluginCode = activePlugin . value ?. code
96110
97- const mappings = buildVariableMappings ( nodes )
111+ t = now ( )
112+ const variableCache = new Map < string , Variable | null > ( )
113+ const mappings = buildVariableMappings ( nodes , variableCache )
114+ stamp ( 'vars' , t )
115+
116+ t = now ( )
117+ const plan = planAssets ( tree )
118+ stamp ( 'plan-assets' , t )
119+
120+ let pluginComponents : Map < string , PluginComponent | null > | undefined
121+ const pluginSkipped = new Set < string > ( )
122+ if ( pluginCode ) {
123+ pluginComponents = await collectPluginComponents ( tree , config , pluginCode , preferredLang )
124+ if ( pluginComponents . size ) {
125+ for ( const [ id , component ] of pluginComponents . entries ( ) ) {
126+ if ( ! component ) continue
127+ const snapshot = tree . nodes . get ( id )
128+ if ( ! snapshot ) continue
129+ snapshot . children . forEach ( ( childId ) => skipDescendants ( childId , tree , pluginSkipped ) )
130+ }
131+ }
132+ }
98133
99134 const assetRegistry = new Map < string , AssetDescriptor > ( )
100- const collected = await collectNodeData ( tree , config , assetRegistry )
135+ const skipIds =
136+ plan . skippedIds . size || pluginSkipped . size
137+ ? new Set < string > ( [ ...plan . skippedIds , ...pluginSkipped ] )
138+ : plan . skippedIds
139+ t = now ( )
140+ const collected = await collectNodeData ( tree , config , assetRegistry , skipIds )
141+ stamp ( 'collect' , t )
101142
102143 // Normalize codeSyntax outputs (e.g. "$kui-space-0") before Tailwind conversion.
103- const usedCandidateIds = normalizeStyleVars ( collected . styles , mappings )
104-
105- const plan = planAssets ( tree )
144+ t = now ( )
145+ const usedCandidateIds = normalizeStyleVars ( collected . styles , mappings , variableCache )
146+ stamp ( 'normalize-vars' , t )
106147
107148 // Post-process styles (negative gap, auto-relative, etc.) after var normalization.
149+ t = now ( )
108150 sanitizeStyles ( tree , collected . styles , plan . vectorRoots )
109151 const layoutStyles = buildLayoutStyles ( collected . styles , plan . vectorRoots )
152+ stamp ( 'layout' , t )
153+
154+ t = now ( )
110155 const svgs = await exportVectorAssets ( tree , plan , config , assetRegistry )
156+ stamp ( 'export-assets' , t )
111157
112158 const nodeMap = new Map < string , SceneNode > ( )
113159 collected . nodes . forEach ( ( snap , id ) => nodeMap . set ( id , snap . node ) )
@@ -118,12 +164,15 @@ export async function handleGetCode(
118164 nodes : nodeMap ,
119165 svgs,
120166 textSegments : collected . textSegments ,
167+ pluginComponents,
121168 pluginCode,
122169 config,
123170 preferredLang
124171 }
125172
173+ t = now ( )
126174 const componentTree = await renderTree ( rootId , tree , ctx )
175+ stamp ( 'render' , t )
127176
128177 if ( ! componentTree ) {
129178 throw new Error ( 'Unable to build markup for the current selection.' )
@@ -132,52 +181,70 @@ export async function handleGetCode(
132181 const resolvedLang = preferredLang ?? ctx . detectedLang ?? 'jsx'
133182 const rootTag = collected . nodes . get ( rootId ) ?. tag
134183
184+ t = now ( )
135185 const rawMarkup =
136186 typeof componentTree === 'string'
137187 ? normalizeRootString ( componentTree , rootTag , rootId , resolvedLang )
138188 : stringifyComponent ( componentTree , {
139189 lang : resolvedLang ,
140190 isInline : ( tag ) => COMPACT_TAGS . has ( tag )
141191 } )
192+ stamp ( 'stringify' , t )
142193
194+ t = now ( )
143195 const MAX_CODE_CHARS = Math . floor ( MCP_MAX_PAYLOAD_BYTES * 0.6 )
144196 let { code, truncated } = truncateCode ( rawMarkup , MAX_CODE_CHARS )
197+ stamp ( 'truncate' , t )
145198
146199 // Token pipeline: detect -> transform -> rewrite -> detect
147200 const candidateIds = usedCandidateIds . size
148201 ? new Set < string > ( [ ...mappings . variableIds , ...usedCandidateIds ] )
149202 : mappings . variableIds
150- const variableCache = new Map < string , Variable | null > ( )
203+ t = now ( )
151204 const sourceIndex = buildSourceNameIndex ( candidateIds , variableCache )
152205 const sourceNames = new Set ( sourceIndex . keys ( ) )
153206 const usedNamesRaw = extractTokenNames ( code , sourceNames )
207+ stamp ( 'tokens:detect' , t )
154208
209+ t = now ( )
155210 const { rewriteMap, finalBridge } = await applyPluginTransformToNames (
156211 usedNamesRaw ,
157212 sourceIndex ,
158213 pluginCode ,
159214 config
160215 )
161216
217+ let hasRenames = false
218+ for ( const [ key , next ] of rewriteMap ) {
219+ if ( key !== next ) {
220+ hasRenames = true
221+ break
222+ }
223+ }
224+
162225 code = rewriteTokenNamesInCode ( code , rewriteMap )
163226 if ( code . length > MAX_CODE_CHARS ) {
164227 code = code . slice ( 0 , MAX_CODE_CHARS )
165228 truncated = true
166229 }
167230
168- const usedNamesFinal = extractTokenNames ( code , sourceNames )
169- const finalBridgeFiltered = filterBridge ( finalBridge , usedNamesFinal )
231+ const usedNamesFinal = hasRenames ? extractTokenNames ( code , sourceNames ) : usedNamesRaw
232+ const finalBridgeFiltered = hasRenames ? filterBridge ( finalBridge , usedNamesFinal ) : finalBridge
233+ stamp ( 'tokens:rewrite' , t )
170234
235+ t = now ( )
171236 const { usedTokens, tokensByCanonical, canonicalById } = await buildUsedTokens (
172237 usedNamesFinal ,
173238 finalBridgeFiltered ,
174239 config ,
175240 pluginCode ,
176241 variableCache
177242 )
243+ stamp ( 'tokens:used' , t )
178244
179245 let resolvedTokens : Record < string , string > | undefined
180246 if ( resolveTokens ) {
247+ t = now ( )
181248 const resolvedByFinal = buildResolvedTokenMap (
182249 finalBridgeFiltered ,
183250 tokensByCanonical ,
@@ -190,6 +257,7 @@ export async function handleGetCode(
190257 truncated = true
191258 }
192259 resolvedTokens = mapResolvedTokens ( resolvedByFinal )
260+ stamp ( 'tokens:resolve' , t )
193261 }
194262
195263 const warnings = buildGetCodeWarnings ( code , MAX_CODE_CHARS , truncated )
@@ -209,6 +277,14 @@ export async function handleGetCode(
209277 }
210278 : undefined
211279
280+ const elapsed = Math . round ( ( now ( ) - startedAt ) * 10 ) / 10
281+ console . info ( `[tempad-dev] get_code total ${ elapsed } ms` )
282+ if ( timings . length ) {
283+ const detail = timings . map ( ( [ label , ms ] ) => `${ label } =${ ms } ms` ) . join ( ' ' )
284+ const info = `nodes=${ tree . order . length } text=${ collected . textSegments . size } vectors=${ plan . vectorRoots . size } assets=${ assetRegistry . size } `
285+ console . info ( `[tempad-dev] get_code timings ${ detail } (${ info } )` )
286+ }
287+
212288 return {
213289 lang : resolvedLang ,
214290 code,
@@ -219,6 +295,31 @@ export async function handleGetCode(
219295 }
220296}
221297
298+ async function collectPluginComponents (
299+ tree : VisibleTree ,
300+ config : CodegenConfig ,
301+ pluginCode : string ,
302+ preferredLang ?: CodeLanguage
303+ ) : Promise < Map < string , PluginComponent | null > > {
304+ const out = new Map < string , PluginComponent | null > ( )
305+ for ( const id of tree . order ) {
306+ const snapshot = tree . nodes . get ( id )
307+ if ( ! snapshot ) continue
308+ if ( snapshot . node . type !== 'INSTANCE' ) continue
309+ const component = await resolvePluginComponent ( snapshot . node , config , pluginCode , preferredLang )
310+ out . set ( id , component )
311+ }
312+ return out
313+ }
314+
315+ function skipDescendants ( id : string , tree : VisibleTree , skipped : Set < string > ) : void {
316+ const node = tree . nodes . get ( id )
317+ if ( ! node ) return
318+ if ( skipped . has ( id ) ) return
319+ skipped . add ( id )
320+ node . children . forEach ( ( childId ) => skipDescendants ( childId , tree , skipped ) )
321+ }
322+
222323function normalizeRootString (
223324 content : string ,
224325 fallbackTag : string | undefined ,
0 commit comments