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