@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.
@@ -0,0 +1,277 @@
1
+ // v3 splits `Sheet.Frame` into `Sheet.Container` (layout) and `Sheet.Background`
2
+ // (surface). Every Frame becomes a Container whose first child is a Background
3
+ // carrying the surface props, so a migrated sheet keeps the surface v2 gave it.
4
+
5
+ import {
6
+ Node,
7
+ SyntaxKind,
8
+ type JsxAttribute,
9
+ type JsxElement,
10
+ type JsxOpeningElement,
11
+ type JsxSelfClosingElement,
12
+ type SourceFile,
13
+ } from 'ts-morph'
14
+ import type { Flag } from './convert'
15
+ import type { createProvenance } from './provenance'
16
+
17
+ export interface SheetFrameReport {
18
+ label: string
19
+ line: number
20
+ before: string
21
+ after: string
22
+ flags: Flag[]
23
+ }
24
+
25
+ const surfaceProps = new Set([
26
+ 'bg',
27
+ 'background',
28
+ 'backgroundColor',
29
+ 'backgroundImage',
30
+ 'borderColor',
31
+ 'borderRadius',
32
+ 'borderStyle',
33
+ 'borderWidth',
34
+ 'boxShadow',
35
+ 'disableHideBottomOverflow',
36
+ 'elevate',
37
+ 'elevation',
38
+ 'elevationAndroid',
39
+ 'outlineColor',
40
+ 'outlineOffset',
41
+ 'outlineStyle',
42
+ 'outlineWidth',
43
+ ])
44
+
45
+ function isSurfaceProp(name: string): boolean {
46
+ return (
47
+ surfaceProps.has(name) ||
48
+ /^border[A-Z].*(Color|Radius|Style|Width)$/.test(name) ||
49
+ /^shadow[A-Z]/.test(name)
50
+ )
51
+ }
52
+
53
+ interface Edit {
54
+ start: number
55
+ end: number
56
+ text: string
57
+ }
58
+
59
+ function applyEdits(source: string, base: number, edits: readonly Edit[]): string {
60
+ let text = source
61
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
62
+ text = `${text.slice(0, edit.start - base)}${edit.text}${text.slice(edit.end - base)}`
63
+ }
64
+ return text
65
+ }
66
+
67
+ /** `Sheet.Frame`, `Dialog.Sheet.Frame`: the sheet expression and the part name */
68
+ function sheetPart(node: Node): { sheet: string; part: Node; partName: string } | null {
69
+ if (!Node.isPropertyAccessExpression(node)) return null
70
+ const owner = node.getExpression()
71
+ const ownerName = Node.isPropertyAccessExpression(owner)
72
+ ? owner.getName()
73
+ : Node.isIdentifier(owner)
74
+ ? owner.getText()
75
+ : null
76
+ if (ownerName !== 'Sheet') return null
77
+ return { sheet: owner.getText(), part: node.getNameNode(), partName: node.getName() }
78
+ }
79
+
80
+ function frameOpening(
81
+ opening: JsxOpeningElement | JsxSelfClosingElement,
82
+ provenance: ReturnType<typeof createProvenance>
83
+ ): { sheet: string; part: Node } | null {
84
+ const found = sheetPart(opening.getTagNameNode())
85
+ if (!found || found.partName !== 'Frame') return null
86
+ if (!provenance.isTamaguiElement(opening)) return null
87
+ return found
88
+ }
89
+
90
+ function attributeName(attribute: JsxAttribute): string | null {
91
+ const name = attribute.getNameNode()
92
+ return Node.isIdentifier(name) ? name.getText() : null
93
+ }
94
+
95
+ function rewriteFrame(
96
+ opening: JsxOpeningElement | JsxSelfClosingElement,
97
+ sheet: string,
98
+ part: Node
99
+ ): { after: string; start: number; end: number; flags: Flag[] } {
100
+ const element = Node.isJsxOpeningElement(opening)
101
+ ? opening.getParentIfKindOrThrow(SyntaxKind.JsxElement)
102
+ : opening
103
+ const start = element.getStart()
104
+ const end = element.getEnd()
105
+ const source = element.getText()
106
+ const edits: Edit[] = [
107
+ { start: part.getStart(), end: part.getEnd(), text: 'Container' },
108
+ ]
109
+ const flags: Flag[] = []
110
+
111
+ const moved: string[] = []
112
+ let previousEnd = opening.getTagNameNode().getEnd()
113
+ for (const attribute of opening.getAttributes()) {
114
+ if (Node.isJsxSpreadAttribute(attribute)) {
115
+ flags.push({
116
+ code: 'sheet-frame-spread',
117
+ detail: `${attribute.getText()} stays on ${sheet}.Container; move any surface props it carries onto ${sheet}.Background by hand`,
118
+ })
119
+ } else {
120
+ const name = attributeName(attribute)
121
+ if (name !== null && isSurfaceProp(name)) {
122
+ moved.push(attribute.getText())
123
+ edits.push({ start: previousEnd, end: attribute.getEnd(), text: '' })
124
+ }
125
+ }
126
+ previousEnd = attribute.getEnd()
127
+ }
128
+ const background = `<${sheet}.Background${moved.length ? ` ${moved.join(' ')}` : ''} />`
129
+
130
+ if (Node.isJsxSelfClosingElement(element)) {
131
+ // `<Sheet.Frame />` closes over nothing, so the Background is its only child
132
+ edits.push({
133
+ start: element.getEnd() - 2,
134
+ end: element.getEnd(),
135
+ text: `>${background}</${sheet}.Container>`,
136
+ })
137
+ // a trailing space before `/>` would separate the last attribute from `>`
138
+ const beforeClose = source.slice(0, -2)
139
+ if (/\s$/.test(beforeClose)) {
140
+ edits.push({
141
+ start: element.getEnd() - 2 - (beforeClose.length - beforeClose.trimEnd().length),
142
+ end: element.getEnd() - 2,
143
+ text: '',
144
+ })
145
+ }
146
+ return { after: applyEdits(source, start, edits), start, end, flags }
147
+ }
148
+
149
+ const closingPart = sheetPart(element.getClosingElement().getTagNameNode())
150
+ if (closingPart) {
151
+ edits.push({
152
+ start: closingPart.part.getStart(),
153
+ end: closingPart.part.getEnd(),
154
+ text: 'Container',
155
+ })
156
+ }
157
+
158
+ const firstChild = element
159
+ .getJsxChildren()
160
+ .find((child) => !Node.isJsxText(child) || child.getText().trim() !== '')
161
+ const existing =
162
+ firstChild &&
163
+ (Node.isJsxElement(firstChild) || Node.isJsxSelfClosingElement(firstChild))
164
+ ? sheetPart(
165
+ (Node.isJsxElement(firstChild)
166
+ ? firstChild.getOpeningElement()
167
+ : firstChild
168
+ ).getTagNameNode()
169
+ )
170
+ : null
171
+ if (existing?.partName === 'Background') {
172
+ if (moved.length) {
173
+ const existingOpening = Node.isJsxElement(firstChild)
174
+ ? firstChild.getOpeningElement()
175
+ : (firstChild as JsxSelfClosingElement)
176
+ const attributes = existingOpening.getAttributes()
177
+ const anchor = attributes.length
178
+ ? attributes[attributes.length - 1]!.getEnd()
179
+ : existingOpening.getTagNameNode().getEnd()
180
+ edits.push({ start: anchor, end: anchor, text: ` ${moved.join(' ')}` })
181
+ }
182
+ return { after: applyEdits(source, start, edits), start, end, flags }
183
+ }
184
+
185
+ // a Frame written one child per line gets the Background on its own line at
186
+ // the children's indentation; an inline one stays inline
187
+ const afterOpening = source.slice(opening.getEnd() - start)
188
+ const childLine = /^(\r?\n[ \t]*)/.exec(afterOpening)
189
+ edits.push({
190
+ start: opening.getEnd(),
191
+ end: opening.getEnd(),
192
+ text: childLine ? `${childLine[1]}${background}` : background,
193
+ })
194
+ return { after: applyEdits(source, start, edits), start, end, flags }
195
+ }
196
+
197
+ /**
198
+ * Rewrites every provable `Sheet.Frame` element in the file. In write mode the
199
+ * source is edited in place, one element at a time: an edit forgets every node
200
+ * in the file, so each round re-reads the file for the next Frame.
201
+ */
202
+ export function convertSheetFrames(
203
+ sourceFile: SourceFile,
204
+ provenance: ReturnType<typeof createProvenance>,
205
+ write: boolean
206
+ ): SheetFrameReport[] {
207
+ const reports: SheetFrameReport[] = []
208
+
209
+ const openings = (): Array<{
210
+ opening: JsxOpeningElement | JsxSelfClosingElement
211
+ sheet: string
212
+ part: Node
213
+ }> =>
214
+ [
215
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
216
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
217
+ ]
218
+ .sort((left, right) => left.getStart() - right.getStart())
219
+ .flatMap((opening) => {
220
+ const found = frameOpening(opening, provenance)
221
+ return found ? [{ opening, ...found }] : []
222
+ })
223
+
224
+ const record = (
225
+ opening: JsxOpeningElement | JsxSelfClosingElement,
226
+ sheet: string,
227
+ part: Node
228
+ ) => {
229
+ const element = Node.isJsxOpeningElement(opening)
230
+ ? opening.getParentIfKindOrThrow(SyntaxKind.JsxElement)
231
+ : opening
232
+ const before = element.getText()
233
+ const rewritten = rewriteFrame(opening, sheet, part)
234
+ reports.push({
235
+ label: `<${sheet}.Frame>`,
236
+ line: opening.getStartLineNumber(),
237
+ before,
238
+ after: rewritten.after,
239
+ flags: rewritten.flags,
240
+ })
241
+ return rewritten
242
+ }
243
+
244
+ if (write) {
245
+ for (let next = openings()[0]; next; next = openings()[0]) {
246
+ const rewritten = record(next.opening, next.sheet, next.part)
247
+ sourceFile.replaceText([rewritten.start, rewritten.end], rewritten.after)
248
+ }
249
+ } else {
250
+ for (const next of openings()) record(next.opening, next.sheet, next.part)
251
+ }
252
+
253
+ for (const call of sourceFile
254
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
255
+ .filter((call) => provenance.isTamaguiStyledCall(call))
256
+ .sort((left, right) => right.getStart() - left.getStart())) {
257
+ const target = call.getArguments()[0]
258
+ const found = target ? sheetPart(target) : null
259
+ if (!found || found.partName !== 'Frame') continue
260
+ const before = target!.getText()
261
+ reports.push({
262
+ label: `styled(${before}, …)`,
263
+ line: call.getStartLineNumber(),
264
+ before,
265
+ after: `${found.sheet}.Container`,
266
+ flags: [
267
+ {
268
+ code: 'sheet-frame-styled',
269
+ detail: `${before} is split into ${found.sheet}.Container and ${found.sheet}.Background; the styled target is now the Container, so move any surface styles in this config to a styled ${found.sheet}.Background`,
270
+ },
271
+ ],
272
+ })
273
+ if (write) found.part.replaceWithText('Container')
274
+ }
275
+
276
+ return reports.sort((left, right) => left.line - right.line)
277
+ }