@atlaskit/css 0.5.1 → 0.5.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,13 @@
1
1
  # @atlaskit/css
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#150360](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/150360)
8
+ [`284490a8c1813`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/284490a8c1813) -
9
+ Add codemod to migrate @atlaskit/primitives from using Emotion APIs towards Compiled.
10
+
3
11
  ## 0.5.1
4
12
 
5
13
  ### Patch Changes
@@ -0,0 +1,334 @@
1
+ import type {
2
+ API,
3
+ ASTPath,
4
+ default as core,
5
+ FileInfo,
6
+ ImportDeclaration,
7
+ Options,
8
+ } from 'jscodeshift';
9
+
10
+ import {
11
+ backgroundColorMap,
12
+ borderColorMap,
13
+ borderRadiusMap,
14
+ borderWidthMap,
15
+ dimensionMap,
16
+ fillMap,
17
+ fontFamilyMap,
18
+ fontMap,
19
+ fontWeightMap,
20
+ layerMap,
21
+ negativeSpaceMap,
22
+ opacityMap,
23
+ positiveSpaceMap,
24
+ shadowMap,
25
+ surfaceColorMap,
26
+ textColorMap,
27
+ textWeightMap,
28
+ } from './style-maps.partial';
29
+
30
+ const styleMaps = {
31
+ ...backgroundColorMap,
32
+ ...borderColorMap,
33
+ ...borderRadiusMap,
34
+ ...borderWidthMap,
35
+ ...dimensionMap,
36
+ ...fillMap,
37
+ ...fontFamilyMap,
38
+ ...fontMap,
39
+ ...fontWeightMap,
40
+ ...layerMap,
41
+ ...negativeSpaceMap,
42
+ ...opacityMap,
43
+ ...positiveSpaceMap,
44
+ ...shadowMap,
45
+ ...surfaceColorMap,
46
+ ...textColorMap,
47
+ ...textWeightMap,
48
+ };
49
+
50
+ export default function transformer(fileInfo: FileInfo, { jscodeshift: j }: API, options: Options) {
51
+ const base = j(fileInfo.source);
52
+
53
+ // replace xcss with cssMap
54
+ const xcssSpecifier = getImportSpecifier(j, base, 'xcss');
55
+ if (!xcssSpecifier) {
56
+ return;
57
+ }
58
+
59
+ addJsxPragma(j, base);
60
+
61
+ replaceXcssWithCssMap(j, base, xcssSpecifier);
62
+
63
+ updateImports(j, base);
64
+
65
+ return base.toSource();
66
+ }
67
+
68
+ function addJsxPragma(j: core.JSCodeshift, source: ReturnType<typeof j>) {
69
+ const jsxPragma = [j.commentBlock('*\n * @jsxRuntime classic\n * @jsx jsx\n ', true, false)];
70
+
71
+ const rootNode = source.get().node;
72
+ const existingComments = rootNode.comments || [];
73
+
74
+ const hasJsxPragma = existingComments.some(
75
+ (comment: core.Comment) =>
76
+ comment.value.includes('@jsxRuntime classic') && comment.value.includes('@jsx jsx'),
77
+ );
78
+
79
+ if (!hasJsxPragma) {
80
+ rootNode.comments = [...existingComments, ...jsxPragma];
81
+ }
82
+ }
83
+
84
+ function replaceXcssWithCssMap(
85
+ j: core.JSCodeshift,
86
+ source: ReturnType<typeof j>,
87
+ specifier: string,
88
+ ) {
89
+ const cssMapProperties: core.ObjectProperty[] = [];
90
+
91
+ source
92
+ .find(j.CallExpression, {
93
+ // get all xcss calls
94
+ callee: {
95
+ type: 'Identifier',
96
+ name: specifier,
97
+ },
98
+ })
99
+ .forEach((path) => {
100
+ const args = path.node.arguments;
101
+ if (args.length === 1 && args[0].type === 'ObjectExpression') {
102
+ // get the parent variable declaration
103
+ // e.g. const buttonStyles = xcss({ color: 'red' });
104
+ const parentVariableDeclaration = path.parentPath?.parentPath?.parentPath?.node;
105
+ if (parentVariableDeclaration && parentVariableDeclaration.type === 'VariableDeclaration') {
106
+ const variableDeclarator = parentVariableDeclaration.declarations.find(
107
+ (declaration: core.VariableDeclarator) => declaration.init === path.node,
108
+ );
109
+
110
+ if (variableDeclarator && variableDeclarator.type === 'VariableDeclarator') {
111
+ const variableName = variableDeclarator.id.name; // e.g. buttonStyles
112
+ const key = getCssMapKey(variableName); // buttonStyles -> button to put in cssMap as the key e.g. styles = cssMap({ button: { color: 'red' } });
113
+
114
+ const cssMapObject = j.objectProperty(
115
+ j.identifier(key),
116
+ ensureSelectorAmpersand(j, args[0]),
117
+ );
118
+ cssMapProperties.push(cssMapObject);
119
+
120
+ j(path.parentPath.parentPath.parentPath).remove(); // remove original xcss object
121
+ }
122
+ }
123
+ }
124
+ });
125
+
126
+ // create new cssMap with the combined xcss object properties
127
+ if (cssMapProperties.length > 0) {
128
+ const cssMapObject = j.objectExpression(cssMapProperties);
129
+ const cssMapVariableDeclaration = j.variableDeclaration('const', [
130
+ j.variableDeclarator(
131
+ j.identifier('styles'),
132
+ j.callExpression(j.identifier('cssMap'), [cssMapObject]),
133
+ ),
134
+ ]);
135
+ source.get().node.program.body.unshift(cssMapVariableDeclaration);
136
+ }
137
+
138
+ // update the xcss prop references to use the new cssMap object
139
+ source
140
+ .find(j.JSXAttribute, {
141
+ name: {
142
+ type: 'JSXIdentifier',
143
+ name: 'xcss',
144
+ },
145
+ })
146
+ .forEach((path) => {
147
+ const value = path.node.value;
148
+ // e.g. <Box xcss={buttonStyles} />
149
+ if (value && value.type === 'JSXExpressionContainer') {
150
+ const expression = value.expression;
151
+ if (expression.type === 'Identifier') {
152
+ // <Box xcss={buttonStyles} /> -> <Box xcss={styles.button} />
153
+ expression.name = `styles.${getCssMapKey(expression.name)}`;
154
+ // <Box xcss={[baseStyles, otherStyles]} /> -> <Box xcss={[styles.base, styles.otherStyles]} />
155
+ } else if (expression.type === 'ArrayExpression') {
156
+ expression.elements.forEach((element) => {
157
+ if (element?.type === 'Identifier') {
158
+ element.name = `styles.${getCssMapKey(element.name)}`;
159
+ // <Box xcss={condition && styles} /> -> <Box xcss={condition && styles.root} />
160
+ } else if (
161
+ element?.type === 'LogicalExpression' &&
162
+ element.right.type === 'Identifier'
163
+ ) {
164
+ element.right.name = `styles.${getCssMapKey(element.right.name)}`;
165
+ }
166
+ });
167
+ }
168
+ }
169
+ });
170
+
171
+ // replace style keys with their corresponding values from the map
172
+ source.find(j.ObjectProperty).forEach((path) => {
173
+ const key = path.node.key;
174
+ const value = path.node.value;
175
+
176
+ if (key.type === 'Identifier' && value.type === 'StringLiteral') {
177
+ const styleKey = value.value as keyof typeof styleMaps;
178
+ if (styleMaps[styleKey]) {
179
+ const mapValue = styleMaps[styleKey];
180
+ if (typeof mapValue === 'string' && mapValue.startsWith('var(')) {
181
+ // token call
182
+ j(path).replaceWith(
183
+ j.objectProperty(
184
+ key,
185
+ j.callExpression(j.identifier('token'), [j.stringLiteral(styleKey)]),
186
+ ),
187
+ );
188
+ } else {
189
+ // non-token call, just use the value
190
+ j(path).replaceWith(j.objectProperty(key, j.literal(mapValue)));
191
+ }
192
+ } else {
193
+ // copy the original object property and value when not in styleMaps
194
+ // e.g. color: 'red'
195
+ j(path).replaceWith(j.objectProperty(key, value));
196
+ }
197
+ }
198
+ });
199
+ }
200
+
201
+ // this only accounts for if the selector starts with a colon
202
+ // e.g. { ':hover': { ... } } -> { '&:hover': { ... } }
203
+ function ensureSelectorAmpersand(j: core.JSCodeshift, objectExpression: core.ObjectExpression) {
204
+ objectExpression.properties.forEach((property) => {
205
+ if (property.type === 'ObjectProperty' && property.key.type === 'StringLiteral') {
206
+ const key = property.key.value;
207
+ if (key.startsWith(':')) {
208
+ property.key = j.stringLiteral(`&${key}`);
209
+ }
210
+ }
211
+ });
212
+ return objectExpression;
213
+ }
214
+
215
+ function updateImports(j: core.JSCodeshift, source: ReturnType<typeof j>) {
216
+ // remove xcss import
217
+ source
218
+ .find(j.ImportDeclaration)
219
+ .filter((path: ASTPath<ImportDeclaration>) => path.node.source.value === '@atlaskit/primitives')
220
+ .forEach((path) => {
221
+ if (path.node.specifiers) {
222
+ path.node.specifiers = path.node.specifiers.filter(
223
+ (specifier) => specifier.local?.name !== 'xcss',
224
+ );
225
+ }
226
+ });
227
+
228
+ const existingImports = source.find(j.ImportDeclaration);
229
+
230
+ const hasCssMapImport = existingImports.some(
231
+ (path) => path.node.source.value === '@atlaskit/css',
232
+ );
233
+ if (!hasCssMapImport) {
234
+ const cssMapImport = j.importDeclaration(
235
+ [j.importSpecifier(j.identifier('cssMap'))],
236
+ j.literal('@atlaskit/css'),
237
+ );
238
+ source.get().node.program.body.unshift(cssMapImport);
239
+ }
240
+
241
+ const hasTokenImport = existingImports.some(
242
+ (path) => path.node.source.value === '@atlaskit/tokens',
243
+ );
244
+ if (!hasTokenImport) {
245
+ const tokenImport = j.importDeclaration(
246
+ [j.importSpecifier(j.identifier('token'))],
247
+ j.literal('@atlaskit/tokens'),
248
+ );
249
+ source.get().node.program.body.unshift(tokenImport);
250
+ }
251
+
252
+ // update existing @atlaskit/primitives imports to @atlaskit/primitives/compiled
253
+ // e.g. import { Box } from '@atlaskit/primitives' -> import { Box } from '@atlaskit/primitives/compiled'
254
+ source
255
+ .find(j.ImportDeclaration)
256
+ .filter((path: ASTPath<ImportDeclaration>) => path.node.source.value === '@atlaskit/primitives')
257
+ .forEach((path) => {
258
+ path.node.source.value = '@atlaskit/primitives/compiled';
259
+ });
260
+
261
+ const hasJsxImport = existingImports.some((path) => path.node.source.value === '@compiled/react');
262
+ if (!hasJsxImport) {
263
+ // check if there is `import { jsx } from '@emotion/react'`
264
+ // this should be replaced with `import { jsx } from '@compiled/react'`
265
+ const existingEmotionImport = source
266
+ .find(j.ImportDeclaration)
267
+ .filter((path: ASTPath<ImportDeclaration>) => path.node.source.value === '@emotion/react')
268
+ .find(j.ImportSpecifier)
269
+ .filter((path) => path.node.imported.name === 'jsx');
270
+
271
+ const jsxImport = j.importDeclaration(
272
+ [j.importSpecifier(j.identifier('jsx'))],
273
+ j.literal('@compiled/react'),
274
+ );
275
+
276
+ if (existingEmotionImport.size() > 0) {
277
+ // replace jsx import from `@emotion/react` with `@compiled/react`
278
+ existingEmotionImport.closest(j.ImportDeclaration).replaceWith(jsxImport);
279
+ } else {
280
+ // add the new import at the top of the file
281
+ source.get().node.program.body.unshift(jsxImport);
282
+ }
283
+ }
284
+
285
+ // sort import declarations alphabetically
286
+ // probably not necessary as we can rely on prettier on save
287
+ const allImports = source.find(j.ImportDeclaration).nodes();
288
+ allImports.sort((a, b) => {
289
+ if (
290
+ typeof a.source.value === 'undefined' ||
291
+ typeof b.source.value === 'undefined' ||
292
+ a.source.value === null ||
293
+ b.source.value === null
294
+ ) {
295
+ return 0;
296
+ }
297
+
298
+ return a.source.value > b.source.value ? 1 : -1;
299
+ });
300
+ source.get().node.program.body = [
301
+ ...allImports,
302
+ ...source
303
+ .get()
304
+ .node.program.body.filter((node: core.Node) => node.type !== 'ImportDeclaration'),
305
+ ];
306
+ }
307
+
308
+ // look for xcss import
309
+ function getImportSpecifier(j: core.JSCodeshift, source: any, specifier: string) {
310
+ const specifiers = source
311
+ .find(j.ImportDeclaration)
312
+ .filter((path: ASTPath<ImportDeclaration>) => path.node.source.value === '@atlaskit/primitives')
313
+ .find(j.ImportSpecifier)
314
+ .filter((path: any) => path.node.imported.name === specifier);
315
+
316
+ if (!specifiers.length) {
317
+ return null;
318
+ }
319
+ return specifiers.nodes()[0]!.local!.name;
320
+ }
321
+
322
+ /*
323
+ * Logic to determine the key for the cssMap object
324
+ * e.g. styles -> root, buttonStyles -> button
325
+ * We might want nicer logic in the future with some smarts/context
326
+ * about the element it's being applied to
327
+ * e.g. if the element just has one style, we should just use the key 'root'
328
+ */
329
+ function getCssMapKey(variableName: string): string {
330
+ if (variableName.toLowerCase() === 'styles') {
331
+ return 'root';
332
+ }
333
+ return variableName.replace(/Styles$/i, '');
334
+ }