@planningcenter/tapestry-migration-cli 3.2.2-rc.7 → 3.2.2-rc.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "3.2.2-rc.7",
3
+ "version": "3.2.2-rc.9",
4
4
  "description": "CLI tool for Tapestry migrations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@emotion/react": "^11.14.0",
35
- "@planningcenter/tapestry": "^3.2.2-rc.7",
35
+ "@planningcenter/tapestry": "^3.2.2-rc.9",
36
36
  "@planningcenter/tapestry-react": "^4.11.5",
37
37
  "@types/jscodeshift": "^17.3.0",
38
38
  "@types/node": "^20.0.0",
@@ -52,5 +52,5 @@
52
52
  "publishConfig": {
53
53
  "access": "public"
54
54
  },
55
- "gitHead": "1ec52cee28372c0710142a4257c9c60b4b6b7ccd"
55
+ "gitHead": "4810e0377bd9ec8ee2c3ea474741af940ec9406e"
56
56
  }
@@ -114,6 +114,32 @@ function Test() {
114
114
  })
115
115
  })
116
116
 
117
+ describe("import conflict handling", () => {
118
+ it("should use TCheckbox alias when Checkbox is already imported from another package", () => {
119
+ const input = `
120
+ import { Checkbox } from "some-other-library"
121
+ import { Checkbox as ReactCheckbox } from "@planningcenter/tapestry-react"
122
+
123
+ function Test() {
124
+ return (
125
+ <div>
126
+ <Checkbox />
127
+ <ReactCheckbox label="Test" />
128
+ </div>
129
+ )
130
+ }
131
+ `.trim()
132
+
133
+ const result = applyTransform(input)
134
+ expect(result).toContain('import { Checkbox } from "some-other-library"')
135
+ expect(result).toContain(
136
+ 'import { Checkbox as TCheckbox } from "@planningcenter/tapestry"'
137
+ )
138
+ expect(result).toContain("<TCheckbox")
139
+ expect(result).not.toContain("ReactCheckbox")
140
+ })
141
+ })
142
+
117
143
  describe("edge cases", () => {
118
144
  it("should handle already migrated imports", () => {
119
145
  const input = `
@@ -4,6 +4,7 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
+ conflictAlias: "TCheckbox",
7
8
  fromComponent: "Checkbox",
8
9
  fromPackage: "@planningcenter/tapestry-react",
9
10
  toComponent: "Checkbox",
@@ -96,6 +96,32 @@ function Test() {
96
96
  })
97
97
  })
98
98
 
99
+ describe("import conflict handling", () => {
100
+ it("should use TInput alias when Input is already imported from another package", () => {
101
+ const input = `
102
+ import { Input } from "some-other-library"
103
+ import { Input as ReactInput } from "@planningcenter/tapestry-react"
104
+
105
+ function Test() {
106
+ return (
107
+ <div>
108
+ <Input />
109
+ <ReactInput label="Name" />
110
+ </div>
111
+ )
112
+ }
113
+ `.trim()
114
+
115
+ const result = applyTransform(input)
116
+ expect(result).toContain('import { Input } from "some-other-library"')
117
+ expect(result).toContain(
118
+ 'import { Input as TInput } from "@planningcenter/tapestry"'
119
+ )
120
+ expect(result).toContain("<TInput")
121
+ expect(result).not.toContain("ReactInput")
122
+ })
123
+ })
124
+
99
125
  describe("edge cases", () => {
100
126
  it("should handle already migrated imports", () => {
101
127
  const input = `
@@ -5,6 +5,7 @@ import { transformableInput } from "../transformableInput"
5
5
 
6
6
  const transform: Transform = componentTransformFactory({
7
7
  condition: transformableInput,
8
+ conflictAlias: "TInput",
8
9
  fromComponent: "Input",
9
10
  fromPackage: "@planningcenter/tapestry-react",
10
11
  toComponent: "Input",
@@ -99,6 +99,57 @@ function Test() {
99
99
  expect(importDeclarationMatches).toHaveLength(1)
100
100
  })
101
101
 
102
+ it("uses TInput alias when Input is already imported from another package", () => {
103
+ const input = `
104
+ import { Input } from "some-other-library"
105
+ import { NumberField } from "@planningcenter/tapestry-react"
106
+
107
+ function Test() {
108
+ return (
109
+ <div>
110
+ <Input />
111
+ <NumberField label="Amount" type="number" />
112
+ </div>
113
+ )
114
+ }
115
+ `.trim()
116
+
117
+ const result = applyTransform(input)
118
+ expect(result).toContain('import { Input } from "some-other-library"')
119
+ expect(result).toContain(
120
+ 'import { Input as TInput } from "@planningcenter/tapestry"'
121
+ )
122
+ expect(result).toContain("<TInput")
123
+ expect(result).not.toContain("NumberField")
124
+ })
125
+
126
+ it("adds Input import when fromPackage Input is aliased", () => {
127
+ const input = `
128
+ import { Input as ReactInput, NumberField } from "@planningcenter/tapestry-react"
129
+
130
+ function Test() {
131
+ return (
132
+ <div>
133
+ <ReactInput label="Name" />
134
+ <NumberField label="Amount" type="number" />
135
+ </div>
136
+ )
137
+ }
138
+ `.trim()
139
+
140
+ const result = applyTransform(input)
141
+ expect(result).not.toBeNull()
142
+ // The renamed NumberField becomes <Input>, so an Input import is required —
143
+ // the aliased ReactInput in tapestry-react does not put `Input` in scope.
144
+ expect(result).toContain('import { Input } from "@planningcenter/tapestry"')
145
+ expect(result).toContain("<Input")
146
+ expect(result).not.toContain("NumberField")
147
+ // The aliased ReactInput is left untouched by this transform; the moveInputImport
148
+ // transform later in the pipeline will not match it (different local name) and a
149
+ // separate cleanup is responsible for removing unused imports.
150
+ expect(result).toContain("ReactInput")
151
+ })
152
+
102
153
  it("merges Input import when file already imports Input from tapestry", () => {
103
154
  const input = `
104
155
  import { Input } from "@planningcenter/tapestry"
@@ -2,6 +2,7 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
2
2
 
3
3
  export default componentTransformFactory({
4
4
  condition: () => true,
5
+ conflictAlias: "TInput",
5
6
  fromComponent: "NumberField",
6
7
  fromPackage: "@planningcenter/tapestry-react",
7
8
  toComponent: "Input",
@@ -96,6 +96,32 @@ function Test() {
96
96
  })
97
97
  })
98
98
 
99
+ describe("import conflict handling", () => {
100
+ it("should use TRadio alias when Radio is already imported from another package", () => {
101
+ const input = `
102
+ import { Radio } from "some-other-library"
103
+ import { Radio as ReactRadio } from "@planningcenter/tapestry-react"
104
+
105
+ function Test() {
106
+ return (
107
+ <div>
108
+ <Radio />
109
+ <ReactRadio label="Option" value="a" />
110
+ </div>
111
+ )
112
+ }
113
+ `.trim()
114
+
115
+ const result = applyTransform(input)
116
+ expect(result).toContain('import { Radio } from "some-other-library"')
117
+ expect(result).toContain(
118
+ 'import { Radio as TRadio } from "@planningcenter/tapestry"'
119
+ )
120
+ expect(result).toContain("<TRadio")
121
+ expect(result).not.toContain("ReactRadio")
122
+ })
123
+ })
124
+
99
125
  describe("edge cases", () => {
100
126
  it("should handle already migrated imports", () => {
101
127
  const input = `
@@ -4,6 +4,7 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
+ conflictAlias: "TRadio",
7
8
  fromComponent: "Radio",
8
9
  fromPackage: "@planningcenter/tapestry-react",
9
10
  toComponent: "Radio",
@@ -145,4 +145,28 @@ function Test() {
145
145
  expect(result).toContain("disabled")
146
146
  expect(result).toContain("onChange={() => {}}")
147
147
  })
148
+
149
+ it("should use TSelect alias when Select is already imported from another package", () => {
150
+ const input = `
151
+ import { Select } from "some-other-library"
152
+ import { Select as ReactSelect } from "@planningcenter/tapestry-react"
153
+
154
+ function Test() {
155
+ return (
156
+ <div>
157
+ <Select />
158
+ <ReactSelect label="Test" placeholder="Pick" options={[]} />
159
+ </div>
160
+ )
161
+ }
162
+ `.trim()
163
+
164
+ const result = applyTransform(input)
165
+ expect(result).toContain('import { Select } from "some-other-library"')
166
+ expect(result).toContain(
167
+ 'import { Select as TSelect } from "@planningcenter/tapestry"'
168
+ )
169
+ expect(result).toContain("<TSelect")
170
+ expect(result).not.toContain("ReactSelect")
171
+ })
148
172
  })
@@ -5,6 +5,7 @@ import { transformableSelect } from "../transformableSelect"
5
5
 
6
6
  const transform: Transform = componentTransformFactory({
7
7
  condition: transformableSelect,
8
+ conflictAlias: "TSelect",
8
9
  fromComponent: "Select",
9
10
  fromPackage: "@planningcenter/tapestry-react",
10
11
  toComponent: "Select",
@@ -151,12 +151,14 @@ export function removeImportFromDeclaration(
151
151
  export function addImport({
152
152
  component,
153
153
  conflictAlias,
154
+ fromPackage,
154
155
  j,
155
156
  pkg,
156
157
  source,
157
158
  }: {
158
159
  component: string
159
160
  conflictAlias: string
161
+ fromPackage?: string
160
162
  j: JSCodeshift
161
163
  pkg: string
162
164
  source: Collection
@@ -166,7 +168,28 @@ export function addImport({
166
168
  return existingImportName
167
169
  }
168
170
 
169
- const hasConflict = hasConflictingImport(component, pkg, { j, source })
171
+ // Conflict = component imported from a package OTHER than the source/target packages.
172
+ // Imports from fromPackage will be migrated to pkg by another transform in the pipeline,
173
+ // so they don't constitute a conflict that requires aliasing.
174
+ const excludePackages = fromPackage ? [pkg, fromPackage] : [pkg]
175
+ const hasConflict = hasConflictingImport(component, excludePackages, {
176
+ j,
177
+ source,
178
+ })
179
+
180
+ // If the component is already imported from fromPackage under the same local name
181
+ // (and won't conflict with another package), the pipeline's package-move transform
182
+ // will migrate it later — skip adding a duplicate import here. Aliased imports from
183
+ // fromPackage do not put `component` itself in scope, so they must not trigger this
184
+ // early return.
185
+ const importedFromSourceName = fromPackage
186
+ ? getImportName(component, fromPackage, { j, source })
187
+ : null
188
+ const importedFromSource = importedFromSourceName === component
189
+ if (importedFromSource && !hasConflict) {
190
+ return component
191
+ }
192
+
170
193
  const finalComponentName = hasConflict ? conflictAlias : component
171
194
  const alias =
172
195
  finalComponentName !== component ? finalComponentName : undefined
@@ -240,6 +263,7 @@ export function manageImports(
240
263
  addImport({
241
264
  component: config.toComponent,
242
265
  conflictAlias: config.conflictAlias || targetComponentName,
266
+ fromPackage: config.fromPackage,
243
267
  j,
244
268
  pkg: config.toPackage,
245
269
  source,
@@ -96,6 +96,32 @@ function Test() {
96
96
  })
97
97
  })
98
98
 
99
+ describe("import conflict handling", () => {
100
+ it("should use TTextArea alias when TextArea is already imported from another package", () => {
101
+ const input = `
102
+ import { TextArea } from "some-other-library"
103
+ import { TextArea as ReactTextArea } from "@planningcenter/tapestry-react"
104
+
105
+ function Test() {
106
+ return (
107
+ <div>
108
+ <TextArea />
109
+ <ReactTextArea label="Notes" />
110
+ </div>
111
+ )
112
+ }
113
+ `.trim()
114
+
115
+ const result = applyTransform(input)
116
+ expect(result).toContain('import { TextArea } from "some-other-library"')
117
+ expect(result).toContain(
118
+ 'import { TextArea as TTextArea } from "@planningcenter/tapestry"'
119
+ )
120
+ expect(result).toContain("<TTextArea")
121
+ expect(result).not.toContain("ReactTextArea")
122
+ })
123
+ })
124
+
99
125
  describe("edge cases", () => {
100
126
  it("should handle already migrated imports", () => {
101
127
  const input = `
@@ -4,6 +4,7 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
+ conflictAlias: "TTextArea",
7
8
  fromComponent: "TextArea",
8
9
  fromPackage: "@planningcenter/tapestry-react",
9
10
  toComponent: "TextArea",
@@ -96,6 +96,34 @@ function Test() {
96
96
  })
97
97
  })
98
98
 
99
+ describe("import conflict handling", () => {
100
+ it("should use TToggleSwitch alias when ToggleSwitch is already imported from another package", () => {
101
+ const input = `
102
+ import { ToggleSwitch } from "some-other-library"
103
+ import { ToggleSwitch as ReactToggleSwitch } from "@planningcenter/tapestry-react"
104
+
105
+ function Test() {
106
+ return (
107
+ <div>
108
+ <ToggleSwitch />
109
+ <ReactToggleSwitch />
110
+ </div>
111
+ )
112
+ }
113
+ `.trim()
114
+
115
+ const result = applyTransform(input)
116
+ expect(result).toContain(
117
+ 'import { ToggleSwitch } from "some-other-library"'
118
+ )
119
+ expect(result).toContain(
120
+ 'import { ToggleSwitch as TToggleSwitch } from "@planningcenter/tapestry"'
121
+ )
122
+ expect(result).toContain("<TToggleSwitch")
123
+ expect(result).not.toContain("ReactToggleSwitch")
124
+ })
125
+ })
126
+
99
127
  describe("edge cases", () => {
100
128
  it("should handle already migrated imports", () => {
101
129
  const input = `
@@ -4,6 +4,7 @@ import { componentTransformFactory } from "../../shared/transformFactories/compo
4
4
 
5
5
  const transform: Transform = componentTransformFactory({
6
6
  condition: () => true,
7
+ conflictAlias: "TToggleSwitch",
7
8
  fromComponent: "ToggleSwitch",
8
9
  fromPackage: "@planningcenter/tapestry-react",
9
10
  toComponent: "ToggleSwitch",