@planningcenter/tapestry-migration-cli 2.3.0-rc.4 → 2.3.0-rc.6
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 +2 -2
- package/src/components/button/index.ts +3 -1
- package/src/components/button/transforms/themeVariantToKind.test.ts +401 -0
- package/src/components/button/transforms/themeVariantToKind.ts +90 -0
- package/src/components/shared/actions/addAttribute.test.ts +300 -0
- package/src/components/shared/actions/addAttribute.ts +65 -0
- package/src/components/shared/actions/addComment.test.ts +1 -1
- package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
- package/src/components/shared/actions/getAttributeValue.ts +15 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import jscodeshift, {
|
|
2
|
+
Collection,
|
|
3
|
+
JSCodeshift,
|
|
4
|
+
JSXAttribute,
|
|
5
|
+
JSXElement,
|
|
6
|
+
StringLiteral,
|
|
7
|
+
} from "jscodeshift"
|
|
8
|
+
import { describe, expect, it } from "vitest"
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
attributeCombineFactory,
|
|
12
|
+
AttributeMappingConfig,
|
|
13
|
+
} from "./attributeCombineFactory"
|
|
14
|
+
|
|
15
|
+
const j = jscodeshift.withParser("tsx")
|
|
16
|
+
|
|
17
|
+
function applyTransform(source: string, config: AttributeMappingConfig) {
|
|
18
|
+
const transform = attributeCombineFactory(config)
|
|
19
|
+
const fileInfo = { path: "test.tsx", source }
|
|
20
|
+
return transform(
|
|
21
|
+
fileInfo,
|
|
22
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
23
|
+
{}
|
|
24
|
+
) as string | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("attributeCombineFactory", () => {
|
|
28
|
+
const basicConfig = {
|
|
29
|
+
component: "TestComponent",
|
|
30
|
+
defaults: {
|
|
31
|
+
first: "default1",
|
|
32
|
+
second: "default2",
|
|
33
|
+
},
|
|
34
|
+
mappingTable: {
|
|
35
|
+
"": {
|
|
36
|
+
default2: "emptyDefault2",
|
|
37
|
+
valueA: "emptyA",
|
|
38
|
+
},
|
|
39
|
+
default1: {
|
|
40
|
+
"": "defaultEmpty",
|
|
41
|
+
default2: "defaultResult",
|
|
42
|
+
valueA: "defaultResult",
|
|
43
|
+
valueB: "default1B",
|
|
44
|
+
},
|
|
45
|
+
value1: {
|
|
46
|
+
default2: "result1B",
|
|
47
|
+
valueA: "result1A",
|
|
48
|
+
valueB: "result1B",
|
|
49
|
+
},
|
|
50
|
+
value1Empty: {
|
|
51
|
+
"": "emptyResult",
|
|
52
|
+
default2: "emptyDefault",
|
|
53
|
+
},
|
|
54
|
+
value2: {
|
|
55
|
+
default2: "result2B",
|
|
56
|
+
valueA: "result2A",
|
|
57
|
+
valueB: "result2B",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
onUnsupported: ({
|
|
61
|
+
element,
|
|
62
|
+
attributes,
|
|
63
|
+
j,
|
|
64
|
+
}: {
|
|
65
|
+
attributes: { attribute: JSXAttribute | undefined; name: string }[]
|
|
66
|
+
element: JSXElement
|
|
67
|
+
j: JSCodeshift
|
|
68
|
+
source: Collection
|
|
69
|
+
}) => {
|
|
70
|
+
attributes.forEach(
|
|
71
|
+
({
|
|
72
|
+
attribute,
|
|
73
|
+
name,
|
|
74
|
+
}: {
|
|
75
|
+
attribute: JSXAttribute | undefined
|
|
76
|
+
name: string
|
|
77
|
+
}) => {
|
|
78
|
+
if (attribute) {
|
|
79
|
+
const attrValue =
|
|
80
|
+
(attribute.value as StringLiteral)?.value || "unknown"
|
|
81
|
+
const comment = j.jsxText(
|
|
82
|
+
`/* TODO: ${name}="${attrValue}" unsupported */`
|
|
83
|
+
)
|
|
84
|
+
if (!element.children) element.children = []
|
|
85
|
+
element.children.unshift(comment)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
return true
|
|
90
|
+
},
|
|
91
|
+
package: "@test/components",
|
|
92
|
+
sourceAttributes: ["first", "second"] as [string, string],
|
|
93
|
+
targetAttribute: "result",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("static values", () => {
|
|
97
|
+
it("should combine two static attributes", () => {
|
|
98
|
+
const source = `
|
|
99
|
+
import { TestComponent } from "@test/components"
|
|
100
|
+
<TestComponent first="value1" second="valueA">Content</TestComponent>
|
|
101
|
+
`
|
|
102
|
+
|
|
103
|
+
const result = applyTransform(source, basicConfig)
|
|
104
|
+
|
|
105
|
+
expect(result).toContain('result="result1A"')
|
|
106
|
+
expect(result).not.toContain('first="value1"')
|
|
107
|
+
expect(result).not.toContain('second="valueA"')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("should handle missing attributes with defaults", () => {
|
|
111
|
+
const source = `
|
|
112
|
+
import { TestComponent } from "@test/components"
|
|
113
|
+
<TestComponent first="value1">Content</TestComponent>
|
|
114
|
+
`
|
|
115
|
+
|
|
116
|
+
const result = applyTransform(source, basicConfig)
|
|
117
|
+
|
|
118
|
+
expect(result).toContain('result="result1B"')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("should handle both missing attributes with defaults", () => {
|
|
122
|
+
const source = `
|
|
123
|
+
import { TestComponent } from "@test/components"
|
|
124
|
+
<TestComponent>Content</TestComponent>
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
const result = applyTransform(source, basicConfig)
|
|
128
|
+
|
|
129
|
+
expect(result).toBe(null)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("should handle empty string values", () => {
|
|
133
|
+
const source = `
|
|
134
|
+
import { TestComponent } from "@test/components"
|
|
135
|
+
<TestComponent first="value1Empty" second="">Content</TestComponent>
|
|
136
|
+
`
|
|
137
|
+
|
|
138
|
+
const result = applyTransform(source, basicConfig)
|
|
139
|
+
|
|
140
|
+
expect(result).toContain('result="emptyDefault"')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("conditional values", () => {
|
|
145
|
+
it("should handle conditional in first attribute", () => {
|
|
146
|
+
const source = `
|
|
147
|
+
import { TestComponent } from "@test/components"
|
|
148
|
+
<TestComponent first={condition ? "value1" : "value2"} second="valueA">Content</TestComponent>
|
|
149
|
+
`
|
|
150
|
+
|
|
151
|
+
const result = applyTransform(source, basicConfig)
|
|
152
|
+
|
|
153
|
+
expect(result).toContain('result={condition ? "result1A" : "result2A"}')
|
|
154
|
+
expect(result).not.toContain("first=")
|
|
155
|
+
expect(result).not.toContain("second=")
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("should handle conditional in second attribute", () => {
|
|
159
|
+
const source = `
|
|
160
|
+
import { TestComponent } from "@test/components"
|
|
161
|
+
<TestComponent first="value1" second={condition ? "valueA" : "valueB"}>Content</TestComponent>
|
|
162
|
+
`
|
|
163
|
+
|
|
164
|
+
const result = applyTransform(source, basicConfig)
|
|
165
|
+
|
|
166
|
+
expect(result).toContain('result={condition ? "result1A" : "result1B"}')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("should handle conditionals in both attributes with same test", () => {
|
|
170
|
+
const source = `
|
|
171
|
+
import { TestComponent } from "@test/components"
|
|
172
|
+
<TestComponent first={condition ? "value1" : "value2"} second={condition ? "valueA" : "valueB"}>Content</TestComponent>
|
|
173
|
+
`
|
|
174
|
+
|
|
175
|
+
const result = applyTransform(source, basicConfig)
|
|
176
|
+
|
|
177
|
+
expect(result).toContain('result={condition ? "result1A" : "result2B"}')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("should handle conditionals with undefined values", () => {
|
|
181
|
+
const source = `
|
|
182
|
+
import { TestComponent } from "@test/components"
|
|
183
|
+
<TestComponent first={condition ? "value1" : undefined} second="valueA">Content</TestComponent>
|
|
184
|
+
`
|
|
185
|
+
|
|
186
|
+
const result = applyTransform(source, basicConfig)
|
|
187
|
+
|
|
188
|
+
expect(result).toContain(
|
|
189
|
+
'result={condition ? "result1A" : "defaultResult"}'
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it("should handle conditionals with null values", () => {
|
|
194
|
+
const source = `
|
|
195
|
+
import { TestComponent } from "@test/components"
|
|
196
|
+
<TestComponent first={condition ? "value1" : null} second="valueA">Content</TestComponent>
|
|
197
|
+
`
|
|
198
|
+
|
|
199
|
+
const result = applyTransform(source, basicConfig)
|
|
200
|
+
|
|
201
|
+
expect(result).toContain(
|
|
202
|
+
'result={condition ? "result1A" : "defaultResult"}'
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("should handle conditional with null in both branches", () => {
|
|
207
|
+
const source = `
|
|
208
|
+
import { TestComponent } from "@test/components"
|
|
209
|
+
<TestComponent first={condition ? null : null} second="valueA">Content</TestComponent>
|
|
210
|
+
`
|
|
211
|
+
|
|
212
|
+
const result = applyTransform(source, basicConfig)
|
|
213
|
+
|
|
214
|
+
expect(result).toContain(
|
|
215
|
+
'result={condition ? "defaultResult" : "defaultResult"}'
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe("unsupported cases", () => {
|
|
221
|
+
it("should handle unsupported attribute values", () => {
|
|
222
|
+
const source = `
|
|
223
|
+
import { TestComponent } from "@test/components"
|
|
224
|
+
<TestComponent first="unsupported" second="valueA">Content</TestComponent>
|
|
225
|
+
`
|
|
226
|
+
|
|
227
|
+
const result = applyTransform(source, basicConfig)
|
|
228
|
+
|
|
229
|
+
expect(result).toContain('/* TODO: first="unsupported" unsupported */')
|
|
230
|
+
expect(result).not.toContain("result=")
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it("should handle JSX expression containers with string literals", () => {
|
|
234
|
+
const source = `
|
|
235
|
+
import { TestComponent } from "@test/components"
|
|
236
|
+
<TestComponent first={"value1"} second="valueA">Content</TestComponent>
|
|
237
|
+
`
|
|
238
|
+
|
|
239
|
+
const result = applyTransform(source, basicConfig)
|
|
240
|
+
|
|
241
|
+
expect(result).toContain("/* TODO:")
|
|
242
|
+
expect(result).not.toContain("result=")
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it("should handle complex expressions", () => {
|
|
246
|
+
const source = `
|
|
247
|
+
import { TestComponent } from "@test/components"
|
|
248
|
+
<TestComponent first={getValue()} second="valueA">Content</TestComponent>
|
|
249
|
+
`
|
|
250
|
+
|
|
251
|
+
const result = applyTransform(source, basicConfig)
|
|
252
|
+
|
|
253
|
+
expect(result).toContain("/* TODO:")
|
|
254
|
+
expect(result).not.toContain("result=")
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("should handle conditionals with different test names", () => {
|
|
258
|
+
const source = `
|
|
259
|
+
import { TestComponent } from "@test/components"
|
|
260
|
+
<TestComponent first={condition1 ? "value1" : "value2"} second={condition2 ? "valueA" : "valueB"}>Content</TestComponent>
|
|
261
|
+
`
|
|
262
|
+
|
|
263
|
+
const result = applyTransform(source, basicConfig)
|
|
264
|
+
|
|
265
|
+
expect(result).toContain("/* TODO:")
|
|
266
|
+
expect(result).not.toContain("result=")
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe("value normalization", () => {
|
|
271
|
+
it("should apply value normalizers", () => {
|
|
272
|
+
const configWithNormalizer = {
|
|
273
|
+
...basicConfig,
|
|
274
|
+
valueNormalizers: {
|
|
275
|
+
first: (value: string | null | undefined) => {
|
|
276
|
+
if (value === "oldValue") return "value1"
|
|
277
|
+
return value || basicConfig.defaults.first
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const source = `
|
|
283
|
+
import { TestComponent } from "@test/components"
|
|
284
|
+
<TestComponent first="oldValue" second="valueA">Content</TestComponent>
|
|
285
|
+
`
|
|
286
|
+
|
|
287
|
+
const result = applyTransform(source, configWithNormalizer)
|
|
288
|
+
|
|
289
|
+
expect(result).toContain('result="result1A"')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it("should apply normalizers to conditional values", () => {
|
|
293
|
+
const configWithNormalizer = {
|
|
294
|
+
...basicConfig,
|
|
295
|
+
valueNormalizers: {
|
|
296
|
+
first: (value: string | null | undefined) => {
|
|
297
|
+
if (value === "old1") return "value1"
|
|
298
|
+
if (value === "old2") return "value2"
|
|
299
|
+
return value || basicConfig.defaults.first
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const source = `
|
|
305
|
+
import { TestComponent } from "@test/components"
|
|
306
|
+
<TestComponent first={condition ? "old1" : "old2"} second="valueA">Content</TestComponent>
|
|
307
|
+
`
|
|
308
|
+
|
|
309
|
+
const result = applyTransform(source, configWithNormalizer)
|
|
310
|
+
|
|
311
|
+
expect(result).toContain('result={condition ? "result1A" : "result2A"}')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe("edge cases", () => {
|
|
316
|
+
it("should handle components without matching attributes", () => {
|
|
317
|
+
const source = `
|
|
318
|
+
import { TestComponent } from "@test/components"
|
|
319
|
+
<TestComponent other="value">Content</TestComponent>
|
|
320
|
+
`
|
|
321
|
+
|
|
322
|
+
const result = applyTransform(source, basicConfig)
|
|
323
|
+
|
|
324
|
+
expect(result).toBe(null)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it("should handle boolean attribute values", () => {
|
|
328
|
+
const source = `
|
|
329
|
+
import { TestComponent } from "@test/components"
|
|
330
|
+
<TestComponent first={true} second="valueA">Content</TestComponent>
|
|
331
|
+
`
|
|
332
|
+
|
|
333
|
+
const result = applyTransform(source, basicConfig)
|
|
334
|
+
|
|
335
|
+
expect(result).toContain("/* TODO:")
|
|
336
|
+
expect(result).not.toContain("result=")
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("should handle null literal attributes", () => {
|
|
340
|
+
const source = `
|
|
341
|
+
import { TestComponent } from "@test/components"
|
|
342
|
+
<TestComponent first={null} second="valueA">Content</TestComponent>
|
|
343
|
+
`
|
|
344
|
+
|
|
345
|
+
const result = applyTransform(source, basicConfig)
|
|
346
|
+
|
|
347
|
+
expect(result).toContain('result="defaultResult"')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it("should handle false boolean attributes", () => {
|
|
351
|
+
const source = `
|
|
352
|
+
import { TestComponent } from "@test/components"
|
|
353
|
+
<TestComponent first={false} second="valueA">Content</TestComponent>
|
|
354
|
+
`
|
|
355
|
+
|
|
356
|
+
const result = applyTransform(source, basicConfig)
|
|
357
|
+
|
|
358
|
+
expect(result).toContain('result="defaultResult"')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it("should handle empty string in conditionals", () => {
|
|
362
|
+
const source = `
|
|
363
|
+
import { TestComponent } from "@test/components"
|
|
364
|
+
<TestComponent first={condition ? "" : "value1"} second="valueA">Content</TestComponent>
|
|
365
|
+
`
|
|
366
|
+
|
|
367
|
+
const result = applyTransform(source, basicConfig)
|
|
368
|
+
|
|
369
|
+
expect(result).toContain(
|
|
370
|
+
'result={condition ? "defaultResult" : "result1A"}'
|
|
371
|
+
)
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
})
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Collection,
|
|
3
|
+
JSCodeshift,
|
|
4
|
+
JSXAttribute,
|
|
5
|
+
JSXElement,
|
|
6
|
+
Transform,
|
|
7
|
+
} from "jscodeshift"
|
|
8
|
+
|
|
9
|
+
import { addAttribute, Conditional } from "../actions/addAttribute"
|
|
10
|
+
import { getAttribute } from "../actions/getAttribute"
|
|
11
|
+
import { removeAttribute } from "../actions/removeAttribute"
|
|
12
|
+
import { hasAttribute } from "../conditions/hasAttribute"
|
|
13
|
+
import { orConditions } from "../conditions/orConditions"
|
|
14
|
+
import { TransformCondition } from "../types"
|
|
15
|
+
import { attributeTransformFactory } from "./attributeTransformFactory"
|
|
16
|
+
|
|
17
|
+
export interface MappingTable {
|
|
18
|
+
[key: string]: {
|
|
19
|
+
[key: string]: string
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AttributeDefaults {
|
|
24
|
+
[key: string]: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ValueNormalizer {
|
|
28
|
+
(value: string | null | undefined): string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AttributeMappingConfig {
|
|
32
|
+
/** Target component name */
|
|
33
|
+
component: string
|
|
34
|
+
/** Custom condition (optional, will use hasAttribute check by default) */
|
|
35
|
+
condition?: TransformCondition
|
|
36
|
+
/** Default values for missing attributes */
|
|
37
|
+
defaults: AttributeDefaults
|
|
38
|
+
/** Multi-dimensional mapping table */
|
|
39
|
+
mappingTable: MappingTable
|
|
40
|
+
onUnsupported: (resources: {
|
|
41
|
+
attributes: { attribute: JSXAttribute | undefined; name: string }[]
|
|
42
|
+
element: JSXElement
|
|
43
|
+
j: JSCodeshift
|
|
44
|
+
source: Collection
|
|
45
|
+
}) => boolean
|
|
46
|
+
/** Package the component is imported from */
|
|
47
|
+
package: string
|
|
48
|
+
sourceAttributes: [string, string]
|
|
49
|
+
/** Target attribute to map to (e.g., 'kind') */
|
|
50
|
+
targetAttribute: string
|
|
51
|
+
/** Optional value normalizers for each attribute */
|
|
52
|
+
valueNormalizers?: Record<string, ValueNormalizer>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract string value from a JSX attribute
|
|
57
|
+
*/
|
|
58
|
+
function getAttributeStringValue(
|
|
59
|
+
attr: JSXAttribute | undefined,
|
|
60
|
+
defaultValue: string,
|
|
61
|
+
normalizer?: ValueNormalizer
|
|
62
|
+
): string | null {
|
|
63
|
+
if (!attr?.value) {
|
|
64
|
+
return normalizer ? normalizer(defaultValue) : defaultValue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (attr.value.type === "StringLiteral") {
|
|
68
|
+
const value = attr.value.value || defaultValue
|
|
69
|
+
return normalizer ? normalizer(value) : value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (attr.value.type === "JSXExpressionContainer") {
|
|
73
|
+
const { expression } = attr.value
|
|
74
|
+
|
|
75
|
+
// Handle null, boolean false as default
|
|
76
|
+
if (
|
|
77
|
+
expression.type === "NullLiteral" ||
|
|
78
|
+
(expression.type === "BooleanLiteral" && !expression.value)
|
|
79
|
+
) {
|
|
80
|
+
return normalizer ? normalizer(defaultValue) : defaultValue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// String literals in expressions are unsupported
|
|
84
|
+
if (expression.type === "StringLiteral") {
|
|
85
|
+
return null // Unsupported
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null // Unsupported
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getAttributeConditionalValue(
|
|
93
|
+
attr: JSXAttribute | undefined,
|
|
94
|
+
defaultValue: string,
|
|
95
|
+
normalizer?: ValueNormalizer
|
|
96
|
+
): { alternate: string; consequent: string; test: string } | null {
|
|
97
|
+
if (
|
|
98
|
+
attr?.value?.type === "JSXExpressionContainer" &&
|
|
99
|
+
attr.value.expression.type === "ConditionalExpression"
|
|
100
|
+
) {
|
|
101
|
+
const expr = attr.value.expression
|
|
102
|
+
|
|
103
|
+
if (expr.test.type !== "Identifier") return null
|
|
104
|
+
|
|
105
|
+
const test = expr.test.name
|
|
106
|
+
|
|
107
|
+
let consequent: string
|
|
108
|
+
if (expr.consequent.type === "StringLiteral") {
|
|
109
|
+
consequent = expr.consequent.value || defaultValue
|
|
110
|
+
} else if (
|
|
111
|
+
expr.consequent.type === "Identifier" &&
|
|
112
|
+
expr.consequent.name === "undefined"
|
|
113
|
+
) {
|
|
114
|
+
consequent = defaultValue
|
|
115
|
+
} else if (expr.consequent.type === "NullLiteral") {
|
|
116
|
+
consequent = defaultValue
|
|
117
|
+
} else {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let alternate: string
|
|
122
|
+
if (expr.alternate.type === "StringLiteral") {
|
|
123
|
+
alternate = expr.alternate.value || defaultValue
|
|
124
|
+
} else if (
|
|
125
|
+
expr.alternate.type === "Identifier" &&
|
|
126
|
+
expr.alternate.name === "undefined"
|
|
127
|
+
) {
|
|
128
|
+
alternate = defaultValue
|
|
129
|
+
} else if (expr.alternate.type === "NullLiteral") {
|
|
130
|
+
alternate = defaultValue
|
|
131
|
+
} else {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (normalizer) {
|
|
136
|
+
return {
|
|
137
|
+
alternate: normalizer(alternate),
|
|
138
|
+
consequent: normalizer(consequent),
|
|
139
|
+
test,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { alternate, consequent, test }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if attribute is conditional
|
|
151
|
+
*/
|
|
152
|
+
function isConditionalAttribute(attr: JSXAttribute | undefined): boolean {
|
|
153
|
+
return (
|
|
154
|
+
attr?.value?.type === "JSXExpressionContainer" &&
|
|
155
|
+
attr.value.expression.type === "ConditionalExpression"
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Combines two JSX attributes into a mapped result using the provided mapping table
|
|
161
|
+
*/
|
|
162
|
+
function combineAttributes(
|
|
163
|
+
attr1: JSXAttribute | undefined,
|
|
164
|
+
attr2: JSXAttribute | undefined,
|
|
165
|
+
{
|
|
166
|
+
mappingTable,
|
|
167
|
+
defaults,
|
|
168
|
+
valueNormalizers = {},
|
|
169
|
+
attributeNames,
|
|
170
|
+
}: {
|
|
171
|
+
attributeNames: [string, string]
|
|
172
|
+
defaults: AttributeDefaults
|
|
173
|
+
mappingTable: MappingTable
|
|
174
|
+
valueNormalizers?: Record<string, ValueNormalizer>
|
|
175
|
+
}
|
|
176
|
+
): string | Conditional | null {
|
|
177
|
+
const [firstName, secondName] = attributeNames
|
|
178
|
+
const normalizer1 = valueNormalizers[firstName]
|
|
179
|
+
const normalizer2 = valueNormalizers[secondName]
|
|
180
|
+
const default1 = defaults[firstName] || ""
|
|
181
|
+
const default2 = defaults[secondName] || ""
|
|
182
|
+
|
|
183
|
+
const isConditional1 = isConditionalAttribute(attr1)
|
|
184
|
+
const isConditional2 = isConditionalAttribute(attr2)
|
|
185
|
+
|
|
186
|
+
// Handle conditional cases
|
|
187
|
+
if (isConditional1 || isConditional2) {
|
|
188
|
+
const cond1 = isConditional1
|
|
189
|
+
? getAttributeConditionalValue(attr1, default1, normalizer1)
|
|
190
|
+
: null
|
|
191
|
+
const cond2 = isConditional2
|
|
192
|
+
? getAttributeConditionalValue(attr2, default2, normalizer2)
|
|
193
|
+
: null
|
|
194
|
+
const static1 = !isConditional1
|
|
195
|
+
? getAttributeStringValue(attr1, default1, normalizer1)
|
|
196
|
+
: null
|
|
197
|
+
const static2 = !isConditional2
|
|
198
|
+
? getAttributeStringValue(attr2, default2, normalizer2)
|
|
199
|
+
: null
|
|
200
|
+
|
|
201
|
+
// If both are conditional, they must use the same test
|
|
202
|
+
if (cond1 && cond2 && cond1.test !== cond2.test) return null
|
|
203
|
+
|
|
204
|
+
const consequentValue1 = cond1?.consequent ?? static1
|
|
205
|
+
const alternateValue1 = cond1?.alternate ?? static1
|
|
206
|
+
const consequentValue2 = cond2?.consequent ?? static2
|
|
207
|
+
const alternateValue2 = cond2?.alternate ?? static2
|
|
208
|
+
|
|
209
|
+
// Check that we have all required values (null means unsupported, empty string is valid)
|
|
210
|
+
if (
|
|
211
|
+
consequentValue1 === null ||
|
|
212
|
+
consequentValue2 === null ||
|
|
213
|
+
alternateValue1 === null ||
|
|
214
|
+
alternateValue2 === null
|
|
215
|
+
) {
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const consequent = mappingTable[consequentValue1]?.[consequentValue2]
|
|
220
|
+
const alternate = mappingTable[alternateValue1]?.[alternateValue2]
|
|
221
|
+
|
|
222
|
+
if (!consequent || !alternate) return null
|
|
223
|
+
|
|
224
|
+
// Use the test from whichever attribute is conditional
|
|
225
|
+
const test = cond1?.test ?? cond2?.test
|
|
226
|
+
if (!test) return null
|
|
227
|
+
|
|
228
|
+
return { alternate, consequent, test }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const static1 = getAttributeStringValue(attr1, default1, normalizer1)
|
|
232
|
+
const static2 = getAttributeStringValue(attr2, default2, normalizer2)
|
|
233
|
+
|
|
234
|
+
if (!static1 || !static2) return null
|
|
235
|
+
|
|
236
|
+
const result = mappingTable[static1]?.[static2]
|
|
237
|
+
return result || null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function attributeCombineFactory({
|
|
241
|
+
sourceAttributes,
|
|
242
|
+
targetAttribute,
|
|
243
|
+
mappingTable,
|
|
244
|
+
defaults,
|
|
245
|
+
valueNormalizers = {},
|
|
246
|
+
component,
|
|
247
|
+
package: packageName,
|
|
248
|
+
condition,
|
|
249
|
+
onUnsupported,
|
|
250
|
+
}: AttributeMappingConfig): Transform {
|
|
251
|
+
const defaultCondition = orConditions(
|
|
252
|
+
...sourceAttributes.map((attr: string) => hasAttribute(attr))
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
type AttributeWithName = {
|
|
256
|
+
attribute: JSXAttribute | undefined
|
|
257
|
+
name: string
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return attributeTransformFactory({
|
|
261
|
+
condition: condition || defaultCondition,
|
|
262
|
+
targetComponent: component,
|
|
263
|
+
targetPackage: packageName,
|
|
264
|
+
transform: (
|
|
265
|
+
element: JSXElement,
|
|
266
|
+
{ j, source }: { j: JSCodeshift; source: Collection }
|
|
267
|
+
) => {
|
|
268
|
+
const attributes: AttributeWithName[] = sourceAttributes.map(
|
|
269
|
+
(name: string) => ({
|
|
270
|
+
attribute: getAttribute({ element, name }),
|
|
271
|
+
name,
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
const [attr1, attr2] = attributes.map(
|
|
276
|
+
({ attribute }: { attribute: JSXAttribute | undefined }) => attribute
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const mappingResult = combineAttributes(attr1, attr2, {
|
|
280
|
+
attributeNames: sourceAttributes,
|
|
281
|
+
defaults,
|
|
282
|
+
mappingTable,
|
|
283
|
+
valueNormalizers,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
attributes.forEach(({ attribute, name }) => {
|
|
287
|
+
if (attribute) {
|
|
288
|
+
removeAttribute(name, { element, j, source })
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
if (mappingResult === null) {
|
|
293
|
+
return onUnsupported({ attributes, element, j, source })
|
|
294
|
+
}
|
|
295
|
+
addAttribute({ element, j, name: targetAttribute, value: mappingResult })
|
|
296
|
+
|
|
297
|
+
return true
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
}
|