@atlaskit/icon 33.1.1 → 33.1.2

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
@@ -1,5 +1,15 @@
1
1
  # @atlaskit/icon
2
2
 
3
+ ## 33.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`08170da1fbf62`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/08170da1fbf62) -
8
+ Migrate spacing prop usages on icons to Flex wrapper
9
+ - [`08170da1fbf62`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/08170da1fbf62) -
10
+ Update codemod and eslint to handle different scenarios to migrate spacing props
11
+ - Updated dependencies
12
+
3
13
  ## 33.1.1
4
14
 
5
15
  ### Patch Changes
@@ -1,5 +1,57 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
1
4
  import type { API, ASTPath, Collection, FileInfo, JSXElement, default as core } from 'jscodeshift';
2
5
 
6
+ // Accumulated across all files in this worker — printed as a summary at process exit
7
+ const packagesNeedingCssDep = new Set<string>();
8
+ const emotionSkippedFiles: string[] = [];
9
+ const dynamicSpacingFiles: string[] = [];
10
+ const unresolvableIconFiles: string[] = [];
11
+
12
+ process.on('exit', () => {
13
+ const lines: string[] = [];
14
+
15
+ if (emotionSkippedFiles.length > 0) {
16
+ lines.push('');
17
+ lines.push('━━━ ⏭ FILES SKIPPED — Emotion imports detected (manual migration required) ━━━');
18
+ lines.push(' These files mix Emotion and Compiled. To migrate:');
19
+ lines.push(' Option A: Replace Emotion imports with @compiled/react equivalents first.');
20
+ lines.push(' Option B: Manually wrap icon(s) in <Flex xcss={...}> + cssMap.');
21
+ emotionSkippedFiles.forEach((f) => lines.push(` • ${f}`));
22
+ }
23
+
24
+ if (dynamicSpacingFiles.length > 0) {
25
+ lines.push('');
26
+ lines.push('━━━ ⚠️ DYNAMIC SPACING — manual migration needed ━━━');
27
+ lines.push(' spacing prop has a dynamic/variable value. Manually wrap the icon in');
28
+ lines.push(' <Flex xcss={iconSpacingStyles.spaceXXX}> with the correct token.');
29
+ lines.push(' An eslint-disable comment has been added to suppress the lint error.');
30
+ dynamicSpacingFiles.forEach((f) => lines.push(` • ${f}`));
31
+ }
32
+
33
+ if (unresolvableIconFiles.length > 0) {
34
+ lines.push('');
35
+ lines.push('━━━ ⚠️ UNRESOLVABLE ICON — manual migration needed ━━━');
36
+ lines.push(' Icon component uses a member expression or aliased variable name.');
37
+ lines.push(' The codemod cannot statically resolve these. Manually wrap the icon in');
38
+ lines.push(' <Flex xcss={iconSpacingStyles.spaceXXX}> and remove the spacing prop.');
39
+ unresolvableIconFiles.forEach((f) => lines.push(` • ${f}`));
40
+ }
41
+
42
+ if (packagesNeedingCssDep.size > 0) {
43
+ lines.push('');
44
+ lines.push('━━━ ⚠️ PACKAGES NEEDING package.json UPDATE ━━━');
45
+ lines.push(` Add "@atlaskit/css": "*" to dependencies in each package.json below:`);
46
+ [...packagesNeedingCssDep].sort().forEach((p) => lines.push(` • ${p}`));
47
+ }
48
+
49
+ if (lines.length > 0) {
50
+ // eslint-disable-next-line no-console
51
+ console.warn(lines.join('\n'));
52
+ }
53
+ });
54
+
3
55
  const ICON_PACKAGES = ['@atlaskit/icon/core', '@atlaskit/icon-lab/core'];
4
56
  const FLEX_COMPILED_PACKAGE = '@atlaskit/primitives/compiled';
5
57
  const FLEX_NON_COMPILED_PACKAGE = '@atlaskit/primitives';
