@tamagui/codemod-flat-values 3.0.0-beta.889.1 → 3.0.0-beta.901.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/dist/builtInNames.mjs +2 -1
- package/dist/builtInNames.mjs.map +1 -1
- package/dist/convert.mjs +98 -23
- package/dist/convert.mjs.map +1 -1
- package/dist/grammar.mjs +6 -1
- package/dist/grammar.mjs.map +1 -1
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/report.mjs +22 -6
- package/dist/report.mjs.map +1 -1
- package/dist/sheetAnatomy.mjs +200 -0
- package/dist/sheetAnatomy.mjs.map +1 -0
- package/package.json +6 -6
- package/src/builtInNames.ts +2 -0
- package/src/convert.ts +149 -45
- package/src/grammar.ts +10 -0
- package/src/index.ts +32 -1
- package/src/report.ts +46 -18
- package/src/sheetAnatomy.ts +277 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/codemod-flat-values",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.901.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"typecheck": "tsc --noEmit"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@tamagui/helpers": "3.0.0-beta.
|
|
30
|
-
"@tamagui/language-service": "3.0.0-beta.
|
|
31
|
-
"@tamagui/shorthands": "3.0.0-beta.
|
|
32
|
-
"@tamagui/style-grammar": "3.0.0-beta.
|
|
29
|
+
"@tamagui/helpers": "3.0.0-beta.901.1",
|
|
30
|
+
"@tamagui/language-service": "3.0.0-beta.901.1",
|
|
31
|
+
"@tamagui/shorthands": "3.0.0-beta.901.1",
|
|
32
|
+
"@tamagui/style-grammar": "3.0.0-beta.901.1",
|
|
33
33
|
"ts-morph": "^28.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tamagui/build": "3.0.0-beta.
|
|
36
|
+
"@tamagui/build": "3.0.0-beta.901.1",
|
|
37
37
|
"typescript": "~6.0.3"
|
|
38
38
|
},
|
|
39
39
|
"repository": {
|
package/src/builtInNames.ts
CHANGED
|
@@ -5,6 +5,8 @@ export const v6CodemodBuiltInNameReplacements = {
|
|
|
5
5
|
// v3 removed backgroundActive after its component defaults had already
|
|
6
6
|
// stopped resolving; press is the corrected active-state default
|
|
7
7
|
backgroundActive: 'background-press',
|
|
8
|
+
// v3 configs no longer alias `true`; the default config pointed it at `4`
|
|
9
|
+
true: '4',
|
|
8
10
|
} as const
|
|
9
11
|
|
|
10
12
|
const builtInTokenPattern = new RegExp(
|
package/src/convert.ts
CHANGED
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
type JsxAttribute,
|
|
22
22
|
type JsxOpeningElement,
|
|
23
23
|
type JsxSelfClosingElement,
|
|
24
|
+
type NoSubstitutionTemplateLiteral,
|
|
24
25
|
type ObjectLiteralExpression,
|
|
25
26
|
type PropertyAssignment,
|
|
27
|
+
type StringLiteral,
|
|
26
28
|
} from 'ts-morph'
|
|
27
29
|
import type { ContainerPlan } from './containers'
|
|
28
30
|
import {
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
sharedPayload,
|
|
46
48
|
shorthands,
|
|
47
49
|
styleProps,
|
|
50
|
+
tokenVariantProps,
|
|
48
51
|
unitSuffix,
|
|
49
52
|
type ConversionReason,
|
|
50
53
|
type ConversionTargets,
|
|
@@ -179,6 +182,8 @@ export interface Site {
|
|
|
179
182
|
members: Member[]
|
|
180
183
|
comments: Map<number, readonly string[]>
|
|
181
184
|
extras: Array<{ index: number; text: string }>
|
|
185
|
+
/** token variant values respelled in place; they count as converted output */
|
|
186
|
+
respelled: EmittedProgram[]
|
|
182
187
|
warnings: Flag[]
|
|
183
188
|
flags: Flag[]
|
|
184
189
|
inventory: Flag[]
|
|
@@ -206,6 +211,7 @@ function createSite(
|
|
|
206
211
|
members: [],
|
|
207
212
|
comments: new Map(),
|
|
208
213
|
extras: [],
|
|
214
|
+
respelled: [],
|
|
209
215
|
warnings: [],
|
|
210
216
|
flags: [],
|
|
211
217
|
inventory: [],
|
|
@@ -263,6 +269,16 @@ function addAssessment(
|
|
|
263
269
|
}
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
/** a member the conversion owns, so the rewrite prints it from the site's entries */
|
|
273
|
+
function isConvertedName(name: string): boolean {
|
|
274
|
+
return (
|
|
275
|
+
name === 'group' ||
|
|
276
|
+
styleProps.has(name) ||
|
|
277
|
+
tokenVariantProps.has(name) ||
|
|
278
|
+
isLegacyConditionName(name)
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
266
282
|
function addFlag(list: Flag[], code: string, detail: string): void {
|
|
267
283
|
if (!list.some((flag) => flag.code === code && flag.detail === detail)) {
|
|
268
284
|
list.push({ code, detail })
|
|
@@ -276,21 +292,32 @@ function addNote(site: Site, note: string): void {
|
|
|
276
292
|
const legacyPaletteStepPattern =
|
|
277
293
|
/(?:^|[^\w-])\$?((?:gray|mauve|slate|sage|olive|sand|tomato|red|ruby|crimson|pink|plum|purple|violet|iris|indigo|blue|cyan|teal|jade|green|grass|bronze|gold|brown|orange|amber|yellow|lime|mint|sky)(?:1[0-2]|[1-9]))(?![\w-])/g
|
|
278
294
|
|
|
279
|
-
|
|
295
|
+
const legacyTrueTokenPattern = /(?:^|[^\w-])\$true(?![\w-])/
|
|
296
|
+
|
|
297
|
+
/** configuration risks visible in the authored literals of one value */
|
|
298
|
+
function legacyTokenWarnings(prop: string, values: readonly string[]): Flag[] {
|
|
299
|
+
const warnings: Flag[] = []
|
|
300
|
+
if (values.some((value) => legacyTrueTokenPattern.test(value))) {
|
|
301
|
+
warnings.push({
|
|
302
|
+
code: 'legacy-true-token',
|
|
303
|
+
detail: `${prop} spelled the \`$true\` alias, written as \`4\` because the default config aliased it there; confirm the app's tokens agree`,
|
|
304
|
+
})
|
|
305
|
+
}
|
|
280
306
|
const names = new Set<string>()
|
|
281
307
|
for (const value of values) {
|
|
282
308
|
legacyPaletteStepPattern.lastIndex = 0
|
|
283
309
|
for (const match of value.matchAll(legacyPaletteStepPattern)) names.add(match[1])
|
|
284
310
|
}
|
|
285
|
-
if (!names.size) return
|
|
311
|
+
if (!names.size) return warnings
|
|
286
312
|
const formatted = [...names]
|
|
287
313
|
.sort()
|
|
288
314
|
.map((name) => `\`${name}\``)
|
|
289
315
|
.join(', ')
|
|
290
|
-
|
|
316
|
+
warnings.push({
|
|
291
317
|
code: 'legacy-palette-token',
|
|
292
318
|
detail: `${prop} preserves ${formatted}, which @tamagui/config/v6 does not define; choose an absolute palette token or an adaptive colorN value`,
|
|
293
|
-
}
|
|
319
|
+
})
|
|
320
|
+
return warnings
|
|
294
321
|
}
|
|
295
322
|
|
|
296
323
|
// —— value classification ————————————————————————————————————————————————
|
|
@@ -304,8 +331,8 @@ interface Classification {
|
|
|
304
331
|
dynamic: boolean
|
|
305
332
|
/** a problem with the value itself, raised on sight */
|
|
306
333
|
problem: Flag | null
|
|
307
|
-
/**
|
|
308
|
-
|
|
334
|
+
/** non-blocking configuration risks visible in the authored literals */
|
|
335
|
+
warnings: Flag[]
|
|
309
336
|
/** raised only when a clause forces the value into a program */
|
|
310
337
|
blocked: Flag | null
|
|
311
338
|
/** recorded for a migration that is not the flat-value migration */
|
|
@@ -317,7 +344,7 @@ const empty: Classification = {
|
|
|
317
344
|
text: null,
|
|
318
345
|
dynamic: false,
|
|
319
346
|
problem: null,
|
|
320
|
-
|
|
347
|
+
warnings: [],
|
|
321
348
|
blocked: null,
|
|
322
349
|
inventory: null,
|
|
323
350
|
}
|
|
@@ -402,7 +429,7 @@ function classifyDynamic(
|
|
|
402
429
|
payload: interpolate(prop, tree.text, tree.kind, registry),
|
|
403
430
|
text: rewrittenTokenText,
|
|
404
431
|
dynamic: true,
|
|
405
|
-
|
|
432
|
+
warnings: legacyTokenWarnings(prop, tree.strings),
|
|
406
433
|
}
|
|
407
434
|
}
|
|
408
435
|
if (tree) {
|
|
@@ -469,8 +496,9 @@ function pushBase(
|
|
|
469
496
|
const index = site.index++
|
|
470
497
|
|
|
471
498
|
if (literalString !== null) {
|
|
472
|
-
const warning
|
|
473
|
-
|
|
499
|
+
for (const warning of legacyTokenWarnings(prop, [literalString])) {
|
|
500
|
+
addFlag(site.warnings, warning.code, warning.detail)
|
|
501
|
+
}
|
|
474
502
|
if (literalString.includes('$')) {
|
|
475
503
|
site.legacy = true
|
|
476
504
|
const allowed = assessProgram(site, prop, [])
|
|
@@ -588,8 +616,8 @@ function pushBase(
|
|
|
588
616
|
site.legacy = true
|
|
589
617
|
addFlag(site.flags, classified.problem.code, classified.problem.detail)
|
|
590
618
|
}
|
|
591
|
-
|
|
592
|
-
addFlag(site.warnings,
|
|
619
|
+
for (const warning of classified.warnings) {
|
|
620
|
+
addFlag(site.warnings, warning.code, warning.detail)
|
|
593
621
|
}
|
|
594
622
|
if (classified.inventory) {
|
|
595
623
|
addFlag(site.inventory, classified.inventory.code, classified.inventory.detail)
|
|
@@ -824,12 +852,8 @@ function pushLegacy(
|
|
|
824
852
|
for (let index = 0; index < evaluated.leaves.length; index++) {
|
|
825
853
|
const leaf = evaluated.leaves[index]
|
|
826
854
|
const classified = classifyDynamic(leaf.prop, leaf.expression, site.registry)
|
|
827
|
-
|
|
828
|
-
addFlag(
|
|
829
|
-
site.warnings,
|
|
830
|
-
classified.warning.code,
|
|
831
|
-
`${leaf.path}: ${classified.warning.detail}`
|
|
832
|
-
)
|
|
855
|
+
for (const warning of classified.warnings) {
|
|
856
|
+
addFlag(site.warnings, warning.code, `${leaf.path}: ${warning.detail}`)
|
|
833
857
|
}
|
|
834
858
|
if (classified.payload === null) {
|
|
835
859
|
const flag = classified.problem ?? classified.blocked
|
|
@@ -1393,9 +1417,7 @@ function isConvertedJsxAttribute(attribute: Node): boolean {
|
|
|
1393
1417
|
if (Node.isJsxSpreadAttribute(attribute)) return true
|
|
1394
1418
|
if (!Node.isJsxAttribute(attribute)) return false
|
|
1395
1419
|
const name = jsxAttributeName(attribute)
|
|
1396
|
-
return (
|
|
1397
|
-
!!name && (name === 'group' || styleProps.has(name) || isLegacyConditionName(name))
|
|
1398
|
-
)
|
|
1420
|
+
return !!name && isConvertedName(name)
|
|
1399
1421
|
}
|
|
1400
1422
|
|
|
1401
1423
|
function rewriteJsxSite(
|
|
@@ -1429,16 +1451,19 @@ function rewriteJsxSite(
|
|
|
1429
1451
|
// impossible to review.
|
|
1430
1452
|
//
|
|
1431
1453
|
// The authored indentation is dropped from the prefix and the closing suffix
|
|
1432
|
-
// so every emitted line is indented by replaceWithText alone
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1435
|
-
//
|
|
1454
|
+
// so every emitted line is indented by replaceWithText alone: ts-morph puts
|
|
1455
|
+
// each new line at the element's own indentation, so an attribute line
|
|
1456
|
+
// carries one relative step and the closing `>` none. Keeping the authored
|
|
1457
|
+
// whitespace would add to what ts-morph supplies and stagger the attributes,
|
|
1458
|
+
// and taking control of the whitespace directly is not an option:
|
|
1459
|
+
// SourceFile.replaceText forgets every node in the file and the rest of the
|
|
1460
|
+
// pass then reads freed nodes.
|
|
1436
1461
|
const authoredMultiline =
|
|
1437
1462
|
prefix.includes('\n') ||
|
|
1438
1463
|
source.slice(first.getStart() - start, last.getEnd() - start).includes('\n')
|
|
1439
1464
|
opening.replaceWithText(
|
|
1440
1465
|
authoredMultiline
|
|
1441
|
-
? `${prefix.replace(/[ \t]+$/, '')}${rendered.join('\n')}${suffix.replace(/\n[ \t]+/, '\n')}`
|
|
1466
|
+
? `${prefix.replace(/[ \t]+$/, '')}${rendered.map((text) => ` ${text}`).join('\n')}${suffix.replace(/\n[ \t]+/, '\n')}`
|
|
1442
1467
|
: `${prefix}${rendered.join(' ')}${suffix}`
|
|
1443
1468
|
)
|
|
1444
1469
|
}
|
|
@@ -1495,11 +1520,7 @@ export function convertJsxSite(
|
|
|
1495
1520
|
if (name !== null) {
|
|
1496
1521
|
// a member the conversion leaves authored has to print as the JSX
|
|
1497
1522
|
// attribute it becomes here, not as the object member it was
|
|
1498
|
-
if (
|
|
1499
|
-
name === 'group' ||
|
|
1500
|
-
styleProps.has(name) ||
|
|
1501
|
-
isLegacyConditionName(name)
|
|
1502
|
-
) {
|
|
1523
|
+
if (isConvertedName(name)) {
|
|
1503
1524
|
pushStyledProperty(
|
|
1504
1525
|
site,
|
|
1505
1526
|
name,
|
|
@@ -1553,6 +1574,10 @@ export function convertJsxSite(
|
|
|
1553
1574
|
pushLegacy(site, name, text, jsxExpression(attribute), attribute)
|
|
1554
1575
|
continue
|
|
1555
1576
|
}
|
|
1577
|
+
if (tokenVariantProps.has(name)) {
|
|
1578
|
+
if (pushTokenVariant(site, name, attribute, text)) before.push(text)
|
|
1579
|
+
continue
|
|
1580
|
+
}
|
|
1556
1581
|
if (!styleProps.has(name)) continue
|
|
1557
1582
|
before.push(text)
|
|
1558
1583
|
|
|
@@ -1576,7 +1601,7 @@ export function convertJsxSite(
|
|
|
1576
1601
|
line: sourceFile.getLineAndColumnAtPos(opening.getStart()).line,
|
|
1577
1602
|
before: before.join(' '),
|
|
1578
1603
|
after: entries.map((entry) => entry.text).join(' ') || '(no style props left)',
|
|
1579
|
-
programs,
|
|
1604
|
+
programs: [...programs, ...site.respelled],
|
|
1580
1605
|
assessments: site.assessments,
|
|
1581
1606
|
assessmentVerdict: assessmentVerdict(site.assessments),
|
|
1582
1607
|
warnings: site.warnings,
|
|
@@ -1620,6 +1645,10 @@ function pushStyledProperty(
|
|
|
1620
1645
|
pushLegacy(site, name, text, initializer, property)
|
|
1621
1646
|
return
|
|
1622
1647
|
}
|
|
1648
|
+
if (tokenVariantProps.has(name)) {
|
|
1649
|
+
pushTokenVariant(site, name, property, text)
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1623
1652
|
if (!styleProps.has(name)) return
|
|
1624
1653
|
|
|
1625
1654
|
const literal =
|
|
@@ -1629,6 +1658,86 @@ function pushStyledProperty(
|
|
|
1629
1658
|
pushBase(site, name, text, literal === null ? initializer : null, literal)
|
|
1630
1659
|
}
|
|
1631
1660
|
|
|
1661
|
+
/**
|
|
1662
|
+
* A variant prop whose value is a size token (`size="$4"`, `elevation="$2"`).
|
|
1663
|
+
* No program is built for it, but v3 looks the value up as written and `$4`
|
|
1664
|
+
* finds nothing, so every `$token` string literal in the value is respelled,
|
|
1665
|
+
* a conditional on both arms. `$true` aliased the default size, which v3
|
|
1666
|
+
* spells as the boolean. Returns whether the member is legacy at all:
|
|
1667
|
+
* respelled, or flagged because it cannot be.
|
|
1668
|
+
*/
|
|
1669
|
+
function pushTokenVariant(
|
|
1670
|
+
site: Site,
|
|
1671
|
+
name: string,
|
|
1672
|
+
node: JsxAttribute | PropertyAssignment,
|
|
1673
|
+
text: string
|
|
1674
|
+
): boolean {
|
|
1675
|
+
const index = site.index++
|
|
1676
|
+
const initializer = node.getInitializer()
|
|
1677
|
+
const value =
|
|
1678
|
+
initializer !== undefined && Node.isJsxExpression(initializer)
|
|
1679
|
+
? initializer.getExpression()
|
|
1680
|
+
: initializer
|
|
1681
|
+
const literals =
|
|
1682
|
+
value === undefined
|
|
1683
|
+
? []
|
|
1684
|
+
: [value, ...value.getDescendants()].filter(
|
|
1685
|
+
(candidate): candidate is StringLiteral | NoSubstitutionTemplateLiteral =>
|
|
1686
|
+
(Node.isStringLiteral(candidate) ||
|
|
1687
|
+
Node.isNoSubstitutionTemplateLiteral(candidate)) &&
|
|
1688
|
+
candidate.getLiteralValue().startsWith('$')
|
|
1689
|
+
)
|
|
1690
|
+
if (value === undefined || literals.length === 0) {
|
|
1691
|
+
site.members.push({ type: 'passthrough', index, text })
|
|
1692
|
+
return false
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const source = value.getText()
|
|
1696
|
+
const start = value.getStart()
|
|
1697
|
+
let rewritten = ''
|
|
1698
|
+
let cursor = start
|
|
1699
|
+
for (const literal of literals) {
|
|
1700
|
+
let replacement = 'true'
|
|
1701
|
+
if (literal.getLiteralValue() !== '$true') {
|
|
1702
|
+
const flat = flatStringValue(literal.getLiteralValue(), site.registry)
|
|
1703
|
+
if (flat.text === null) {
|
|
1704
|
+
site.legacy = true
|
|
1705
|
+
addFlag(
|
|
1706
|
+
site.flags,
|
|
1707
|
+
flat.error?.code ?? 'unsupported-legacy-value',
|
|
1708
|
+
`${name}: ${flat.error?.message ?? `${literal.getText()} has no flat spelling`}`
|
|
1709
|
+
)
|
|
1710
|
+
site.members.push({ type: 'passthrough', index, text })
|
|
1711
|
+
return true
|
|
1712
|
+
}
|
|
1713
|
+
const quote = literal.getText()[0]
|
|
1714
|
+
replacement = `${quote}${flat.text}${quote}`
|
|
1715
|
+
}
|
|
1716
|
+
rewritten += source.slice(cursor - start, literal.getStart() - start)
|
|
1717
|
+
rewritten += replacement
|
|
1718
|
+
cursor = literal.getEnd()
|
|
1719
|
+
}
|
|
1720
|
+
rewritten += source.slice(cursor - start)
|
|
1721
|
+
|
|
1722
|
+
site.legacy = true
|
|
1723
|
+
const output = Node.isJsxAttribute(node)
|
|
1724
|
+
? Node.isStringLiteral(initializer)
|
|
1725
|
+
? rewritten === 'true'
|
|
1726
|
+
? name
|
|
1727
|
+
: `${name}=${rewritten}`
|
|
1728
|
+
: `${name}={${rewritten}}`
|
|
1729
|
+
: site.kind === 'jsx'
|
|
1730
|
+
? `${name}={${rewritten}}`
|
|
1731
|
+
: textWithOuterComments(node, `${node.getNameNode().getText()}: ${rewritten}`)
|
|
1732
|
+
site.members.push({ type: 'passthrough', index, text: output })
|
|
1733
|
+
site.respelled.push({
|
|
1734
|
+
name,
|
|
1735
|
+
value: rewritten,
|
|
1736
|
+
dynamic: literals.length !== 1 || literals[0] !== value,
|
|
1737
|
+
})
|
|
1738
|
+
return true
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1632
1741
|
export function convertStyleObject(
|
|
1633
1742
|
object: ObjectLiteralExpression,
|
|
1634
1743
|
kind: SiteKind,
|
|
@@ -1651,11 +1760,7 @@ export function convertStyleObject(
|
|
|
1651
1760
|
if (Node.isPropertyAssignment(nested)) {
|
|
1652
1761
|
const name = propertyName(nested.getNameNode())
|
|
1653
1762
|
if (name !== null) {
|
|
1654
|
-
if (
|
|
1655
|
-
name === 'group' ||
|
|
1656
|
-
styleProps.has(name) ||
|
|
1657
|
-
isLegacyConditionName(name)
|
|
1658
|
-
) {
|
|
1763
|
+
if (isConvertedName(name)) {
|
|
1659
1764
|
pushStyledProperty(site, name, nested)
|
|
1660
1765
|
} else {
|
|
1661
1766
|
site.members.push({
|
|
@@ -1697,8 +1802,7 @@ export function convertStyleObject(
|
|
|
1697
1802
|
}
|
|
1698
1803
|
const name = propertyName(nameNode)
|
|
1699
1804
|
if (name === null) continue
|
|
1700
|
-
if (!
|
|
1701
|
-
continue
|
|
1805
|
+
if (!isConvertedName(name)) continue
|
|
1702
1806
|
before.push(compact(property.getText()))
|
|
1703
1807
|
pushStyledProperty(site, name, property)
|
|
1704
1808
|
}
|
|
@@ -1713,7 +1817,7 @@ export function convertStyleObject(
|
|
|
1713
1817
|
line: sourceFile.getLineAndColumnAtPos(object.getStart()).line,
|
|
1714
1818
|
before: before.join(', '),
|
|
1715
1819
|
after: entries.map((entry) => entry.text).join(', ') || '(no style props left)',
|
|
1716
|
-
programs,
|
|
1820
|
+
programs: [...programs, ...site.respelled],
|
|
1717
1821
|
assessments: site.assessments,
|
|
1718
1822
|
assessmentVerdict: assessmentVerdict(site.assessments),
|
|
1719
1823
|
warnings: site.warnings,
|
|
@@ -1742,9 +1846,7 @@ function isConvertedStyledProperty(property: Node): boolean {
|
|
|
1742
1846
|
const nameNode = property.getNameNode()
|
|
1743
1847
|
if (Node.isComputedPropertyName(nameNode)) return false
|
|
1744
1848
|
const name = propertyName(nameNode)
|
|
1745
|
-
return (
|
|
1746
|
-
!!name && (name === 'group' || styleProps.has(name) || isLegacyConditionName(name))
|
|
1747
|
-
)
|
|
1849
|
+
return !!name && isConvertedName(name)
|
|
1748
1850
|
}
|
|
1749
1851
|
|
|
1750
1852
|
function allCommentTexts(node: Node): string[] {
|
|
@@ -1762,7 +1864,7 @@ function allCommentTexts(node: Node): string[] {
|
|
|
1762
1864
|
.map((entry) => entry[1])
|
|
1763
1865
|
}
|
|
1764
1866
|
|
|
1765
|
-
function textWithOuterComments(node: Node): string {
|
|
1867
|
+
function textWithOuterComments(node: Node, text = node.getText()): string {
|
|
1766
1868
|
const comments = new Map<number, string>()
|
|
1767
1869
|
for (const range of [
|
|
1768
1870
|
...node.getLeadingCommentRanges(),
|
|
@@ -1773,7 +1875,9 @@ function textWithOuterComments(node: Node): string {
|
|
|
1773
1875
|
const prefix = [...comments.entries()]
|
|
1774
1876
|
.sort((left, right) => left[0] - right[0])
|
|
1775
1877
|
.map((entry) => entry[1])
|
|
1776
|
-
|
|
1878
|
+
// the member sits one step inside its object and rewriteStyleObject supplies
|
|
1879
|
+
// that step for the first line only, so the lines after a comment carry it
|
|
1880
|
+
return [...prefix, text].join('\n ')
|
|
1777
1881
|
}
|
|
1778
1882
|
|
|
1779
1883
|
function rewriteStyleObject(
|
package/src/grammar.ts
CHANGED
|
@@ -87,6 +87,16 @@ export const styleProps: ReadonlySet<string> = new Set<string>([
|
|
|
87
87
|
...Object.values(shorthands),
|
|
88
88
|
])
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* variant props whose values are size tokens. no program is built for them,
|
|
92
|
+
* but v3 reads the value as a size-scale key and a `$` spelling finds nothing
|
|
93
|
+
*/
|
|
94
|
+
export const tokenVariantProps: ReadonlySet<string> = new Set<string>([
|
|
95
|
+
'size',
|
|
96
|
+
'elevation',
|
|
97
|
+
'iconSize',
|
|
98
|
+
])
|
|
99
|
+
|
|
90
100
|
/** the media keys of the V6 default config, plus the two motion queries */
|
|
91
101
|
export const codemodMediaNames: readonly string[] = [
|
|
92
102
|
...defaultMediaKeys,
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, relative, resolve } from 'node:path'
|
|
|
4
4
|
import { resolveTamaguiHost } from '@tamagui/language-service/host'
|
|
5
5
|
import { stylePropsTextOnly } from '@tamagui/helpers'
|
|
6
6
|
import {
|
|
7
|
+
IndentationText,
|
|
7
8
|
ModuleKind,
|
|
8
9
|
ModuleResolutionKind,
|
|
9
10
|
Node,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
} from './grammar'
|
|
35
36
|
import { createProvenance } from './provenance'
|
|
36
37
|
import { renderReport, type FileReport } from './report'
|
|
38
|
+
import { convertSheetFrames } from './sheetAnatomy'
|
|
37
39
|
|
|
38
40
|
type Provenance = ReturnType<typeof createProvenance>
|
|
39
41
|
|
|
@@ -86,6 +88,11 @@ function collectFiles(inputs: readonly string[]): {
|
|
|
86
88
|
const project = new Project({
|
|
87
89
|
tsConfigFilePath,
|
|
88
90
|
skipAddingFilesFromTsConfig: true,
|
|
91
|
+
// ts-morph re-indents every multi-line replacement from the indentation it
|
|
92
|
+
// computes for the node with this unit. The default four-space unit puts a
|
|
93
|
+
// JSX child two columns past where two-space source authored it, and every
|
|
94
|
+
// attribute line of a rewritten element staggered with it
|
|
95
|
+
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
89
96
|
compilerOptions: {
|
|
90
97
|
allowJs: false,
|
|
91
98
|
jsx: 4,
|
|
@@ -226,6 +233,26 @@ function variantSites(
|
|
|
226
233
|
): SiteReport[] {
|
|
227
234
|
const sites: SiteReport[] = []
|
|
228
235
|
|
|
236
|
+
// variant defaults are spelled like the props they set, so a size token here
|
|
237
|
+
// needs the same respelling as one on an element
|
|
238
|
+
const defaults = config.getProperty('defaultVariants')
|
|
239
|
+
if (Node.isPropertyAssignment(defaults)) {
|
|
240
|
+
const object = unwrapExpression(defaults.getInitializerOrThrow())
|
|
241
|
+
if (Node.isObjectLiteralExpression(object)) {
|
|
242
|
+
const site = convertStyleObject(
|
|
243
|
+
object,
|
|
244
|
+
'styled',
|
|
245
|
+
`${label} defaultVariants`,
|
|
246
|
+
registry,
|
|
247
|
+
containers,
|
|
248
|
+
targets,
|
|
249
|
+
host,
|
|
250
|
+
write
|
|
251
|
+
)
|
|
252
|
+
if (site) sites.push(site)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
229
256
|
const variants = config.getProperty('variants')
|
|
230
257
|
if (Node.isPropertyAssignment(variants)) {
|
|
231
258
|
const object = unwrapExpression(variants.getInitializerOrThrow())
|
|
@@ -311,6 +338,9 @@ function inspectFile(
|
|
|
311
338
|
provenance: Provenance,
|
|
312
339
|
write: boolean
|
|
313
340
|
): FileReport {
|
|
341
|
+
// the anatomy rewrite runs first so the Background it adds, and the surface
|
|
342
|
+
// props it moves there, go through the flat-value conversion below
|
|
343
|
+
const sheetFrames = convertSheetFrames(sourceFile, provenance, write)
|
|
314
344
|
const containers = planContainers(sourceFile, registry)
|
|
315
345
|
const targets = conversionTargets(sourceFile.getFilePath())
|
|
316
346
|
const sites: SiteReport[] = []
|
|
@@ -375,6 +405,7 @@ function inspectFile(
|
|
|
375
405
|
file: relative(projectRoot, sourceFile.getFilePath()),
|
|
376
406
|
sites,
|
|
377
407
|
functionalVariants,
|
|
408
|
+
sheetFrames,
|
|
378
409
|
}
|
|
379
410
|
}
|
|
380
411
|
|
|
@@ -528,5 +559,5 @@ if (write) {
|
|
|
528
559
|
console.log(`wrote ${reportPath}`)
|
|
529
560
|
if (write) console.log(`rewrote ${written} source files`)
|
|
530
561
|
console.log(
|
|
531
|
-
`${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.ignoredFiles} source files ignored`
|
|
562
|
+
`${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`
|
|
532
563
|
)
|
package/src/report.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Flag, SiteReport } from './convert'
|
|
2
2
|
import type { FunctionalVariantReport } from './functionalVariants'
|
|
3
|
+
import type { SheetFrameReport } from './sheetAnatomy'
|
|
3
4
|
|
|
4
5
|
export interface FileReport {
|
|
5
6
|
file: string
|
|
6
7
|
sites: SiteReport[]
|
|
7
8
|
functionalVariants: FunctionalVariantReport[]
|
|
9
|
+
sheetFrames: SheetFrameReport[]
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
function countCodes(flags: Iterable<Flag>): Array<[string, number]> {
|
|
@@ -31,6 +33,9 @@ export interface ReportSummary {
|
|
|
31
33
|
functionalVariantConverted: number
|
|
32
34
|
functionalVariantFlagged: number
|
|
33
35
|
functionalVariantFlags: Record<string, number>
|
|
36
|
+
sheetFrames: number
|
|
37
|
+
/** rewritten, but with a spread or a styled target a human has to place */
|
|
38
|
+
sheetFramesFlagged: number
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export function renderReport(
|
|
@@ -42,6 +47,8 @@ export function renderReport(
|
|
|
42
47
|
): { text: string; summary: ReportSummary } {
|
|
43
48
|
const sites = files.flatMap((file) => file.sites)
|
|
44
49
|
const functionalVariants = files.flatMap((file) => file.functionalVariants)
|
|
50
|
+
const sheetFrames = files.flatMap((file) => file.sheetFrames)
|
|
51
|
+
const flaggedSheetFrames = sheetFrames.filter((site) => site.flags.length > 0)
|
|
45
52
|
const convertedFunctionalVariants = functionalVariants.filter((site) => site.converted)
|
|
46
53
|
const flaggedFunctionalVariants = functionalVariants.filter((site) => !site.converted)
|
|
47
54
|
const clean = sites.filter(
|
|
@@ -72,22 +79,17 @@ export function renderReport(
|
|
|
72
79
|
site.warnings.length === 0 &&
|
|
73
80
|
site.assessmentVerdict === 'clean'
|
|
74
81
|
).length
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(
|
|
83
|
-
)
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
(file) =>
|
|
87
|
-
file.sites.some((site) => site.legacyLeft > 0) ||
|
|
88
|
-
file.functionalVariants.some((site) => !site.converted)
|
|
89
|
-
)
|
|
90
|
-
.map((file) => file.file)
|
|
82
|
+
const touched = (file: FileReport) =>
|
|
83
|
+
file.sites.length > 0 ||
|
|
84
|
+
file.functionalVariants.length > 0 ||
|
|
85
|
+
file.sheetFrames.length > 0
|
|
86
|
+
const blocked = (file: FileReport) =>
|
|
87
|
+
file.sites.some((site) => site.legacyLeft > 0) ||
|
|
88
|
+
file.functionalVariants.some((site) => !site.converted) ||
|
|
89
|
+
file.sheetFrames.some((site) => site.flags.length > 0)
|
|
90
|
+
const readyFiles = files.filter((file) => touched(file) && !blocked(file))
|
|
91
|
+
const filesWithSites = files.filter(touched).length
|
|
92
|
+
const blockedFiles = files.filter(blocked).map((file) => file.file)
|
|
91
93
|
const functionalFlagCounts = countCodes(
|
|
92
94
|
flaggedFunctionalVariants.flatMap((site) => [
|
|
93
95
|
...new Map(site.flags.map((flag) => [flag.code, flag])).values(),
|
|
@@ -119,6 +121,8 @@ export function renderReport(
|
|
|
119
121
|
`- ${functionalVariants.length} functional variant sites found`,
|
|
120
122
|
`- ${convertedFunctionalVariants.length} functional variants have automatic styled.dynamic rewrites`,
|
|
121
123
|
`- ${flaggedFunctionalVariants.length} functional variants need manual migration`,
|
|
124
|
+
`- ${sheetFrames.length} Sheet.Frame sites rewritten to Sheet.Container plus Sheet.Background`,
|
|
125
|
+
`- ${flaggedSheetFrames.length} Sheet.Frame sites need a human to place a spread or a styled target`,
|
|
122
126
|
'',
|
|
123
127
|
'### Functional variant flag reasons',
|
|
124
128
|
'',
|
|
@@ -128,7 +132,7 @@ export function renderReport(
|
|
|
128
132
|
'',
|
|
129
133
|
'### Remaining manual migration',
|
|
130
134
|
'',
|
|
131
|
-
`${readyFiles.length} of ${filesWithSites} files have no legacy condition object
|
|
135
|
+
`${readyFiles.length} of ${filesWithSites} files have no legacy condition object, flagged functional variant, or flagged Sheet.Frame left after`,
|
|
132
136
|
'conversion. V3 has no compatibility setting; finish the remaining files directly:',
|
|
133
137
|
blockedFiles.length
|
|
134
138
|
? blockedFiles.map((file) => `\`${file}\``).join(', ')
|
|
@@ -188,8 +192,30 @@ export function renderReport(
|
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
for (const file of files) {
|
|
191
|
-
if (!file
|
|
195
|
+
if (!touched(file)) continue
|
|
192
196
|
lines.push('', `## \`${file.file}\``, '')
|
|
197
|
+
for (const site of file.sheetFrames) {
|
|
198
|
+
lines.push(
|
|
199
|
+
`### ${site.label} at line ${site.line} (${site.flags.length ? 'review' : 'automatic'})`,
|
|
200
|
+
'',
|
|
201
|
+
'Before:',
|
|
202
|
+
'',
|
|
203
|
+
'```tsx',
|
|
204
|
+
site.before,
|
|
205
|
+
'```',
|
|
206
|
+
'',
|
|
207
|
+
'Automatic rewrite:',
|
|
208
|
+
'',
|
|
209
|
+
'```tsx',
|
|
210
|
+
site.after,
|
|
211
|
+
'```'
|
|
212
|
+
)
|
|
213
|
+
if (site.flags.length) {
|
|
214
|
+
lines.push('', 'Flags:', '')
|
|
215
|
+
for (const flag of site.flags) lines.push(`- **${flag.code}**: ${flag.detail}`)
|
|
216
|
+
}
|
|
217
|
+
lines.push('')
|
|
218
|
+
}
|
|
193
219
|
for (const site of file.functionalVariants) {
|
|
194
220
|
lines.push(
|
|
195
221
|
`### ${site.label} functional variant at line ${site.line} (${site.converted ? 'automatic' : 'manual'})`,
|
|
@@ -295,6 +321,8 @@ export function renderReport(
|
|
|
295
321
|
functionalVariantConverted: convertedFunctionalVariants.length,
|
|
296
322
|
functionalVariantFlagged: flaggedFunctionalVariants.length,
|
|
297
323
|
functionalVariantFlags: Object.fromEntries(functionalFlagCounts),
|
|
324
|
+
sheetFrames: sheetFrames.length,
|
|
325
|
+
sheetFramesFlagged: flaggedSheetFrames.length,
|
|
298
326
|
},
|
|
299
327
|
}
|
|
300
328
|
}
|