@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 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-135](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-snapshot-135) (2026-07-24)
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
- **Note:** Version bump only for package @instructure/ui-codemods
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
- const entries = fs.readdirSync(
41
- `${__dirname}/__testfixtures__/${codemod.name}`,
42
- { withFileTypes: true }
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