@@ -57,12 +109,21 @@ function getIconImportSpecifiers(j: core.JSCodeshift, source: Collection<any>):
57
109
  return specifiers;
58
110
  }
59
111
 
112
+ function sortImportSpecifiers(specifiers: any[]): void {
113
+ specifiers.sort((a, b) => {
114
+ const aName = a.imported?.name ?? '';
115
+ const bName = b.imported?.name ?? '';
116
+ return aName.localeCompare(bName);
117
+ });
118
+ }
119
+
60
120
  function ensureNamedImport(j: core.JSCodeshift, specifiers: any[], name: string): void {
61
121
  const alreadyImported = specifiers.some(
62
122
  (s) => s.type === 'ImportSpecifier' && s.imported?.name === name,
63
123
  );
64
124
  if (!alreadyImported) {
65
125
  specifiers.push(j.importSpecifier(j.identifier(name)));
126
+ sortImportSpecifiers(specifiers);
66
127
  }
67
128
  }
68
129
 
@@ -89,11 +150,22 @@ function insertFlexImport(j: core.JSCodeshift, source: Collection<any>): void {
89
150
  .filter((path) => path.node.source.value === FLEX_NON_COMPILED_PACKAGE);
90
151
 
91
152
  if (nonCompiledImports.length > 0) {
92
- nonCompiledImports.forEach((path) => {
93
- path.node.source = j.stringLiteral(FLEX_COMPILED_PACKAGE);
94
- ensureNamedImport(j, path.node.specifiers || [], 'Flex');
95
- });
96
- return;
153
+ // Only rewrite @atlaskit/primitives → /compiled if the file doesn't use xcss.
154
+ // xcss is NOT exported from /compiled — rewriting would cause type errors.
155
+ const hasXcss = nonCompiledImports.some((path) =>
156
+ (path.node.specifiers || []).some(
157
+ (s: any) => s.type === 'ImportSpecifier' && s.imported?.name === 'xcss',
158
+ ),
159
+ );
160
+
161
+ if (!hasXcss) {
162
+ nonCompiledImports.forEach((path) => {
163
+ path.node.source = j.stringLiteral(FLEX_COMPILED_PACKAGE);
164
+ ensureNamedImport(j, path.node.specifiers || [], 'Flex');
165
+ });
166
+ return;
167
+ }
168
+ // If xcss is used, don't rewrite — add a new /compiled import for Flex instead.
97
169
  }
98
170
 
99
171
  const newImport = j.importDeclaration(
@@ -121,6 +193,19 @@ function insertCssMapImport(j: core.JSCodeshift, source: Collection<any>): void
121
193
  return;
122
194
  }
123
195
 
196
+ // If cssMap is already imported from another package (e.g. @compiled/react), don't add a duplicate
197
+ const cssMapAlreadyImported = source
198
+ .find(j.ImportDeclaration)
199
+ .filter((path) =>
200
+ (path.node.specifiers || []).some(
201
+ (s: any) => s.type === 'ImportSpecifier' && s.imported?.name === 'cssMap',
202
+ ),
203
+ );
204
+
205
+ if (cssMapAlreadyImported.length > 0) {
206
+ return;
207
+ }
208
+
124
209
  const newImport = j.importDeclaration(
125
210
  [j.importSpecifier(j.identifier('cssMap'))],
126
211
  j.stringLiteral(CSS_PACKAGE),
@@ -261,9 +346,33 @@ function addEslintDisableComment(
261
346
  path: ASTPath<JSXElement>,
262
347
  reason: string,
263
348
  ): void {
264
- const comment = j.line(
265
- ` eslint-disable-next-line @atlaskit/design-system/no-icon-spacing-prop -- TODO: ${reason}`,
349
+ // JSX requires comments inside children to be wrapped in {/* */}.
350
+ // We achieve this by inserting a JSXExpressionContainer with an empty expression
351
+ // and attaching the block comment to it — jscodeshift prints this as {/* ... */}.
352
+ const emptyExpr = j.jsxEmptyExpression();
353
+ (emptyExpr as any).innerComments = [
354
+ {
355
+ type: 'CommentBlock',
356
+ value: ` eslint-disable-next-line @atlaskit/design-system/no-icon-spacing-prop -- TODO: ${reason} `,
357
+ },
358
+ ];
359
+ const commentContainer = j.jsxExpressionContainer(emptyExpr);
360
+
361
+ // Insert the {/* */} container as a sibling before the icon element in its parent's children
362
+ const parent = path.parent;
363
+ if (parent && parent.value && Array.isArray(parent.value.children)) {
364
+ const idx = parent.value.children.indexOf(path.value);
365
+ if (idx !== -1) {
366
+ parent.value.children.splice(idx, 0, commentContainer);
367
+ return;
368
+ }
369
+ }
370
+
371
+ // Fallback: attach as a leading comment on the node itself
372
+ const comment = j.block(
373
+ ` eslint-disable-next-line @atlaskit/design-system/no-icon-spacing-prop -- TODO: ${reason} `,
266
374
  );
375
+ comment.leading = true;
267
376
  const node = path.value as any;
268
377
  node.comments = [comment, ...(node.comments || [])];
269
378
  }
@@ -335,6 +444,50 @@ function replaceWithWrapped(
335
444
  }
336
445
  }
337
446
 
447
+ const EMOTION_PACKAGES = ['@emotion/react', '@emotion/styled', '@emotion/core'];
448
+
449
+ /**
450
+ * Check if the file imports from Emotion. If so, it can't be migrated automatically
451
+ * because mixing Emotion and Compiled (used by @atlaskit/primitives/compiled) in the
452
+ * same file causes type-checking and runtime errors. These files need manual migration.
453
+ */
454
+ function hasEmotionImports(j: core.JSCodeshift, source: Collection<any>): boolean {
455
+ return (
456
+ source
457
+ .find(j.ImportDeclaration)
458
+ .filter((p) => EMOTION_PACKAGES.includes(p.node.source.value as string)).length > 0
459
+ );
460
+ }
461
+
462
+ /**
463
+ * Check if @atlaskit/css is listed in the nearest package.json.
464
+ * If not, warn the user that they need to add it.
465
+ */
466
+ function checkCssPackageDependency(filePath: string | undefined): boolean {
467
+ if (!filePath) {
468
+ return true; // In test environments, file.path may be undefined — skip the check
469
+ }
470
+ let dir = path.dirname(filePath);
471
+ while (dir !== path.dirname(dir)) {
472
+ const pkgPath = path.join(dir, 'package.json');
473
+ if (fs.existsSync(pkgPath)) {
474
+ try {
475
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
476
+ const deps = {
477
+ ...(pkg.dependencies || {}),
478
+ ...(pkg.devDependencies || {}),
479
+ ...(pkg.peerDependencies || {}),
480
+ };
481
+ return CSS_PACKAGE in deps;
482
+ } catch {
483
+ return false;
484
+ }
485
+ }
486
+ dir = path.dirname(dir);
487
+ }
488
+ return false;
489
+ }
490
+
338
491
  export default function transformer(file: FileInfo, api: API): string {
339
492
  const j = api.jscodeshift;
340
493
  const source = j(file.source);
@@ -344,6 +497,51 @@ export default function transformer(file: FileInfo, api: API): string {
344
497
  return file.source;
345
498
  }
346
499
 
500
+ // Skip files that import from Emotion — mixing Emotion and Compiled causes
501
+ // type-checking and runtime errors. These files need manual migration.
502
+ if (hasEmotionImports(j, source)) {
503
+ if (file.path) {
504
+ emotionSkippedFiles.push(file.path);
505
+ }
506
+ return file.source;
507
+ }
508
+
509
+ // Detect unresolvable cases: JSX elements with spacing prop that are NOT in iconSpecifiers
510
+ // (member expressions like <appearanceIconStyles.Icon> or aliased variables like <Icon>)
511
+ source
512
+ .find(j.JSXElement)
513
+ .filter((path) => {
514
+ const name = path.value.openingElement.name;
515
+ const attrs = path.value.openingElement.attributes || [];
516
+ const hasSpacing = attrs.some(
517
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name?.name === 'spacing',
518
+ );
519
+ if (!hasSpacing) {
520
+ return false;
521
+ }
522
+ // Member expression: <appearanceIconStyles.Icon spacing="spacious">
523
+ if (name.type === 'JSXMemberExpression') {
524
+ return true;
525
+ }
526
+ // Aliased variable: <Icon spacing="spacious"> where Icon is not a known icon specifier
527
+ // but could be a re-assigned icon (heuristic: PascalCase identifier not in specifiers)
528
+ if (name.type === 'JSXIdentifier' && !iconSpecifiers.includes(name.name)) {
529
+ const n = name.name;
530
+ return n[0] === n[0].toUpperCase() && n !== 'Flex' && n !== 'Box' && n !== 'Inline';
531
+ }
532
+ return false;
533
+ })
534
+ .forEach((path) => {
535
+ if (file.path) {
536
+ const name = path.value.openingElement.name;
537
+ const nameStr =
538
+ name.type === 'JSXMemberExpression'
539
+ ? `<${(name.object as any).name}.${(name.property as any).name}>`
540
+ : `<${(name as any).name}>`;
541
+ unresolvableIconFiles.push(`${file.path} — ${nameStr} (member expression or alias)`);
542
+ }
543
+ });
544
+
347
545
  const iconJSXWithSpacing = source
348
546
  .find(j.JSXElement)
349
547
  .filter((path) => {
@@ -360,6 +558,27 @@ export default function transformer(file: FileInfo, api: API): string {
360
558
  return file.source;
361
559
  }
362
560
 
561
+ // Warn if @atlaskit/css is not in the package's dependencies
562
+ if (!checkCssPackageDependency(file.path)) {
563
+ // Find the nearest package.json and record the package name
564
+ if (file.path) {
565
+ let dir = path.dirname(file.path);
566
+ while (dir !== path.dirname(dir)) {
567
+ const pkgPath = path.join(dir, 'package.json');
568
+ if (fs.existsSync(pkgPath)) {
569
+ try {
570
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
571
+ packagesNeedingCssDep.add(`${pkg.name ?? dir} (${pkgPath})`);
572
+ } catch {
573
+ packagesNeedingCssDep.add(dir);
574
+ }
575
+ break;
576
+ }
577
+ dir = path.dirname(dir);
578
+ }
579
+ }
580
+ }
581
+
363
582
  const paddingTokensUsed = new Set<string>();
364
583
  let needsFlexImport = false;
365
584
 
@@ -372,6 +591,9 @@ export default function transformer(file: FileInfo, api: API): string {
372
591
  path,
373
592
  'Manually migrate spacing prop to Flex primitive (spread props detected)',
374
593
  );
594
+ if (file.path) {
595
+ dynamicSpacingFiles.push(`${file.path} — spread props`);
596
+ }
375
597
  return;
376
598
  }
377
599
 
@@ -381,6 +603,9 @@ export default function transformer(file: FileInfo, api: API): string {
381
603
  path,
382
604
  'Manually migrate spacing prop to Flex primitive (dynamic spacing value detected)',
383
605
  );
606
+ if (file.path) {
607
+ dynamicSpacingFiles.push(`${file.path} — dynamic spacing value`);
608
+ }
384
609
  return;
385
610
  }
386
611
 
@@ -136,7 +136,7 @@ const App = () => <DropdownMenu iconAfter={<Flex xcss={iconSpacingStyles.space07
136
136
  `import AddIcon from '@atlaskit/icon/core/add';
137
137
  const App = (props: any) => <AddIcon {...props} spacing="spacious" label="" />;`,
138
138
  `import AddIcon from '@atlaskit/icon/core/add';
139
- const App = (props: any) => // eslint-disable-next-line @atlaskit/design-system/no-icon-spacing-prop -- TODO: Manually migrate spacing prop to Flex primitive (spread props detected)
139
+ const App = (props: any) => /* eslint-disable-next-line @atlaskit/design-system/no-icon-spacing-prop -- TODO: Manually migrate spacing prop to Flex primitive (spread props detected) */
140
140
  <AddIcon {...props} spacing="spacious" label="" />;`,
141
141
  'should add eslint-disable comment inline and skip when spread props are present',
142
142
  );
@@ -147,11 +147,52 @@ const App = (props: any) => // eslint-disable-next-line @atlaskit/design-system/
147
147
  `import AddIcon from '@atlaskit/icon/core/add';
148
148
  const App = ({ spacing }: any) => <AddIcon label="" spacing={spacing} />;`,
149
149
  `import AddIcon from '@atlaskit/icon/core/add';
150
- const App = ({ spacing }: any) => // eslint-disable-next-line @atlaskit/design-system/no-icon-spacing-prop -- TODO: Manually migrate spacing prop to Flex primitive (dynamic spacing value detected)
150
+ const App = ({ spacing }: any) => /* eslint-disable-next-line @atlaskit/design-system/no-icon-spacing-prop -- TODO: Manually migrate spacing prop to Flex primitive (dynamic spacing value detected) */
151
151
  <AddIcon label="" spacing={spacing} />;`,
152
152
  'should add eslint-disable comment inline and skip when spacing is a dynamic expression',
153
153
  );
154
154
 
155
+ defineInlineTest(
156
+ { default: transformer, parser: 'tsx' },
157
+ {},
158
+ `import { css } from '@emotion/react';
159
+ import AddIcon from '@atlaskit/icon/core/add';
160
+ const App = () => <AddIcon label="" spacing="spacious" />;`,
161
+ `import { css } from '@emotion/react';
162
+ import AddIcon from '@atlaskit/icon/core/add';
163
+ const App = () => <AddIcon label="" spacing="spacious" />;`,
164
+ 'should skip files that import from @emotion/react and leave them unchanged',
165
+ );
166
+
167
+ defineInlineTest(
168
+ { default: transformer, parser: 'tsx' },
169
+ {},
170
+ `import styled from '@emotion/styled';
171
+ import AddIcon from '@atlaskit/icon/core/add';
172
+ const App = () => <AddIcon label="" spacing="spacious" />;`,
173
+ `import styled from '@emotion/styled';
174
+ import AddIcon from '@atlaskit/icon/core/add';
175
+ const App = () => <AddIcon label="" spacing="spacious" />;`,
176
+ 'should skip files that import from @emotion/styled and leave them unchanged',
177
+ );
178
+
179
+ defineInlineTest(
180
+ { default: transformer, parser: 'tsx' },
181
+ {},
182
+ `import { cssMap } from '@compiled/react';
183
+ import AddIcon from '@atlaskit/icon/core/add';
184
+ const App = () => <AddIcon label="" spacing="spacious" />;`,
185
+ `import { Flex } from "@atlaskit/primitives/compiled";
186
+ import { token } from "@atlaskit/tokens";
187
+ import { cssMap } from '@compiled/react';
188
+ import AddIcon from '@atlaskit/icon/core/add';
189
+
190
+ ${space050Block}
191
+
192
+ const App = () => <Flex xcss={iconSpacingStyles.space050}><AddIcon label="" /></Flex>;`,
193
+ 'should not add duplicate cssMap import when cssMap already imported from @compiled/react',
194
+ );
195
+
155
196
  defineInlineTest(
156
197
  { default: transformer, parser: 'tsx' },
157
198
  {},
@@ -211,7 +252,7 @@ import AddIcon from '@atlaskit/icon/core/add';
211
252
  const App = () => <AddIcon label="" spacing="spacious" />;`,
212
253
  `import { cssMap } from "@atlaskit/css";
213
254
  import { token } from "@atlaskit/tokens";
214
- import { Inline, Flex } from '@atlaskit/primitives/compiled';
255
+ import { Flex, Inline } from '@atlaskit/primitives/compiled';
215
256
  import AddIcon from '@atlaskit/icon/core/add';
216
257
 
217
258
  ${space050Block}
@@ -262,7 +303,7 @@ import AddIcon from '@atlaskit/icon/core/add';
262
303
  const App = () => <AddIcon label="" spacing="spacious" />;`,
263
304
  `import { cssMap } from "@atlaskit/css";
264
305
  import { token } from "@atlaskit/tokens";
265
- import { Inline, Flex } from "@atlaskit/primitives/compiled";
306
+ import { Flex, Inline } from "@atlaskit/primitives/compiled";
266
307
  import AddIcon from '@atlaskit/icon/core/add';
267
308
 
268
309
  ${space050Block}
@@ -271,6 +312,26 @@ const App = () => <Flex xcss={iconSpacingStyles.space050}><AddIcon label="" /></
271
312
  'should add Flex and update @atlaskit/primitives to @atlaskit/primitives/compiled',
272
313
  );
273
314
 
315
+ defineInlineTest(
316
+ { default: transformer, parser: 'tsx' },
317
+ {},
318
+ `import { Pressable, xcss } from '@atlaskit/primitives';
319
+ import AddIcon from '@atlaskit/icon/core/add';
320
+ const buttonStyles = xcss({ padding: 'space.100' });
321
+ const App = () => <AddIcon label="" spacing="spacious" />;`,
322
+ `import { cssMap } from "@atlaskit/css";
323
+ import { Flex } from "@atlaskit/primitives/compiled";
324
+ import { token } from "@atlaskit/tokens";
325
+ import { Pressable, xcss } from '@atlaskit/primitives';
326
+ import AddIcon from '@atlaskit/icon/core/add';
327
+
328
+ ${space050Block}
329
+
330
+ const buttonStyles = xcss({ padding: 'space.100' });
331
+ const App = () => <Flex xcss={iconSpacingStyles.space050}><AddIcon label="" /></Flex>;`,
332
+ 'should add new /compiled import when @atlaskit/primitives uses xcss (cannot rewrite)',
333
+ );
334
+
274
335
  defineInlineTest(
275
336
  { default: transformer, parser: 'tsx' },
276
337
  {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/icon",
3
- "version": "33.1.1",
3
+ "version": "33.1.2",
4
4
  "description": "An icon is a symbol representing a command, device, directory, or common action.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -59,13 +59,13 @@
59
59
  "@atlaskit/link": "^3.3.0",
60
60
  "@atlaskit/logo": "^19.10.0",
61
61
  "@atlaskit/menu": "^8.4.0",
62
- "@atlaskit/modal-dialog": "^14.13.0",
62
+ "@atlaskit/modal-dialog": "^14.14.0",
63
63
  "@atlaskit/primitives": "^18.1.0",
64
64
  "@atlaskit/section-message": "^8.12.0",
65
65
  "@atlaskit/textfield": "^8.2.0",
66
66
  "@atlaskit/theme": "^22.0.0",
67
67
  "@atlaskit/toggle": "^15.2.0",
68
- "@atlaskit/tooltip": "^21.0.0",
68
+ "@atlaskit/tooltip": "^21.1.0",
69
69
  "@atlassian/feature-flags-test-utils": "^1.0.0",
70
70
  "@atlassian/ssr-tests": "workspace:^",
71
71
  "@atlassian/structured-docs-types": "workspace:^",