@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 +41 -0
- package/cli.js +90 -1
- package/package.json +2 -2
- package/transforms/__testfixtures__/wc-add-barrel-imports.input.tsx +20 -0
- package/transforms/__testfixtures__/wc-add-barrel-imports.output.tsx +19 -0
- package/transforms/__tests__/wc-add-barrel-imports-test.js +17 -0
- package/transforms/__tests__/wc-report-non-barrel-imports-test.js +181 -0
- package/transforms/carbon-wc-imports.js +129 -0
- package/transforms/wc-add-barrel-imports.js +116 -0
- package/transforms/wc-report-non-barrel-imports.js +156 -0
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.
|
|
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.
|
|
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": "
|
|
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';
|