@carbon/upgrade 11.44.0 → 11.45.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/README.md CHANGED
@@ -342,6 +342,47 @@ npx @carbon/upgrade migrate slug-prop-to-decorator-prop --write
342
342
  <Component decorator="my-identifier">Content</Component>
343
343
  ```
344
344
 
345
+ ### Unstable / preview Pagination to Pagination
346
+
347
+ Migrates `unstable_Pagination` / `preview_Pagination` to the stable `Pagination`
348
+ component. Drops `PageSelector` imports and children render-props, since the
349
+ stable component renders an equivalent page-select control by default.
350
+
351
+ **Usage:**
352
+
353
+ ```bash
354
+ npx @carbon/upgrade migrate unstable-pagination-to-pagination --write
355
+ ```
356
+
357
+ **Example:**
358
+
359
+ ```jsx
360
+ // Before
361
+ import {
362
+ unstable_Pagination as Pagination,
363
+ unstable_PageSelector as PageSelector,
364
+ } from '@carbon/react';
365
+
366
+ <Pagination pageSizes={[10, 20, 30]} totalItems={100}>
367
+ {({ currentPage, onSetPage, totalPages }) => (
368
+ <PageSelector
369
+ currentPage={currentPage}
370
+ onChange={(event) => onSetPage(event.target.value)}
371
+ totalPages={totalPages}
372
+ />
373
+ )}
374
+ </Pagination>;
375
+
376
+ // After
377
+ import { Pagination } from '@carbon/react';
378
+
379
+ <Pagination pageSizes={[10, 20, 30]} totalItems={100} />;
380
+ ```
381
+
382
+ Custom page-select children are removed and a `TODO` comment is left to migrate
383
+ them with `renderPageSelect` if needed. Omit `pageSizes` to hide the items per
384
+ page selector.
385
+
345
386
  ### FeatureFlag deprecate flags prop
346
387
 
347
388
  Updates FeatureFlags component to use individual boolean props instead of the
package/cli.js CHANGED
@@ -36694,6 +36694,62 @@ const upgrades = [
36694
36694
  }
36695
36695
  ],
36696
36696
  migrations: [
36697
+ {
36698
+ name: "wc-report-non-barrel-imports",
36699
+ description: "Report @carbon/web-components class-file (non-barrel) imports that register nothing under v3 pure exports",
36700
+ migrate: async (options) => {
36701
+ const transform = path.default.join(TRANSFORM_DIR, "wc-report-non-barrel-imports.js");
36702
+ const paths = Array.isArray(options.paths) && options.paths.length > 0 ? options.paths : await (0, import_out.default)([
36703
+ "**/*.js",
36704
+ "**/*.jsx",
36705
+ "**/*.ts",
36706
+ "**/*.tsx"
36707
+ ], {
36708
+ cwd: options.workspaceDir,
36709
+ ignore: [
36710
+ "**/es/**",
36711
+ "**/lib/**",
36712
+ "**/umd/**",
36713
+ "**/node_modules/**",
36714
+ "**/storybook-static/**"
36715
+ ]
36716
+ });
36717
+ await runCodemod(options, {
36718
+ dry: !options.write,
36719
+ transform,
36720
+ paths,
36721
+ verbose: options.verbose
36722
+ });
36723
+ }
36724
+ },
36725
+ {
36726
+ name: "wc-add-barrel-imports",
36727
+ description: "Add/collapse @carbon/web-components component barrel imports so class-file imports still register under v3 pure exports",
36728
+ migrate: async (options) => {
36729
+ const transform = path.default.join(TRANSFORM_DIR, "wc-add-barrel-imports.js");
36730
+ const paths = Array.isArray(options.paths) && options.paths.length > 0 ? options.paths : await (0, import_out.default)([
36731
+ "**/*.js",
36732
+ "**/*.jsx",
36733
+ "**/*.ts",
36734
+ "**/*.tsx"
36735
+ ], {
36736
+ cwd: options.workspaceDir,
36737
+ ignore: [
36738
+ "**/es/**",
36739
+ "**/lib/**",
36740
+ "**/umd/**",
36741
+ "**/node_modules/**",
36742
+ "**/storybook-static/**"
36743
+ ]
36744
+ });
36745
+ await runCodemod(options, {
36746
+ dry: !options.write,
36747
+ transform,
36748
+ paths,
36749
+ verbose: options.verbose
36750
+ });
36751
+ }
36752
+ },
36697
36753
  {
36698
36754
  name: "icons-react-size-prop",
36699
36755
  description: "Update imports and size usage for @carbon/icons-react",
@@ -37465,6 +37521,39 @@ const upgrades = [
37465
37521
  parser: "tsx"
37466
37522
  });
37467
37523
  }
37524
+ },
37525
+ {
37526
+ name: "unstable-pagination-to-pagination",
37527
+ description: "Replace unstable_Pagination / preview_Pagination with Pagination and remove unstable_PageSelector / preview_PageSelector. Custom page-select children must use renderPageSelect and may need a manual update.",
37528
+ migrate: async (options) => {
37529
+ const transform = path.default.join(TRANSFORM_DIR, "unstable-pagination-to-pagination.js");
37530
+ const paths = Array.isArray(options.paths) && options.paths.length > 0 ? options.paths : await (0, import_out.default)([
37531
+ "**/*.js",
37532
+ "**/*.jsx",
37533
+ "**/*.ts",
37534
+ "**/*.tsx"
37535
+ ], {
37536
+ cwd: options.workspaceDir,
37537
+ ignore: [
37538
+ "**/es/**",
37539
+ "**/lib/**",
37540
+ "**/umd/**",
37541
+ "**/node_modules/**",
37542
+ "**/storybook-static/**",
37543
+ "**/dist/**",
37544
+ "**/build/**",
37545
+ "**/*.d.ts",
37546
+ "**/coverage/**"
37547
+ ]
37548
+ });
37549
+ await runCodemod(options, {
37550
+ dry: !options.write,
37551
+ transform,
37552
+ paths,
37553
+ verbose: options.verbose,
37554
+ parser: "tsx"
37555
+ });
37556
+ }
37468
37557
  }
37469
37558
  ]
37470
37559
  },
@@ -37486,7 +37575,7 @@ const upgrades = [
37486
37575
  //#endregion
37487
37576
  //#region package.json
37488
37577
  var name = "@carbon/upgrade";
37489
- var version = "11.44.0";
37578
+ var version = "11.45.0";
37490
37579
  //#endregion
37491
37580
  //#region ../../node_modules/y18n/build/index.cjs
37492
37581
  var require_build$3 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@carbon/upgrade",
3
3
  "description": "A tool for upgrading Carbon versions",
4
- "version": "11.44.0",
4
+ "version": "11.45.0",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
7
7
  "carbon-upgrade": "./bin/carbon-upgrade.js"
@@ -60,5 +60,5 @@
60
60
  "@ibm/telemetry-js": "^1.5.0",
61
61
  "jscodeshift": "^17.0.0"
62
62
  },
63
- "gitHead": "7518c84ffd00f22434fe19d83119692c12fccb2f"
63
+ "gitHead": "acdc7d80f45eb01c892b1697403726c4dbc24a57"
64
64
  }
@@ -0,0 +1,20 @@
1
+ import { html, LitElement } from 'lit';
2
+ // side-effect class imports of one component -> collapse to a single barrel
3
+ import '@carbon/web-components/es/components/data-table/table.js';
4
+ import '@carbon/web-components/es/components/data-table/table-head.js';
5
+ import '@carbon/web-components/es/components/data-table/table-row.js';
6
+ // value import -> keep binding, add barrel alongside
7
+ import CDSButton from '@carbon/web-components/es/components/button/button.js';
8
+ // named value import for a component whose barrel is already present below
9
+ import { CDSSearch } from '@carbon/web-components/es/components/search/search.js';
10
+ import '@carbon/web-components/es/components/search/index.js';
11
+ // type-only -> untouched
12
+ import type CDSModal from '@carbon/web-components/es/components/modal/modal.js';
13
+ // already a barrel -> untouched
14
+ import '@carbon/web-components/es/components/tag/index.js';
15
+
16
+ export class Widget extends LitElement {
17
+ render() {
18
+ return html`<cds-button></cds-button>`;
19
+ }
20
+ }
@@ -0,0 +1,19 @@
1
+ import { html, LitElement } from 'lit';
2
+ // side-effect class imports of one component -> collapse to a single barrel
3
+ import '@carbon/web-components/es/components/data-table/index.js';
4
+ // value import -> keep binding, add barrel alongside
5
+ import CDSButton from '@carbon/web-components/es/components/button/button.js';
6
+ import '@carbon/web-components/es/components/button/index.js';
7
+ // named value import for a component whose barrel is already present below
8
+ import { CDSSearch } from '@carbon/web-components/es/components/search/search.js';
9
+ import '@carbon/web-components/es/components/search/index.js';
10
+ // type-only -> untouched
11
+ import type CDSModal from '@carbon/web-components/es/components/modal/modal.js';
12
+ // already a barrel -> untouched
13
+ import '@carbon/web-components/es/components/tag/index.js';
14
+
15
+ export class Widget extends LitElement {
16
+ render() {
17
+ return html`<cds-button></cds-button>`;
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { defineTest } = require('jscodeshift/dist/testUtils');
11
+
12
+ // covers:
13
+ // - side-effect collapse: i.e. 3 data-table class imports into 1 barrel
14
+ // - default value import: named value import with barrel already present;
15
+ // type-only imports
16
+ // - tpre-existing barrels
17
+ defineTest(__dirname, 'wc-add-barrel-imports', null, 'wc-add-barrel-imports');
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const jscodeshift = require('jscodeshift');
11
+ const { collectNonBarrelWebComponentImports } = require('../carbon-wc-imports');
12
+ const { formatFileBlock } = require('../wc-report-non-barrel-imports');
13
+
14
+ const j = jscodeshift.withParser('tsx');
15
+
16
+ function findings(source) {
17
+ return collectNonBarrelWebComponentImports(j, j(source));
18
+ }
19
+
20
+ const sampleFindings = [
21
+ {
22
+ specifier: '@carbon/web-components/es/components/search/search.js',
23
+ component: 'search',
24
+ kind: 'side-effect',
25
+ typeOnly: false,
26
+ suggestedBarrel: '@carbon/web-components/es/components/search/index.js',
27
+ line: 16,
28
+ },
29
+ {
30
+ specifier:
31
+ '@carbon/web-components/es/components/overflow-menu/overflow-menu-item.js',
32
+ component: 'overflow-menu',
33
+ kind: 'default',
34
+ typeOnly: true,
35
+ suggestedBarrel:
36
+ '@carbon/web-components/es/components/overflow-menu/index.js',
37
+ line: 8,
38
+ },
39
+ ];
40
+
41
+ describe('collectNonBarrelWebComponentImports', () => {
42
+ it('flags a side-effect class-file import', () => {
43
+ const found = findings(
44
+ `import '@carbon/web-components/es/components/search/search.js';`
45
+ );
46
+ expect(found).toHaveLength(1);
47
+ expect(found[0]).toMatchObject({
48
+ component: 'search',
49
+ kind: 'side-effect',
50
+ typeOnly: false,
51
+ suggestedBarrel: '@carbon/web-components/es/components/search/index.js',
52
+ });
53
+ });
54
+
55
+ it('flags a default class import', () => {
56
+ const found = findings(
57
+ `import CDSSearch from '@carbon/web-components/es/components/search/search.js';`
58
+ );
59
+ expect(found).toHaveLength(1);
60
+ expect(found[0].kind).toBe('default');
61
+ });
62
+
63
+ it('flags a named class import (no .js extension)', () => {
64
+ const found = findings(
65
+ `import { CDSSearch } from '@carbon/web-components/es/components/search/search';`
66
+ );
67
+ expect(found).toHaveLength(1);
68
+ expect(found[0].kind).toBe('named');
69
+ });
70
+
71
+ it('flags a namespace import', () => {
72
+ const found = findings(
73
+ `import * as Search from '@carbon/web-components/es/components/search/search.js';`
74
+ );
75
+ expect(found[0].kind).toBe('namespace');
76
+ });
77
+
78
+ it('flags a nested class file and still suggests the top-level barrel', () => {
79
+ const found = findings(
80
+ `import CDSTable from '@carbon/web-components/es/components/data-table/table.js';`
81
+ );
82
+ expect(found[0]).toMatchObject({
83
+ component: 'data-table',
84
+ suggestedBarrel:
85
+ '@carbon/web-components/es/components/data-table/index.js',
86
+ });
87
+ });
88
+
89
+ it('marks type-only imports for review', () => {
90
+ const found = findings(
91
+ `import type CDSSearch from '@carbon/web-components/es/components/search/search.js';`
92
+ );
93
+ expect(found[0]).toMatchObject({ kind: 'type', typeOnly: true });
94
+ });
95
+
96
+ it('does NOT flag the component barrel (file, bare dir, or /index)', () => {
97
+ expect(
98
+ findings(`import '@carbon/web-components/es/components/search/index.js';`)
99
+ ).toHaveLength(0);
100
+ expect(
101
+ findings(`import '@carbon/web-components/es/components/search';`)
102
+ ).toHaveLength(0);
103
+ expect(
104
+ findings(
105
+ `import { CDSSearch } from '@carbon/web-components/es/components/search/index';`
106
+ )
107
+ ).toHaveLength(0);
108
+ });
109
+
110
+ it('does NOT flag defs imports (constants/types, register nothing)', () => {
111
+ expect(
112
+ findings(
113
+ `import { SEARCH_SIZE } from '@carbon/web-components/es/components/search/defs.js';`
114
+ )
115
+ ).toHaveLength(0);
116
+ });
117
+
118
+ it('does NOT flag non-carbon or top-level package imports', () => {
119
+ expect(findings(`import { html } from 'lit';`)).toHaveLength(0);
120
+ expect(
121
+ findings(`import '@carbon/web-components/es/index.js';`)
122
+ ).toHaveLength(0);
123
+ expect(
124
+ findings(
125
+ `import { defineCustomElement } from '@carbon/web-components/es/globals/register.js';`
126
+ )
127
+ ).toHaveLength(0);
128
+ });
129
+
130
+ it('reports the source line number', () => {
131
+ const source = [
132
+ `import { html } from 'lit';`,
133
+ `import CDSSearch from '@carbon/web-components/es/components/search/search.js';`,
134
+ ].join('\n');
135
+ expect(findings(source)[0].line).toBe(2);
136
+ });
137
+
138
+ it('collects multiple findings across a file', () => {
139
+ const source = [
140
+ `import '@carbon/web-components/es/components/search/search.js';`,
141
+ `import '@carbon/web-components/es/components/button/index.js';`,
142
+ `import CDSDropdown from '@carbon/web-components/es/components/dropdown/dropdown.js';`,
143
+ ].join('\n');
144
+ const found = findings(source);
145
+ expect(found.map((f) => f.component)).toEqual(['search', 'dropdown']);
146
+ });
147
+ });
148
+
149
+ describe('formatFileBlock', () => {
150
+ it('renders a grouped, prefix-shortened tree (no ANSI when color off)', () => {
151
+ const out = formatFileBlock('src/history-toolbar.ts', sampleFindings, {
152
+ color: false,
153
+ });
154
+
155
+ // header + tree connectors, sorted by line (8 before 16)
156
+ expect(out).toContain('src/history-toolbar.ts');
157
+ expect(out.indexOf('overflow-menu/overflow-menu-item.js')).toBeLessThan(
158
+ out.indexOf('search/search.js')
159
+ );
160
+ expect(out).toContain('├─');
161
+ expect(out).toContain('└─');
162
+
163
+ // component-prefix stripped on both the import and the barrel
164
+ expect(out).toContain('search/search.js');
165
+ expect(out).toContain('search/index.js');
166
+ expect(out).not.toContain('@carbon/web-components/es/components');
167
+
168
+ // kinds + review labeling + per-file summary
169
+ expect(out).toContain('(side-effect)');
170
+ expect(out).toContain('(type · review)');
171
+ expect(out).toContain('1 needs a barrel, 1 to review');
172
+
173
+ // no color codes when disabled
174
+ expect(out).not.toContain('\x1b[');
175
+ });
176
+
177
+ it('emits ANSI escapes when color is on', () => {
178
+ const out = formatFileBlock('src/a.ts', sampleFindings, { color: true });
179
+ expect(out).toContain('\x1b[');
180
+ });
181
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ * Shared detection for imports of `@carbon/web-components` component class
12
+ * files (or any non-barrel entry) under `es|lib/components/<name>/...`.
13
+ *
14
+ * In the new (starting v3) pure-exports model, importing a component's class
15
+ * file registers nothing — only the component barrel (`<name>/index.js`) calls
16
+ * `defineCustomElement`. A consumer that imports the class file but renders
17
+ * `<cds-name>` gets an inert (un-upgraded) element: it renders to nothing, with
18
+ * no error thrown.
19
+ *
20
+ * This is the AST counterpart (consumer source) version of the internal
21
+ * web components tool `tools/check-registration-coverage.mjs`, which audits
22
+ * Carbon's own source as part of CI).
23
+ *
24
+ * Consumed by:
25
+ * - `wc-report-non-barrel-imports` (advisory reporting only, no changes)
26
+ * - the additive barrel-registration codemod (planned)
27
+ */
28
+
29
+ // @carbon/web-components/<es|lib>/components/<name>[/<sub...>]
30
+ const COMPONENT_IMPORT_RE =
31
+ /^(@carbon\/web-components\/(?:es|lib)\/components\/([^/]+))(?:\/(.+))?$/;
32
+
33
+ /**
34
+ * Classify an ImportDeclaration by how it binds the module
35
+ *
36
+ * @param {object} node an ImportDeclaration AST node
37
+ * @returns {{ kind: 'side-effect'|'default'|'named'|'namespace'|'type',
38
+ * typeOnly: boolean }}
39
+ */
40
+ function classifyKind(node) {
41
+ // `import type ... from` (whole-declaration type import)
42
+ if (node.importKind === 'type') {
43
+ return { kind: 'type', typeOnly: true };
44
+ }
45
+
46
+ const specs = node.specifiers || [];
47
+
48
+ // `import '...'` - imported purely for side effects
49
+ if (specs.length === 0) {
50
+ return { kind: 'side-effect', typeOnly: false };
51
+ }
52
+
53
+ // `import { type A, type B } from` - every specifier is a type
54
+ if (specs.every((s) => s.importKind === 'type')) {
55
+ return { kind: 'type', typeOnly: true };
56
+ }
57
+
58
+ if (specs.some((s) => s.type === 'ImportNamespaceSpecifier')) {
59
+ return { kind: 'namespace', typeOnly: false };
60
+ }
61
+
62
+ if (specs.some((s) => s.type === 'ImportDefaultSpecifier')) {
63
+ return { kind: 'default', typeOnly: false };
64
+ }
65
+
66
+ return { kind: 'named', typeOnly: false };
67
+ }
68
+
69
+ /**
70
+ * Find every non-barrel `@carbon/web-components` component import in a file
71
+ *
72
+ * @param {import('jscodeshift').JSCodeshift} j
73
+ * @param {import('jscodeshift').Collection} root
74
+ * @returns {Array<{ specifier: string, component: string,
75
+ * kind: string, typeOnly: boolean, suggestedBarrel: string,
76
+ * line: (number|null) }>}
77
+ */
78
+ function collectNonBarrelWebComponentImports(j, root) {
79
+ const findings = [];
80
+
81
+ root.find(j.ImportDeclaration).forEach((p) => {
82
+ const node = p.node;
83
+ const specifier = node.source && node.source.value;
84
+
85
+ if (typeof specifier !== 'string') {
86
+ return;
87
+ }
88
+
89
+ const match = specifier.match(COMPONENT_IMPORT_RE);
90
+ if (!match) {
91
+ return;
92
+ }
93
+
94
+ const barrelBase = match[1]; // @carbon/web-components/es/components/<name>
95
+ const component = match[2];
96
+ let sub = match[3]; // undefined for a bare `.../components/<name>` import
97
+
98
+ if (sub) {
99
+ sub = sub.replace(/\.js$/, '');
100
+ }
101
+
102
+ // Barrel imports already register. A bare dir (`sub` undefined) and
103
+ // `<name>/index` both resolve to `<name>/index.js` — nothing to flag
104
+ if (!sub || sub === 'index') {
105
+ return;
106
+ }
107
+
108
+ // `defs` modules are constants/types by convention; they register nothing
109
+ // and aren't expected to (aligns with check-registration-coverage.mjs)
110
+ if (sub.split('/').pop() === 'defs') {
111
+ return;
112
+ }
113
+
114
+ const { kind, typeOnly } = classifyKind(node);
115
+
116
+ findings.push({
117
+ specifier,
118
+ component,
119
+ kind,
120
+ typeOnly,
121
+ suggestedBarrel: `${barrelBase}/index.js`,
122
+ line: node.loc ? node.loc.start.line : null,
123
+ });
124
+ });
125
+
126
+ return findings;
127
+ }
128
+
129
+ module.exports = { collectNonBarrelWebComponentImports, classifyKind };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { collectNonBarrelWebComponentImports } = require('./carbon-wc-imports');
11
+
12
+ /**
13
+ * Ensure every `@carbon/web-components` element a file imports as a non-barrel
14
+ * class file still registers under v3 pure exports, by making sure the
15
+ * component barrel (`<name>/index.js`) is imported
16
+ *
17
+ * Per import kind:
18
+ * - side-effect (`import '.../button/button.js'`) — rewritten to the barrel
19
+ * (`import '.../button/index.js'`); multiple side-effect imports of the same
20
+ * component collapse into one barrel import. Safe: a class-file side-effect
21
+ * import registers nothing in v3, so it is dead already.
22
+ * - default / named / namespace (value imports) — left untouched (the class
23
+ * binding is preserved), and a side-effect barrel import is *added* alongside.
24
+ * The barrel exports the class as a named export with no default, so the path
25
+ * can't simply be swapped; adding is the safe move, and `defineCustomElement`
26
+ * is idempotent so a duplicate registration can't throw.
27
+ * - type-only — ignored; registers/renders nothing
28
+ * - if the component barrel is already imported, redundant side-effect class
29
+ * imports of that component are dropped.
30
+ *
31
+ * Idempotent. Run it (dry by default):
32
+ * npx @carbon/upgrade migrate wc-add-barrel-imports [paths...]
33
+ * npx @carbon/upgrade migrate wc-add-barrel-imports --write [paths...]
34
+ */
35
+ function transform(fileInfo, api) {
36
+ const j = api.jscodeshift;
37
+ const root = j(fileInfo.source);
38
+
39
+ const findings = collectNonBarrelWebComponentImports(j, root).filter(
40
+ (f) => !f.typeOnly
41
+ );
42
+ if (findings.length === 0) {
43
+ return fileInfo.source;
44
+ }
45
+
46
+ // group the non-barrel imports by component
47
+ const byComponent = new Map();
48
+ for (const f of findings) {
49
+ let group = byComponent.get(f.component);
50
+ if (!group) {
51
+ group = { barrel: f.suggestedBarrel, specifiers: new Set() };
52
+ byComponent.set(f.component, group);
53
+ }
54
+ group.specifiers.add(f.specifier);
55
+ }
56
+
57
+ const stripJs = (value) => value.replace(/\.js$/, '');
58
+ // string source of import path, or null
59
+ const srcOf = (p) => {
60
+ const source = p.node && p.node.source;
61
+ return source && typeof source.value === 'string' ? source.value : null;
62
+ };
63
+ const importPaths = root.find(j.ImportDeclaration).paths();
64
+ let changed = false;
65
+
66
+ for (const { barrel, specifiers } of byComponent.values()) {
67
+ const barrelDir = barrel.replace(/\/index\.js$/, '');
68
+ const barrelForms = new Set([`${barrelDir}/index`, barrelDir]);
69
+
70
+ const barrelAlreadyPresent = importPaths.some((p) => {
71
+ const value = srcOf(p);
72
+ return value != null && barrelForms.has(stripJs(value));
73
+ });
74
+
75
+ const relevant = importPaths.filter((p) => {
76
+ const value = srcOf(p);
77
+ return value != null && specifiers.has(value);
78
+ });
79
+ const sideEffectPaths = relevant.filter(
80
+ (p) => (p.node.specifiers || []).length === 0
81
+ );
82
+ const valuePaths = relevant.filter(
83
+ (p) => (p.node.specifiers || []).length > 0
84
+ );
85
+
86
+ if (barrelAlreadyPresent) {
87
+ // drop class-file side-effect imports
88
+ for (const p of sideEffectPaths) {
89
+ j(p).remove();
90
+ changed = true;
91
+ }
92
+ continue;
93
+ }
94
+
95
+ if (sideEffectPaths.length > 0) {
96
+ // collapse - first side-effect import becomes barrent, drop the rest
97
+ sideEffectPaths[0].node.source = j.stringLiteral(barrel);
98
+ for (let i = 1; i < sideEffectPaths.length; i++) {
99
+ j(sideEffectPaths[i]).remove();
100
+ }
101
+ changed = true;
102
+ } else {
103
+ // value imports - keep and add one-side-effect barrel import
104
+ valuePaths[0].insertAfter(
105
+ j.importDeclaration([], j.stringLiteral(barrel))
106
+ );
107
+ changed = true;
108
+ }
109
+ }
110
+
111
+ return changed ? root.toSource({ quote: 'single' }) : fileInfo.source;
112
+ }
113
+
114
+ module.exports = transform;
115
+ // Parse TS/TSX consumer source (the tsx parser also handles plain JS/JSX).
116
+ module.exports.parser = 'tsx';
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+ const { collectNonBarrelWebComponentImports } = require('./carbon-wc-imports');
12
+
13
+ /**
14
+ * Flags imports of `@carbon/web-components` component class files, i.e. any
15
+ * non-barrel entry under `es/components/<name>/...`. Starting with v3 pure
16
+ * exports, these register nothing, so an element imported this way but rendered
17
+ * in markup upgrades to nothing — it renders inert, with no error. Import the
18
+ * component barrel (`<name>/index.js`) instead, or run the barrel-registration
19
+ * codemod.
20
+ *
21
+ * Type-only imports are reported as "review" — they may be intentional (the
22
+ * element is registered elsewhere, or the class is imported only as a type or
23
+ * to subclass). Only the runtime `:not(:defined)` diagnostic reliably reflects
24
+ * what actually renders inert.
25
+ *
26
+ * This transform reports only, it never changes source. Output is grouped one
27
+ * block per file.
28
+ *
29
+ * npx @carbon/upgrade migrate wc-report-non-barrel-imports [paths...]
30
+ */
31
+
32
+ // Strip `@carbon/web-components/<es|lib>/components/` prefix so the
33
+ // report shows just `search/search.js` -> `search/index.js`.
34
+ const COMPONENT_PREFIX_RE =
35
+ /^@carbon\/web-components\/(?:es|lib)\/components\//;
36
+
37
+ const shorten = (specifier) => specifier.replace(COMPONENT_PREFIX_RE, '');
38
+
39
+ const ANSI = {
40
+ reset: '\x1b[0m',
41
+ bold: '\x1b[1m',
42
+ dim: '\x1b[2m',
43
+ orange: '\x1b[38;5;208m',
44
+ yellow: '\x1b[33m',
45
+ gray: '\x1b[90m',
46
+ cyan: '\x1b[36m',
47
+ };
48
+
49
+ function colorEnabled() {
50
+ // on by default; set `NO_COLOR=1` or `FORCE_COLOR=0` to disable
51
+ if (process.env.NO_COLOR != null) {
52
+ return false;
53
+ }
54
+ if (process.env.FORCE_COLOR != null) {
55
+ return process.env.FORCE_COLOR !== '0';
56
+ }
57
+ return true;
58
+ }
59
+
60
+ function makePaint(enabled) {
61
+ return (codes, str) => {
62
+ if (!enabled) {
63
+ return str;
64
+ }
65
+ const open = (Array.isArray(codes) ? codes : [codes]).join('');
66
+ return `${open}${str}${ANSI.reset}`;
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Render findings as a grouped tree block
72
+ *
73
+ * @param {string} displayPath relative path shown in the header
74
+ * @param {Array<object>} findings from collectNonBarrelWebComponentImports
75
+ * @param {{ color?: boolean }} [options]
76
+ * @returns {string}
77
+ */
78
+ function formatFileBlock(displayPath, findings, options = {}) {
79
+ const paint = makePaint(options.color ?? false);
80
+
81
+ const rows = findings
82
+ .slice()
83
+ .sort((a, b) => (a.line ?? 0) - (b.line ?? 0))
84
+ .map((f) => ({
85
+ line: f.line == null ? '' : `L${f.line}`,
86
+ spec: shorten(f.specifier),
87
+ kind: f.typeOnly ? 'type · review' : f.kind,
88
+ barrel: shorten(f.suggestedBarrel),
89
+ typeOnly: f.typeOnly,
90
+ }));
91
+
92
+ const lineWidth = Math.max(...rows.map((r) => r.line.length), 0);
93
+ const specWidth = Math.max(...rows.map((r) => r.spec.length), 0);
94
+ const kindWidth = Math.max(...rows.map((r) => r.kind.length), 0);
95
+
96
+ const needs = rows.filter((r) => !r.typeOnly).length;
97
+ const review = rows.length - needs;
98
+ const summaryBits = [];
99
+ if (needs) {
100
+ summaryBits.push(`${needs} need${needs === 1 ? 's' : ''} a barrel`);
101
+ }
102
+ if (review) {
103
+ summaryBits.push(`${review} to review`);
104
+ }
105
+
106
+ const header =
107
+ paint([ANSI.bold, ANSI.orange], displayPath) +
108
+ paint(ANSI.dim, ` (${summaryBits.join(', ')})`);
109
+
110
+ const lines = rows.map((r, i) => {
111
+ const connector = i === rows.length - 1 ? '└─' : '├─';
112
+ const lineCol = paint(ANSI.dim, r.line.padEnd(lineWidth));
113
+ const specCol = r.spec.padEnd(specWidth);
114
+ const kindColor = r.typeOnly ? ANSI.cyan : ANSI.gray;
115
+ const kindCol = paint(kindColor, `(${r.kind})`.padEnd(kindWidth + 2));
116
+ const arrow = paint(ANSI.dim, '→');
117
+ const barrel = paint(ANSI.yellow, r.barrel);
118
+
119
+ return ` ${connector} ${lineCol} ${specCol} ${kindCol} ${arrow} ${barrel}`;
120
+ });
121
+
122
+ return [header, ...lines].join('\n');
123
+ }
124
+
125
+ function transform(fileInfo, api) {
126
+ const j = api.jscodeshift;
127
+ const findings = collectNonBarrelWebComponentImports(j, j(fileInfo.source));
128
+
129
+ if (findings.length) {
130
+ let displayPath = fileInfo.path;
131
+ try {
132
+ const rel = path.relative(process.cwd(), fileInfo.path);
133
+ if (rel && !rel.startsWith('..')) {
134
+ displayPath = rel;
135
+ }
136
+ } catch {
137
+ // keep the raw path
138
+ }
139
+
140
+ // one log block per file
141
+ console.log(
142
+ '\n' +
143
+ formatFileBlock(displayPath, findings, {
144
+ color: colorEnabled(),
145
+ })
146
+ );
147
+ }
148
+
149
+ // report only - no changes
150
+ return fileInfo.source;
151
+ }
152
+
153
+ module.exports = transform;
154
+ module.exports.formatFileBlock = formatFileBlock;
155
+ // parse TS/TSX consumer source (tsx parser also handles plain JS/JSX)
156
+ module.exports.parser = 'tsx';