@planningcenter/tapestry-migration-cli 3.3.2-qa-889.0 → 3.4.0
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/dist/tapestry-react-shim.cjs +17 -8
- package/package.json +3 -3
- package/src/components/button/transforms/childrenToLabel.ts +10 -3
- package/src/components/button/transforms/iconToIconButton.ts +0 -1
- package/src/components/button/transforms/moveButtonImport.ts +0 -1
- package/src/components/checkbox/transforms/childrenToLabel.ts +10 -3
- package/src/components/checkbox/transforms/moveCheckboxImport.ts +0 -1
- package/src/components/input/transforms/moveInputImport.ts +0 -1
- package/src/components/input/transforms/numberFieldRenameToInput.ts +0 -1
- package/src/components/link/index.ts +0 -2
- package/src/components/link/transforms/childrenToLabel.test.ts +36 -3
- package/src/components/link/transforms/childrenToLabel.ts +10 -3
- package/src/components/link/transforms/moveLinkImport.ts +0 -1
- package/src/components/link/transforms/unsupportedProps.ts +2 -1
- package/src/components/radio/transforms/moveRadioImport.ts +0 -1
- package/src/components/select/transforms/moveSelectImport.ts +0 -1
- package/src/components/shared/actions/addAttribute.ts +18 -2
- package/src/components/shared/actions/createWrapper.ts +2 -1
- package/src/components/shared/helpers/childrenToLabelHelpers.ts +24 -1
- package/src/components/shared/transformFactories/helpers/addImport.test.ts +45 -0
- package/src/components/shared/transformFactories/helpers/manageImports.ts +4 -2
- package/src/components/text-area/transforms/moveTextAreaImport.ts +0 -1
- package/src/components/toggle-switch/transforms/moveToggleSwitchImport.ts +0 -1
- package/src/components/link/transforms/targetBlankToExternal.test.ts +0 -221
- package/src/components/link/transforms/targetBlankToExternal.ts +0 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@emotion/react": "^11.14.0",
|
|
33
|
-
"@planningcenter/tapestry": "^3.
|
|
33
|
+
"@planningcenter/tapestry": "^3.4.0",
|
|
34
34
|
"@planningcenter/tapestry-react": "^4.11.5",
|
|
35
35
|
"@types/jscodeshift": "^17.3.0",
|
|
36
36
|
"@types/node": "^20.0.0",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "0a541a2444b2d89e7fab714a3c6772336479b0fd"
|
|
54
54
|
}
|
|
@@ -27,10 +27,17 @@ const transform: Transform = attributeTransformFactory({
|
|
|
27
27
|
return true
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const { isSimpleText, textContent } = extractTextContent(
|
|
30
|
+
const { expression, isSimpleText, textContent } = extractTextContent(
|
|
31
|
+
element.children!
|
|
32
|
+
)
|
|
31
33
|
|
|
32
|
-
if (isSimpleText && textContent) {
|
|
33
|
-
addAttribute({
|
|
34
|
+
if (expression || (isSimpleText && textContent)) {
|
|
35
|
+
addAttribute({
|
|
36
|
+
element,
|
|
37
|
+
j,
|
|
38
|
+
name: "label",
|
|
39
|
+
value: expression ?? textContent,
|
|
40
|
+
})
|
|
34
41
|
removeChildren(element)
|
|
35
42
|
|
|
36
43
|
if (options?.verbose) {
|
|
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
4
4
|
|
|
5
5
|
const transform: Transform = componentTransformFactory({
|
|
6
6
|
condition: () => true,
|
|
7
|
-
conflictAlias: "TButton",
|
|
8
7
|
fromComponent: "Button",
|
|
9
8
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
9
|
toComponent: "Button",
|
|
@@ -27,10 +27,17 @@ const transform: Transform = attributeTransformFactory({
|
|
|
27
27
|
return true
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const { isSimpleText, textContent } = extractTextContent(
|
|
30
|
+
const { expression, isSimpleText, textContent } = extractTextContent(
|
|
31
|
+
element.children!
|
|
32
|
+
)
|
|
31
33
|
|
|
32
|
-
if (isSimpleText && textContent) {
|
|
33
|
-
addAttribute({
|
|
34
|
+
if (expression || (isSimpleText && textContent)) {
|
|
35
|
+
addAttribute({
|
|
36
|
+
element,
|
|
37
|
+
j,
|
|
38
|
+
name: "label",
|
|
39
|
+
value: expression ?? textContent,
|
|
40
|
+
})
|
|
34
41
|
removeChildren(element)
|
|
35
42
|
|
|
36
43
|
if (options?.verbose) {
|
|
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
4
4
|
|
|
5
5
|
const transform: Transform = componentTransformFactory({
|
|
6
6
|
condition: () => true,
|
|
7
|
-
conflictAlias: "TCheckbox",
|
|
8
7
|
fromComponent: "Checkbox",
|
|
9
8
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
9
|
toComponent: "Checkbox",
|
|
@@ -5,7 +5,6 @@ import { transformableInput } from "../transformableInput"
|
|
|
5
5
|
|
|
6
6
|
const transform: Transform = componentTransformFactory({
|
|
7
7
|
condition: transformableInput,
|
|
8
|
-
conflictAlias: "TInput",
|
|
9
8
|
fromComponent: "Input",
|
|
10
9
|
fromPackage: "@planningcenter/tapestry-react",
|
|
11
10
|
toComponent: "Input",
|
|
@@ -2,7 +2,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
2
2
|
|
|
3
3
|
export default componentTransformFactory({
|
|
4
4
|
condition: () => true,
|
|
5
|
-
conflictAlias: "TInput",
|
|
6
5
|
fromComponent: "NumberField",
|
|
7
6
|
fromPackage: "@planningcenter/tapestry-react",
|
|
8
7
|
toComponent: "Input",
|
|
@@ -9,7 +9,6 @@ import removeAs from "./transforms/removeAs"
|
|
|
9
9
|
import removeInlineMember from "./transforms/removeInlineMember"
|
|
10
10
|
import removeInlineProp from "./transforms/removeInlineProp"
|
|
11
11
|
import reviewStyles from "./transforms/reviewStyles"
|
|
12
|
-
import targetBlankToExternal from "./transforms/targetBlankToExternal"
|
|
13
12
|
import tooltipToWrapper from "./transforms/tooltipToWrapper"
|
|
14
13
|
import toToHref from "./transforms/toToHref"
|
|
15
14
|
import unsupportedProps from "./transforms/unsupportedProps"
|
|
@@ -23,7 +22,6 @@ const transform: Transform = (fileInfo, api, options) => {
|
|
|
23
22
|
removeInlineProp,
|
|
24
23
|
tooltipToWrapper,
|
|
25
24
|
toToHref,
|
|
26
|
-
targetBlankToExternal,
|
|
27
25
|
innerRefToRef,
|
|
28
26
|
removeAs,
|
|
29
27
|
childrenToLabel,
|
|
@@ -81,6 +81,39 @@ function Test() {
|
|
|
81
81
|
const result = applyTransform(input)
|
|
82
82
|
expect(result).toContain('<Link href="/help" label="GetHelpNow" />')
|
|
83
83
|
})
|
|
84
|
+
|
|
85
|
+
it("should convert a lone identifier expression to label prop", () => {
|
|
86
|
+
const input = `
|
|
87
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
88
|
+
|
|
89
|
+
function Test({ registration_form_url }) {
|
|
90
|
+
return (
|
|
91
|
+
<Link external href={registration_form_url}>
|
|
92
|
+
{registration_form_url}
|
|
93
|
+
</Link>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
`.trim()
|
|
97
|
+
|
|
98
|
+
const result = applyTransform(input)
|
|
99
|
+
expect(result).toContain("label={registration_form_url}")
|
|
100
|
+
expect(result).not.toContain("complex children")
|
|
101
|
+
expect(result).not.toContain(">\n")
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("should convert a lone member expression to label prop", () => {
|
|
105
|
+
const input = `
|
|
106
|
+
import { Link } from "@planningcenter/tapestry-react"
|
|
107
|
+
|
|
108
|
+
function Test({ data }) {
|
|
109
|
+
return <Link href={data.href}>{data.label}</Link>
|
|
110
|
+
}
|
|
111
|
+
`.trim()
|
|
112
|
+
|
|
113
|
+
const result = applyTransform(input)
|
|
114
|
+
expect(result).toContain("label={data.label}")
|
|
115
|
+
expect(result).not.toContain("complex children")
|
|
116
|
+
})
|
|
84
117
|
})
|
|
85
118
|
|
|
86
119
|
describe("complex children scenarios", () => {
|
|
@@ -116,12 +149,12 @@ function Test() {
|
|
|
116
149
|
expect(result).toContain("</Link>")
|
|
117
150
|
})
|
|
118
151
|
|
|
119
|
-
it("should add comment for expression children", () => {
|
|
152
|
+
it("should add comment for conditional expression children", () => {
|
|
120
153
|
const input = `
|
|
121
154
|
import { Link } from "@planningcenter/tapestry-react"
|
|
122
155
|
|
|
123
|
-
function Test({
|
|
124
|
-
return <Link href="/home">{
|
|
156
|
+
function Test({ isActive }) {
|
|
157
|
+
return <Link href="/home">{isActive ? "Active" : "Inactive"}</Link>
|
|
125
158
|
}
|
|
126
159
|
`.trim()
|
|
127
160
|
|
|
@@ -27,10 +27,17 @@ const transform: Transform = attributeTransformFactory({
|
|
|
27
27
|
return true
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const { isSimpleText, textContent } = extractTextContent(
|
|
30
|
+
const { expression, isSimpleText, textContent } = extractTextContent(
|
|
31
|
+
element.children!
|
|
32
|
+
)
|
|
31
33
|
|
|
32
|
-
if (isSimpleText && textContent) {
|
|
33
|
-
addAttribute({
|
|
34
|
+
if (expression || (isSimpleText && textContent)) {
|
|
35
|
+
addAttribute({
|
|
36
|
+
element,
|
|
37
|
+
j,
|
|
38
|
+
name: "label",
|
|
39
|
+
value: expression ?? textContent,
|
|
40
|
+
})
|
|
34
41
|
removeChildren(element)
|
|
35
42
|
|
|
36
43
|
if (options?.verbose) {
|
|
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
4
4
|
|
|
5
5
|
const transform: Transform = componentTransformFactory({
|
|
6
6
|
condition: () => true,
|
|
7
|
-
conflictAlias: "TLink",
|
|
8
7
|
fromComponent: "Link",
|
|
9
8
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
9
|
toComponent: "Link",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BUTTON_LINK_SUPPORTED_PROPS } from "../../shared/helpers/unsupportedPropsHelpers"
|
|
2
2
|
import { unsupportedPropsFactory } from "../../shared/transformFactories/unsupportedPropsFactory"
|
|
3
3
|
|
|
4
|
-
// Note: 'target' and 'rel' are NOT included because they are handled by targetBlankToExternal transform
|
|
5
4
|
const LINK_SPECIFIC_PROPS = [
|
|
6
5
|
"download",
|
|
7
6
|
"external",
|
|
@@ -10,6 +9,8 @@ const LINK_SPECIFIC_PROPS = [
|
|
|
10
9
|
"media",
|
|
11
10
|
"ping",
|
|
12
11
|
"referrerPolicy",
|
|
12
|
+
"rel",
|
|
13
|
+
"target",
|
|
13
14
|
"type",
|
|
14
15
|
]
|
|
15
16
|
|
|
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
4
4
|
|
|
5
5
|
const transform: Transform = componentTransformFactory({
|
|
6
6
|
condition: () => true,
|
|
7
|
-
conflictAlias: "TRadio",
|
|
8
7
|
fromComponent: "Radio",
|
|
9
8
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
9
|
toComponent: "Radio",
|
|
@@ -5,7 +5,6 @@ import { transformableSelect } from "../transformableSelect"
|
|
|
5
5
|
|
|
6
6
|
const transform: Transform = componentTransformFactory({
|
|
7
7
|
condition: transformableSelect,
|
|
8
|
-
conflictAlias: "TSelect",
|
|
9
8
|
fromComponent: "Select",
|
|
10
9
|
fromPackage: "@planningcenter/tapestry-react",
|
|
11
10
|
toComponent: "Select",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JSCodeshift, JSXElement } from "jscodeshift"
|
|
1
|
+
import { JSCodeshift, JSXElement, JSXExpressionContainer } from "jscodeshift"
|
|
2
2
|
|
|
3
3
|
export interface Conditional {
|
|
4
4
|
alternate: string
|
|
@@ -6,6 +6,14 @@ export interface Conditional {
|
|
|
6
6
|
test: string
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
type ExpressionNode = JSXExpressionContainer["expression"]
|
|
10
|
+
|
|
11
|
+
function isExpressionNode(
|
|
12
|
+
value: string | boolean | Conditional | ExpressionNode
|
|
13
|
+
): value is ExpressionNode {
|
|
14
|
+
return typeof value === "object" && value !== null && "type" in value
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
function formatValue(value: string | boolean, j: JSCodeshift) {
|
|
10
18
|
if (typeof value === "string") {
|
|
11
19
|
return j.stringLiteral(value)
|
|
@@ -27,11 +35,19 @@ export function addAttribute({
|
|
|
27
35
|
element: JSXElement
|
|
28
36
|
j: JSCodeshift
|
|
29
37
|
name: string
|
|
30
|
-
value: string | boolean | Conditional | null
|
|
38
|
+
value: string | boolean | Conditional | ExpressionNode | null
|
|
31
39
|
}) {
|
|
32
40
|
if (value === null) return
|
|
33
41
|
const attributes = element.openingElement.attributes || []
|
|
34
42
|
|
|
43
|
+
// An AST expression node is wrapped in braces, e.g. label={url}
|
|
44
|
+
if (isExpressionNode(value)) {
|
|
45
|
+
attributes.push(
|
|
46
|
+
j.jsxAttribute(j.jsxIdentifier(name), j.jsxExpressionContainer(value))
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
if (
|
|
36
52
|
typeof value === "object" &&
|
|
37
53
|
"test" in value &&
|
|
@@ -18,7 +18,8 @@ export function createWrapper({
|
|
|
18
18
|
wrapperPackage,
|
|
19
19
|
wrapperProps = [],
|
|
20
20
|
}: {
|
|
21
|
-
|
|
21
|
+
/** Alias to use if the wrapper conflicts with an existing import. Defaults to `T${wrapperName}`. */
|
|
22
|
+
conflictAlias?: string
|
|
22
23
|
element: JSXElement
|
|
23
24
|
j: JSCodeshift
|
|
24
25
|
source: Collection
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
import { JSXElement } from "jscodeshift"
|
|
1
|
+
import { JSXElement, JSXExpressionContainer } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
type LabelExpression = JSXExpressionContainer["expression"]
|
|
2
4
|
|
|
3
5
|
export function extractTextContent(
|
|
4
6
|
children: NonNullable<JSXElement["children"]>
|
|
5
7
|
): {
|
|
8
|
+
expression?: LabelExpression
|
|
6
9
|
isSimpleText: boolean
|
|
7
10
|
textContent: string
|
|
8
11
|
} {
|
|
12
|
+
const meaningfulChildren = children.filter(
|
|
13
|
+
(child) => !(child.type === "JSXText" && !child.value.trim())
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// A lone expression that simply references a value (e.g. {url} or {data.href})
|
|
17
|
+
// is not "complex" — it can be passed straight through to the label prop.
|
|
18
|
+
const onlyChild = meaningfulChildren[0]
|
|
19
|
+
if (
|
|
20
|
+
meaningfulChildren.length === 1 &&
|
|
21
|
+
onlyChild.type === "JSXExpressionContainer" &&
|
|
22
|
+
(onlyChild.expression.type === "Identifier" ||
|
|
23
|
+
onlyChild.expression.type === "MemberExpression")
|
|
24
|
+
) {
|
|
25
|
+
return {
|
|
26
|
+
expression: onlyChild.expression,
|
|
27
|
+
isSimpleText: false,
|
|
28
|
+
textContent: "",
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
let textContent = ""
|
|
10
33
|
|
|
11
34
|
for (const child of children) {
|
|
@@ -254,6 +254,51 @@ describe("addImport", () => {
|
|
|
254
254
|
expect(tapestryImport.at(0).get().value.specifiers).toHaveLength(1)
|
|
255
255
|
})
|
|
256
256
|
|
|
257
|
+
it("should default conflictAlias to `T${component}` when none is provided and a conflict exists", () => {
|
|
258
|
+
const source = createSource(`
|
|
259
|
+
import React from "react"
|
|
260
|
+
import { Button } from "@some/other-package"
|
|
261
|
+
`)
|
|
262
|
+
|
|
263
|
+
const result = addImport({
|
|
264
|
+
component: "Button",
|
|
265
|
+
j,
|
|
266
|
+
pkg: "@planningcenter/tapestry",
|
|
267
|
+
source,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(result).toBe("TButton")
|
|
271
|
+
|
|
272
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
273
|
+
source: { value: "@planningcenter/tapestry" },
|
|
274
|
+
})
|
|
275
|
+
expect(tapestryImport).toHaveLength(1)
|
|
276
|
+
|
|
277
|
+
const buttonSpec = tapestryImport.at(0).get().value.specifiers?.[0]
|
|
278
|
+
expect(buttonSpec?.imported?.name).toBe("Button")
|
|
279
|
+
expect(buttonSpec?.local?.name).toBe("TButton")
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("should not alias when no conflict exists even without a conflictAlias", () => {
|
|
283
|
+
const source = createSource('import React from "react"')
|
|
284
|
+
|
|
285
|
+
const result = addImport({
|
|
286
|
+
component: "Button",
|
|
287
|
+
j,
|
|
288
|
+
pkg: "@planningcenter/tapestry",
|
|
289
|
+
source,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
expect(result).toBe("Button")
|
|
293
|
+
|
|
294
|
+
const tapestryImport = source.find(j.ImportDeclaration, {
|
|
295
|
+
source: { value: "@planningcenter/tapestry" },
|
|
296
|
+
})
|
|
297
|
+
const buttonSpec = tapestryImport.at(0).get().value.specifiers?.[0]
|
|
298
|
+
expect(buttonSpec?.imported?.name).toBe("Button")
|
|
299
|
+
expect(buttonSpec?.local).toBeNull()
|
|
300
|
+
})
|
|
301
|
+
|
|
257
302
|
it("should not use conflictAlias when no conflict exists", () => {
|
|
258
303
|
const source = createSource('import React from "react"')
|
|
259
304
|
|
|
@@ -157,7 +157,8 @@ export function addImport({
|
|
|
157
157
|
source,
|
|
158
158
|
}: {
|
|
159
159
|
component: string
|
|
160
|
-
|
|
160
|
+
/** Alias to use if the target component conflicts with an existing import. Defaults to `T${component}`. */
|
|
161
|
+
conflictAlias?: string
|
|
161
162
|
fromPackage?: string
|
|
162
163
|
j: JSCodeshift
|
|
163
164
|
pkg: string
|
|
@@ -190,7 +191,8 @@ export function addImport({
|
|
|
190
191
|
return component
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
const
|
|
194
|
+
const resolvedConflictAlias = conflictAlias ?? `T${component}`
|
|
195
|
+
const finalComponentName = hasConflict ? resolvedConflictAlias : component
|
|
194
196
|
const alias =
|
|
195
197
|
finalComponentName !== component ? finalComponentName : undefined
|
|
196
198
|
|
|
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
4
4
|
|
|
5
5
|
const transform: Transform = componentTransformFactory({
|
|
6
6
|
condition: () => true,
|
|
7
|
-
conflictAlias: "TTextArea",
|
|
8
7
|
fromComponent: "TextArea",
|
|
9
8
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
9
|
toComponent: "TextArea",
|
|
@@ -4,7 +4,6 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
|
|
|
4
4
|
|
|
5
5
|
const transform: Transform = componentTransformFactory({
|
|
6
6
|
condition: () => true,
|
|
7
|
-
conflictAlias: "TToggleSwitch",
|
|
8
7
|
fromComponent: "ToggleSwitch",
|
|
9
8
|
fromPackage: "@planningcenter/tapestry-react",
|
|
10
9
|
toComponent: "ToggleSwitch",
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import jscodeshift from "jscodeshift"
|
|
2
|
-
import { describe, expect, it } from "vitest"
|
|
3
|
-
|
|
4
|
-
import targetBlankToExternal from "./targetBlankToExternal"
|
|
5
|
-
|
|
6
|
-
const j = jscodeshift.withParser("tsx")
|
|
7
|
-
|
|
8
|
-
function applyTransform(source: string, verbose = false): string | null {
|
|
9
|
-
return targetBlankToExternal(
|
|
10
|
-
{ path: "test.tsx", source },
|
|
11
|
-
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
12
|
-
{ verbose }
|
|
13
|
-
) as string | null
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("targetBlankToExternal transform", () => {
|
|
17
|
-
describe("basic transformations", () => {
|
|
18
|
-
it("should transform Link with target='_blank' to external", () => {
|
|
19
|
-
const input = `
|
|
20
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
21
|
-
|
|
22
|
-
export default function Test() {
|
|
23
|
-
return <Link href="/external" target="_blank">External Link</Link>
|
|
24
|
-
}
|
|
25
|
-
`.trim()
|
|
26
|
-
|
|
27
|
-
const expected = `
|
|
28
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
29
|
-
|
|
30
|
-
export default function Test() {
|
|
31
|
-
return <Link href="/external" external>External Link</Link>;
|
|
32
|
-
}
|
|
33
|
-
`.trim()
|
|
34
|
-
|
|
35
|
-
const result = applyTransform(input)
|
|
36
|
-
expect(result?.trim()).toBe(expected)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it("should remove target and rel attributes while adding external", () => {
|
|
40
|
-
const input = `
|
|
41
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
42
|
-
|
|
43
|
-
export default function Test() {
|
|
44
|
-
return (
|
|
45
|
-
<Link
|
|
46
|
-
href="/external"
|
|
47
|
-
target="_blank"
|
|
48
|
-
rel="noopener noreferrer"
|
|
49
|
-
className="external-link"
|
|
50
|
-
>
|
|
51
|
-
External Link
|
|
52
|
-
</Link>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
`.trim()
|
|
56
|
-
|
|
57
|
-
const result = applyTransform(input)
|
|
58
|
-
expect(result).toContain("external")
|
|
59
|
-
expect(result).toContain('className="external-link"')
|
|
60
|
-
expect(result).not.toContain('target="_blank"')
|
|
61
|
-
expect(result).not.toContain('rel="noopener noreferrer"')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it("should only transform Links with target='_blank'", () => {
|
|
65
|
-
const input = `
|
|
66
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
67
|
-
|
|
68
|
-
export default function Test() {
|
|
69
|
-
return (
|
|
70
|
-
<div>
|
|
71
|
-
<Link href="/internal" target="_self">Internal Link</Link>
|
|
72
|
-
<Link href="/external" target="_blank">External Link</Link>
|
|
73
|
-
</div>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
`.trim()
|
|
77
|
-
|
|
78
|
-
const result = applyTransform(input)
|
|
79
|
-
expect(result).toContain('target="_self"') // Should remain
|
|
80
|
-
expect(result).toContain("external") // Should be added
|
|
81
|
-
expect(result).not.toContain('target="_blank"') // Should be removed
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it("should handle Links without rel attribute", () => {
|
|
85
|
-
const input = `
|
|
86
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
87
|
-
|
|
88
|
-
export default function Test() {
|
|
89
|
-
return <Link href="/external" target="_blank">External Link</Link>
|
|
90
|
-
}
|
|
91
|
-
`.trim()
|
|
92
|
-
|
|
93
|
-
const result = applyTransform(input)
|
|
94
|
-
expect(result).toContain("external")
|
|
95
|
-
expect(result).not.toContain('target="_blank"')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it("should handle Links with only rel attribute (no target)", () => {
|
|
99
|
-
const input = `
|
|
100
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
101
|
-
|
|
102
|
-
export default function Test() {
|
|
103
|
-
return <Link href="/internal" rel="noopener">Internal Link</Link>
|
|
104
|
-
}
|
|
105
|
-
`.trim()
|
|
106
|
-
|
|
107
|
-
const result = applyTransform(input)
|
|
108
|
-
expect(result).toBeNull() // No changes should be made, transform returns null
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
describe("edge cases", () => {
|
|
113
|
-
it("should handle multiple Link components", () => {
|
|
114
|
-
const input = `
|
|
115
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
116
|
-
|
|
117
|
-
export default function Test() {
|
|
118
|
-
return (
|
|
119
|
-
<div>
|
|
120
|
-
<Link href="/page1" target="_blank">Page 1</Link>
|
|
121
|
-
<Link href="/page2" target="_blank" rel="noopener">Page 2</Link>
|
|
122
|
-
<Link href="/page3">Page 3</Link>
|
|
123
|
-
</div>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
`.trim()
|
|
127
|
-
|
|
128
|
-
const result = applyTransform(input)
|
|
129
|
-
expect(result).toContain("external") // Should appear twice
|
|
130
|
-
expect(result).not.toContain('target="_blank"')
|
|
131
|
-
expect(result).not.toContain('rel="noopener"')
|
|
132
|
-
expect(result).toContain('href="/page3"') // Should remain unchanged
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it("should handle target={'_blank'} format", () => {
|
|
136
|
-
const input = `
|
|
137
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
138
|
-
|
|
139
|
-
export default function Test() {
|
|
140
|
-
return <Link href="/external" target={'_blank'}>External Link</Link>
|
|
141
|
-
}
|
|
142
|
-
`.trim()
|
|
143
|
-
|
|
144
|
-
const result = applyTransform(input)
|
|
145
|
-
expect(result).toContain("external")
|
|
146
|
-
expect(result).not.toContain("target={'_blank'}")
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it("should handle dynamic target values", () => {
|
|
150
|
-
const input = `
|
|
151
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
152
|
-
|
|
153
|
-
export default function Test() {
|
|
154
|
-
return <Link href="/external" target={isExternal ? "_blank" : "_self"}>Link</Link>
|
|
155
|
-
}
|
|
156
|
-
`.trim()
|
|
157
|
-
|
|
158
|
-
const result = applyTransform(input)
|
|
159
|
-
expect(result).toBeNull() // No changes should be made for dynamic values
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it("should handle self-closing Link components", () => {
|
|
163
|
-
const input = `
|
|
164
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
165
|
-
|
|
166
|
-
export default function Test() {
|
|
167
|
-
return <Link href="/external" target="_blank" />
|
|
168
|
-
}
|
|
169
|
-
`.trim()
|
|
170
|
-
|
|
171
|
-
const result = applyTransform(input)
|
|
172
|
-
expect(result).toContain("external")
|
|
173
|
-
expect(result).not.toContain('target="_blank"')
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
describe("import handling", () => {
|
|
178
|
-
it("should only transform when Link is imported from correct package", () => {
|
|
179
|
-
const input = `
|
|
180
|
-
import { Link } from "react-router-dom"
|
|
181
|
-
|
|
182
|
-
export default function Test() {
|
|
183
|
-
return <Link to="/external" target="_blank">External Link</Link>
|
|
184
|
-
}
|
|
185
|
-
`.trim()
|
|
186
|
-
|
|
187
|
-
const result = applyTransform(input)
|
|
188
|
-
expect(result).toBeNull() // No changes should be made
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it("should handle aliased imports", () => {
|
|
192
|
-
const input = `
|
|
193
|
-
import { Link as TapestryLink } from "@planningcenter/tapestry-react"
|
|
194
|
-
|
|
195
|
-
export default function Test() {
|
|
196
|
-
return <TapestryLink href="/external" target="_blank">External Link</TapestryLink>
|
|
197
|
-
}
|
|
198
|
-
`.trim()
|
|
199
|
-
|
|
200
|
-
const result = applyTransform(input)
|
|
201
|
-
expect(result).toContain("external")
|
|
202
|
-
expect(result).not.toContain('target="_blank"')
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it("should add CHANGED comment when verbose is enabled", () => {
|
|
206
|
-
const input = `
|
|
207
|
-
import { Link } from "@planningcenter/tapestry-react"
|
|
208
|
-
|
|
209
|
-
export default function Test() {
|
|
210
|
-
return <Link href="/external" target="_blank">External Link</Link>
|
|
211
|
-
}
|
|
212
|
-
`.trim()
|
|
213
|
-
|
|
214
|
-
const result = applyTransform(input, true)
|
|
215
|
-
expect(result).toContain("external")
|
|
216
|
-
expect(result).toContain(
|
|
217
|
-
"CHANGED: tapestry-migration (external): target='_blank' converted to external prop"
|
|
218
|
-
)
|
|
219
|
-
})
|
|
220
|
-
})
|
|
221
|
-
})
|