@instructure/ui-codemods 11.7.4-snapshot-135 → 11.7.4-snapshot-136
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/CHANGELOG.md +5 -2
- package/lib/__node_tests__/codemods.test.ts +8 -0
- package/lib/__node_tests__/runTest.ts +8 -5
- package/lib/index.ts +3 -1
- package/lib/multiVersionThemeVariablesCodemod/migrateComponentOverridesToThemeOverride.ts +480 -0
- package/lib/multiVersionThemeVariablesCodemod/multiVersionThemeVariablesCodemod.ts +49 -0
- package/lib/multiVersionThemeVariablesCodemod/themeVariableMappings/index.ts +1255 -0
- package/lib/multiVersionThemeVariablesCodemod/transformThemeVariables.ts +734 -0
- package/lib/utils/codemodHelpers.ts +23 -3
- package/package.json +3 -2
- package/tsconfig.build.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
## [11.7.4-snapshot-
|
|
6
|
+
## [11.7.4-snapshot-136](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-snapshot-136) (2026-07-24)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **ui-codemods:** update tokens and move theme.componentOverrides to themeOverride.components ([7f94400](https://github.com/instructure/instructure-ui/commit/7f94400b25e32effc7b748d4c77ced7ecb1a70ef))
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type MockInstance,
|
|
39
39
|
vi
|
|
40
40
|
} from 'vitest'
|
|
41
|
+
import multiVersionThemeVariablesCodemod from '../multiVersionThemeVariablesCodemod/multiVersionThemeVariablesCodemod'
|
|
41
42
|
|
|
42
43
|
describe('test codemods', () => {
|
|
43
44
|
let consoleLogMock: ReturnType<typeof vi.spyOn>
|
|
@@ -86,4 +87,11 @@ describe('test codemods', () => {
|
|
|
86
87
|
it('test migrating legacy icons to lucide/custom icons', () => {
|
|
87
88
|
runTest(migrateToNewIcons)
|
|
88
89
|
})
|
|
90
|
+
|
|
91
|
+
// All fixtures run through the full `multiVersionThemeVariablesCodemod` (both
|
|
92
|
+
// legs: token renames + provider override relocation), so every `.output` is
|
|
93
|
+
// the real end-to-end result a user gets - no single-leg intermediate states.
|
|
94
|
+
it('test multi-version theme variables codemod (end-to-end)', () => {
|
|
95
|
+
runTest(multiVersionThemeVariablesCodemod)
|
|
96
|
+
})
|
|
89
97
|
})
|
|
@@ -37,10 +37,9 @@ import { expect, vi } from 'vitest'
|
|
|
37
37
|
* @param codemod The codemod to run
|
|
38
38
|
*/
|
|
39
39
|
export function runTest(codemod: Transform) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
40
|
+
// Fixtures live in `./__testfixtures__/[codemod name]/`.
|
|
41
|
+
const fixturesDir = `${__dirname}/__testfixtures__/${codemod.name}`
|
|
42
|
+
const entries = fs.readdirSync(fixturesDir, { withFileTypes: true })
|
|
44
43
|
|
|
45
44
|
let fixturesRun = 0
|
|
46
45
|
entries.forEach((entry) => {
|
|
@@ -66,7 +65,11 @@ export function runTest(codemod: Transform) {
|
|
|
66
65
|
|
|
67
66
|
if (isWarningTest) {
|
|
68
67
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
69
|
-
|
|
68
|
+
// Isolate this fixture's warnings: transform fixtures run above (via
|
|
69
|
+
// runInlineTest) don't spy console.warn, and depending on directory
|
|
70
|
+
// enumeration order their warnings can be attributed to this spy. Clear
|
|
71
|
+
// any pre-run calls so the count reflects only this fixture's codemod run.
|
|
72
|
+
warnSpy.mockClear()
|
|
70
73
|
try {
|
|
71
74
|
const j = jscodeshift.withParser('tsx')
|
|
72
75
|
const fileInfo = { path: inputPath, source: input }
|
package/lib/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import warnTableCaptionMissing from './warnTableCaptionMissing'
|
|
|
31
31
|
import warnCodeEditorRemoved from './warnCodeEditorRemoved'
|
|
32
32
|
import migrateToNewIcons from './migrateToNewIcons'
|
|
33
33
|
import updateInstUIImportVersions from './updateInstUIImportVersions'
|
|
34
|
+
import multiVersionThemeVariablesCodemod from './multiVersionThemeVariablesCodemod/multiVersionThemeVariablesCodemod'
|
|
34
35
|
|
|
35
36
|
export {
|
|
36
37
|
updateV10Breaking,
|
|
@@ -41,5 +42,6 @@ export {
|
|
|
41
42
|
warnTableCaptionMissing,
|
|
42
43
|
warnCodeEditorRemoved,
|
|
43
44
|
migrateToNewIcons,
|
|
44
|
-
updateInstUIImportVersions
|
|
45
|
+
updateInstUIImportVersions,
|
|
46
|
+
multiVersionThemeVariablesCodemod
|
|
45
47
|
}
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2015 - present Instructure, Inc.
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
import { JSCodeshift, Collection, Transform } from 'jscodeshift'
|
|
25
|
+
import instUICodemodExecutor from '../utils/instUICodemodExecutor'
|
|
26
|
+
import type { InstUICodemod } from '../utils/instUICodemodExecutor'
|
|
27
|
+
import { isJSXAttribute } from '../utils/codemodTypeCheckers'
|
|
28
|
+
import { findImport, printWarning } from '../utils/codemodHelpers'
|
|
29
|
+
import { namedTypes } from 'ast-types'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Migrates the legacy provider-level theme override
|
|
33
|
+
* <InstUISettingsProvider theme={{ componentOverrides: { Comp: {...} } }} />
|
|
34
|
+
* to the new theming-system shape
|
|
35
|
+
* <InstUISettingsProvider themeOverride={{ components: { Comp: {...} } }} />
|
|
36
|
+
*
|
|
37
|
+
* This is purely structural: v2 components (`withStyleNew`) only read overrides
|
|
38
|
+
* from `themeOverride.components`, never from the legacy `theme.componentOverrides`
|
|
39
|
+
* channel, so every component override must move - regardless of whether its
|
|
40
|
+
* tokens have changed. Token renames are handled separately by
|
|
41
|
+
* `transformThemeVariables`.
|
|
42
|
+
*/
|
|
43
|
+
const migrateComponentOverridesToThemeOverride: Transform = (
|
|
44
|
+
file,
|
|
45
|
+
api,
|
|
46
|
+
options?: { fileName?: string; usePrettier?: boolean }
|
|
47
|
+
) => {
|
|
48
|
+
return instUICodemodExecutor(
|
|
49
|
+
migrateComponentOverridesToThemeOverrideCodemod,
|
|
50
|
+
file,
|
|
51
|
+
api,
|
|
52
|
+
options
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ObjectExpression = namedTypes.ObjectExpression
|
|
57
|
+
type ObjectProperty = namedTypes.ObjectProperty
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Finds a direct, simple `key: { ... }` property on an object expression.
|
|
61
|
+
*/
|
|
62
|
+
const findObjectProperty = (
|
|
63
|
+
obj: ObjectExpression,
|
|
64
|
+
name: string
|
|
65
|
+
): ObjectProperty | undefined => {
|
|
66
|
+
return obj.properties.find(
|
|
67
|
+
(p): p is ObjectProperty =>
|
|
68
|
+
p.type === 'ObjectProperty' &&
|
|
69
|
+
p.key.type === 'Identifier' &&
|
|
70
|
+
p.key.name === name
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolves a token key name whether written as an identifier (`padding`) or a
|
|
76
|
+
* quoted string literal (`'padding'`).
|
|
77
|
+
*/
|
|
78
|
+
const getTokenKeyName = (key: ObjectProperty['key']): string | undefined => {
|
|
79
|
+
if (key.type === 'Identifier') return key.name
|
|
80
|
+
if (key.type === 'Literal' || (key.type as string) === 'StringLiteral') {
|
|
81
|
+
const value = (key as namedTypes.Literal).value
|
|
82
|
+
if (typeof value === 'string') return value
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Appends `source`'s token properties into `target`, skipping any whose key is
|
|
89
|
+
* already present in `target`. Two overrides can't be merged into a single token
|
|
90
|
+
* key (a duplicate object key would silently clobber the first at runtime), so
|
|
91
|
+
* the first value is kept and each dropped duplicate is warned about with
|
|
92
|
+
* `reason` for manual reconciliation.
|
|
93
|
+
*/
|
|
94
|
+
const mergeTokenPropsDeduped = (
|
|
95
|
+
target: ObjectExpression,
|
|
96
|
+
source: ObjectExpression,
|
|
97
|
+
slotName: string,
|
|
98
|
+
filePath: string,
|
|
99
|
+
reason: string
|
|
100
|
+
) => {
|
|
101
|
+
const seenTokens = new Set(
|
|
102
|
+
target.properties
|
|
103
|
+
.map((p) =>
|
|
104
|
+
p.type === 'ObjectProperty' ? getTokenKeyName(p.key) : undefined
|
|
105
|
+
)
|
|
106
|
+
.filter((n): n is string => !!n)
|
|
107
|
+
)
|
|
108
|
+
source.properties.forEach((sourceProp) => {
|
|
109
|
+
const name =
|
|
110
|
+
sourceProp.type === 'ObjectProperty'
|
|
111
|
+
? getTokenKeyName(sourceProp.key)
|
|
112
|
+
: undefined
|
|
113
|
+
if (name && seenTokens.has(name)) {
|
|
114
|
+
printWarning(
|
|
115
|
+
filePath,
|
|
116
|
+
sourceProp.loc?.start.line,
|
|
117
|
+
`Duplicate \`${name}\` override for \`${slotName}\`: ${reason} Kept the ` +
|
|
118
|
+
`first value and dropped this one - these overrides can no longer differ ` +
|
|
119
|
+
`for \`${name}\`, so reconcile it manually.`
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
if (name) seenTokens.add(name)
|
|
124
|
+
target.properties.push(sourceProp)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Merges the `components` entries of `source` into `target`. If a component key
|
|
130
|
+
* already exists in `target`, the relocated token properties are appended to it
|
|
131
|
+
* (duplicate token keys dropped + warned); otherwise the whole entry is added.
|
|
132
|
+
*/
|
|
133
|
+
const mergeComponentEntries = (
|
|
134
|
+
target: ObjectExpression,
|
|
135
|
+
source: ObjectExpression,
|
|
136
|
+
filePath: string
|
|
137
|
+
) => {
|
|
138
|
+
source.properties.forEach((sourceProp) => {
|
|
139
|
+
if (
|
|
140
|
+
sourceProp.type !== 'ObjectProperty' ||
|
|
141
|
+
sourceProp.key.type !== 'Identifier'
|
|
142
|
+
) {
|
|
143
|
+
// spreads / computed keys: just carry them over untouched
|
|
144
|
+
target.properties.push(sourceProp)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
const existing = findObjectProperty(target, sourceProp.key.name)
|
|
148
|
+
if (
|
|
149
|
+
existing &&
|
|
150
|
+
existing.value.type === 'ObjectExpression' &&
|
|
151
|
+
sourceProp.value.type === 'ObjectExpression'
|
|
152
|
+
) {
|
|
153
|
+
mergeTokenPropsDeduped(
|
|
154
|
+
existing.value,
|
|
155
|
+
sourceProp.value,
|
|
156
|
+
sourceProp.key.name,
|
|
157
|
+
filePath,
|
|
158
|
+
`the relocated \`theme.componentOverrides\` and the existing ` +
|
|
159
|
+
`\`themeOverride.components\` both set it.`
|
|
160
|
+
)
|
|
161
|
+
} else {
|
|
162
|
+
target.properties.push(sourceProp)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Combines entries in the SAME `components` object that share a key. The v1
|
|
169
|
+
* `Checkbox` and `CheckboxFacade` are one and the same `Checkbox` component in
|
|
170
|
+
* v2, so re-keying `CheckboxFacade` -> `Checkbox` leaves two `Checkbox` entries.
|
|
171
|
+
* A duplicate object key silently clobbers the first at runtime, so we merge
|
|
172
|
+
* their token objects into one.
|
|
173
|
+
*/
|
|
174
|
+
const dedupeComponentKeys = (
|
|
175
|
+
components: ObjectExpression,
|
|
176
|
+
filePath: string
|
|
177
|
+
) => {
|
|
178
|
+
const seen = new Map<string, ObjectProperty>()
|
|
179
|
+
const merged: ObjectExpression['properties'] = []
|
|
180
|
+
components.properties.forEach((prop) => {
|
|
181
|
+
if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
|
|
182
|
+
const existing = seen.get(prop.key.name)
|
|
183
|
+
if (
|
|
184
|
+
existing &&
|
|
185
|
+
existing.value.type === 'ObjectExpression' &&
|
|
186
|
+
prop.value.type === 'ObjectExpression'
|
|
187
|
+
) {
|
|
188
|
+
mergeTokenPropsDeduped(
|
|
189
|
+
existing.value,
|
|
190
|
+
prop.value,
|
|
191
|
+
prop.key.name,
|
|
192
|
+
filePath,
|
|
193
|
+
`two override keys resolve to the same \`themeOverride.components\` key ` +
|
|
194
|
+
`(\`${prop.key.name}\`) - e.g. both a dotted (\`X.Y\`) and dot-stripped ` +
|
|
195
|
+
`(\`XY\`) form were set - and each set it.`
|
|
196
|
+
)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
seen.set(prop.key.name, prop)
|
|
200
|
+
}
|
|
201
|
+
merged.push(prop)
|
|
202
|
+
})
|
|
203
|
+
// eslint-disable-next-line no-param-reassign
|
|
204
|
+
components.properties = merged
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const SPREAD_WARNING =
|
|
208
|
+
'A spread (`...`) inside `componentOverrides` could not be inspected - theme ' +
|
|
209
|
+
'variable renames and child-component key changes inside it were not applied. ' +
|
|
210
|
+
'Please review it manually.'
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Provider overrides are always relocated to the new `themeOverride.components`
|
|
214
|
+
* channel. But v1 components (imported from v11.6 or earlier) still read the
|
|
215
|
+
* legacy `theme.componentOverrides` channel, so their overrides stop applying
|
|
216
|
+
* after the move. A `componentOverrides` key is a plain string with no version
|
|
217
|
+
* info, so we can't tell which keys are v1 - warn unconditionally whenever a
|
|
218
|
+
* provider's overrides are relocated.
|
|
219
|
+
*/
|
|
220
|
+
const V1_OVERRIDE_WARNING =
|
|
221
|
+
'Component overrides were moved from `theme.componentOverrides` to ' +
|
|
222
|
+
'`themeOverride.components`. Components imported from v11.6 or earlier still ' +
|
|
223
|
+
'read the legacy `theme.componentOverrides` channel, so any such component ' +
|
|
224
|
+
'inside this provider will no longer receive its override - upgrade those ' +
|
|
225
|
+
'components to v11.7 or `latest`.'
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Converts dotted child-component keys to the dot-stripped `componentId` form
|
|
229
|
+
* the new theming system expects (e.g. `'List.Item'` -> `ListItem`,
|
|
230
|
+
* `'Table.RowHeader'` -> `TableRowHeader`), matching `withStyleNew`'s
|
|
231
|
+
* `componentId.replace('.', '')`. v2 reads `themeOverride.components[componentId]`
|
|
232
|
+
* with the dot removed, so a relocated dotted key would otherwise be dead.
|
|
233
|
+
* Non-dotted keys are left untouched.
|
|
234
|
+
*/
|
|
235
|
+
const normalizeComponentKeys = (
|
|
236
|
+
j: JSCodeshift,
|
|
237
|
+
components: ObjectExpression,
|
|
238
|
+
filePath: string
|
|
239
|
+
) => {
|
|
240
|
+
components.properties.forEach((prop) => {
|
|
241
|
+
// A spread at the componentOverrides level (`{ ...x, Comp: {} }`) may hide
|
|
242
|
+
// component overrides we can't see - warn.
|
|
243
|
+
if (prop.type === 'SpreadElement') {
|
|
244
|
+
printWarning(filePath, prop.loc?.start.line, SPREAD_WARNING)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (prop.type !== 'ObjectProperty') return
|
|
248
|
+
const { key } = prop
|
|
249
|
+
|
|
250
|
+
// Computed keys (e.g. `[List.Item.componentId]`) are runtime values we
|
|
251
|
+
// can't resolve statically. The new theming system keys components by the
|
|
252
|
+
// dot-stripped componentId, so a computed key would be dead in v2 - warn
|
|
253
|
+
// and leave it for the developer to convert manually.
|
|
254
|
+
if (prop.computed) {
|
|
255
|
+
printWarning(
|
|
256
|
+
filePath,
|
|
257
|
+
prop.loc?.start.line,
|
|
258
|
+
'Computed component key in `componentOverrides` could not be migrated ' +
|
|
259
|
+
'automatically. The new theming system keys `themeOverride.components` ' +
|
|
260
|
+
'by the dot-stripped componentId (e.g. `ListItem`); convert this key ' +
|
|
261
|
+
'manually.'
|
|
262
|
+
)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// A spread inside a component's token object (`Comp: { ...x, token }`) may
|
|
267
|
+
// hide theme variables we can't rename - warn.
|
|
268
|
+
if (
|
|
269
|
+
prop.value?.type === 'ObjectExpression' &&
|
|
270
|
+
prop.value.properties.some((p) => p.type === 'SpreadElement')
|
|
271
|
+
) {
|
|
272
|
+
printWarning(filePath, prop.value.loc?.start.line, SPREAD_WARNING)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let name: string | undefined
|
|
276
|
+
if (key.type === 'Identifier') {
|
|
277
|
+
name = key.name
|
|
278
|
+
} else if (
|
|
279
|
+
key.type === 'Literal' ||
|
|
280
|
+
(key.type as string) === 'StringLiteral'
|
|
281
|
+
) {
|
|
282
|
+
const value = (key as namedTypes.Literal).value
|
|
283
|
+
if (typeof value === 'string') name = value
|
|
284
|
+
}
|
|
285
|
+
if (name) {
|
|
286
|
+
// Dot-strip child-component keys to the v2 componentId form
|
|
287
|
+
// (`List.Item` -> `ListItem`). Every v2 component reads its own
|
|
288
|
+
// dot-stripped slot, so there is no cross-component re-keying.
|
|
289
|
+
const newName = name.replace('.', '')
|
|
290
|
+
if (newName !== name) {
|
|
291
|
+
// eslint-disable-next-line no-param-reassign
|
|
292
|
+
prop.key = j.identifier(newName)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// Dot-stripping may have produced two entries with the same key (e.g. a file
|
|
298
|
+
// with both `'List.Item'` and `ListItem`); merge them.
|
|
299
|
+
dedupeComponentKeys(components, filePath)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Main codemod logic
|
|
303
|
+
const migrateComponentOverridesToThemeOverrideCodemod: InstUICodemod = (
|
|
304
|
+
j: JSCodeshift,
|
|
305
|
+
root: Collection,
|
|
306
|
+
filePath: string
|
|
307
|
+
) => {
|
|
308
|
+
let hasModifications = false
|
|
309
|
+
|
|
310
|
+
const providerImport = findImport(j, root, 'InstUISettingsProvider', [
|
|
311
|
+
'@instructure/ui',
|
|
312
|
+
'@instructure/emotion'
|
|
313
|
+
])
|
|
314
|
+
if (!providerImport) {
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
root.findJSXElements(providerImport).forEach((provider) => {
|
|
319
|
+
const attributes = provider.node.openingElement.attributes
|
|
320
|
+
if (!attributes) return
|
|
321
|
+
|
|
322
|
+
const themeAttr = attributes.find(
|
|
323
|
+
(attr) => isJSXAttribute(attr) && attr.name.name === 'theme'
|
|
324
|
+
) as namedTypes.JSXAttribute | undefined
|
|
325
|
+
|
|
326
|
+
// Case 3: no static `theme={{ ... }}` object -> nothing to migrate.
|
|
327
|
+
// (Dynamic themes - functions, identifiers - are left untouched; the
|
|
328
|
+
// token codemod warns about those.)
|
|
329
|
+
if (
|
|
330
|
+
!themeAttr ||
|
|
331
|
+
themeAttr.value?.type !== 'JSXExpressionContainer' ||
|
|
332
|
+
themeAttr.value.expression?.type !== 'ObjectExpression'
|
|
333
|
+
) {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const themeObject = themeAttr.value.expression
|
|
338
|
+
|
|
339
|
+
// Theme-name-scoped overrides (`theme.themeOverrides.<themeName>`) apply only
|
|
340
|
+
// when that named theme is active. The new theming system's `themeOverride`
|
|
341
|
+
// is unconditional - there is no "override only under theme X" equivalent -
|
|
342
|
+
// so this can't be migrated. Leave it untouched and warn.
|
|
343
|
+
const themeOverridesProp = findObjectProperty(themeObject, 'themeOverrides')
|
|
344
|
+
if (themeOverridesProp) {
|
|
345
|
+
printWarning(
|
|
346
|
+
filePath,
|
|
347
|
+
themeOverridesProp.loc?.start.line,
|
|
348
|
+
'Theme-specific overrides (`theme.themeOverrides.<themeName>`) are no ' +
|
|
349
|
+
'longer supported - the new theming system applies `themeOverride` ' +
|
|
350
|
+
'unconditionally, with no per-theme equivalent. Left unchanged; ' +
|
|
351
|
+
'migrate it manually.'
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const coProp = findObjectProperty(themeObject, 'componentOverrides')
|
|
356
|
+
|
|
357
|
+
// Case 3 (cont.): theme has no `componentOverrides` key -> leave it.
|
|
358
|
+
if (!coProp) {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const componentsValue = coProp.value
|
|
363
|
+
|
|
364
|
+
// An empty static override has nothing to move (Case 4); a non-static value
|
|
365
|
+
// is always relocated.
|
|
366
|
+
const hasOverrides =
|
|
367
|
+
componentsValue.type !== 'ObjectExpression' ||
|
|
368
|
+
componentsValue.properties.length > 0
|
|
369
|
+
|
|
370
|
+
// Case 5 needs an existing `themeOverride` that is an object literal to merge
|
|
371
|
+
// into. If one is present but opaque (a variable, call, ternary...), we can
|
|
372
|
+
// neither merge into it nor add a second `themeOverride` (two of the same
|
|
373
|
+
// attribute is invalid JSX - the later one silently wins). Leave everything
|
|
374
|
+
// untouched and warn so the developer moves the overrides manually.
|
|
375
|
+
const existingThemeOverride = attributes.find(
|
|
376
|
+
(attr) => isJSXAttribute(attr) && attr.name.name === 'themeOverride'
|
|
377
|
+
) as namedTypes.JSXAttribute | undefined
|
|
378
|
+
|
|
379
|
+
const existingIsMergeableObject =
|
|
380
|
+
existingThemeOverride?.value?.type === 'JSXExpressionContainer' &&
|
|
381
|
+
existingThemeOverride.value.expression?.type === 'ObjectExpression'
|
|
382
|
+
|
|
383
|
+
if (hasOverrides && existingThemeOverride && !existingIsMergeableObject) {
|
|
384
|
+
printWarning(
|
|
385
|
+
filePath,
|
|
386
|
+
coProp.loc?.start.line,
|
|
387
|
+
'`componentOverrides` could not be moved to the ' +
|
|
388
|
+
'`themeOverride.components` prop because this InstUISettingsProvider ' +
|
|
389
|
+
'already has a `themeOverride` whose value is not an object literal, ' +
|
|
390
|
+
'so the two could not be merged. Please move the overrides manually.'
|
|
391
|
+
)
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (componentsValue.type === 'ObjectExpression') {
|
|
396
|
+
// Child-component keys must use the dot-stripped componentId in v2.
|
|
397
|
+
normalizeComponentKeys(j, componentsValue, filePath)
|
|
398
|
+
} else {
|
|
399
|
+
// A non-static componentOverrides value (a variable/expression) is still
|
|
400
|
+
// relocated to the new channel, but we can't rename tokens or fix child
|
|
401
|
+
// keys inside it - warn so the developer checks it.
|
|
402
|
+
printWarning(
|
|
403
|
+
filePath,
|
|
404
|
+
coProp.loc?.start.line,
|
|
405
|
+
'`componentOverrides` was relocated to the `themeOverride.components` ' +
|
|
406
|
+
'prop, but its value is not a static object - theme variable renames ' +
|
|
407
|
+
'and child-component key changes inside it could not be applied. ' +
|
|
408
|
+
'Please review it manually.'
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Remove `componentOverrides` from the theme object.
|
|
413
|
+
themeObject.properties = themeObject.properties.filter((p) => p !== coProp)
|
|
414
|
+
hasModifications = true
|
|
415
|
+
|
|
416
|
+
// Overrides were relocated to the new channel; warn that any component
|
|
417
|
+
// imported from v11.6 or earlier will no longer pick them up.
|
|
418
|
+
if (hasOverrides) {
|
|
419
|
+
printWarning(filePath, coProp.loc?.start.line, V1_OVERRIDE_WARNING)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Case 5: element already has an object-literal `themeOverride` -> merge.
|
|
423
|
+
if (
|
|
424
|
+
hasOverrides &&
|
|
425
|
+
existingThemeOverride?.value?.type === 'JSXExpressionContainer' &&
|
|
426
|
+
existingThemeOverride.value.expression?.type === 'ObjectExpression'
|
|
427
|
+
) {
|
|
428
|
+
const toObject = existingThemeOverride.value.expression
|
|
429
|
+
const componentsProp = findObjectProperty(toObject, 'components')
|
|
430
|
+
if (componentsProp && componentsProp.value.type === 'ObjectExpression') {
|
|
431
|
+
if (componentsValue.type === 'ObjectExpression') {
|
|
432
|
+
mergeComponentEntries(componentsProp.value, componentsValue, filePath)
|
|
433
|
+
} else {
|
|
434
|
+
// opaque value: spread it into the existing components object
|
|
435
|
+
componentsProp.value.properties.push(
|
|
436
|
+
j.spreadElement(
|
|
437
|
+
componentsValue as Parameters<typeof j.spreadElement>[0]
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
toObject.properties.push(
|
|
443
|
+
j.objectProperty(j.identifier('components'), componentsValue)
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
} else if (hasOverrides) {
|
|
447
|
+
// Build a fresh `themeOverride={{ components: { ... } }}` attribute.
|
|
448
|
+
const newAttr = j.jsxAttribute(
|
|
449
|
+
j.jsxIdentifier('themeOverride'),
|
|
450
|
+
j.jsxExpressionContainer(
|
|
451
|
+
j.objectExpression([
|
|
452
|
+
j.objectProperty(j.identifier('components'), componentsValue)
|
|
453
|
+
])
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
// Insert right after `theme` so attribute order stays natural.
|
|
457
|
+
const themeIndex = attributes.indexOf(themeAttr)
|
|
458
|
+
attributes.splice(themeIndex + 1, 0, newAttr)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Cases 1 & 2: drop the `theme` prop entirely if it is now empty,
|
|
462
|
+
// otherwise keep the remaining theme keys (e.g. `{ ...canvas }`).
|
|
463
|
+
if (themeObject.properties.length === 0) {
|
|
464
|
+
// eslint-disable-next-line no-param-reassign
|
|
465
|
+
provider.node.openingElement.attributes = attributes.filter(
|
|
466
|
+
(attr) => attr !== themeAttr
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
return hasModifications
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// NOTE: intentionally NOT a default export, so `jscodeshift -t <file>` cannot run
|
|
475
|
+
// it standalone. It is one step of the multi-version migration; run it via the
|
|
476
|
+
// `multiVersionThemeVariablesCodemod` entry point.
|
|
477
|
+
export {
|
|
478
|
+
migrateComponentOverridesToThemeOverride,
|
|
479
|
+
migrateComponentOverridesToThemeOverrideCodemod
|
|
480
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2015 - present Instructure, Inc.
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Transform } from 'jscodeshift'
|
|
26
|
+
import instUICodemodExecutor from '../utils/instUICodemodExecutor'
|
|
27
|
+
import { transformThemeVariablesCodemod } from './transformThemeVariables'
|
|
28
|
+
import { migrateComponentOverridesToThemeOverrideCodemod } from './migrateComponentOverridesToThemeOverride'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Runs all InstUI multi-version upgrade codemods
|
|
32
|
+
*/
|
|
33
|
+
const multiVersionThemeVariablesCodemod: Transform = (
|
|
34
|
+
file,
|
|
35
|
+
api,
|
|
36
|
+
options?: { fileName?: string; usePrettier?: boolean }
|
|
37
|
+
) => {
|
|
38
|
+
return instUICodemodExecutor(
|
|
39
|
+
[
|
|
40
|
+
transformThemeVariablesCodemod,
|
|
41
|
+
migrateComponentOverridesToThemeOverrideCodemod
|
|
42
|
+
],
|
|
43
|
+
file,
|
|
44
|
+
api,
|
|
45
|
+
options
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default multiVersionThemeVariablesCodemod
|