@tamagui/codemod-flat-values 0.0.0-bootstrap.0 → 3.0.0-beta.1093.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +234 -1
  2. package/dist/builtInNames.mjs +14 -0
  3. package/dist/builtInNames.mjs.map +1 -0
  4. package/dist/containers.mjs +141 -0
  5. package/dist/containers.mjs.map +1 -0
  6. package/dist/convert.mjs +1233 -0
  7. package/dist/convert.mjs.map +1 -0
  8. package/dist/expressions.mjs +188 -0
  9. package/dist/expressions.mjs.map +1 -0
  10. package/dist/functionalVariants.mjs +469 -0
  11. package/dist/functionalVariants.mjs.map +1 -0
  12. package/dist/grammar.mjs +73 -0
  13. package/dist/grammar.mjs.map +1 -0
  14. package/dist/index.mjs +374 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/legacyConditions.mjs +293 -0
  17. package/dist/legacyConditions.mjs.map +1 -0
  18. package/dist/legacyNames.mjs +130 -0
  19. package/dist/legacyNames.mjs.map +1 -0
  20. package/dist/provenance.mjs +137 -0
  21. package/dist/provenance.mjs.map +1 -0
  22. package/dist/report.mjs +188 -0
  23. package/dist/report.mjs.map +1 -0
  24. package/dist/sheetAnatomy.mjs +200 -0
  25. package/dist/sheetAnatomy.mjs.map +1 -0
  26. package/dist/structuredNative.mjs +275 -0
  27. package/dist/structuredNative.mjs.map +1 -0
  28. package/dist/transition.mjs +257 -0
  29. package/dist/transition.mjs.map +1 -0
  30. package/package.json +35 -7
  31. package/src/builtInNames.ts +23 -0
  32. package/src/containers.ts +227 -0
  33. package/src/convert.ts +1977 -0
  34. package/src/expressions.ts +220 -0
  35. package/src/functionalVariants.ts +642 -0
  36. package/src/grammar.ts +190 -0
  37. package/src/index.ts +589 -0
  38. package/src/legacyConditions.ts +357 -0
  39. package/src/legacyNames.ts +160 -0
  40. package/src/provenance.ts +210 -0
  41. package/src/report.ts +362 -0
  42. package/src/sheetAnatomy.ts +277 -0
  43. package/src/structuredNative.ts +459 -0
  44. package/src/transition.ts +415 -0
