@tamagui/codemod-flat-values 0.0.0-bootstrap.0 → 3.0.0-beta.637.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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Only Tamagui style syntax converts. A `hoverStyle` object or a `$token` string
|
|
2
|
+
// means nothing to Emotion, restyle, or a local `styled` helper, so a site is only
|
|
3
|
+
// a conversion site when its binding is provably Tamagui.
|
|
4
|
+
//
|
|
5
|
+
// Provenance is followed through module specifiers, never guessed from a name:
|
|
6
|
+
// an import from `tamagui`/`@tamagui/*`, a re-export chain of relative modules
|
|
7
|
+
// ending in one, or a local value assigned from a Tamagui `styled()` call.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Node,
|
|
11
|
+
SyntaxKind,
|
|
12
|
+
type CallExpression,
|
|
13
|
+
type Identifier,
|
|
14
|
+
type JsxOpeningElement,
|
|
15
|
+
type JsxSelfClosingElement,
|
|
16
|
+
type SourceFile,
|
|
17
|
+
} from 'ts-morph'
|
|
18
|
+
|
|
19
|
+
const tamaguiModule = /^(tamagui|@tamagui\/)/
|
|
20
|
+
|
|
21
|
+
/** the identifier a JSX tag or a call ultimately binds to (`Card.Header` → `Card`) */
|
|
22
|
+
function rootIdentifier(node: Node): Identifier | null {
|
|
23
|
+
let current = node
|
|
24
|
+
while (Node.isPropertyAccessExpression(current)) current = current.getExpression()
|
|
25
|
+
return Node.isIdentifier(current) ? current : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createProvenance(): {
|
|
29
|
+
isTamaguiElement: (opening: JsxOpeningElement | JsxSelfClosingElement) => boolean
|
|
30
|
+
isTamaguiStyledCall: (call: CallExpression) => boolean
|
|
31
|
+
} {
|
|
32
|
+
const resolved = new Map<Node, boolean>()
|
|
33
|
+
const active = new Set<Node>()
|
|
34
|
+
|
|
35
|
+
/** whether a declaration binds a value that came from Tamagui */
|
|
36
|
+
const fromDeclaration = (declaration: Node): boolean => {
|
|
37
|
+
const cached = resolved.get(declaration)
|
|
38
|
+
if (cached !== undefined) return cached
|
|
39
|
+
// an export cycle must terminate, and a binding is not Tamagui until proven
|
|
40
|
+
if (active.has(declaration)) return false
|
|
41
|
+
active.add(declaration)
|
|
42
|
+
const answer = computeDeclaration(declaration)
|
|
43
|
+
active.delete(declaration)
|
|
44
|
+
resolved.set(declaration, answer)
|
|
45
|
+
return answer
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** the named export of a module reached by a relative specifier */
|
|
49
|
+
const fromModule = (
|
|
50
|
+
specifier: string,
|
|
51
|
+
source: SourceFile | undefined,
|
|
52
|
+
name: string
|
|
53
|
+
): boolean => {
|
|
54
|
+
if (tamaguiModule.test(specifier)) return true
|
|
55
|
+
if (source === undefined) return false
|
|
56
|
+
return exportedBinding(source, name)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const bindings = new Map<string, boolean>()
|
|
60
|
+
const openBindings = new Set<string>()
|
|
61
|
+
|
|
62
|
+
const exportedBinding = (source: SourceFile, name: string): boolean => {
|
|
63
|
+
const key = `${source.getFilePath()}#${name}`
|
|
64
|
+
const cached = bindings.get(key)
|
|
65
|
+
if (cached !== undefined) return cached
|
|
66
|
+
// a re-export cycle must terminate, and a binding is not Tamagui until proven
|
|
67
|
+
if (openBindings.has(key)) return false
|
|
68
|
+
openBindings.add(key)
|
|
69
|
+
const answer = computeBinding(source, name)
|
|
70
|
+
openBindings.delete(key)
|
|
71
|
+
bindings.set(key, answer)
|
|
72
|
+
return answer
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const computeBinding = (source: SourceFile, name: string): boolean => {
|
|
76
|
+
// the re-export walk runs first: it answers from the module specifier alone,
|
|
77
|
+
// without asking the checker to load a package's whole declaration file
|
|
78
|
+
for (const declaration of source.getExportDeclarations()) {
|
|
79
|
+
const specifier = declaration.getModuleSpecifierValue()
|
|
80
|
+
const named = declaration.getNamedExports()
|
|
81
|
+
if (specifier === undefined) {
|
|
82
|
+
// `export { View }`: the local binding is what carries the provenance
|
|
83
|
+
for (const element of named) {
|
|
84
|
+
if ((element.getAliasNode() ?? element.getNameNode()).getText() !== name) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (fromDeclaration(element)) return true
|
|
88
|
+
}
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
if (!named.length && !declaration.getNamespaceExport()) {
|
|
92
|
+
// `export * from '...'`: the name can come from any of them
|
|
93
|
+
if (fromModule(specifier, declaration.getModuleSpecifierSourceFile(), name)) {
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
for (const element of named) {
|
|
99
|
+
if ((element.getAliasNode() ?? element.getNameNode()).getText() !== name) continue
|
|
100
|
+
if (
|
|
101
|
+
fromModule(
|
|
102
|
+
specifier,
|
|
103
|
+
declaration.getModuleSpecifierSourceFile(),
|
|
104
|
+
element.getName()
|
|
105
|
+
)
|
|
106
|
+
) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const [exportName, declarations] of source.getExportedDeclarations()) {
|
|
113
|
+
if (exportName !== name) continue
|
|
114
|
+
for (const declaration of declarations) {
|
|
115
|
+
if (fromDeclaration(declaration)) return true
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const computeDeclaration = (declaration: Node): boolean => {
|
|
122
|
+
if (Node.isImportSpecifier(declaration)) {
|
|
123
|
+
const importDeclaration = declaration.getImportDeclaration()
|
|
124
|
+
return fromModule(
|
|
125
|
+
importDeclaration.getModuleSpecifierValue(),
|
|
126
|
+
importDeclaration.getModuleSpecifierSourceFile(),
|
|
127
|
+
declaration.getName()
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
if (Node.isImportClause(declaration) || Node.isNamespaceImport(declaration)) {
|
|
131
|
+
const importDeclaration = declaration.getFirstAncestorByKindOrThrow(
|
|
132
|
+
SyntaxKind.ImportDeclaration
|
|
133
|
+
)
|
|
134
|
+
return tamaguiModule.test(importDeclaration.getModuleSpecifierValue())
|
|
135
|
+
}
|
|
136
|
+
if (Node.isExportSpecifier(declaration)) {
|
|
137
|
+
const exportDeclaration = declaration.getExportDeclaration()
|
|
138
|
+
const specifier = exportDeclaration.getModuleSpecifierValue()
|
|
139
|
+
if (specifier !== undefined) {
|
|
140
|
+
return fromModule(
|
|
141
|
+
specifier,
|
|
142
|
+
exportDeclaration.getModuleSpecifierSourceFile(),
|
|
143
|
+
declaration.getName()
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
// `export { View }`: whatever declares View in this file is the binding
|
|
147
|
+
for (const local of declaration.getLocalTargetDeclarations()) {
|
|
148
|
+
if (fromDeclaration(local)) return true
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
if (Node.isVariableDeclaration(declaration)) {
|
|
153
|
+
const initializer = declaration.getInitializer()
|
|
154
|
+
return initializer !== undefined && isTamaguiValue(initializer)
|
|
155
|
+
}
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fromIdentifier = (identifier: Identifier): boolean => {
|
|
160
|
+
const symbol = identifier.getSymbol()
|
|
161
|
+
if (symbol === undefined) return false
|
|
162
|
+
for (const declaration of symbol.getDeclarations()) {
|
|
163
|
+
if (fromDeclaration(declaration)) return true
|
|
164
|
+
}
|
|
165
|
+
// an alias the checker already followed across files
|
|
166
|
+
const aliased = symbol.getAliasedSymbol()
|
|
167
|
+
if (aliased === undefined) return false
|
|
168
|
+
for (const declaration of aliased.getDeclarations()) {
|
|
169
|
+
if (fromDeclaration(declaration)) return true
|
|
170
|
+
}
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const isTamaguiStyledCall = (call: CallExpression): boolean => {
|
|
175
|
+
const callee = rootIdentifier(call.getExpression())
|
|
176
|
+
if (callee === null || callee.getText() !== 'styled') return false
|
|
177
|
+
return fromIdentifier(callee)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A value re-bound from Tamagui: an alias (`const Sheet = SheetRaw as ...`), a
|
|
182
|
+
* member of one, or the result of a Tamagui factory (`styled`,
|
|
183
|
+
* `withStaticProperties`). A local factory over a Tamagui component is not one:
|
|
184
|
+
* whatever it returns is that function's contract, not Tamagui's.
|
|
185
|
+
*/
|
|
186
|
+
function isTamaguiValue(expression: Node): boolean {
|
|
187
|
+
let current = expression
|
|
188
|
+
while (
|
|
189
|
+
Node.isParenthesizedExpression(current) ||
|
|
190
|
+
Node.isAsExpression(current) ||
|
|
191
|
+
Node.isNonNullExpression(current)
|
|
192
|
+
) {
|
|
193
|
+
current = current.getExpression()
|
|
194
|
+
}
|
|
195
|
+
const identifier = rootIdentifier(
|
|
196
|
+
Node.isCallExpression(current) ? current.getExpression() : current
|
|
197
|
+
)
|
|
198
|
+
return identifier !== null && fromIdentifier(identifier)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
isTamaguiElement: (opening) => {
|
|
203
|
+
const tag = rootIdentifier(opening.getTagNameNode())
|
|
204
|
+
// a lowercase tag is an intrinsic element, never a Tamagui component
|
|
205
|
+
if (tag === null || !/^[A-Z_$]/.test(tag.getText())) return false
|
|
206
|
+
return fromIdentifier(tag)
|
|
207
|
+
},
|
|
208
|
+
isTamaguiStyledCall,
|
|
209
|
+
}
|
|
210
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { Flag, SiteReport } from './convert'
|
|
2
|
+
|
|
3
|
+
export interface FileReport {
|
|
4
|
+
file: string
|
|
5
|
+
sites: SiteReport[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function countCodes(flags: Iterable<Flag>): Array<[string, number]> {
|
|
9
|
+
const counts = new Map<string, number>()
|
|
10
|
+
for (const flag of flags) counts.set(flag.code, (counts.get(flag.code) ?? 0) + 1)
|
|
11
|
+
return [...counts].sort(
|
|
12
|
+
([leftCode, leftCount], [rightCode, rightCount]) =>
|
|
13
|
+
rightCount - leftCount || leftCode.localeCompare(rightCode)
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ReportSummary {
|
|
18
|
+
sites: number
|
|
19
|
+
clean: number
|
|
20
|
+
needsRelocation: number
|
|
21
|
+
unknownHost: number
|
|
22
|
+
ineligible: number
|
|
23
|
+
flagged: number
|
|
24
|
+
warnings: number
|
|
25
|
+
/** sites with nothing to convert until the runtime catches up */
|
|
26
|
+
waiting: number
|
|
27
|
+
ignoredFiles: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderReport(
|
|
31
|
+
files: readonly FileReport[],
|
|
32
|
+
corpus: readonly string[],
|
|
33
|
+
registryDiagnostics: readonly string[],
|
|
34
|
+
ignoredFiles = 0,
|
|
35
|
+
write = false
|
|
36
|
+
): { text: string; summary: ReportSummary } {
|
|
37
|
+
const sites = files.flatMap((file) => file.sites)
|
|
38
|
+
const clean = sites.filter(
|
|
39
|
+
(site) =>
|
|
40
|
+
site.flags.length === 0 &&
|
|
41
|
+
site.warnings.length === 0 &&
|
|
42
|
+
site.assessmentVerdict === 'clean'
|
|
43
|
+
)
|
|
44
|
+
const flagged = sites.filter((site) => site.flags.length > 0)
|
|
45
|
+
const warnings = sites.filter((site) => site.warnings.length > 0)
|
|
46
|
+
const needsRelocation = sites.filter(
|
|
47
|
+
(site) => site.assessmentVerdict === 'needs-relocation'
|
|
48
|
+
)
|
|
49
|
+
const unknownHost = sites.filter((site) => site.assessmentVerdict === 'unknown-host')
|
|
50
|
+
const ineligible = sites.filter((site) => site.assessmentVerdict === 'ineligible')
|
|
51
|
+
const waiting = clean.filter((site) => site.programs.length === 0)
|
|
52
|
+
const jsx = sites.filter((site) => site.kind === 'jsx')
|
|
53
|
+
const styled = sites.filter((site) => site.kind === 'styled')
|
|
54
|
+
const cleanJsx = jsx.filter(
|
|
55
|
+
(site) =>
|
|
56
|
+
site.flags.length === 0 &&
|
|
57
|
+
site.warnings.length === 0 &&
|
|
58
|
+
site.assessmentVerdict === 'clean'
|
|
59
|
+
).length
|
|
60
|
+
const cleanStyled = styled.filter(
|
|
61
|
+
(site) =>
|
|
62
|
+
site.flags.length === 0 &&
|
|
63
|
+
site.warnings.length === 0 &&
|
|
64
|
+
site.assessmentVerdict === 'clean'
|
|
65
|
+
).length
|
|
66
|
+
const readyFiles = files.filter(
|
|
67
|
+
(file) => file.sites.length > 0 && file.sites.every((site) => site.legacyLeft === 0)
|
|
68
|
+
)
|
|
69
|
+
const filesWithSites = files.filter((file) => file.sites.length > 0).length
|
|
70
|
+
const blockedFiles = files
|
|
71
|
+
.filter((file) => file.sites.some((site) => site.legacyLeft > 0))
|
|
72
|
+
.map((file) => file.file)
|
|
73
|
+
|
|
74
|
+
const lines = [
|
|
75
|
+
'# Flat-values codemod dry-run',
|
|
76
|
+
'',
|
|
77
|
+
`Corpus: ${corpus.map((entry) => `\`${entry}\``).join(', ')}.`,
|
|
78
|
+
'',
|
|
79
|
+
write
|
|
80
|
+
? 'Statically safe conversions were written in place. Flagged legacy syntax remains for manual migration.'
|
|
81
|
+
: 'Dry run only: no source files were written. Pass `--write` to apply statically safe conversions.',
|
|
82
|
+
'',
|
|
83
|
+
'## Summary',
|
|
84
|
+
'',
|
|
85
|
+
`- ${sites.length} conversion sites found`,
|
|
86
|
+
`- ${clean.length - waiting.length} converted with no open questions`,
|
|
87
|
+
`- ${needsRelocation.length} need relocation because the authored target or host cannot evaluate the conversion`,
|
|
88
|
+
`- ${unknownHost.length} have an unverified host type`,
|
|
89
|
+
`- ${ineligible.length} use properties that cannot carry flat clauses`,
|
|
90
|
+
`- ${waiting.length} have nothing to convert until the runtime catches up (see below)`,
|
|
91
|
+
`- ${flagged.length} have syntax or ordering flags for manual work`,
|
|
92
|
+
`- ${warnings.length} have configuration warnings for manual review`,
|
|
93
|
+
`- ${ignoredFiles} source files skipped by \`.tamagui-flat-values-ignore\` markers`,
|
|
94
|
+
`- ${jsx.length} JSX sites: ${cleanJsx} clean, ${jsx.length - cleanJsx} need review`,
|
|
95
|
+
`- ${styled.length} styled config sites: ${cleanStyled} clean, ${styled.length - cleanStyled} need review`,
|
|
96
|
+
'',
|
|
97
|
+
'### Remaining manual migration',
|
|
98
|
+
'',
|
|
99
|
+
`${readyFiles.length} of ${filesWithSites} files have no legacy condition object left after`,
|
|
100
|
+
'conversion. V3 has no compatibility setting; finish the remaining files directly:',
|
|
101
|
+
blockedFiles.length
|
|
102
|
+
? blockedFiles.map((file) => `\`${file}\``).join(', ')
|
|
103
|
+
: 'every file in this corpus is fully migrated.',
|
|
104
|
+
'',
|
|
105
|
+
'### Flag reasons',
|
|
106
|
+
'',
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
const flagCounts = countCodes(flagged.flatMap((site) => site.flags))
|
|
110
|
+
lines.push(
|
|
111
|
+
...(flagCounts.length
|
|
112
|
+
? flagCounts.map(([code, count]) => `- ${code}: ${count}`)
|
|
113
|
+
: ['- none'])
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const warningCounts = countCodes(warnings.flatMap((site) => site.warnings))
|
|
117
|
+
lines.push(
|
|
118
|
+
'',
|
|
119
|
+
'### Configuration warning reasons',
|
|
120
|
+
'',
|
|
121
|
+
...(warningCounts.length
|
|
122
|
+
? warningCounts.map(([code, count]) => `- ${code}: ${count}`)
|
|
123
|
+
: ['- none'])
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const inventoryCounts = countCodes(sites.flatMap((site) => site.inventory))
|
|
127
|
+
if (inventoryCounts.length) {
|
|
128
|
+
lines.push(
|
|
129
|
+
'',
|
|
130
|
+
'### Values left authored for another migration',
|
|
131
|
+
'',
|
|
132
|
+
'These are not flat-value migration work. The conversion keeps them exactly as',
|
|
133
|
+
'authored and records them so their follow-up migrations have a corpus inventory.',
|
|
134
|
+
'',
|
|
135
|
+
...inventoryCounts.map(([code, count]) => `- ${code}: ${count}`)
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const pendingCounts = countCodes(sites.flatMap((site) => site.pending))
|
|
140
|
+
if (pendingCounts.length) {
|
|
141
|
+
lines.push(
|
|
142
|
+
'',
|
|
143
|
+
'### Waiting on runtime support',
|
|
144
|
+
'',
|
|
145
|
+
'The conversion is known but not offered, because the runtime cannot read it yet.',
|
|
146
|
+
'Each pending reason below names the exact host or merge contract that must land',
|
|
147
|
+
'before the suggested source is safe to apply.',
|
|
148
|
+
'',
|
|
149
|
+
...pendingCounts.map(([code, count]) => `- ${code}: ${count}`)
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (registryDiagnostics.length) {
|
|
154
|
+
lines.push('', '### Modifier registry diagnostics', '')
|
|
155
|
+
for (const diagnostic of registryDiagnostics) lines.push(`- ${diagnostic}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
if (!file.sites.length) continue
|
|
160
|
+
lines.push('', `## \`${file.file}\``, '')
|
|
161
|
+
for (const site of file.sites) {
|
|
162
|
+
const status = [
|
|
163
|
+
site.assessmentVerdict === 'clean' ? null : site.assessmentVerdict,
|
|
164
|
+
site.flags.length ? 'syntax-blocked' : null,
|
|
165
|
+
site.warnings.length ? 'configuration-warning' : null,
|
|
166
|
+
]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(', ')
|
|
169
|
+
lines.push(
|
|
170
|
+
`### ${site.label} at line ${site.line} (${status || 'clean'})`,
|
|
171
|
+
'',
|
|
172
|
+
'Before:',
|
|
173
|
+
'',
|
|
174
|
+
'```tsx',
|
|
175
|
+
site.before,
|
|
176
|
+
'```',
|
|
177
|
+
'',
|
|
178
|
+
'After:',
|
|
179
|
+
'',
|
|
180
|
+
'```tsx',
|
|
181
|
+
site.after,
|
|
182
|
+
'```'
|
|
183
|
+
)
|
|
184
|
+
if (site.flags.length) {
|
|
185
|
+
lines.push('', 'Flags:', '')
|
|
186
|
+
for (const flag of site.flags) lines.push(`- **${flag.code}**: ${flag.detail}`)
|
|
187
|
+
}
|
|
188
|
+
if (site.warnings.length) {
|
|
189
|
+
lines.push('', 'Configuration warnings:', '')
|
|
190
|
+
for (const warning of site.warnings) {
|
|
191
|
+
lines.push(`- **${warning.code}**: ${warning.detail}`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (site.assessments.length) {
|
|
195
|
+
lines.push('', 'Conversion assessment:', '')
|
|
196
|
+
for (const assessment of site.assessments) {
|
|
197
|
+
for (const reason of assessment.reasons) {
|
|
198
|
+
lines.push(
|
|
199
|
+
`- **${assessment.verdict}: ${assessment.property}**: ${reason.message}. Remedy: ${reason.remedy}.`
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (site.inventory.length) {
|
|
205
|
+
lines.push('', 'Left authored:', '')
|
|
206
|
+
for (const flag of site.inventory)
|
|
207
|
+
lines.push(`- **${flag.code}**: ${flag.detail}`)
|
|
208
|
+
}
|
|
209
|
+
if (site.pending.length) {
|
|
210
|
+
lines.push('', 'Waiting on runtime support:', '')
|
|
211
|
+
for (const flag of site.pending) lines.push(`- **${flag.code}**: ${flag.detail}`)
|
|
212
|
+
}
|
|
213
|
+
if (site.notes.length) {
|
|
214
|
+
lines.push('', 'Notes:', '')
|
|
215
|
+
for (const note of site.notes) lines.push(`- ${note}`)
|
|
216
|
+
}
|
|
217
|
+
lines.push('')
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
text: `${lines.join('\n')}\n`,
|
|
223
|
+
summary: {
|
|
224
|
+
sites: sites.length,
|
|
225
|
+
clean: clean.length,
|
|
226
|
+
needsRelocation: needsRelocation.length,
|
|
227
|
+
unknownHost: unknownHost.length,
|
|
228
|
+
ineligible: ineligible.length,
|
|
229
|
+
flagged: flagged.length,
|
|
230
|
+
warnings: warnings.length,
|
|
231
|
+
waiting: waiting.length,
|
|
232
|
+
ignoredFiles,
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|