@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/codemod-flat-values",
3
- "version": "3.0.0-beta.889.1",
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.889.1",
30
- "@tamagui/language-service": "3.0.0-beta.889.1",
31
- "@tamagui/shorthands": "3.0.0-beta.889.1",
32
- "@tamagui/style-grammar": "3.0.0-beta.889.1",
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.889.1",
36
+ "@tamagui/build": "3.0.0-beta.901.1",
37
37
  "typescript": "~6.0.3"
38
38
  },
39
39
  "repository": {
@@ -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
- function legacyPaletteStepWarning(prop: string, values: readonly string[]): Flag | null {
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 null
311
+ if (!names.size) return warnings
286
312
  const formatted = [...names]
287
313
  .sort()
288
314
  .map((name) => `\`${name}\``)
289
315
  .join(', ')
290
- return {
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
- /** a non-blocking configuration risk visible in the authored literals */
308
- warning: Flag | null
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
- warning: null,
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
- warning: legacyPaletteStepWarning(prop, tree.strings),
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 = legacyPaletteStepWarning(prop, [literalString])
473
- if (warning) addFlag(site.warnings, warning.code, warning.detail)
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
- if (classified.warning) {
592
- addFlag(site.warnings, classified.warning.code, classified.warning.detail)
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
- if (classified.warning) {
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. Keeping it would
1433
- // add to what ts-morph supplies and stagger the attributes, and taking control
1434
- // of the whitespace directly is not an option: SourceFile.replaceText forgets
1435
- // every node in the file and the rest of the pass then reads freed nodes.
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 (!styleProps.has(name) && !isLegacyConditionName(name) && name !== 'group')
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
- return [...prefix, node.getText()].join('\n')
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 readyFiles = files.filter(
76
- (file) =>
77
- (file.sites.length > 0 || file.functionalVariants.length > 0) &&
78
- file.sites.every((site) => site.legacyLeft === 0) &&
79
- file.functionalVariants.every((site) => site.converted)
80
- )
81
- const filesWithSites = files.filter(
82
- (file) => file.sites.length > 0 || file.functionalVariants.length > 0
83
- ).length
84
- const blockedFiles = files
85
- .filter(
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 or flagged functional variant left after`,
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.sites.length && !file.functionalVariants.length) continue
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
  }