@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,415 @@
1
+ // v2 `transition` values that v3 removed: the `['preset', {...}]` array, the
2
+ // `default` preset selector, a per-property `type` naming a preset, and spring
3
+ // physics written directly on the transition object.
4
+ //
5
+ // The migration itself lives in the grammar package, so the codemod and the
6
+ // runtime agree on what a v2 value meant. This file only decides whether a
7
+ // value is v2 at all, reads it out of the syntax tree, and prints the result.
8
+
9
+ import { Node, SyntaxKind, type Expression, type SourceFile } from 'ts-morph'
10
+ import {
11
+ migrateLegacyTransition,
12
+ printMigratedTransition,
13
+ TRANSITION_RESERVED_KEYS,
14
+ type LegacyTransitionValue,
15
+ } from '@tamagui/style-grammar/tooling'
16
+ import type { Flag } from './convert'
17
+ import { staticLeafValue, unwrapExpression } from './expressions'
18
+ import type { createProvenance } from './provenance'
19
+
20
+ /** spring physics v2 accepted inline, which v3 spells under `spring` */
21
+ const legacySpringKeys = new Set([
22
+ 'stiffness',
23
+ 'damping',
24
+ 'mass',
25
+ 'velocity',
26
+ 'overshootClamping',
27
+ 'restDisplacementThreshold',
28
+ 'restSpeedThreshold',
29
+ 'tension',
30
+ 'friction',
31
+ 'bounciness',
32
+ 'speed',
33
+ ])
34
+
35
+ const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$-]*$/
36
+
37
+ /**
38
+ * the expression as a plain value, or null when any part of it is computed.
39
+ * a transition is small and always authored inline, so a value that is not
40
+ * fully static is left exactly as written rather than half-migrated.
41
+ */
42
+ function staticValue(expression: Expression): { value: unknown } | null {
43
+ const current = unwrapExpression(expression)
44
+
45
+ const leaf = staticLeafValue(current)
46
+ if (leaf) return { value: leaf.value }
47
+
48
+ if (Node.isArrayLiteralExpression(current)) {
49
+ const values: unknown[] = []
50
+ for (const element of current.getElements()) {
51
+ const item = staticValue(element)
52
+ if (!item) return null
53
+ values.push(item.value)
54
+ }
55
+ return { value: values }
56
+ }
57
+
58
+ if (Node.isObjectLiteralExpression(current)) {
59
+ const value: Record<string, unknown> = {}
60
+ for (const property of current.getProperties()) {
61
+ if (!Node.isPropertyAssignment(property)) return null
62
+ const nameNode = property.getNameNode()
63
+ if (Node.isComputedPropertyName(nameNode)) return null
64
+ const name = Node.isIdentifier(nameNode)
65
+ ? nameNode.getText()
66
+ : Node.isStringLiteral(nameNode)
67
+ ? nameNode.getLiteralValue()
68
+ : null
69
+ if (name === null) return null
70
+ const item = staticValue(property.getInitializerOrThrow())
71
+ if (!item) return null
72
+ value[name] = item.value
73
+ }
74
+ return { value }
75
+ }
76
+
77
+ return null
78
+ }
79
+
80
+ /** the `animateOnly` list, or null when the value is not a static string array */
81
+ function animateOnlyList(expression: Expression): readonly string[] | null {
82
+ const evaluated = staticValue(expression)
83
+ if (!evaluated || !Array.isArray(evaluated.value)) return null
84
+ const list: string[] = []
85
+ for (const item of evaluated.value) {
86
+ if (typeof item !== 'string') return null
87
+ list.push(item)
88
+ }
89
+ return list
90
+ }
91
+
92
+ /** is this the v2 spelling, so migrating it changes the meaning of the source */
93
+ function isLegacyTransition(value: unknown): boolean {
94
+ if (Array.isArray(value)) return true
95
+ if (!value || typeof value !== 'object') return false
96
+
97
+ const object = value as Record<string, unknown>
98
+ if (object.default !== undefined) return true
99
+ for (const key in object) {
100
+ if (legacySpringKeys.has(key)) return true
101
+ if (TRANSITION_RESERVED_KEYS.has(key)) continue
102
+ const entry = object[key]
103
+ if (!entry || typeof entry !== 'object') continue
104
+ const config = entry as Record<string, unknown>
105
+ if (config.type !== undefined) return true
106
+ for (const nested in config) {
107
+ if (legacySpringKeys.has(nested)) return true
108
+ }
109
+ }
110
+ return false
111
+ }
112
+
113
+ /**
114
+ * every identifier-shaped string in the value. v2 read a bare identifier there
115
+ * as the name of a configured animation, and the codemod has no config, so it
116
+ * takes the authored spelling at its word.
117
+ */
118
+ function presetNamesIn(value: unknown, names: Set<string>): void {
119
+ if (typeof value === 'string') {
120
+ if (identifierPattern.test(value)) names.add(value)
121
+ return
122
+ }
123
+ if (Array.isArray(value)) {
124
+ for (const item of value) presetNamesIn(item, names)
125
+ return
126
+ }
127
+ if (!value || typeof value !== 'object') return
128
+ for (const key in value as Record<string, unknown>) {
129
+ presetNamesIn((value as Record<string, unknown>)[key], names)
130
+ }
131
+ }
132
+
133
+ interface TransitionMigration {
134
+ /** the migrated value as source, ready to print inside `{...}` */
135
+ text: string | null
136
+ /** true when the value is a bare string, which a jsx attribute quotes */
137
+ isString: boolean
138
+ /** why a v2 value could not be migrated, for the report */
139
+ error: string | null
140
+ }
141
+
142
+ /**
143
+ * migrates one `transition` value.
144
+ *
145
+ * returns null when the value is already the v3 spelling or is not statically
146
+ * known, both of which mean the site is left exactly as authored.
147
+ */
148
+ function migrateTransitionValue(
149
+ expression: Expression | null,
150
+ animateOnly: readonly string[] | undefined
151
+ ): TransitionMigration | null {
152
+ if (expression === null) return null
153
+
154
+ const evaluated = staticValue(expression)
155
+ if (!evaluated) {
156
+ // a computed value is only a finding when the syntax around it is v2
157
+ if (animateOnly) {
158
+ return {
159
+ text: null,
160
+ isString: false,
161
+ error:
162
+ 'the removed `animateOnly` narrows a computed transition, so it needs a hand migration to `properties`',
163
+ }
164
+ }
165
+ return Node.isArrayLiteralExpression(unwrapExpression(expression))
166
+ ? {
167
+ text: null,
168
+ isString: false,
169
+ error:
170
+ 'the v2 transition array form holds a computed value, so it needs a hand migration to `{ preset }`',
171
+ }
172
+ : null
173
+ }
174
+ if (!animateOnly && !isLegacyTransition(evaluated.value)) return null
175
+
176
+ const names = new Set<string>()
177
+ presetNamesIn(evaluated.value, names)
178
+
179
+ const migrated = migrateLegacyTransition(
180
+ evaluated.value as LegacyTransitionValue,
181
+ names,
182
+ animateOnly
183
+ )
184
+ if (!migrated.ok) {
185
+ return {
186
+ text: null,
187
+ isString: false,
188
+ error: migrated.diagnostics.map((one) => one.message).join('; '),
189
+ }
190
+ }
191
+ return {
192
+ text: printMigratedTransition(migrated.value),
193
+ isString: typeof migrated.value === 'string',
194
+ error: null,
195
+ }
196
+ }
197
+
198
+ export interface TransitionReport {
199
+ label: string
200
+ line: number
201
+ before: string
202
+ after: string
203
+ flags: Flag[]
204
+ }
205
+
206
+ /**
207
+ * rewrites every v2 `transition` value in the file.
208
+ *
209
+ * this is its own pass rather than part of a style site, because it respells
210
+ * one value and nothing else. routing it through site assembly would pull the
211
+ * whole element's already-migrated props back through the program printer for
212
+ * no reason.
213
+ */
214
+ export function convertTransitions(
215
+ sourceFile: SourceFile,
216
+ provenance: ReturnType<typeof createProvenance>,
217
+ write: boolean
218
+ ): TransitionReport[] {
219
+ const found: Array<{
220
+ label: string
221
+ node: Node
222
+ value: Expression | null
223
+ /** the removed `animateOnly` sitting next to it, to fold in and delete */
224
+ animateOnly: readonly string[] | undefined
225
+ animateOnlyNode: Node | null
226
+ /** set when the site cannot be migrated at all, so it only reports */
227
+ blocked: string | null
228
+ replace: (text: string) => void
229
+ }> = []
230
+
231
+ /** the `animateOnly` next to a transition, and how to read its list */
232
+ const readAnimateOnly = (
233
+ node: Node | null
234
+ ): { list: readonly string[] | undefined } => {
235
+ if (!node) return { list: undefined }
236
+ const initializer = Node.isJsxAttribute(node)
237
+ ? node.getInitializer()
238
+ : Node.isPropertyAssignment(node)
239
+ ? node.getInitializer()
240
+ : undefined
241
+ const expression =
242
+ initializer && Node.isJsxExpression(initializer)
243
+ ? initializer.getExpression()
244
+ : (initializer as Expression | undefined)
245
+ return { list: (expression && animateOnlyList(expression)) ?? undefined }
246
+ }
247
+
248
+ for (const opening of [
249
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
250
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
251
+ ]) {
252
+ if (!provenance.isTamaguiElement(opening)) continue
253
+
254
+ type Attribute = ReturnType<typeof opening.getAttributes>[number]
255
+ let transitionNode: Attribute | null = null
256
+ let animateOnlyNode: Attribute | null = null
257
+ for (const attribute of opening.getAttributes()) {
258
+ if (!Node.isJsxAttribute(attribute)) continue
259
+ const name = attribute.getNameNode().getText()
260
+ if (name === 'transition') transitionNode = attribute
261
+ else if (name === 'animateOnly') animateOnlyNode = attribute
262
+ }
263
+
264
+ // `transition="quick"` is a string literal initializer; `transition={...}`
265
+ // wraps its value in a JsxExpression. both carry a value to migrate.
266
+ const initializer = transitionNode?.getInitializer()
267
+ const expression = Node.isJsxExpression(initializer)
268
+ ? (initializer.getExpression() ?? null)
269
+ : Node.isStringLiteral(initializer)
270
+ ? initializer
271
+ : null
272
+
273
+ const { list: animateOnly } = readAnimateOnly(animateOnlyNode)
274
+ const tag = opening.getTagNameNode().getText()
275
+
276
+ if (animateOnlyNode && (!transitionNode || !animateOnly)) {
277
+ // an `animateOnly` with no transition to fold into, or a computed list:
278
+ // both need a person, so report rather than guess at a property list
279
+ found.push({
280
+ label: `<${tag} animateOnly>`,
281
+ node: animateOnlyNode,
282
+ value: null,
283
+ animateOnly: undefined,
284
+ animateOnlyNode: null,
285
+ blocked: transitionNode
286
+ ? 'the removed `animateOnly` holds a computed list; spell it as `properties` inside the transition'
287
+ : 'the removed `animateOnly` has no `transition` next to it; spell it as `properties` wherever the transition is authored',
288
+ replace: () => {},
289
+ })
290
+ continue
291
+ }
292
+ if (!transitionNode) continue
293
+
294
+ found.push({
295
+ label: `<${tag} transition>`,
296
+ node: transitionNode,
297
+ value: expression,
298
+ animateOnly,
299
+ animateOnlyNode,
300
+ blocked: null,
301
+ replace: (text) =>
302
+ transitionNode!.replaceWithText(
303
+ text.startsWith('"') ? `transition=${text}` : `transition={${text}}`
304
+ ),
305
+ })
306
+ }
307
+
308
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
309
+ if (!provenance.isTamaguiStyledCall(call)) continue
310
+ for (const literal of call.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
311
+ let transitionNode: ReturnType<typeof literal.getProperty> | undefined
312
+ let animateOnlyNode: typeof transitionNode
313
+ for (const property of literal.getProperties()) {
314
+ if (!Node.isPropertyAssignment(property)) continue
315
+ const nameNode = property.getNameNode()
316
+ const name = Node.isIdentifier(nameNode)
317
+ ? nameNode.getText()
318
+ : Node.isStringLiteral(nameNode)
319
+ ? nameNode.getLiteralValue()
320
+ : null
321
+ if (name === 'transition') transitionNode = property
322
+ else if (name === 'animateOnly') animateOnlyNode = property
323
+ }
324
+
325
+ const { list: animateOnly } = readAnimateOnly(animateOnlyNode ?? null)
326
+
327
+ if (animateOnlyNode && (!transitionNode || !animateOnly)) {
328
+ found.push({
329
+ label: `styled(…) animateOnly`,
330
+ node: animateOnlyNode,
331
+ value: null,
332
+ animateOnly: undefined,
333
+ animateOnlyNode: null,
334
+ blocked: transitionNode
335
+ ? 'the removed `animateOnly` holds a computed list; spell it as `properties` inside the transition'
336
+ : 'the removed `animateOnly` has no `transition` next to it; spell it as `properties` wherever the transition is authored',
337
+ replace: () => {},
338
+ })
339
+ continue
340
+ }
341
+ if (!transitionNode || !Node.isPropertyAssignment(transitionNode)) continue
342
+
343
+ const assignment = transitionNode
344
+ found.push({
345
+ label: `styled(…) transition`,
346
+ node: assignment,
347
+ value: assignment.getInitializerOrThrow(),
348
+ animateOnly,
349
+ animateOnlyNode: animateOnlyNode ?? null,
350
+ blocked: null,
351
+ replace: (text) => assignment.replaceWithText(`transition: ${text}`),
352
+ })
353
+ }
354
+ }
355
+
356
+ const reports: TransitionReport[] = []
357
+ // one edit list, applied strictly back to front so every earlier node's
358
+ // position stays valid. an `animateOnly` whose list folded into a transition
359
+ // is its own edit and takes its turn in that same order.
360
+ const edits: Array<{ start: number; apply: () => void }> = []
361
+
362
+ for (const candidate of found) {
363
+ const migrated = candidate.blocked
364
+ ? { text: null, isString: false, error: candidate.blocked }
365
+ : migrateTransitionValue(candidate.value, candidate.animateOnly)
366
+ if (migrated === null) continue
367
+ const report: TransitionReport = {
368
+ label: candidate.label,
369
+ line: candidate.node.getStartLineNumber(),
370
+ before: candidate.node.getText(),
371
+ after: candidate.node.getText(),
372
+ flags: [],
373
+ }
374
+ if (migrated.text === null) {
375
+ report.flags.push({
376
+ code: 'unsupported-legacy-value',
377
+ detail: migrated.error ?? '',
378
+ })
379
+ } else {
380
+ // a bare string reads as an ordinary jsx attribute, not an expression
381
+ const text =
382
+ migrated.isString && Node.isJsxAttribute(candidate.node)
383
+ ? `"${migrated.text.slice(1, -1).replace(/"/g, '&quot;')}"`
384
+ : migrated.text
385
+ report.after = Node.isJsxAttribute(candidate.node)
386
+ ? migrated.isString
387
+ ? `transition=${text}`
388
+ : `transition={${text}}`
389
+ : `transition: ${text}`
390
+ edits.push({
391
+ start: candidate.node.getStart(),
392
+ apply: () => candidate.replace(text),
393
+ })
394
+ const removed = candidate.animateOnlyNode
395
+ if (removed) {
396
+ edits.push({
397
+ start: removed.getStart(),
398
+ apply: () => {
399
+ if (Node.isJsxAttribute(removed)) removed.remove()
400
+ else if (Node.isPropertyAssignment(removed)) removed.remove()
401
+ },
402
+ })
403
+ }
404
+ }
405
+ reports.push(report)
406
+ }
407
+
408
+ if (write) {
409
+ edits.sort((left, right) => right.start - left.start)
410
+ for (const edit of edits) edit.apply()
411
+ }
412
+
413
+ reports.sort((left, right) => left.line - right.line)
414
+ return reports
415
+ }