@tamagui/codemod-flat-values 3.0.0-beta.922.1 → 3.0.0
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 +20 -0
- package/dist/convert.mjs +1 -1
- package/dist/convert.mjs.map +1 -1
- package/dist/expressions.mjs +1 -1
- package/dist/expressions.mjs.map +1 -1
- package/dist/grammar.mjs +7 -4
- package/dist/grammar.mjs.map +1 -1
- package/dist/index.mjs +335 -13
- package/dist/index.mjs.map +1 -1
- package/dist/provenance.mjs +10 -1
- package/dist/provenance.mjs.map +1 -1
- package/dist/report.mjs +16 -3
- package/dist/report.mjs.map +1 -1
- package/dist/transition.mjs +257 -0
- package/dist/transition.mjs.map +1 -0
- package/package.json +8 -8
- package/src/convert.ts +1 -1
- package/src/expressions.ts +1 -1
- package/src/grammar.ts +11 -4
- package/src/index.ts +478 -14
- package/src/provenance.ts +11 -0
- package/src/report.ts +36 -2
- package/src/transition.ts +415 -0
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { dirname, relative, resolve } from 'node:path'
|
|
4
|
-
import { resolveTamaguiHost } from '@tamagui/language-service/host'
|
|
5
4
|
import { stylePropsTextOnly } from '@tamagui/helpers'
|
|
6
5
|
import {
|
|
7
6
|
IndentationText,
|
|
@@ -12,13 +11,17 @@ import {
|
|
|
12
11
|
ScriptTarget,
|
|
13
12
|
SyntaxKind,
|
|
14
13
|
ts,
|
|
14
|
+
type CallExpression,
|
|
15
15
|
type Expression,
|
|
16
|
+
type JsxOpeningElement,
|
|
17
|
+
type JsxSelfClosingElement,
|
|
16
18
|
type ObjectLiteralExpression,
|
|
19
|
+
type PropertyAssignment,
|
|
17
20
|
type SourceFile,
|
|
18
21
|
} from 'ts-morph'
|
|
19
22
|
import { planContainers, type ContainerPlan } from './containers'
|
|
20
23
|
import { convertJsxSite, convertStyleObject, type SiteReport } from './convert'
|
|
21
|
-
import { compact, unwrapExpression } from './expressions'
|
|
24
|
+
import { compact, numericValue, runtimeType, unwrapExpression } from './expressions'
|
|
22
25
|
import {
|
|
23
26
|
addFunctionalVariantTypeImports,
|
|
24
27
|
convertFunctionalVariants,
|
|
@@ -37,8 +40,409 @@ import {
|
|
|
37
40
|
import { createProvenance } from './provenance'
|
|
38
41
|
import { renderReport, type FileReport } from './report'
|
|
39
42
|
import { convertSheetFrames } from './sheetAnatomy'
|
|
43
|
+
import { convertTransitions } from './transition'
|
|
44
|
+
import { isLegacyConditionName } from './legacyNames'
|
|
40
45
|
|
|
41
46
|
type Provenance = ReturnType<typeof createProvenance>
|
|
47
|
+
type SourceSemantics = 'v2-pixels' | 'v3-ratios'
|
|
48
|
+
|
|
49
|
+
interface SourceEdit {
|
|
50
|
+
start: number
|
|
51
|
+
end: number
|
|
52
|
+
text: string
|
|
53
|
+
dynamic: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function propertyName(node: Node): string | null {
|
|
57
|
+
if (
|
|
58
|
+
Node.isIdentifier(node) ||
|
|
59
|
+
Node.isStringLiteral(node) ||
|
|
60
|
+
Node.isNumericLiteral(node)
|
|
61
|
+
) {
|
|
62
|
+
return node.getText().replace(/^['"]|['"]$/g, '')
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isLineHeightName(name: string | null): boolean {
|
|
68
|
+
return name === 'lineHeight' || name === 'lh'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractedTokenValue(expression: Expression): boolean {
|
|
72
|
+
const nodes = [expression, ...expression.getDescendants()]
|
|
73
|
+
return nodes.some(
|
|
74
|
+
(node) =>
|
|
75
|
+
(Node.isPropertyAccessExpression(node) && node.getName() === 'val') ||
|
|
76
|
+
(Node.isCallExpression(node) &&
|
|
77
|
+
Node.isIdentifier(node.getExpression()) &&
|
|
78
|
+
node.getExpression().getText() === 'getVariableValue')
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pixelReplacement(
|
|
83
|
+
expression: Expression
|
|
84
|
+
): { text: string; dynamic: boolean } | { flag: SiteReport['flags'][number] } | null {
|
|
85
|
+
const current = unwrapExpression(expression)
|
|
86
|
+
const number = numericValue(current)
|
|
87
|
+
if (number !== null) return { text: JSON.stringify(`${number}px`), dynamic: false }
|
|
88
|
+
const type = runtimeType(current)
|
|
89
|
+
if (type.kind === 'string') return null
|
|
90
|
+
|
|
91
|
+
if (extractedTokenValue(current)) {
|
|
92
|
+
return {
|
|
93
|
+
flag: {
|
|
94
|
+
code: 'ambiguous-line-height-token-value',
|
|
95
|
+
detail: `lineHeight value "${compact(current.getText())}" extracts a token value whose units are no longer visible; replace it with an explicit ratio or px length by hand`,
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (type.kind === 'number') {
|
|
101
|
+
return { text: `\`\${${current.getText()}}px\``, dynamic: true }
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
flag: {
|
|
105
|
+
code: 'ambiguous-line-height-expression',
|
|
106
|
+
detail: `lineHeight value "${compact(current.getText())}" has no provable numeric type; replace old pixel semantics with an explicit px length by hand`,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function declarationContainsNumericLineHeight(expression: Expression): boolean {
|
|
112
|
+
const symbol = expression.getSymbol()
|
|
113
|
+
if (!symbol) return false
|
|
114
|
+
for (const declaration of symbol.getDeclarations()) {
|
|
115
|
+
for (const property of [
|
|
116
|
+
...(Node.isPropertyAssignment(declaration) ? [declaration] : []),
|
|
117
|
+
...declaration.getDescendantsOfKind(SyntaxKind.PropertyAssignment),
|
|
118
|
+
]) {
|
|
119
|
+
if (!isLineHeightName(propertyName(property.getNameNode()))) continue
|
|
120
|
+
const initializer = property.getInitializer()
|
|
121
|
+
if (!initializer) continue
|
|
122
|
+
const value = unwrapExpression(initializer)
|
|
123
|
+
if (numericValue(value) !== null || runtimeType(value).kind === 'number')
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function lineHeightValueIsUnmigrated(expression: Expression): boolean {
|
|
131
|
+
const current = unwrapExpression(expression)
|
|
132
|
+
if (Node.isObjectLiteralExpression(current)) {
|
|
133
|
+
return current.getProperties().some((property) => {
|
|
134
|
+
if (!Node.isPropertyAssignment(property)) return false
|
|
135
|
+
const initializer = property.getInitializer()
|
|
136
|
+
return initializer !== undefined && lineHeightValueIsUnmigrated(initializer)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
if (Node.isConditionalExpression(current)) {
|
|
140
|
+
return (
|
|
141
|
+
lineHeightValueIsUnmigrated(current.getWhenTrue()) ||
|
|
142
|
+
lineHeightValueIsUnmigrated(current.getWhenFalse())
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return pixelReplacement(current) !== null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function replaceWithin(
|
|
149
|
+
source: string,
|
|
150
|
+
sourceStart: number,
|
|
151
|
+
edits: readonly SourceEdit[]
|
|
152
|
+
): string {
|
|
153
|
+
let output = source
|
|
154
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
155
|
+
output =
|
|
156
|
+
output.slice(0, edit.start - sourceStart) +
|
|
157
|
+
edit.text +
|
|
158
|
+
output.slice(edit.end - sourceStart)
|
|
159
|
+
}
|
|
160
|
+
return output
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function migrateLegacyLineHeights(
|
|
164
|
+
sourceFile: SourceFile,
|
|
165
|
+
provenance: Provenance
|
|
166
|
+
): SiteReport[] {
|
|
167
|
+
const edits: SourceEdit[] = []
|
|
168
|
+
const claimed = new Set<number>()
|
|
169
|
+
const reports: SiteReport[] = []
|
|
170
|
+
|
|
171
|
+
const scanLineHeightExpression = (
|
|
172
|
+
expression: Expression,
|
|
173
|
+
contextEdits: SourceEdit[],
|
|
174
|
+
flags: SiteReport['flags']
|
|
175
|
+
) => {
|
|
176
|
+
const current = unwrapExpression(expression)
|
|
177
|
+
if (Node.isObjectLiteralExpression(current)) {
|
|
178
|
+
for (const property of current.getProperties()) {
|
|
179
|
+
if (!Node.isPropertyAssignment(property)) continue
|
|
180
|
+
const initializer = property.getInitializer()
|
|
181
|
+
if (initializer) scanLineHeightExpression(initializer, contextEdits, flags)
|
|
182
|
+
}
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if (Node.isConditionalExpression(current)) {
|
|
186
|
+
scanLineHeightExpression(current.getWhenTrue(), contextEdits, flags)
|
|
187
|
+
scanLineHeightExpression(current.getWhenFalse(), contextEdits, flags)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (claimed.has(current.getStart())) return
|
|
191
|
+
const replacement = pixelReplacement(current)
|
|
192
|
+
if (!replacement) return
|
|
193
|
+
claimed.add(current.getStart())
|
|
194
|
+
if ('flag' in replacement) {
|
|
195
|
+
flags.push(replacement.flag)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
const edit = {
|
|
199
|
+
start: current.getStart(),
|
|
200
|
+
end: current.getEnd(),
|
|
201
|
+
text: replacement.text,
|
|
202
|
+
dynamic: replacement.dynamic,
|
|
203
|
+
}
|
|
204
|
+
edits.push(edit)
|
|
205
|
+
contextEdits.push(edit)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const scanProperty = (
|
|
209
|
+
property: PropertyAssignment,
|
|
210
|
+
contextEdits: SourceEdit[],
|
|
211
|
+
flags: SiteReport['flags']
|
|
212
|
+
) => {
|
|
213
|
+
if (!isLineHeightName(propertyName(property.getNameNode()))) return
|
|
214
|
+
const initializer = property.getInitializer()
|
|
215
|
+
if (initializer) scanLineHeightExpression(initializer, contextEdits, flags)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const scanObject = (
|
|
219
|
+
object: ObjectLiteralExpression,
|
|
220
|
+
contextEdits: SourceEdit[],
|
|
221
|
+
flags: SiteReport['flags']
|
|
222
|
+
) => {
|
|
223
|
+
for (const property of object.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
224
|
+
scanProperty(property, contextEdits, flags)
|
|
225
|
+
}
|
|
226
|
+
for (const property of object.getProperties()) {
|
|
227
|
+
if (Node.isPropertyAssignment(property)) scanProperty(property, contextEdits, flags)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const addReport = (
|
|
232
|
+
node: Node,
|
|
233
|
+
kind: SiteReport['kind'],
|
|
234
|
+
label: string,
|
|
235
|
+
contextEdits: SourceEdit[],
|
|
236
|
+
flags: SiteReport['flags']
|
|
237
|
+
) => {
|
|
238
|
+
if (!contextEdits.length && !flags.length) return
|
|
239
|
+
const before = compact(node.getText())
|
|
240
|
+
reports.push({
|
|
241
|
+
kind,
|
|
242
|
+
label,
|
|
243
|
+
line: sourceFile.getLineAndColumnAtPos(node.getStart()).line,
|
|
244
|
+
before,
|
|
245
|
+
after: compact(replaceWithin(node.getText(), node.getStart(), contextEdits)),
|
|
246
|
+
programs: contextEdits.map((edit) => ({
|
|
247
|
+
name: 'lineHeight',
|
|
248
|
+
value: edit.text,
|
|
249
|
+
dynamic: edit.dynamic,
|
|
250
|
+
})),
|
|
251
|
+
assessments: [],
|
|
252
|
+
assessmentVerdict: 'clean',
|
|
253
|
+
warnings: [],
|
|
254
|
+
flags,
|
|
255
|
+
inventory: [],
|
|
256
|
+
pending: [],
|
|
257
|
+
notes: [
|
|
258
|
+
contextEdits.length
|
|
259
|
+
? 'numeric lineHeight used V2 pixel semantics and now carries an explicit px unit'
|
|
260
|
+
: 'lineHeight was left authored for a manual unit decision',
|
|
261
|
+
],
|
|
262
|
+
legacyLeft: flags.length,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const scanJsx = (opening: JsxOpeningElement | JsxSelfClosingElement) => {
|
|
267
|
+
const contextEdits: SourceEdit[] = []
|
|
268
|
+
const flags: SiteReport['flags'] = []
|
|
269
|
+
for (const attribute of opening.getAttributes()) {
|
|
270
|
+
if (Node.isJsxSpreadAttribute(attribute)) {
|
|
271
|
+
const expression = unwrapExpression(attribute.getExpression())
|
|
272
|
+
if (Node.isObjectLiteralExpression(expression)) {
|
|
273
|
+
scanObject(expression, contextEdits, flags)
|
|
274
|
+
} else if (declarationContainsNumericLineHeight(expression)) {
|
|
275
|
+
flags.push({
|
|
276
|
+
code: 'ambiguous-shared-line-height-style',
|
|
277
|
+
detail: `spread "${compact(expression.getText())}" contains numeric lineHeight and may also be consumed outside Tamagui; migrate it at this Tamagui boundary by hand`,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
if (!Node.isJsxAttribute(attribute)) continue
|
|
283
|
+
const nameNode = attribute.getNameNode()
|
|
284
|
+
const name = Node.isIdentifier(nameNode) ? nameNode.getText() : null
|
|
285
|
+
const initializer = attribute.getInitializer()
|
|
286
|
+
if (isLineHeightName(name)) {
|
|
287
|
+
if (!Node.isJsxExpression(initializer)) continue
|
|
288
|
+
const expression = initializer.getExpression()
|
|
289
|
+
if (!expression) continue
|
|
290
|
+
const current = unwrapExpression(expression)
|
|
291
|
+
const number = numericValue(current)
|
|
292
|
+
if (number !== null && !claimed.has(current.getStart())) {
|
|
293
|
+
claimed.add(current.getStart())
|
|
294
|
+
const edit = {
|
|
295
|
+
start: initializer.getStart(),
|
|
296
|
+
end: initializer.getEnd(),
|
|
297
|
+
text: JSON.stringify(`${number}px`),
|
|
298
|
+
dynamic: false,
|
|
299
|
+
}
|
|
300
|
+
edits.push(edit)
|
|
301
|
+
contextEdits.push(edit)
|
|
302
|
+
} else {
|
|
303
|
+
scanLineHeightExpression(current, contextEdits, flags)
|
|
304
|
+
}
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
if (isLegacyConditionName(name || '') && Node.isJsxExpression(initializer)) {
|
|
308
|
+
const expression = initializer.getExpression()
|
|
309
|
+
if (expression) {
|
|
310
|
+
const current = unwrapExpression(expression)
|
|
311
|
+
for (const object of [
|
|
312
|
+
...(Node.isObjectLiteralExpression(current) ? [current] : []),
|
|
313
|
+
...current.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression),
|
|
314
|
+
]) {
|
|
315
|
+
scanObject(object, contextEdits, flags)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
if (name !== 'style' || !Node.isJsxExpression(initializer)) continue
|
|
321
|
+
const expression = initializer.getExpression()
|
|
322
|
+
if (!expression) continue
|
|
323
|
+
const current = unwrapExpression(expression)
|
|
324
|
+
const objects = [
|
|
325
|
+
...(Node.isObjectLiteralExpression(current) ? [current] : []),
|
|
326
|
+
...current.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression),
|
|
327
|
+
]
|
|
328
|
+
if (objects.length) {
|
|
329
|
+
for (const object of objects) scanObject(object, contextEdits, flags)
|
|
330
|
+
} else if (declarationContainsNumericLineHeight(current)) {
|
|
331
|
+
flags.push({
|
|
332
|
+
code: 'ambiguous-shared-line-height-style',
|
|
333
|
+
detail: `style "${compact(current.getText())}" contains numeric lineHeight and may also be consumed by React Native or Restyle; migrate it at this Tamagui boundary by hand`,
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
addReport(
|
|
338
|
+
opening,
|
|
339
|
+
'jsx',
|
|
340
|
+
`<${opening.getTagNameNode().getText()}> lineHeight`,
|
|
341
|
+
contextEdits,
|
|
342
|
+
flags
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const scanCall = (call: CallExpression, label: string) => {
|
|
347
|
+
const contextEdits: SourceEdit[] = []
|
|
348
|
+
const flags: SiteReport['flags'] = []
|
|
349
|
+
for (const argument of call.getArguments()) {
|
|
350
|
+
for (const object of [
|
|
351
|
+
...(Node.isObjectLiteralExpression(argument) ? [argument] : []),
|
|
352
|
+
...argument.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression),
|
|
353
|
+
]) {
|
|
354
|
+
scanObject(object, contextEdits, flags)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
addReport(call, 'styled', label, contextEdits, flags)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const wrapperCache = new Map<Node, boolean>()
|
|
361
|
+
for (const opening of [
|
|
362
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
|
|
363
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
|
|
364
|
+
]) {
|
|
365
|
+
if (provenance.isTamaguiElement(opening)) {
|
|
366
|
+
scanJsx(opening)
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
const hasLegacyValue = opening.getAttributes().some((attribute) => {
|
|
370
|
+
if (!Node.isJsxAttribute(attribute)) return false
|
|
371
|
+
const name = propertyName(attribute.getNameNode())
|
|
372
|
+
const initializer = attribute.getInitializer()
|
|
373
|
+
if (!initializer || !Node.isJsxExpression(initializer)) return false
|
|
374
|
+
const expression = initializer.getExpression()
|
|
375
|
+
if (!expression) return false
|
|
376
|
+
if (isLineHeightName(name)) return lineHeightValueIsUnmigrated(expression)
|
|
377
|
+
if (name !== 'style') return false
|
|
378
|
+
const current = unwrapExpression(expression)
|
|
379
|
+
const properties = current.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
|
|
380
|
+
return properties.length
|
|
381
|
+
? properties.some((property) => {
|
|
382
|
+
const value = property.getInitializer()
|
|
383
|
+
return (
|
|
384
|
+
isLineHeightName(propertyName(property.getNameNode())) &&
|
|
385
|
+
value !== undefined &&
|
|
386
|
+
lineHeightValueIsUnmigrated(value)
|
|
387
|
+
)
|
|
388
|
+
})
|
|
389
|
+
: declarationContainsNumericLineHeight(current)
|
|
390
|
+
})
|
|
391
|
+
if (!hasLegacyValue) continue
|
|
392
|
+
|
|
393
|
+
const tag = opening.getTagNameNode()
|
|
394
|
+
const symbol = tag.getSymbol()
|
|
395
|
+
if (!symbol) continue
|
|
396
|
+
const declarations = (symbol.getAliasedSymbol() ?? symbol).getDeclarations()
|
|
397
|
+
const isLocalWrapper = declarations.some((declaration) => {
|
|
398
|
+
const cached = wrapperCache.get(declaration)
|
|
399
|
+
if (cached !== undefined) return cached
|
|
400
|
+
const file = declaration.getSourceFile()
|
|
401
|
+
const filePath = file.getFilePath()
|
|
402
|
+
const local =
|
|
403
|
+
!file.isDeclarationFile() &&
|
|
404
|
+
!filePath.includes('/node_modules/') &&
|
|
405
|
+
!relative(projectRoot, filePath).startsWith('..')
|
|
406
|
+
const wrapsTamagui =
|
|
407
|
+
local &&
|
|
408
|
+
[
|
|
409
|
+
...declaration.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
|
|
410
|
+
...declaration.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
|
|
411
|
+
].some((child) => provenance.isTamaguiElement(child))
|
|
412
|
+
wrapperCache.set(declaration, wrapsTamagui)
|
|
413
|
+
return wrapsTamagui
|
|
414
|
+
})
|
|
415
|
+
if (!isLocalWrapper) continue
|
|
416
|
+
addReport(
|
|
417
|
+
opening,
|
|
418
|
+
'jsx',
|
|
419
|
+
`<${tag.getText()}> lineHeight`,
|
|
420
|
+
[],
|
|
421
|
+
[
|
|
422
|
+
{
|
|
423
|
+
code: 'ambiguous-wrapper-line-height',
|
|
424
|
+
detail: `local wrapper <${tag.getText()}> renders Tamagui JSX, so numeric lineHeight may still be V2 pixels, but the wrapper may transform units; migrate this call site by hand`,
|
|
425
|
+
},
|
|
426
|
+
]
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
431
|
+
if (provenance.isTamaguiStyledCall(call)) {
|
|
432
|
+
scanCall(
|
|
433
|
+
call,
|
|
434
|
+
`styled(${compact(call.getArguments()[0]?.getText() ?? 'unknown')}, …) lineHeight`
|
|
435
|
+
)
|
|
436
|
+
} else if (provenance.isTamaguiStyleCall(call)) {
|
|
437
|
+
scanCall(call, `${compact(call.getExpression().getText())}(…) lineHeight`)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (edits.length) {
|
|
442
|
+
sourceFile.replaceWithText(replaceWithin(sourceFile.getFullText(), 0, edits))
|
|
443
|
+
}
|
|
444
|
+
return reports
|
|
445
|
+
}
|
|
42
446
|
|
|
43
447
|
// every path is resolved against the directory the codemod is invoked from, so it
|
|
44
448
|
// migrates the project you are standing in whether that is an app or this repo
|
|
@@ -99,7 +503,7 @@ function collectFiles(inputs: readonly string[]): {
|
|
|
99
503
|
jsx: 4,
|
|
100
504
|
target: ScriptTarget.ES2020,
|
|
101
505
|
module: ModuleKind.ESNext,
|
|
102
|
-
moduleResolution: ModuleResolutionKind.
|
|
506
|
+
moduleResolution: ModuleResolutionKind.Bundler,
|
|
103
507
|
skipLibCheck: true,
|
|
104
508
|
strictNullChecks: true,
|
|
105
509
|
baseUrl: projectRoot,
|
|
@@ -324,12 +728,23 @@ for (const [shorthand, longhand] of Object.entries(shorthands)) {
|
|
|
324
728
|
}
|
|
325
729
|
|
|
326
730
|
function typeAwareHost(node: Node): HostView | undefined {
|
|
327
|
-
const checker = node.getProject().getTypeChecker()
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
731
|
+
const checker = node.getProject().getTypeChecker()
|
|
732
|
+
const componentType = checker.getTypeAtLocation(node)
|
|
733
|
+
if (!componentType.getProperty('staticConfig')) return undefined
|
|
734
|
+
|
|
735
|
+
const propsTypes = componentType
|
|
736
|
+
.getCallSignatures()
|
|
737
|
+
.flatMap((signature) => signature.getParameters().slice(0, 1))
|
|
738
|
+
.map((props) => checker.getTypeOfSymbolAtLocation(props, node))
|
|
739
|
+
if (propsTypes.length === 0) return undefined
|
|
740
|
+
|
|
741
|
+
const host: HostView = {
|
|
742
|
+
displayName: node.getText(),
|
|
743
|
+
accepts: (property) =>
|
|
744
|
+
propsTypes.some(
|
|
745
|
+
(propsType) => propsType.getApparentType().getProperty(property) !== undefined
|
|
746
|
+
),
|
|
747
|
+
}
|
|
333
748
|
|
|
334
749
|
// The conversion resolves an authored shorthand to its longhand before asking
|
|
335
750
|
// the host, and `onlyAllowShorthands: true` omits exactly those longhands from
|
|
@@ -357,14 +772,32 @@ function inspectFile(
|
|
|
357
772
|
sourceFile: SourceFile,
|
|
358
773
|
registry: ModifierRegistryView,
|
|
359
774
|
provenance: Provenance,
|
|
775
|
+
sourceSemantics: SourceSemantics,
|
|
776
|
+
lineHeightOnly: boolean,
|
|
360
777
|
write: boolean
|
|
361
778
|
): FileReport {
|
|
779
|
+
const lineHeightSites =
|
|
780
|
+
sourceSemantics === 'v2-pixels'
|
|
781
|
+
? migrateLegacyLineHeights(sourceFile, provenance)
|
|
782
|
+
: []
|
|
783
|
+
if (lineHeightOnly) {
|
|
784
|
+
return {
|
|
785
|
+
file: relative(projectRoot, sourceFile.getFilePath()),
|
|
786
|
+
sites: lineHeightSites,
|
|
787
|
+
functionalVariants: [],
|
|
788
|
+
sheetFrames: [],
|
|
789
|
+
transitions: [],
|
|
790
|
+
}
|
|
791
|
+
}
|
|
362
792
|
// the anatomy rewrite runs first so the Background it adds, and the surface
|
|
363
793
|
// props it moves there, go through the flat-value conversion below
|
|
364
794
|
const sheetFrames = convertSheetFrames(sourceFile, provenance, write)
|
|
795
|
+
// the transition respelling touches one value, so it runs on its own rather
|
|
796
|
+
// than turning every element that has a v2 transition into a style site
|
|
797
|
+
const transitions = convertTransitions(sourceFile, provenance, write)
|
|
365
798
|
const containers = planContainers(sourceFile, registry)
|
|
366
799
|
const targets = conversionTargets(sourceFile.getFilePath())
|
|
367
|
-
const sites: SiteReport[] = []
|
|
800
|
+
const sites: SiteReport[] = [...lineHeightSites]
|
|
368
801
|
const functionalVariants: FunctionalVariantReport[] = []
|
|
369
802
|
const requiredTypeImports: RequiredTypeImport[] = []
|
|
370
803
|
const styledCalls = sourceFile
|
|
@@ -427,6 +860,7 @@ function inspectFile(
|
|
|
427
860
|
sites,
|
|
428
861
|
functionalVariants,
|
|
429
862
|
sheetFrames,
|
|
863
|
+
transitions,
|
|
430
864
|
}
|
|
431
865
|
}
|
|
432
866
|
|
|
@@ -440,6 +874,10 @@ const usage = `Converts Tamagui style syntax to V3 flat property values and repo
|
|
|
440
874
|
)})
|
|
441
875
|
--json <path> also write the machine-readable report
|
|
442
876
|
--write rewrite every statically safe conversion in place
|
|
877
|
+
--source-semantics <v2-pixels|v3-ratios>
|
|
878
|
+
interpret numeric Tamagui lineHeight from that source contract
|
|
879
|
+
(default: v3-ratios)
|
|
880
|
+
--line-height-only apply and report only the selected lineHeight source migration
|
|
443
881
|
--help print this
|
|
444
882
|
|
|
445
883
|
Run it from your project root, which is where paths and the tsconfig resolve from.
|
|
@@ -450,11 +888,15 @@ function parseArguments(argv: readonly string[]): {
|
|
|
450
888
|
jsonPath: string | null
|
|
451
889
|
inputs: string[]
|
|
452
890
|
write: boolean
|
|
891
|
+
sourceSemantics: SourceSemantics
|
|
892
|
+
lineHeightOnly: boolean
|
|
453
893
|
} {
|
|
454
894
|
const inputs: string[] = []
|
|
455
895
|
let reportPath = defaultReportPath
|
|
456
896
|
let jsonPath: string | null = null
|
|
457
897
|
let write = false
|
|
898
|
+
let sourceSemantics: SourceSemantics = 'v3-ratios'
|
|
899
|
+
let lineHeightOnly = false
|
|
458
900
|
|
|
459
901
|
for (let index = 0; index < argv.length; index++) {
|
|
460
902
|
const argument = argv[index]
|
|
@@ -466,6 +908,20 @@ function parseArguments(argv: readonly string[]): {
|
|
|
466
908
|
write = true
|
|
467
909
|
continue
|
|
468
910
|
}
|
|
911
|
+
if (argument === '--line-height-only') {
|
|
912
|
+
lineHeightOnly = true
|
|
913
|
+
continue
|
|
914
|
+
}
|
|
915
|
+
if (argument === '--source-semantics') {
|
|
916
|
+
const next = argv[index + 1]
|
|
917
|
+
if (next !== 'v2-pixels' && next !== 'v3-ratios') {
|
|
918
|
+
console.error(`${argument} requires v2-pixels or v3-ratios\n\n${usage}`)
|
|
919
|
+
process.exit(2)
|
|
920
|
+
}
|
|
921
|
+
sourceSemantics = next
|
|
922
|
+
index++
|
|
923
|
+
continue
|
|
924
|
+
}
|
|
469
925
|
if (argument === '--report' || argument === '--json') {
|
|
470
926
|
const next = argv[index + 1]
|
|
471
927
|
if (!next) {
|
|
@@ -493,10 +949,11 @@ function parseArguments(argv: readonly string[]): {
|
|
|
493
949
|
process.exit(2)
|
|
494
950
|
}
|
|
495
951
|
|
|
496
|
-
return { reportPath, jsonPath, inputs, write }
|
|
952
|
+
return { reportPath, jsonPath, inputs, write, sourceSemantics, lineHeightOnly }
|
|
497
953
|
}
|
|
498
954
|
|
|
499
|
-
const { reportPath, jsonPath, inputs, write } =
|
|
955
|
+
const { reportPath, jsonPath, inputs, write, sourceSemantics, lineHeightOnly } =
|
|
956
|
+
parseArguments(process.argv.slice(2))
|
|
500
957
|
const { sourceFiles, ignoredFiles } = collectFiles(inputs)
|
|
501
958
|
for (const sourceFile of sourceFiles) {
|
|
502
959
|
const diagnostics = (
|
|
@@ -520,7 +977,14 @@ const modifierRegistry = createModifierRegistry({
|
|
|
520
977
|
})
|
|
521
978
|
const provenance = createProvenance()
|
|
522
979
|
const files = sourceFiles.map((sourceFile) =>
|
|
523
|
-
inspectFile(
|
|
980
|
+
inspectFile(
|
|
981
|
+
sourceFile,
|
|
982
|
+
modifierRegistry.registry,
|
|
983
|
+
provenance,
|
|
984
|
+
sourceSemantics,
|
|
985
|
+
lineHeightOnly,
|
|
986
|
+
write
|
|
987
|
+
)
|
|
524
988
|
)
|
|
525
989
|
if (write) {
|
|
526
990
|
for (const sourceFile of sourceFiles) {
|
|
@@ -580,5 +1044,5 @@ if (write) {
|
|
|
580
1044
|
console.log(`wrote ${reportPath}`)
|
|
581
1045
|
if (write) console.log(`rewrote ${written} source files`)
|
|
582
1046
|
console.log(
|
|
583
|
-
`${summary.sites} style sites: ${summary.clean - summary.waiting} clean, ${summary.needsRelocation} need relocation, ${summary.unknownHost} unknown host, ${summary.ineligible} ineligible, ${summary.waiting} waiting on runtime support, ${summary.flagged} syntax-flagged; ${summary.functionalVariantSites} functional variants: ${summary.functionalVariantConverted} automatic, ${summary.functionalVariantFlagged} flagged; ${summary.sheetFrames} Sheet.Frame sites: ${summary.sheetFramesFlagged} need review; ${summary.ignoredFiles} source files ignored`
|
|
1047
|
+
`${summary.sites} style sites: ${summary.clean - summary.waiting} clean, ${summary.needsRelocation} need relocation, ${summary.unknownHost} unknown host, ${summary.ineligible} ineligible, ${summary.waiting} waiting on runtime support, ${summary.flagged} syntax-flagged; ${summary.functionalVariantSites} functional variants: ${summary.functionalVariantConverted} automatic, ${summary.functionalVariantFlagged} flagged; ${summary.sheetFrames} Sheet.Frame sites: ${summary.sheetFramesFlagged} need review; ${summary.transitions} v2 transitions: ${summary.transitionsFlagged} need review; ${summary.ignoredFiles} source files ignored`
|
|
584
1048
|
)
|
package/src/provenance.ts
CHANGED
|
@@ -28,6 +28,7 @@ function rootIdentifier(node: Node): Identifier | null {
|
|
|
28
28
|
export function createProvenance(): {
|
|
29
29
|
isTamaguiElement: (opening: JsxOpeningElement | JsxSelfClosingElement) => boolean
|
|
30
30
|
isTamaguiStyledCall: (call: CallExpression) => boolean
|
|
31
|
+
isTamaguiStyleCall: (call: CallExpression) => boolean
|
|
31
32
|
} {
|
|
32
33
|
const resolved = new Map<Node, boolean>()
|
|
33
34
|
const active = new Set<Node>()
|
|
@@ -177,6 +178,15 @@ export function createProvenance(): {
|
|
|
177
178
|
return fromIdentifier(callee)
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
const isTamaguiStyleCall = (call: CallExpression): boolean => {
|
|
182
|
+
const callee = call.getExpression()
|
|
183
|
+
if (!Node.isPropertyAccessExpression(callee)) return false
|
|
184
|
+
const method = callee.getName()
|
|
185
|
+
if (method !== 'style' && method !== 'resolve' && method !== 'dynamic') return false
|
|
186
|
+
const owner = rootIdentifier(callee.getExpression())
|
|
187
|
+
return owner !== null && fromIdentifier(owner)
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
/**
|
|
181
191
|
* A value re-bound from Tamagui: an alias (`const Sheet = SheetRaw as ...`), a
|
|
182
192
|
* member of one, or the result of a Tamagui factory (`styled`,
|
|
@@ -206,5 +216,6 @@ export function createProvenance(): {
|
|
|
206
216
|
return fromIdentifier(tag)
|
|
207
217
|
},
|
|
208
218
|
isTamaguiStyledCall,
|
|
219
|
+
isTamaguiStyleCall,
|
|
209
220
|
}
|
|
210
221
|
}
|