@tamagui/codemod-flat-values 0.0.0-bootstrap.0 → 3.0.0-beta.637.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.
@@ -0,0 +1,459 @@
1
+ import { Node, SyntaxKind, type Expression } from 'ts-morph'
2
+ import { numericValue, unwrapExpression } from './expressions'
3
+ import { parseTransformString, sharedPayload, type ModifierRegistryView } from './grammar'
4
+
5
+ export interface StructuredNativeClassification {
6
+ payload: string | null
7
+ blocked: { code: string; detail: string } | null
8
+ }
9
+
10
+ const staticTransformOperations: ReadonlySet<string> = new Set([
11
+ 'perspective',
12
+ 'rotate',
13
+ 'rotateX',
14
+ 'rotateY',
15
+ 'rotateZ',
16
+ 'scale',
17
+ 'scaleX',
18
+ 'scaleY',
19
+ 'skewX',
20
+ 'skewY',
21
+ 'translateX',
22
+ 'translateY',
23
+ ])
24
+
25
+ function blocked(code: string, detail: string): StructuredNativeClassification {
26
+ return { payload: null, blocked: { code, detail } }
27
+ }
28
+
29
+ function staticString(expression: Expression): string | null {
30
+ const current = unwrapExpression(expression)
31
+ return Node.isStringLiteral(current) || Node.isNoSubstitutionTemplateLiteral(current)
32
+ ? current.getLiteralValue()
33
+ : null
34
+ }
35
+
36
+ function hasOnlyStringValues(expression: Expression): boolean {
37
+ const current = unwrapExpression(expression)
38
+ if (
39
+ Node.isStringLiteral(current) ||
40
+ Node.isNoSubstitutionTemplateLiteral(current) ||
41
+ Node.isTemplateExpression(current)
42
+ ) {
43
+ return true
44
+ }
45
+
46
+ const type = current.getType()
47
+ const parts = type.isUnion() ? type.getUnionTypes() : [type]
48
+ let strings = 0
49
+ for (const part of parts) {
50
+ if (part.isNull() || part.isUndefined()) continue
51
+ if (!part.isString() && !part.isStringLiteral()) return false
52
+ strings++
53
+ }
54
+ return strings > 0
55
+ }
56
+
57
+ function staticTransformArray(
58
+ expression: Expression,
59
+ source: string
60
+ ): StructuredNativeClassification {
61
+ const array = unwrapExpression(expression)
62
+ if (!Node.isArrayLiteralExpression(array)) {
63
+ return blocked(
64
+ 'structured-transform-dynamic',
65
+ `transform value "${source}" is not an inline transform array; keep dynamic and Animated arrays authored`
66
+ )
67
+ }
68
+
69
+ const functions: string[] = []
70
+ for (const element of array.getElements()) {
71
+ if (!Node.isObjectLiteralExpression(element)) {
72
+ return blocked(
73
+ 'structured-transform-dynamic',
74
+ `transform value "${source}" contains a spread or computed entry; keep dynamic and Animated arrays authored`
75
+ )
76
+ }
77
+ const properties = element.getProperties()
78
+ if (properties.length !== 1 || !Node.isPropertyAssignment(properties[0])) {
79
+ return blocked(
80
+ 'structured-transform-entry',
81
+ `transform value "${source}" must have exactly one static operation per array entry`
82
+ )
83
+ }
84
+
85
+ const property = properties[0]
86
+ const nameNode = property.getNameNode()
87
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
88
+ return blocked(
89
+ 'structured-transform-entry',
90
+ `transform value "${source}" contains a computed operation name`
91
+ )
92
+ }
93
+ const operation = Node.isStringLiteral(nameNode)
94
+ ? nameNode.getLiteralValue()
95
+ : nameNode.getText()
96
+ const initializer = unwrapExpression(property.getInitializerOrThrow())
97
+
98
+ if (operation === 'matrix' || operation === 'matrix3d') {
99
+ return blocked(
100
+ 'structured-transform-matrix',
101
+ `transform value "${source}" contains "${operation}", whose React Native array and portable CSS function shapes do not match; keep it authored`
102
+ )
103
+ }
104
+ if (!staticTransformOperations.has(operation)) {
105
+ return blocked(
106
+ 'structured-transform-operation',
107
+ `transform value "${source}" uses unsupported operation "${operation}"`
108
+ )
109
+ }
110
+
111
+ const number = numericValue(initializer)
112
+ let value: string
113
+ if (number !== null) {
114
+ if (
115
+ operation === 'rotate' ||
116
+ operation === 'rotateX' ||
117
+ operation === 'rotateY' ||
118
+ operation === 'rotateZ' ||
119
+ operation === 'skewX' ||
120
+ operation === 'skewY'
121
+ ) {
122
+ return blocked(
123
+ 'structured-transform-unit',
124
+ `transform operation "${operation}" in "${source}" needs an explicit deg or rad string`
125
+ )
126
+ }
127
+ value =
128
+ operation === 'scale' || operation === 'scaleX' || operation === 'scaleY'
129
+ ? String(number)
130
+ : `${number}px`
131
+ } else if (
132
+ Node.isStringLiteral(initializer) ||
133
+ Node.isNoSubstitutionTemplateLiteral(initializer)
134
+ ) {
135
+ value = initializer.getLiteralValue()
136
+ } else {
137
+ return blocked(
138
+ 'structured-transform-dynamic',
139
+ `transform operation "${operation}" in "${source}" is computed; keep dynamic and Animated arrays authored`
140
+ )
141
+ }
142
+ functions.push(`${operation}(${value})`)
143
+ }
144
+
145
+ if (functions.length === 0) {
146
+ return blocked(
147
+ 'structured-transform-empty',
148
+ `transform value "${source}" is empty; no portable flat base can preserve its reset semantics`
149
+ )
150
+ }
151
+
152
+ const payload = functions.join(' ')
153
+ const parsed = parseTransformString(payload)
154
+ if (parsed.errors.length) {
155
+ return blocked(
156
+ `structured-transform-${parsed.errors[0].code}`,
157
+ `${parsed.errors[0].message}; keep "${source}" authored`
158
+ )
159
+ }
160
+ return { payload, blocked: null }
161
+ }
162
+
163
+ function staticFontVariantArray(
164
+ expression: Expression,
165
+ source: string
166
+ ): StructuredNativeClassification {
167
+ const array = unwrapExpression(expression)
168
+ if (!Node.isArrayLiteralExpression(array)) {
169
+ return blocked(
170
+ 'structured-font-variant-dynamic',
171
+ `fontVariant value "${source}" is not an inline string array`
172
+ )
173
+ }
174
+
175
+ const variants: string[] = []
176
+ for (const element of array.getElements()) {
177
+ const variant = staticString(element)
178
+ if (variant === null) {
179
+ return blocked(
180
+ 'structured-font-variant-dynamic',
181
+ `fontVariant value "${source}" contains a computed entry; keep it authored`
182
+ )
183
+ }
184
+ variants.push(variant)
185
+ }
186
+ return variants.length
187
+ ? { payload: variants.join(' '), blocked: null }
188
+ : blocked(
189
+ 'structured-font-variant-empty',
190
+ `fontVariant value "${source}" is empty; no CSS token list preserves that reset`
191
+ )
192
+ }
193
+
194
+ function staticBackgroundImageArray(
195
+ expression: Expression,
196
+ source: string
197
+ ): StructuredNativeClassification {
198
+ const array = unwrapExpression(expression)
199
+ if (!Node.isArrayLiteralExpression(array)) {
200
+ return blocked(
201
+ 'structured-background-image-dynamic',
202
+ `backgroundImage value "${source}" is not an inline gradient array`
203
+ )
204
+ }
205
+ if (array.getElements().length !== 1) {
206
+ return blocked(
207
+ 'structured-background-image-layers',
208
+ `backgroundImage value "${source}" must contain exactly one gradient; native conditional evaluation does not accept multiple layers`
209
+ )
210
+ }
211
+
212
+ const gradient = array.getElements()[0]
213
+ if (!Node.isObjectLiteralExpression(gradient)) {
214
+ return blocked(
215
+ 'structured-background-image-dynamic',
216
+ `backgroundImage value "${source}" contains a spread or computed gradient`
217
+ )
218
+ }
219
+
220
+ const fields = new Map<string, Expression>()
221
+ for (const member of gradient.getProperties()) {
222
+ if (!Node.isPropertyAssignment(member)) {
223
+ return blocked(
224
+ 'structured-background-image-dynamic',
225
+ `backgroundImage value "${source}" contains a spread or computed gradient field`
226
+ )
227
+ }
228
+ const nameNode = member.getNameNode()
229
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
230
+ return blocked(
231
+ 'structured-background-image-dynamic',
232
+ `backgroundImage value "${source}" contains a computed gradient field`
233
+ )
234
+ }
235
+ const name = Node.isStringLiteral(nameNode)
236
+ ? nameNode.getLiteralValue()
237
+ : nameNode.getText()
238
+ if (
239
+ fields.has(name) ||
240
+ (name !== 'type' && name !== 'direction' && name !== 'colorStops')
241
+ ) {
242
+ return blocked(
243
+ 'structured-background-image-shape',
244
+ `backgroundImage value "${source}" contains unsupported field "${name}"`
245
+ )
246
+ }
247
+ fields.set(name, member.getInitializerOrThrow())
248
+ }
249
+
250
+ const type = fields.get('type')
251
+ const typeName = type ? staticString(type) : null
252
+ if (typeName !== 'linear-gradient') {
253
+ return blocked(
254
+ 'structured-background-image-kind',
255
+ `backgroundImage value "${source}" is not a static linear-gradient object`
256
+ )
257
+ }
258
+
259
+ const parts: string[] = []
260
+ const direction = fields.get('direction')
261
+ if (direction) {
262
+ const value = staticString(direction)
263
+ if (value === null) {
264
+ return blocked(
265
+ 'structured-background-image-dynamic',
266
+ `backgroundImage value "${source}" has a computed direction`
267
+ )
268
+ }
269
+ parts.push(value)
270
+ }
271
+
272
+ const colorStopsExpression = fields.get('colorStops')
273
+ const colorStops = colorStopsExpression
274
+ ? unwrapExpression(colorStopsExpression)
275
+ : undefined
276
+ if (!colorStops || !Node.isArrayLiteralExpression(colorStops)) {
277
+ return blocked(
278
+ 'structured-background-image-dynamic',
279
+ `backgroundImage value "${source}" does not have an inline colorStops array`
280
+ )
281
+ }
282
+ if (colorStops.getElements().length < 2) {
283
+ return blocked(
284
+ 'structured-background-image-stops',
285
+ `backgroundImage value "${source}" needs at least two color stops`
286
+ )
287
+ }
288
+
289
+ const stopElements = colorStops.getElements()
290
+ for (const [stopIndex, stop] of stopElements.entries()) {
291
+ if (!Node.isObjectLiteralExpression(stop)) {
292
+ return blocked(
293
+ 'structured-background-image-dynamic',
294
+ `backgroundImage value "${source}" contains a spread or computed color stop`
295
+ )
296
+ }
297
+
298
+ let color: string | null | undefined
299
+ let hasColor = false
300
+ let positions: string[] = []
301
+ let hasPositions = false
302
+ for (const member of stop.getProperties()) {
303
+ if (!Node.isPropertyAssignment(member)) {
304
+ return blocked(
305
+ 'structured-background-image-dynamic',
306
+ `backgroundImage value "${source}" contains a spread or computed color-stop field`
307
+ )
308
+ }
309
+ const nameNode = member.getNameNode()
310
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
311
+ return blocked(
312
+ 'structured-background-image-dynamic',
313
+ `backgroundImage value "${source}" contains a computed color-stop field`
314
+ )
315
+ }
316
+ const name = Node.isStringLiteral(nameNode)
317
+ ? nameNode.getLiteralValue()
318
+ : nameNode.getText()
319
+ if (name === 'color' && !hasColor) {
320
+ hasColor = true
321
+ const value = unwrapExpression(member.getInitializerOrThrow())
322
+ if (value.getKind() === SyntaxKind.NullKeyword) {
323
+ color = null
324
+ continue
325
+ }
326
+ const staticColor = staticString(value)
327
+ if (staticColor === null) {
328
+ return blocked(
329
+ 'structured-background-image-dynamic',
330
+ `backgroundImage value "${source}" contains a computed color`
331
+ )
332
+ }
333
+ color = staticColor
334
+ continue
335
+ }
336
+ if (name === 'positions' && !hasPositions) {
337
+ hasPositions = true
338
+ const value = unwrapExpression(member.getInitializerOrThrow())
339
+ if (!Node.isArrayLiteralExpression(value)) {
340
+ return blocked(
341
+ 'structured-background-image-dynamic',
342
+ `backgroundImage value "${source}" contains computed color-stop positions`
343
+ )
344
+ }
345
+ positions = []
346
+ for (const positionExpression of value.getElements()) {
347
+ const number = numericValue(positionExpression)
348
+ if (number !== null) {
349
+ positions.push(`${number}px`)
350
+ continue
351
+ }
352
+ const position = staticString(positionExpression)
353
+ if (position === null) {
354
+ return blocked(
355
+ 'structured-background-image-dynamic',
356
+ `backgroundImage value "${source}" contains a computed color-stop position`
357
+ )
358
+ }
359
+ if (!position.endsWith('%')) {
360
+ return blocked(
361
+ 'structured-background-image-position',
362
+ `backgroundImage value "${source}" has position "${position}"; React Native gradient objects accept numeric points or percentage strings`
363
+ )
364
+ }
365
+ positions.push(position)
366
+ }
367
+ continue
368
+ }
369
+ return blocked(
370
+ 'structured-background-image-shape',
371
+ `backgroundImage value "${source}" contains unsupported or repeated color-stop field "${name}"`
372
+ )
373
+ }
374
+ if (!hasColor || color === undefined) {
375
+ return blocked(
376
+ 'structured-background-image-shape',
377
+ `backgroundImage value "${source}" contains a color stop without a color`
378
+ )
379
+ }
380
+ if (color === null) {
381
+ if (
382
+ positions.length !== 1 ||
383
+ stopIndex === 0 ||
384
+ stopIndex === stopElements.length - 1
385
+ ) {
386
+ return blocked(
387
+ 'structured-background-image-hint',
388
+ `backgroundImage value "${source}" has an invalid transition hint; it needs one position between two colored stops`
389
+ )
390
+ }
391
+ parts.push(positions[0])
392
+ } else if (positions.length > 2) {
393
+ for (const position of positions) parts.push(`${color} ${position}`)
394
+ } else {
395
+ parts.push([color, ...positions].join(' '))
396
+ }
397
+ }
398
+
399
+ return { payload: `linear-gradient(${parts.join(', ')})`, blocked: null }
400
+ }
401
+
402
+ type StructuredNativeSerializer = (
403
+ expression: Expression,
404
+ source: string
405
+ ) => StructuredNativeClassification
406
+
407
+ /**
408
+ * Each entry names one native structure with a verified CSS-shaped spelling.
409
+ * A new object or array shape stays authored until it has its own serializer.
410
+ */
411
+ const structuredNativeSerializers: Readonly<
412
+ Partial<Record<string, StructuredNativeSerializer>>
413
+ > = Object.freeze({
414
+ backgroundImage: staticBackgroundImageArray,
415
+ fontVariant: staticFontVariantArray,
416
+ transform: staticTransformArray,
417
+ })
418
+
419
+ /**
420
+ * The migration table is intentionally property-shaped. A structured value
421
+ * stays in its natural React Native representation until a condition forces it
422
+ * into one flat string program.
423
+ */
424
+ export function classifyStructuredNativeValue(
425
+ property: string,
426
+ expression: Expression,
427
+ source: string,
428
+ registry: ModifierRegistryView
429
+ ): StructuredNativeClassification | null {
430
+ const serializer = structuredNativeSerializers[property]
431
+ if (serializer) {
432
+ if (hasOnlyStringValues(expression)) return null
433
+ const result = serializer(expression, source)
434
+ if (result.payload === null) return result
435
+
436
+ const flattened = sharedPayload(property, result.payload, registry)
437
+ const error = flattened.errors[0]
438
+ if (error || flattened.payload === null) {
439
+ return blocked(
440
+ error?.code ?? 'unsupported-structured-value',
441
+ `${property}: ${error?.message ?? `"${result.payload}" has no flat spelling`}`
442
+ )
443
+ }
444
+ return { payload: flattened.payload, blocked: null }
445
+ }
446
+ const current = unwrapExpression(expression)
447
+ if (
448
+ !Node.isObjectLiteralExpression(current) &&
449
+ !Node.isArrayLiteralExpression(current)
450
+ ) {
451
+ return null
452
+ }
453
+
454
+ const code = property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
455
+ return blocked(
456
+ `structured-${code}`,
457
+ `${property} value "${source}" has no verified CSS-shaped migration rule; keep it authored`
458
+ )
459
+ }