@tamagui/codemod-flat-values 0.0.0-bootstrap.0 → 3.0.0-beta.1093.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +234 -1
  2. package/dist/builtInNames.mjs +14 -0
  3. package/dist/builtInNames.mjs.map +1 -0
  4. package/dist/containers.mjs +141 -0
  5. package/dist/containers.mjs.map +1 -0
  6. package/dist/convert.mjs +1233 -0
  7. package/dist/convert.mjs.map +1 -0
  8. package/dist/expressions.mjs +188 -0
  9. package/dist/expressions.mjs.map +1 -0
  10. package/dist/functionalVariants.mjs +469 -0
  11. package/dist/functionalVariants.mjs.map +1 -0
  12. package/dist/grammar.mjs +73 -0
  13. package/dist/grammar.mjs.map +1 -0
  14. package/dist/index.mjs +374 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/legacyConditions.mjs +293 -0
  17. package/dist/legacyConditions.mjs.map +1 -0
  18. package/dist/legacyNames.mjs +130 -0
  19. package/dist/legacyNames.mjs.map +1 -0
  20. package/dist/provenance.mjs +137 -0
  21. package/dist/provenance.mjs.map +1 -0
  22. package/dist/report.mjs +188 -0
  23. package/dist/report.mjs.map +1 -0
  24. package/dist/sheetAnatomy.mjs +200 -0
  25. package/dist/sheetAnatomy.mjs.map +1 -0
  26. package/dist/structuredNative.mjs +275 -0
  27. package/dist/structuredNative.mjs.map +1 -0
  28. package/dist/transition.mjs +257 -0
  29. package/dist/transition.mjs.map +1 -0
  30. package/package.json +35 -7
  31. package/src/builtInNames.ts +23 -0
  32. package/src/containers.ts +227 -0
  33. package/src/convert.ts +1977 -0
  34. package/src/expressions.ts +220 -0
  35. package/src/functionalVariants.ts +642 -0
  36. package/src/grammar.ts +190 -0
  37. package/src/index.ts +589 -0
  38. package/src/legacyConditions.ts +357 -0
  39. package/src/legacyNames.ts +160 -0
  40. package/src/provenance.ts +210 -0
  41. package/src/report.ts +362 -0
  42. package/src/sheetAnatomy.ts +277 -0
  43. package/src/structuredNative.ts +459 -0
  44. package/src/transition.ts +415 -0
