@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.
- package/README.md +234 -1
- package/dist/builtInNames.mjs +14 -0
- package/dist/builtInNames.mjs.map +1 -0
- package/dist/containers.mjs +141 -0
- package/dist/containers.mjs.map +1 -0
- package/dist/convert.mjs +1233 -0
- package/dist/convert.mjs.map +1 -0
- package/dist/expressions.mjs +188 -0
- package/dist/expressions.mjs.map +1 -0
- package/dist/functionalVariants.mjs +469 -0
- package/dist/functionalVariants.mjs.map +1 -0
- package/dist/grammar.mjs +73 -0
- package/dist/grammar.mjs.map +1 -0
- package/dist/index.mjs +374 -0
- package/dist/index.mjs.map +1 -0
- package/dist/legacyConditions.mjs +293 -0
- package/dist/legacyConditions.mjs.map +1 -0
- package/dist/legacyNames.mjs +130 -0
- package/dist/legacyNames.mjs.map +1 -0
- package/dist/provenance.mjs +137 -0
- package/dist/provenance.mjs.map +1 -0
- package/dist/report.mjs +188 -0
- package/dist/report.mjs.map +1 -0
- package/dist/sheetAnatomy.mjs +200 -0
- package/dist/sheetAnatomy.mjs.map +1 -0
- package/dist/structuredNative.mjs +275 -0
- package/dist/structuredNative.mjs.map +1 -0
- package/dist/transition.mjs +257 -0
- package/dist/transition.mjs.map +1 -0
- package/package.json +35 -7
- package/src/builtInNames.ts +23 -0
- package/src/containers.ts +227 -0
- package/src/convert.ts +1977 -0
- package/src/expressions.ts +220 -0
- package/src/functionalVariants.ts +642 -0
- package/src/grammar.ts +190 -0
- package/src/index.ts +589 -0
- package/src/legacyConditions.ts +357 -0
- package/src/legacyNames.ts +160 -0
- package/src/provenance.ts +210 -0
- package/src/report.ts +362 -0
- package/src/sheetAnatomy.ts +277 -0
- package/src/structuredNative.ts +459 -0
- 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, '"')}"`
|
|
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
|
+
}
|