package/src/index.ts ADDED
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
3
+ import { dirname, relative, resolve } from 'node:path'
4
+ import { resolveTamaguiHost } from '@tamagui/language-service/host'
5
+ import { stylePropsTextOnly } from '@tamagui/helpers'
6
+ import {
7
+ IndentationText,
8
+ ModuleKind,
9
+ ModuleResolutionKind,
10
+ Node,
11
+ Project,
12
+ ScriptTarget,
13
+ SyntaxKind,
14
+ ts,
15
+ type Expression,
16
+ type ObjectLiteralExpression,
17
+ type SourceFile,
18
+ } from 'ts-morph'
19
+ import { planContainers, type ContainerPlan } from './containers'
20
+ import { convertJsxSite, convertStyleObject, type SiteReport } from './convert'
21
+ import { compact, unwrapExpression } from './expressions'
22
+ import {
23
+ addFunctionalVariantTypeImports,
24
+ convertFunctionalVariants,
25
+ type FunctionalVariantReport,
26
+ type RequiredTypeImport,
27
+ } from './functionalVariants'
28
+ import {
29
+ codemodMediaNames,
30
+ createModifierRegistry,
31
+ grammarPlatformNames,
32
+ shorthands,
33
+ type ConversionTargets,
34
+ type HostView,
35
+ type ModifierRegistryView,
36
+ } from './grammar'
37
+ import { createProvenance } from './provenance'
38
+ import { renderReport, type FileReport } from './report'
39
+ import { convertSheetFrames } from './sheetAnatomy'
40
+ import { convertTransitions } from './transition'
41
+
42
+ type Provenance = ReturnType<typeof createProvenance>
43
+
44
+ // every path is resolved against the directory the codemod is invoked from, so it
45
+ // migrates the project you are standing in whether that is an app or this repo
46
+ const projectRoot = process.cwd()
47
+ const defaultReportPath = resolve(projectRoot, 'tamagui-flat-values-report.md')
48
+ const ignoreMarker = '.tamagui-flat-values-ignore'
49
+ const ignoredDirectories = new Map<string, boolean>()
50
+
51
+ function isIgnored(filePath: string): boolean {
52
+ let directory = dirname(filePath)
53
+ const visited: string[] = []
54
+ while (
55
+ directory === projectRoot ||
56
+ !relative(projectRoot, directory).startsWith('..')
57
+ ) {
58
+ const cached = ignoredDirectories.get(directory)
59
+ if (cached !== undefined) {
60
+ for (const seen of visited) ignoredDirectories.set(seen, cached)
61
+ return cached
62
+ }
63
+ visited.push(directory)
64
+ if (existsSync(resolve(directory, ignoreMarker))) {
65
+ for (const seen of visited) ignoredDirectories.set(seen, true)
66
+ return true
67
+ }
68
+ if (directory === projectRoot) break
69
+ const parent = dirname(directory)
70
+ if (parent === directory) break
71
+ directory = parent
72
+ }
73
+ for (const seen of visited) ignoredDirectories.set(seen, false)
74
+ return false
75
+ }
76
+
77
+ function collectFiles(inputs: readonly string[]): {
78
+ sourceFiles: SourceFile[]
79
+ ignoredFiles: number
80
+ } {
81
+ // the checker is what proves a JSX tag resolves to a Tamagui component, so a
82
+ // project whose tsconfig cannot be read would silently convert nothing
83
+ const tsConfigFilePath = resolve(projectRoot, 'tsconfig.json')
84
+ if (!existsSync(tsConfigFilePath)) {
85
+ console.error(
86
+ `no tsconfig.json in ${projectRoot}; run the codemod from your project root`
87
+ )
88
+ process.exit(2)
89
+ }
90
+ const project = new Project({
91
+ tsConfigFilePath,
92
+ skipAddingFilesFromTsConfig: true,
93
+ // ts-morph re-indents every multi-line replacement from the indentation it
94
+ // computes for the node with this unit. The default four-space unit puts a
95
+ // JSX child two columns past where two-space source authored it, and every
96
+ // attribute line of a rewritten element staggered with it
97
+ manipulationSettings: { indentationText: IndentationText.TwoSpaces },
98
+ compilerOptions: {
99
+ allowJs: false,
100
+ jsx: 4,
101
+ target: ScriptTarget.ES2020,
102
+ module: ModuleKind.ESNext,
103
+ moduleResolution: ModuleResolutionKind.NodeJs,
104
+ skipLibCheck: true,
105
+ strictNullChecks: true,
106
+ baseUrl: projectRoot,
107
+ },
108
+ })
109
+
110
+ const files = new Map<string, SourceFile>()
111
+ const ignored = new Set<string>()
112
+ const missing: string[] = []
113
+ for (const input of inputs) {
114
+ const path = resolve(projectRoot, input)
115
+ if (!existsSync(path)) {
116
+ missing.push(input)
117
+ continue
118
+ }
119
+ const pattern = /\.[cm]?[jt]sx?$/.test(path) ? path : `${path}/**/*.{ts,tsx}`
120
+ const matched = project.addSourceFilesAtPaths(pattern)
121
+ // an input that matches nothing must never reach the report: a typo in a
122
+ // migration path would otherwise render an empty corpus as ready to cut over
123
+ if (!matched.length) missing.push(input)
124
+ for (const file of matched) {
125
+ const filePath = file.getFilePath()
126
+ if (isIgnored(filePath)) ignored.add(filePath)
127
+ else files.set(filePath, file)
128
+ }
129
+ }
130
+
131
+ if (missing.length) {
132
+ console.error(
133
+ `no source file matched ${missing.map((input) => `"${input}"`).join(', ')}`
134
+ )
135
+ process.exit(2)
136
+ }
137
+
138
+ if (files.size === 0 && ignored.size > 0) {
139
+ console.error(
140
+ `all ${ignored.size} matched source ${ignored.size === 1 ? 'file was' : 'files were'} skipped by ${ignoreMarker}; no migration report was written`
141
+ )
142
+ process.exit(2)
143
+ }
144
+
145
+ return {
146
+ sourceFiles: [...files.values()].sort((left, right) =>
147
+ left.getFilePath().localeCompare(right.getFilePath())
148
+ ),
149
+ ignoredFiles: ignored.size,
150
+ }
151
+ }
152
+
153
+ /** every `$theme-*` spelling the corpus uses, so its themes resolve as modifiers */
154
+ function themeNames(sourceFiles: readonly SourceFile[]): Set<string> {
155
+ const names = new Set(['light', 'dark'])
156
+ for (const sourceFile of sourceFiles) {
157
+ for (const name of conditionNames(sourceFile)) {
158
+ if (name.startsWith('$theme-')) names.add(name.slice('$theme-'.length))
159
+ }
160
+ }
161
+ return names
162
+ }
163
+
164
+ /**
165
+ * Configs may name media queries freely. Any otherwise-unreserved `$name`
166
+ * condition in the migration corpus is therefore a media name; the codemod
167
+ * must not require each app's config to be imported and executed.
168
+ */
169
+ function mediaNames(sourceFiles: readonly SourceFile[]): Set<string> {
170
+ const names = new Set(codemodMediaNames)
171
+ for (const sourceFile of sourceFiles) {
172
+ for (const name of conditionNames(sourceFile)) {
173
+ if (!name.startsWith('$')) continue
174
+ if (
175
+ name.startsWith('$theme-') ||
176
+ name.startsWith('$platform-') ||
177
+ name.startsWith('$group-') ||
178
+ grammarPlatformNames.has(name.slice(1))
179
+ ) {
180
+ continue
181
+ }
182
+ names.add(name.slice(1))
183
+ }
184
+ }
185
+ return names
186
+ }
187
+
188
+ function conditionNames(sourceFile: SourceFile): string[] {
189
+ const names: string[] = []
190
+ for (const attribute of sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
191
+ const name = attribute.getNameNode()
192
+ if (Node.isIdentifier(name)) names.push(name.getText())
193
+ }
194
+ for (const property of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
195
+ const name = property.getNameNode()
196
+ if (Node.isComputedPropertyName(name)) continue
197
+ names.push(name.getText().replace(/^['"]|['"]$/g, ''))
198
+ }
199
+ return names
200
+ }
201
+
202
+ /** every style object a variant value can be: one literal, or one per return */
203
+ function variantStyleObjects(value: Expression): ObjectLiteralExpression[] {
204
+ const current = unwrapExpression(value)
205
+ if (Node.isObjectLiteralExpression(current)) return [current]
206
+ if (Node.isConditionalExpression(current)) {
207
+ return [
208
+ ...variantStyleObjects(current.getWhenTrue()),
209
+ ...variantStyleObjects(current.getWhenFalse()),
210
+ ]
211
+ }
212
+ if (Node.isArrowFunction(current) || Node.isFunctionExpression(current)) {
213
+ const body = current.getBody()
214
+ if (Node.isBlock(body)) {
215
+ return body
216
+ .getDescendantsOfKind(SyntaxKind.ReturnStatement)
217
+ .flatMap((statement) => {
218
+ const returned = statement.getExpression()
219
+ return returned ? variantStyleObjects(returned) : []
220
+ })
221
+ }
222
+ return variantStyleObjects(body as Expression)
223
+ }
224
+ return []
225
+ }
226
+
227
+ function variantSites(
228
+ config: ObjectLiteralExpression,
229
+ label: string,
230
+ registry: ModifierRegistryView,
231
+ containers: ContainerPlan,
232
+ targets: ConversionTargets,
233
+ host: HostView | undefined,
234
+ write: boolean
235
+ ): SiteReport[] {
236
+ const sites: SiteReport[] = []
237
+
238
+ // variant defaults are spelled like the props they set, so a size token here
239
+ // needs the same respelling as one on an element
240
+ const defaults = config.getProperty('defaultVariants')
241
+ if (Node.isPropertyAssignment(defaults)) {
242
+ const object = unwrapExpression(defaults.getInitializerOrThrow())
243
+ if (Node.isObjectLiteralExpression(object)) {
244
+ const site = convertStyleObject(
245
+ object,
246
+ 'styled',
247
+ `${label} defaultVariants`,
248
+ registry,
249
+ containers,
250
+ targets,
251
+ host,
252
+ write
253
+ )
254
+ if (site) sites.push(site)
255
+ }
256
+ }
257
+
258
+ const variants = config.getProperty('variants')
259
+ if (Node.isPropertyAssignment(variants)) {
260
+ const object = unwrapExpression(variants.getInitializerOrThrow())
261
+ if (Node.isObjectLiteralExpression(object)) {
262
+ for (const variant of object.getProperties()) {
263
+ if (!Node.isPropertyAssignment(variant)) continue
264
+ const variantName = compact(variant.getNameNode().getText())
265
+ const branches = unwrapExpression(variant.getInitializerOrThrow())
266
+ if (Node.isCallExpression(branches)) {
267
+ const callee = branches.getExpression()
268
+ if (Node.isPropertyAccessExpression(callee) && callee.getName() === 'dynamic') {
269
+ const body = branches.getArguments()[0]
270
+ if (body && Node.isExpression(body)) {
271
+ for (const style of variantStyleObjects(body)) {
272
+ const site = convertStyleObject(
273
+ style,
274
+ 'styled',
275
+ `${label} variants.${variantName}`,
276
+ registry,
277
+ containers,
278
+ targets,
279
+ host,
280
+ write
281
+ )
282
+ if (site) sites.push(site)
283
+ }
284
+ }
285
+ }
286
+ continue
287
+ }
288
+ if (!Node.isObjectLiteralExpression(branches)) continue
289
+ for (const branch of branches.getProperties()) {
290
+ if (!Node.isPropertyAssignment(branch)) continue
291
+ const branchName = compact(branch.getNameNode().getText())
292
+ for (const style of variantStyleObjects(branch.getInitializerOrThrow())) {
293
+ const site = convertStyleObject(
294
+ style,
295
+ 'styled',
296
+ `${label} variants.${variantName}.${branchName}`,
297
+ registry,
298
+ containers,
299
+ targets,
300
+ host,
301
+ write
302
+ )
303
+ if (site) sites.push(site)
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ return sites
311
+ }
312
+
313
+ function conversionTargets(filePath: string): ConversionTargets {
314
+ if (/\.web\.[cm]?[jt]sx?$/.test(filePath)) return 'web'
315
+ if (/\.native\.[cm]?[jt]sx?$/.test(filePath)) return 'native'
316
+ return 'shared'
317
+ }
318
+
319
+ /** every shorthand spelling a longhand can be written as, keyed by longhand */
320
+ const shorthandSpellings = new Map<string, string[]>()
321
+ for (const [shorthand, longhand] of Object.entries(shorthands)) {
322
+ const spellings = shorthandSpellings.get(longhand)
323
+ if (spellings) spellings.push(shorthand)
324
+ else shorthandSpellings.set(longhand, [shorthand])
325
+ }
326
+
327
+ function typeAwareHost(node: Node): HostView | undefined {
328
+ const checker = node.getProject().getTypeChecker().compilerObject
329
+ const host = resolveTamaguiHost(
330
+ checker as unknown as Parameters<typeof resolveTamaguiHost>[0],
331
+ node.compilerNode as unknown as Parameters<typeof resolveTamaguiHost>[1]
332
+ )
333
+ if (!host) return host
334
+
335
+ // The conversion resolves an authored shorthand to its longhand before asking
336
+ // the host, and `onlyAllowShorthands: true` omits exactly those longhands from
337
+ // the component's prop type. Asking about `borderRadius` on an app configured
338
+ // that way therefore answered "the runtime drops it" for `rounded="$3"`. Host
339
+ // validity is a question about the property, so any spelling of it answers.
340
+ const accepts = (property: string): boolean =>
341
+ host.accepts(property) ||
342
+ (shorthandSpellings.get(property)?.some((spelling) => host.accepts(spelling)) ??
343
+ false)
344
+
345
+ if (node.getText() !== 'View') return { ...host, accepts }
346
+
347
+ // Flat value typing deliberately admits arbitrary strings on narrow style
348
+ // props, so TypeScript alone can no longer distinguish Text-only styles on
349
+ // the primitive View. Keep the host assessment tied to the runtime table for
350
+ // this canonical primitive; styled(View, …) and direct <View> share it.
351
+ return {
352
+ ...host,
353
+ accepts: (property) => !(property in stylePropsTextOnly) && accepts(property),
354
+ }
355
+ }
356
+
357
+ function inspectFile(
358
+ sourceFile: SourceFile,
359
+ registry: ModifierRegistryView,
360
+ provenance: Provenance,
361
+ write: boolean
362
+ ): FileReport {
363
+ // the anatomy rewrite runs first so the Background it adds, and the surface
364
+ // props it moves there, go through the flat-value conversion below
365
+ const sheetFrames = convertSheetFrames(sourceFile, provenance, write)
366
+ // the transition respelling touches one value, so it runs on its own rather
367
+ // than turning every element that has a v2 transition into a style site
368
+ const transitions = convertTransitions(sourceFile, provenance, write)
369
+ const containers = planContainers(sourceFile, registry)
370
+ const targets = conversionTargets(sourceFile.getFilePath())
371
+ const sites: SiteReport[] = []
372
+ const functionalVariants: FunctionalVariantReport[] = []
373
+ const requiredTypeImports: RequiredTypeImport[] = []
374
+ const styledCalls = sourceFile
375
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
376
+ .filter((call) => provenance.isTamaguiStyledCall(call))
377
+ .sort((left, right) => right.getStart() - left.getStart())
378
+ const jsxOpenings = [
379
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
380
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
381
+ ]
382
+ .filter((opening) => provenance.isTamaguiElement(opening))
383
+ .sort((left, right) => right.getStart() - left.getStart())
384
+
385
+ for (const opening of jsxOpenings) {
386
+ const site = convertJsxSite(
387
+ opening,
388
+ registry,
389
+ containers,
390
+ targets,
391
+ typeAwareHost(opening.getTagNameNode()),
392
+ write
393
+ )
394
+ if (site) sites.push(site)
395
+ }
396
+
397
+ for (const call of styledCalls) {
398
+ const component = call.getArguments()[0]
399
+ const host = component ? typeAwareHost(component) : undefined
400
+ const config = unwrapExpression(
401
+ (call.getArguments()[1] as Expression | undefined) ?? call
402
+ )
403
+ if (!Node.isObjectLiteralExpression(config)) continue
404
+ const label = `styled(${compact(call.getArguments()[0]?.getText() ?? 'unknown')}, …)`
405
+ sites.push(...variantSites(config, label, registry, containers, targets, host, write))
406
+ const functional = convertFunctionalVariants(config, label, write)
407
+ functionalVariants.push(...functional.sites)
408
+ requiredTypeImports.push(...functional.requiredTypeImports)
409
+ const site = convertStyleObject(
410
+ config,
411
+ 'styled',
412
+ label,
413
+ registry,
414
+ containers,
415
+ targets,
416
+ host,
417
+ write
418
+ )
419
+ if (site) sites.push(site)
420
+ }
421
+
422
+ sites.sort(
423
+ (left, right) => left.line - right.line || left.label.localeCompare(right.label)
424
+ )
425
+ functionalVariants.sort(
426
+ (left, right) => left.line - right.line || left.label.localeCompare(right.label)
427
+ )
428
+ if (write) addFunctionalVariantTypeImports(sourceFile, requiredTypeImports)
429
+ return {
430
+ file: relative(projectRoot, sourceFile.getFilePath()),
431
+ sites,
432
+ functionalVariants,
433
+ sheetFrames,
434
+ transitions,
435
+ }
436
+ }
437
+
438
+ const usage = `Converts Tamagui style syntax to V3 flat property values and reports what it cannot convert.
439
+
440
+ npx @tamagui/codemod-flat-values [options] <files or directories...>
441
+
442
+ --report <path> where to write the Markdown report (default: ${relative(
443
+ projectRoot,
444
+ defaultReportPath
445
+ )})
446
+ --json <path> also write the machine-readable report
447
+ --write rewrite every statically safe conversion in place
448
+ --help print this
449
+
450
+ Run it from your project root, which is where paths and the tsconfig resolve from.
451
+ Source files are only written with --write.`
452
+
453
+ function parseArguments(argv: readonly string[]): {
454
+ reportPath: string
455
+ jsonPath: string | null
456
+ inputs: string[]
457
+ write: boolean
458
+ } {
459
+ const inputs: string[] = []
460
+ let reportPath = defaultReportPath
461
+ let jsonPath: string | null = null
462
+ let write = false
463
+
464
+ for (let index = 0; index < argv.length; index++) {
465
+ const argument = argv[index]
466
+ if (argument === '--help' || argument === '-h') {
467
+ console.log(usage)
468
+ process.exit(0)
469
+ }
470
+ if (argument === '--write') {
471
+ write = true
472
+ continue
473
+ }
474
+ if (argument === '--report' || argument === '--json') {
475
+ const next = argv[index + 1]
476
+ if (!next) {
477
+ console.error(`${argument} requires a path\n\n${usage}`)
478
+ process.exit(2)
479
+ }
480
+ if (argument === '--report') reportPath = resolve(next)
481
+ else jsonPath = resolve(next)
482
+ index++
483
+ continue
484
+ }
485
+ // an unknown option must never be read as a source path: that would silently
486
+ // scan nothing and report a clean corpus
487
+ if (argument.startsWith('-')) {
488
+ console.error(`unknown option "${argument}"\n\n${usage}`)
489
+ process.exit(2)
490
+ }
491
+ inputs.push(argument)
492
+ }
493
+
494
+ // no implicit corpus: migrating whatever happens to be under the working
495
+ // directory is not something anyone means to ask for
496
+ if (!inputs.length) {
497
+ console.error(`no files or directories given\n\n${usage}`)
498
+ process.exit(2)
499
+ }
500
+
501
+ return { reportPath, jsonPath, inputs, write }
502
+ }
503
+
504
+ const { reportPath, jsonPath, inputs, write } = parseArguments(process.argv.slice(2))
505
+ const { sourceFiles, ignoredFiles } = collectFiles(inputs)
506
+ for (const sourceFile of sourceFiles) {
507
+ const diagnostics = (
508
+ sourceFile.compilerNode as unknown as {
509
+ parseDiagnostics?: readonly { messageText?: unknown }[]
510
+ }
511
+ ).parseDiagnostics
512
+ if (diagnostics?.length) {
513
+ console.error(
514
+ `${relative(projectRoot, sourceFile.getFilePath())}: source has parse errors; no files were written`
515
+ )
516
+ process.exit(2)
517
+ }
518
+ }
519
+ const originals = new Map(
520
+ sourceFiles.map((sourceFile) => [sourceFile.getFilePath(), sourceFile.getFullText()])
521
+ )
522
+ const modifierRegistry = createModifierRegistry({
523
+ mediaNames: mediaNames(sourceFiles),
524
+ themeNames: themeNames(sourceFiles),
525
+ })
526
+ const provenance = createProvenance()
527
+ const files = sourceFiles.map((sourceFile) =>
528
+ inspectFile(sourceFile, modifierRegistry.registry, provenance, write)
529
+ )
530
+ if (write) {
531
+ for (const sourceFile of sourceFiles) {
532
+ const filePath = sourceFile.getFilePath()
533
+ const parsed = ts.createSourceFile(
534
+ filePath,
535
+ sourceFile.getFullText(),
536
+ ScriptTarget.Latest,
537
+ true,
538
+ filePath.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
539
+ ) as typeof sourceFile.compilerNode & {
540
+ parseDiagnostics?: readonly ts.Diagnostic[]
541
+ }
542
+ if (parsed.parseDiagnostics?.length) {
543
+ const details = parsed.parseDiagnostics
544
+ .map((diagnostic) => {
545
+ const start = diagnostic.start ?? 0
546
+ const position = parsed.getLineAndCharacterOfPosition(start)
547
+ const line = parsed.text.split(/\r?\n/)[position.line] ?? ''
548
+ return `${position.line + 1}:${position.character + 1} ${ts.flattenDiagnosticMessageText(
549
+ diagnostic.messageText,
550
+ '\n'
551
+ )}\n ${line.trim()}`
552
+ })
553
+ .join('\n')
554
+ console.error(
555
+ `${relative(projectRoot, filePath)}: rewrite produced parse errors; no files were written\n${details}`
556
+ )
557
+ process.exit(2)
558
+ }
559
+ }
560
+ }
561
+ const { text, summary } = renderReport(
562
+ files,
563
+ inputs.map((input) => relative(projectRoot, resolve(projectRoot, input))),
564
+ modifierRegistry.diagnostics,
565
+ ignoredFiles,
566
+ write
567
+ )
568
+ mkdirSync(dirname(reportPath), { recursive: true })
569
+ writeFileSync(reportPath, text)
570
+ if (jsonPath !== null) {
571
+ mkdirSync(dirname(jsonPath), { recursive: true })
572
+ writeFileSync(jsonPath, `${JSON.stringify({ files, summary }, null, 2)}\n`)
573
+ }
574
+
575
+ let written = 0
576
+ if (write) {
577
+ for (const sourceFile of sourceFiles) {
578
+ const next = sourceFile.getFullText()
579
+ if (next === originals.get(sourceFile.getFilePath())) continue
580
+ writeFileSync(sourceFile.getFilePath(), next)
581
+ written++
582
+ }
583
+ }
584
+
585
+ console.log(`wrote ${reportPath}`)
586
+ if (write) console.log(`rewrote ${written} source files`)
587
+ console.log(
588
+ `${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`
589
+ )