@@ -0,0 +1,642 @@
1
+ import {
2
+ Node,
3
+ SyntaxKind,
4
+ type ArrowFunction,
5
+ type Expression,
6
+ type FunctionExpression,
7
+ type Identifier,
8
+ type ImportDeclaration,
9
+ type Node as MorphNode,
10
+ type ObjectLiteralExpression,
11
+ type SourceFile,
12
+ } from 'ts-morph'
13
+ import type { Flag } from './convert'
14
+ import { unwrapExpression } from './expressions'
15
+
16
+ type FunctionalCallback = ArrowFunction | FunctionExpression
17
+
18
+ interface Replacement {
19
+ start: number
20
+ end: number
21
+ text: string
22
+ }
23
+
24
+ export interface RequiredTypeImport {
25
+ module: 'tamagui' | '@tamagui/core'
26
+ name: string
27
+ localName: string
28
+ }
29
+
30
+ interface CallbackAnalysis {
31
+ callback: FunctionalCallback
32
+ firstName: string | null
33
+ rendered: string
34
+ normalizedBody: string | null
35
+ objectReturn: string | null
36
+ usesEnv: boolean
37
+ propsLine: number | null
38
+ draftBody: string | null
39
+ unsupportedExtras: string[]
40
+ }
41
+
42
+ export interface FunctionalVariantReport {
43
+ label: string
44
+ line: number
45
+ before: string
46
+ after: string
47
+ converted: boolean
48
+ flags: Flag[]
49
+ draft: string | null
50
+ notes: string[]
51
+ }
52
+
53
+ export interface FunctionalVariantConversion {
54
+ sites: FunctionalVariantReport[]
55
+ requiredTypeImports: RequiredTypeImport[]
56
+ }
57
+
58
+ const spreadTypes = new Map([
59
+ ['...size', 'SizeTokens'],
60
+ ['...space', 'SpaceTokens'],
61
+ ['...color', 'ColorTokens'],
62
+ ['...radius', 'RadiusTokens'],
63
+ ['...fontSize', 'FontSizeTokens'],
64
+ ['...zIndex', 'ZIndexTokens'],
65
+ ])
66
+
67
+ const typeKeys = new Map([
68
+ [':number', 'number'],
69
+ [':string', 'string'],
70
+ [':boolean', 'boolean'],
71
+ ])
72
+
73
+ const typeOrder = ['number', 'string', 'boolean'] as const
74
+ const envMembers = new Set(['tokens', 'theme', 'fonts', 'font', 'fontFamily'])
75
+
76
+ function propertyName(node: MorphNode): string | null {
77
+ if (Node.isIdentifier(node) || Node.isPrivateIdentifier(node)) return node.getText()
78
+ if (Node.isStringLiteral(node) || Node.isNumericLiteral(node)) {
79
+ return node.getLiteralText()
80
+ }
81
+ return null
82
+ }
83
+
84
+ function renderNode(node: MorphNode, replacements: readonly Replacement[]): string {
85
+ const start = node.getStart()
86
+ const end = node.getEnd()
87
+ const selected: Replacement[] = []
88
+
89
+ for (const replacement of [...replacements].sort(
90
+ (left, right) => right.end - right.start - (left.end - left.start)
91
+ )) {
92
+ if (replacement.start < start || replacement.end > end) continue
93
+ if (
94
+ selected.some(
95
+ (current) => replacement.start < current.end && replacement.end > current.start
96
+ )
97
+ ) {
98
+ continue
99
+ }
100
+ selected.push(replacement)
101
+ }
102
+
103
+ let text = node.getText()
104
+ for (const replacement of selected.sort((left, right) => right.start - left.start)) {
105
+ const relativeStart = replacement.start - start
106
+ const relativeEnd = replacement.end - start
107
+ text = `${text.slice(0, relativeStart)}${replacement.text}${text.slice(relativeEnd)}`
108
+ }
109
+ return text
110
+ }
111
+
112
+ function referencesWithin(node: MorphNode, definition: Identifier): MorphNode[] {
113
+ return definition
114
+ .findReferencesAsNodes()
115
+ .filter(
116
+ (reference) =>
117
+ reference.getStart() >= node.getStart() && reference.getEnd() <= node.getEnd()
118
+ )
119
+ }
120
+
121
+ function renderObjectReturn(
122
+ expression: Expression,
123
+ replacements: readonly Replacement[]
124
+ ): string {
125
+ let current = expression
126
+ while (Node.isParenthesizedExpression(current)) current = current.getExpression()
127
+ return renderNode(
128
+ Node.isAsExpression(current) || Node.isTypeAssertion(current)
129
+ ? expression
130
+ : unwrapExpression(expression),
131
+ replacements
132
+ )
133
+ }
134
+
135
+ function callbackAnalysis(callback: FunctionalCallback): CallbackAnalysis {
136
+ const body = callback.getBody()
137
+ const parameters = callback.getParameters()
138
+ const firstNameNode = parameters[0]?.getNameNode()
139
+ const firstName = Node.isIdentifier(firstNameNode) ? firstNameNode.getText() : null
140
+ const secondNameNode = parameters[1]?.getNameNode()
141
+ const renderedReplacements: Replacement[] = []
142
+ const normalizedReplacements: Replacement[] = []
143
+ const draftReplacements: Replacement[] = []
144
+ const unsupportedExtras = new Set<string>()
145
+ let propsLine: number | null = null
146
+ let usesEnv = false
147
+
148
+ if (firstNameNode && Node.isIdentifier(firstNameNode)) {
149
+ for (const reference of referencesWithin(body, firstNameNode)) {
150
+ normalizedReplacements.push({
151
+ start: reference.getStart(),
152
+ end: reference.getEnd(),
153
+ text: 'value',
154
+ })
155
+ }
156
+ }
157
+
158
+ if (secondNameNode && Node.isIdentifier(secondNameNode)) {
159
+ const references = referencesWithin(body, secondNameNode)
160
+ usesEnv = references.length > 0
161
+ const otherSameName = body
162
+ .getDescendantsOfKind(SyntaxKind.Identifier)
163
+ .some(
164
+ (identifier) =>
165
+ identifier.getText() === secondNameNode.getText() &&
166
+ !references.some(
167
+ (reference) =>
168
+ reference.getStart() === identifier.getStart() &&
169
+ reference.getEnd() === identifier.getEnd()
170
+ )
171
+ )
172
+ const envNameExists = body
173
+ .getDescendantsOfKind(SyntaxKind.Identifier)
174
+ .some((identifier) => identifier.getText() === 'env')
175
+
176
+ if (secondNameNode.getText() !== 'env' && !otherSameName && !envNameExists) {
177
+ renderedReplacements.push({
178
+ start: secondNameNode.getStart(),
179
+ end: secondNameNode.getEnd(),
180
+ text: 'env',
181
+ })
182
+ for (const reference of references) {
183
+ renderedReplacements.push({
184
+ start: reference.getStart(),
185
+ end: reference.getEnd(),
186
+ text: 'env',
187
+ })
188
+ }
189
+ }
190
+
191
+ for (const reference of references) {
192
+ normalizedReplacements.push({
193
+ start: reference.getStart(),
194
+ end: reference.getEnd(),
195
+ text: 'env',
196
+ })
197
+ draftReplacements.push({
198
+ start: reference.getStart(),
199
+ end: reference.getEnd(),
200
+ text: 'env',
201
+ })
202
+
203
+ const parent = reference.getParent()
204
+ if (
205
+ Node.isPropertyAccessExpression(parent) &&
206
+ parent.getExpression().getStart() === reference.getStart()
207
+ ) {
208
+ const member = parent.getName()
209
+ if (member === 'props') {
210
+ propsLine ??= callback
211
+ .getSourceFile()
212
+ .getLineAndColumnAtPos(parent.getStart()).line
213
+ draftReplacements.push({
214
+ start: parent.getStart(),
215
+ end: parent.getEnd(),
216
+ text: 'props',
217
+ })
218
+ } else if (!envMembers.has(member)) {
219
+ unsupportedExtras.add(member)
220
+ }
221
+ }
222
+ if (
223
+ Node.isElementAccessExpression(parent) &&
224
+ parent.getExpression().getStart() === reference.getStart()
225
+ ) {
226
+ const argument = parent.getArgumentExpression()
227
+ const member =
228
+ argument && Node.isStringLiteral(argument) ? argument.getLiteralValue() : null
229
+ if (member === 'props') {
230
+ propsLine ??= callback
231
+ .getSourceFile()
232
+ .getLineAndColumnAtPos(parent.getStart()).line
233
+ draftReplacements.push({
234
+ start: parent.getStart(),
235
+ end: parent.getEnd(),
236
+ text: 'props',
237
+ })
238
+ } else if (member === null || !envMembers.has(member)) {
239
+ unsupportedExtras.add(member ?? parent.getText())
240
+ }
241
+ }
242
+ }
243
+ } else if (secondNameNode && Node.isObjectBindingPattern(secondNameNode)) {
244
+ for (const element of secondNameNode.getElements()) {
245
+ const bindingName = element.getNameNode()
246
+ const sourceName = propertyName(element.getPropertyNameNode() ?? bindingName)
247
+ if (!sourceName || !Node.isIdentifier(bindingName)) {
248
+ unsupportedExtras.add(element.getText())
249
+ continue
250
+ }
251
+ if (element.getDotDotDotToken()) {
252
+ unsupportedExtras.add(element.getText())
253
+ continue
254
+ }
255
+ const references = referencesWithin(body, bindingName)
256
+ if (sourceName === 'props') {
257
+ propsLine ??= callback
258
+ .getSourceFile()
259
+ .getLineAndColumnAtPos(element.getStart()).line
260
+ for (const reference of references) {
261
+ draftReplacements.push({
262
+ start: reference.getStart(),
263
+ end: reference.getEnd(),
264
+ text: 'props',
265
+ })
266
+ }
267
+ continue
268
+ }
269
+ if (!envMembers.has(sourceName)) {
270
+ unsupportedExtras.add(sourceName)
271
+ continue
272
+ }
273
+ if (references.length) usesEnv = true
274
+ for (const reference of references) {
275
+ normalizedReplacements.push({
276
+ start: reference.getStart(),
277
+ end: reference.getEnd(),
278
+ text: `env.${sourceName}`,
279
+ })
280
+ draftReplacements.push({
281
+ start: reference.getStart(),
282
+ end: reference.getEnd(),
283
+ text: `env.${sourceName}`,
284
+ })
285
+ }
286
+ }
287
+ } else if (secondNameNode) {
288
+ unsupportedExtras.add(secondNameNode.getText())
289
+ }
290
+
291
+ const normalizedBody =
292
+ firstNameNode && !Node.isIdentifier(firstNameNode)
293
+ ? null
294
+ : renderNode(body, normalizedReplacements)
295
+ const draftBody =
296
+ firstNameNode && !Node.isIdentifier(firstNameNode)
297
+ ? null
298
+ : renderNode(body, draftReplacements)
299
+ let objectReturn: string | null = null
300
+ const unwrappedBody = Node.isExpression(body) ? unwrapExpression(body) : body
301
+
302
+ if (Node.isExpression(body) && Node.isObjectLiteralExpression(unwrappedBody)) {
303
+ objectReturn = renderObjectReturn(body, normalizedReplacements)
304
+ } else if (Node.isBlock(body)) {
305
+ const statements = body.getStatements()
306
+ if (statements.length === 1 && Node.isReturnStatement(statements[0])) {
307
+ const returned = statements[0].getExpression()
308
+ if (returned) {
309
+ const unwrapped = unwrapExpression(returned)
310
+ if (Node.isObjectLiteralExpression(unwrapped)) {
311
+ objectReturn = renderObjectReturn(returned, normalizedReplacements)
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ return {
318
+ callback,
319
+ firstName,
320
+ rendered: renderNode(callback, renderedReplacements),
321
+ normalizedBody,
322
+ objectReturn,
323
+ usesEnv,
324
+ propsLine,
325
+ draftBody,
326
+ unsupportedExtras: [...unsupportedExtras].sort(),
327
+ }
328
+ }
329
+
330
+ function styledImport(sourceFile: SourceFile): ImportDeclaration | null {
331
+ for (const declaration of sourceFile.getImportDeclarations()) {
332
+ const module = declaration.getModuleSpecifierValue()
333
+ if (module !== 'tamagui' && module !== '@tamagui/core') continue
334
+ if (
335
+ declaration.getNamedImports().some((specifier) => {
336
+ const localName = specifier.getAliasNode()?.getText() ?? specifier.getName()
337
+ return specifier.getName() === 'styled' && localName === 'styled'
338
+ })
339
+ ) {
340
+ return declaration
341
+ }
342
+ }
343
+ return null
344
+ }
345
+
346
+ function typeReference(
347
+ sourceFile: SourceFile,
348
+ declaration: ImportDeclaration,
349
+ typeName: string
350
+ ): { localName: string; required: RequiredTypeImport | null } {
351
+ const module = declaration.getModuleSpecifierValue() as RequiredTypeImport['module']
352
+ for (const current of sourceFile.getImportDeclarations()) {
353
+ if (current.getModuleSpecifierValue() !== module) continue
354
+ const existing = current
355
+ .getNamedImports()
356
+ .find((specifier) => specifier.getName() === typeName)
357
+ if (existing) {
358
+ return {
359
+ localName: existing.getAliasNode()?.getText() ?? existing.getName(),
360
+ required: null,
361
+ }
362
+ }
363
+ }
364
+
365
+ let localName = typeName
366
+ if (sourceFile.getLocal(localName)) {
367
+ localName = `Tamagui${typeName}`
368
+ let suffix = 2
369
+ while (sourceFile.getLocal(localName)) localName = `Tamagui${typeName}${suffix++}`
370
+ }
371
+ return { localName, required: { module, name: typeName, localName } }
372
+ }
373
+
374
+ function callbackFromProperty(property: MorphNode): FunctionalCallback | null {
375
+ if (!Node.isPropertyAssignment(property)) return null
376
+ const initializer = unwrapExpression(property.getInitializerOrThrow())
377
+ return Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)
378
+ ? initializer
379
+ : null
380
+ }
381
+
382
+ function commentsBefore(properties: readonly MorphNode[]): string {
383
+ const comments = new Map<number, string>()
384
+ for (const property of properties) {
385
+ for (const range of property.getLeadingCommentRanges()) {
386
+ comments.set(range.getPos(), range.getText())
387
+ }
388
+ }
389
+ return [...comments.entries()]
390
+ .sort((left, right) => left[0] - right[0])
391
+ .map((entry) => entry[1])
392
+ .join('\n')
393
+ }
394
+
395
+ export function convertFunctionalVariants(
396
+ config: ObjectLiteralExpression,
397
+ label: string,
398
+ write: boolean
399
+ ): FunctionalVariantConversion {
400
+ const sites: FunctionalVariantReport[] = []
401
+ const requiredTypeImports: RequiredTypeImport[] = []
402
+ const variantsProperty = config.getProperty('variants')
403
+ if (!Node.isPropertyAssignment(variantsProperty)) {
404
+ return { sites, requiredTypeImports }
405
+ }
406
+ const variants = unwrapExpression(variantsProperty.getInitializerOrThrow())
407
+ if (!Node.isObjectLiteralExpression(variants)) return { sites, requiredTypeImports }
408
+
409
+ const sourceFile = config.getSourceFile()
410
+ const importDeclaration = styledImport(sourceFile)
411
+
412
+ for (const variant of variants.getProperties()) {
413
+ if (!Node.isPropertyAssignment(variant)) continue
414
+ const variantName = propertyName(variant.getNameNode())
415
+ if (!variantName) continue
416
+ const initializer = unwrapExpression(variant.getInitializerOrThrow())
417
+ if (!Node.isObjectLiteralExpression(initializer)) continue
418
+
419
+ const functional = initializer.getProperties().flatMap((property) => {
420
+ if (!('getNameNode' in property)) return []
421
+ const name = propertyName(property.getNameNode())
422
+ return name && (name === '...' || name.startsWith('...') || name.startsWith(':'))
423
+ ? [{ name, property }]
424
+ : []
425
+ })
426
+ if (!functional.length) continue
427
+
428
+ const flags: Flag[] = []
429
+ const notes: string[] = []
430
+ const line = sourceFile.getLineAndColumnAtPos(functional[0].property.getStart()).line
431
+ const before = variant.getText()
432
+ const exact = initializer
433
+ .getProperties()
434
+ .filter((property) => !functional.some((entry) => entry.property === property))
435
+ const callbacks = functional.map((entry) => callbackFromProperty(entry.property))
436
+ const analyses = callbacks.map((callback) =>
437
+ callback ? callbackAnalysis(callback) : null
438
+ )
439
+ let after = before
440
+ let draft: string | null = null
441
+ let dynamicText: string | null = null
442
+ let requiredImport: RequiredTypeImport | null = null
443
+
444
+ if (exact.length) {
445
+ flags.push({
446
+ code: 'functional-variant-mixed',
447
+ detail: `variant "${variantName}" combines exact branches with a function key; v3 has no mixed variant form`,
448
+ })
449
+ }
450
+ if (functional.some((entry) => entry.name === '...')) {
451
+ flags.push({
452
+ code: 'functional-variant-catch-all',
453
+ detail: `choose the value type and replace this catch-all with styled.dynamic<YourValue>(...); unknown would erase the prop contract`,
454
+ })
455
+ }
456
+ if (callbacks.some((callback) => callback === null)) {
457
+ flags.push({
458
+ code: 'functional-variant-unsupported',
459
+ detail: `variant "${variantName}" uses a function key whose value is not an inline arrow or function expression`,
460
+ })
461
+ }
462
+ if (
463
+ callbacks.some(
464
+ (callback) =>
465
+ callback &&
466
+ (callback.getParameters().length > 2 ||
467
+ callback.isAsync() ||
468
+ (Node.isFunctionExpression(callback) && callback.isGenerator()))
469
+ )
470
+ ) {
471
+ flags.push({
472
+ code: 'functional-variant-unsupported',
473
+ detail: `variant "${variantName}" uses an async, generator, or three-parameter callback`,
474
+ })
475
+ }
476
+ if (analyses.some((analysis) => analysis?.firstName === null)) {
477
+ flags.push({
478
+ code: 'functional-variant-unsupported',
479
+ detail: `variant "${variantName}" destructures its value parameter; migrate that callback by hand`,
480
+ })
481
+ }
482
+ const unsupportedExtras = [
483
+ ...new Set(analyses.flatMap((analysis) => analysis?.unsupportedExtras ?? [])),
484
+ ]
485
+ if (unsupportedExtras.length) {
486
+ flags.push({
487
+ code: 'functional-variant-unsupported-extras',
488
+ detail: `v3 env has no ${unsupportedExtras.map((name) => `"${name}"`).join(', ')} member`,
489
+ })
490
+ }
491
+
492
+ const propsAnalysis = analyses.find((analysis) => analysis?.propsLine != null)
493
+ if (propsAnalysis) {
494
+ const analysis = propsAnalysis as CallbackAnalysis
495
+ const valueName = analysis.firstName ?? 'value'
496
+ const propAccess = /^[A-Za-z_$][\w$]*$/.test(variantName)
497
+ ? `props.${variantName}`
498
+ : `props[${JSON.stringify(variantName)}]`
499
+ const body = analysis.draftBody
500
+ if (body) {
501
+ const statements = Node.isBlock(analysis.callback.getBody())
502
+ ? body.slice(1, -1).trim()
503
+ : `return ${body}`
504
+ draft = `${variantName}: styled.dynamic<${
505
+ spreadTypes.get(functional[0].name) ??
506
+ typeKeys.get(functional[0].name) ??
507
+ 'YourValue'
508
+ }>()\n\n.resolve((props, env) => {\n const ${valueName} = ${propAccess}\n${statements
509
+ .split('\n')
510
+ .map((statement) => ` ${statement}`)
511
+ .join('\n')}\n})`
512
+ }
513
+ flags.push({
514
+ code: 'functional-variant-needs-resolve',
515
+ detail: `line ${analysis.propsLine} reads sibling props; declare the consumed prop with styled.dynamic<T>() and adapt the generated .resolve draft`,
516
+ })
517
+ }
518
+
519
+ const allTypeKeys = functional.every((entry) => typeKeys.has(entry.name))
520
+ const oneSpread = functional.length === 1 && spreadTypes.has(functional[0].name)
521
+ if (new Set(functional.map((entry) => entry.name)).size !== functional.length) {
522
+ flags.push({
523
+ code: 'functional-variant-unsupported',
524
+ detail: `variant "${variantName}" repeats a function key`,
525
+ })
526
+ }
527
+ if (!functional.some((entry) => entry.name === '...') && !allTypeKeys && !oneSpread) {
528
+ flags.push({
529
+ code: 'functional-variant-unsupported',
530
+ detail: `variant "${variantName}" uses unsupported functional keys ${functional
531
+ .map((entry) => `"${entry.name}"`)
532
+ .join(', ')}`,
533
+ })
534
+ }
535
+
536
+ if (!flags.length && oneSpread) {
537
+ if (!importDeclaration) {
538
+ flags.push({
539
+ code: 'functional-variant-styled-import',
540
+ detail: `the file does not import styled directly from "tamagui" or "@tamagui/core", so the token type source is not provable`,
541
+ })
542
+ } else {
543
+ const typeName = spreadTypes.get(functional[0].name)!
544
+ const reference = typeReference(sourceFile, importDeclaration, typeName)
545
+ requiredImport = reference.required
546
+ dynamicText = `styled.dynamic<${reference.localName}>(${analyses[0]!.rendered})`
547
+ notes.push(
548
+ `uses ${reference.localName} from ${importDeclaration.getModuleSpecifierValue()}`
549
+ )
550
+ }
551
+ } else if (!flags.length && allTypeKeys) {
552
+ const present = new Set(functional.map((entry) => typeKeys.get(entry.name)!))
553
+ const union = typeOrder.filter((type) => present.has(type)).join(' | ')
554
+ if (functional.length === 1) {
555
+ dynamicText = `styled.dynamic<${union}>(${analyses[0]!.rendered})`
556
+ } else {
557
+ const normalizedBodies = analyses.map((analysis) => analysis!.normalizedBody!)
558
+ const sameBody = normalizedBodies.every((body) => body === normalizedBodies[0])
559
+ const envParameter = analyses.some((analysis) => analysis!.usesEnv) ? ', env' : ''
560
+ if (sameBody) {
561
+ dynamicText = `styled.dynamic<${union}>((value${envParameter}) => ${normalizedBodies[0]})`
562
+ } else if (analyses.every((analysis) => analysis!.objectReturn !== null)) {
563
+ const byType = new Map(
564
+ functional.map((entry, index) => [
565
+ typeKeys.get(entry.name)!,
566
+ analyses[index]!.objectReturn!,
567
+ ])
568
+ )
569
+ const ordered = typeOrder.filter((type) => byType.has(type))
570
+ const branches = ordered.map((type, index) =>
571
+ index === ordered.length - 1
572
+ ? `return ${byType.get(type)}`
573
+ : `if (typeof value === '${type}') return ${byType.get(type)}`
574
+ )
575
+ dynamicText = `styled.dynamic<${union}>((value${envParameter}) => {\n${branches
576
+ .map((branch) => ` ${branch}`)
577
+ .join('\n')}\n})`
578
+ } else {
579
+ flags.push({
580
+ code: 'functional-variant-type-bodies',
581
+ detail: `variant "${variantName}" has different type-key bodies; automatic typeof branches require each body to be one object-literal return`,
582
+ })
583
+ }
584
+ }
585
+ }
586
+
587
+ if (dynamicText && !flags.length) {
588
+ const comments = commentsBefore(functional.map((entry) => entry.property))
589
+ const replacement = comments ? `${comments}\n${dynamicText}` : dynamicText
590
+ after = `${variant.getNameNode().getText()}: ${replacement}`
591
+ if (requiredImport) requiredTypeImports.push(requiredImport)
592
+ if (write) variant.setInitializer(replacement)
593
+ }
594
+
595
+ sites.push({
596
+ label: `${label} variants.${variantName}`,
597
+ line,
598
+ before,
599
+ after,
600
+ converted: flags.length === 0,
601
+ flags,
602
+ draft,
603
+ notes,
604
+ })
605
+ }
606
+
607
+ return { sites, requiredTypeImports }
608
+ }
609
+
610
+ export function addFunctionalVariantTypeImports(
611
+ sourceFile: SourceFile,
612
+ imports: readonly RequiredTypeImport[]
613
+ ): void {
614
+ const seen = new Set<string>()
615
+ for (const required of imports) {
616
+ const key = `${required.module}:${required.name}:${required.localName}`
617
+ if (seen.has(key)) continue
618
+ seen.add(key)
619
+ const declaration = sourceFile
620
+ .getImportDeclarations()
621
+ .find((current) => current.getModuleSpecifierValue() === required.module)
622
+ if (!declaration) continue
623
+ if (
624
+ sourceFile
625
+ .getImportDeclarations()
626
+ .some(
627
+ (current) =>
628
+ current.getModuleSpecifierValue() === required.module &&
629
+ current
630
+ .getNamedImports()
631
+ .some((specifier) => specifier.getName() === required.name)
632
+ )
633
+ ) {
634
+ continue
635
+ }
636
+ declaration.addNamedImport({
637
+ name: required.name,
638
+ alias: required.localName === required.name ? undefined : required.localName,
639
+ isTypeOnly: !declaration.isTypeOnly(),
640
+ })
641
+ }
642
+ }