import type { API, ASTPath, default as core, FileInfo, ImportDeclaration, Options, } from 'jscodeshift'; import { backgroundColorMap, borderColorMap, borderRadiusMap, borderWidthMap, dimensionMap, fillMap, fontFamilyMap, fontMap, fontWeightMap, layerMap, negativeSpaceMap, opacityMap, positiveSpaceMap, shadowMap, surfaceColorMap, textColorMap, textWeightMap, } from './style-maps.partial'; const styleMaps = { ...backgroundColorMap, ...borderColorMap, ...borderRadiusMap, ...borderWidthMap, ...dimensionMap, ...fillMap, ...fontFamilyMap, ...fontMap, ...fontWeightMap, ...layerMap, ...negativeSpaceMap, ...opacityMap, ...positiveSpaceMap, ...shadowMap, ...surfaceColorMap, ...textColorMap, ...textWeightMap, }; export default function transformer( fileInfo: FileInfo, { jscodeshift: j }: API, _options: Options, ): string | undefined { const base = j(fileInfo.source); // replace xcss with cssMap const xcssSpecifier = getImportSpecifier(j, base, 'xcss'); if (!xcssSpecifier) { return; } replaceXcssWithCssMap(j, base, xcssSpecifier); updateImports(j, base); addJsxPragma(j, base); return base.toSource(); } function addJsxPragma(j: core.JSCodeshift, source: ReturnType) { const jsxPragma = '*\n * @jsxRuntime classic\n * @jsx jsx\n '; // extract all comments, not just root node const allComments = source.find(j.Comment).nodes(); const hasJsxPragma = allComments.some((comment) => { const value = comment.value; return /@jsxRuntime\s+classic/.test(value) && /@jsx\s+jsx/.test(value); }); if (!hasJsxPragma) { // create new comment block for the jsx pragma const pragmaComment = j.commentBlock(jsxPragma, true, false); // insert at the top of the file const rootNode = source.get().node; if (!rootNode.comments) { rootNode.comments = []; } rootNode.comments.unshift(pragmaComment); } } function replaceXcssWithCssMap( j: core.JSCodeshift, source: ReturnType, specifier: string, ) { const cssMapProperties: core.ObjectProperty[] = []; let firstXcssPath: ASTPath | null = null; let firstUsagePath: ASTPath | null = null; const styleVariables = new Set(); const variableDependencies = new Map>(); const inlineXcssUsages: Array<{ path: ASTPath; keyName: string; jsxAttribute: ASTPath; componentName: string; variableName: string; }> = []; // First pass: collect all xcss variable declarations source .find(j.CallExpression, { // get all xcss calls callee: { type: 'Identifier', name: specifier, }, }) .forEach((path) => { const args = path.node.arguments; if (args.length === 1 && args[0].type === 'ObjectExpression') { // get the parent variable declaration // e.g. const buttonStyles = xcss({ color: 'red' }); const parentVariableDeclaration = path.parentPath?.parentPath?.parentPath?.node; if (parentVariableDeclaration && parentVariableDeclaration.type === 'VariableDeclaration') { const variableDeclarator = parentVariableDeclaration.declarations.find( (declaration: core.VariableDeclarator) => declaration.init === path.node, ); if (variableDeclarator && variableDeclarator.type === 'VariableDeclarator') { const variableName = variableDeclarator.id.name; // e.g. buttonStyles styleVariables.add(variableName); // find dependencies in the xcss object const dependencies = new Set(); if (args[0].type === 'ObjectExpression') { args[0].properties.forEach((prop) => { if (prop.type === 'ObjectProperty' && prop.value.type === 'TemplateLiteral') { prop.value.expressions.forEach((expr) => { if (expr.type === 'Identifier') { dependencies.add(expr.name); } }); } }); } if (dependencies.size > 0) { variableDependencies.set(variableName, dependencies); } } } else { // This is an inline xcss call - check if it's inside a JSX attribute let current = path.parentPath; let jsxElement: ASTPath | null = null; while (current) { if ( current.node.type === 'JSXAttribute' && current.node.name.type === 'JSXIdentifier' && current.node.name.name === 'xcss' ) { // Found inline xcss usage - now find the JSX element let elementCurrent = current.parentPath; while (elementCurrent) { if (elementCurrent.node.type === 'JSXElement') { jsxElement = elementCurrent as ASTPath; break; } elementCurrent = elementCurrent.parentPath; } if (jsxElement && jsxElement.node.openingElement.name.type === 'JSXIdentifier') { const componentName = jsxElement.node.openingElement.name.name; const variableName = `${componentName.toLowerCase()}Styles`; const keyName = 'root'; inlineXcssUsages.push({ path, keyName, jsxAttribute: current as ASTPath, componentName, variableName, }); } break; } current = current.parentPath; } } } }); // find the first usage of any style variable or inline xcss source.find(j.Identifier).forEach((path) => { if ( styleVariables.has(path.node.name) && (!firstUsagePath || (path.node.loc?.start && firstUsagePath.node.loc?.start && (path.node.loc.start.line < firstUsagePath.node.loc.start.line || (path.node.loc.start.line === firstUsagePath.node.loc.start.line && path.node.loc.start.column < firstUsagePath.node.loc.start.column)))) ) { firstUsagePath = path; } }); // Check for inline xcss usage as well for first usage if ( inlineXcssUsages.length > 0 && (!firstUsagePath || inlineXcssUsages[0].path.node.loc?.start) ) { firstUsagePath = inlineXcssUsages[0].path; } // Process variable xcss declarations source .find(j.CallExpression, { callee: { type: 'Identifier', name: specifier, }, }) .forEach((path) => { const args = path.node.arguments; // only process xcss calls that have a single object argument if (args.length === 1 && args[0].type === 'ObjectExpression') { // get the parent variable declaration that contains this xcss call // e.g. const buttonStyles = xcss({...}) const parentVariableDeclaration = path.parentPath?.parentPath?.parentPath?.node; if (parentVariableDeclaration && parentVariableDeclaration.type === 'VariableDeclaration') { // find the variable declarator that initialises with this xcss call // e.g. const buttonStyles = xcss({ color: 'red' }); const variableDeclarator = parentVariableDeclaration.declarations.find( (declaration: core.VariableDeclarator) => declaration.init === path.node, ); if (variableDeclarator && variableDeclarator.type === 'VariableDeclarator') { // convert the variable name to a cssMap key (e.g. myStyles -> root) const variableName = variableDeclarator.id.name; const key = getCssMapKey(variableName); // create a new cssMap property with the key and the processed styles const cssMapObject = j.objectProperty( j.identifier(key), ensureSelectorAmpersand(j, args[0]), ); cssMapProperties.push(cssMapObject); // track the first xcss call if (!firstXcssPath) { firstXcssPath = path; } // remove the original xcss variable declaration since we'll replace it with cssMap j(path.parentPath.parentPath.parentPath).remove(); } } } }); // Process inline xcss usages - create separate cssMap for each const cssMapDeclarations: core.VariableDeclaration[] = []; inlineXcssUsages.forEach(({ path, keyName, jsxAttribute, variableName }) => { const args = path.node.arguments; if (args.length === 1 && args[0].type === 'ObjectExpression') { // Create separate cssMap declaration for each inline xcss const cssMapObject = j.objectProperty( j.identifier(keyName), ensureSelectorAmpersand(j, args[0]), ); const cssMapVariableDeclaration = j.variableDeclaration('const', [ j.variableDeclarator( j.identifier(variableName), j.callExpression(j.identifier('cssMap'), [j.objectExpression([cssMapObject])]), ), ]); cssMapDeclarations.push(cssMapVariableDeclaration); // Update the JSX attribute to use the cssMap reference jsxAttribute.node.value = j.jsxExpressionContainer( j.memberExpression(j.identifier(variableName), j.identifier(keyName)), ); // Track first xcss call if not already set if (!firstXcssPath) { firstXcssPath = path; } } }); // create new cssMap with the combined xcss object properties (for variable declarations) if (cssMapProperties.length > 0) { const cssMapObject = j.objectExpression(cssMapProperties); const cssMapVariableDeclaration = j.variableDeclaration('const', [ j.variableDeclarator( j.identifier('styles'), j.callExpression(j.identifier('cssMap'), [cssMapObject]), ), ]); cssMapDeclarations.unshift(cssMapVariableDeclaration); } // Insert all cssMap declarations if (cssMapDeclarations.length > 0 && firstUsagePath) { const programBody = source.get().node.program.body; let insertIndex = 0; // find the last variable declaration that any style depends on let lastDependencyIndex = -1; for (let i = 0; i < programBody.length; i++) { const node = programBody[i]; if (node.type === 'VariableDeclaration') { const varName = node.declarations[0]?.id.name; if (varName) { // check if this variable is a dependency of any style for (const [, deps] of variableDependencies.entries()) { if (deps.has(varName)) { lastDependencyIndex = i; break; } } } } } // insert after the last dependency if (lastDependencyIndex !== -1) { insertIndex = lastDependencyIndex + 1; } else { // if no dependencies, find the first non-import/type declaration insertIndex = programBody.findIndex( (node: { type: string }) => node.type !== 'ImportDeclaration' && node.type !== 'TypeAlias' && node.type !== 'InterfaceDeclaration', ); } programBody.splice(insertIndex, 0, ...cssMapDeclarations); } // First handle backgroundColor transformation for Box components source .find(j.JSXAttribute, { name: { type: 'JSXIdentifier', name: 'xcss', }, }) .forEach((path) => { const value = path.node.value; if (value && value.type === 'JSXExpressionContainer') { const expression = value.expression; const parentElement = path.parentPath?.node; const isBoxComponent = parentElement?.name?.name === 'Box'; if (isBoxComponent) { if (expression.type === 'Identifier') { const styleKey = getCssMapKey(expression.name); const styleObject = cssMapProperties.find( (prop) => prop.key.type === 'Identifier' && prop.key.name === styleKey, )?.value; if (styleObject?.type === 'ObjectExpression') { const backgroundColorProp = styleObject.properties.find( (prop) => prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === 'backgroundColor', ); if ( backgroundColorProp?.type === 'ObjectProperty' && backgroundColorProp.value.type === 'StringLiteral' ) { // add backgroundColor prop to Box component parentElement.attributes.push( j.jsxAttribute( j.jsxIdentifier('backgroundColor'), j.stringLiteral(backgroundColorProp.value.value), ), ); // remove backgroundColor from cssMap styleObject.properties = styleObject.properties.filter( (prop) => !( prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === 'backgroundColor' ), ); } } } else if (expression.type === 'ArrayExpression') { // handle array of styles const backgroundColorValues: string[] = []; expression.elements.forEach((element) => { if (element?.type === 'Identifier') { const styleKey = getCssMapKey(element.name); const styleObject = cssMapProperties.find( (prop) => prop.key.type === 'Identifier' && prop.key.name === styleKey, )?.value; if (styleObject?.type === 'ObjectExpression') { const backgroundColorProp = styleObject.properties.find( (prop) => prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === 'backgroundColor', ); if ( backgroundColorProp?.type === 'ObjectProperty' && backgroundColorProp.value.type === 'StringLiteral' ) { backgroundColorValues.push(backgroundColorProp.value.value); // remove backgroundColor from cssMap styleObject.properties = styleObject.properties.filter( (prop) => !( prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === 'backgroundColor' ), ); } } } }); // if we found backgroundColor values, add to Box component if (backgroundColorValues.length > 0) { if (backgroundColorValues.length === 1) { // single backgroundColor value parentElement.attributes.push( j.jsxAttribute( j.jsxIdentifier('backgroundColor'), j.stringLiteral(backgroundColorValues[0]), ), ); } else { // multiple backgroundColor values - use ternary for conditional const conditions = expression.elements .map((element, index) => { if ( element?.type === 'LogicalExpression' && element.left.type === 'Identifier' ) { return `${element.left.name} ? "${backgroundColorValues[index]}" :`; } return `"${backgroundColorValues[index]}"`; }) .join(' '); parentElement.attributes.push( j.jsxAttribute( j.jsxIdentifier('backgroundColor'), j.jsxExpressionContainer(j.identifier(conditions)), ), ); } } } } } }); // update the xcss prop references to use the new cssMap object source .find(j.JSXAttribute, { name: { type: 'JSXIdentifier', name: 'xcss', }, }) .forEach((path) => { const value = path.node.value; // e.g. if (value && value.type === 'JSXExpressionContainer') { const expression = value.expression; if (expression.type === 'Identifier') { if (styleVariables.has(expression.name)) { // => path.node.value = j.jsxExpressionContainer( j.memberExpression( j.identifier('styles'), j.identifier(getCssMapKey(expression.name)), ), ); } } else if (expression.type === 'ArrayExpression') { // => expression.elements.forEach((element) => { if (element?.type === 'Identifier' && styleVariables.has(element.name)) { const memberExpression = j.memberExpression( j.identifier('styles'), j.identifier(getCssMapKey(element.name)), ); // Replace the identifier with the member expression Object.assign(element, memberExpression); } else if ( // => element?.type === 'LogicalExpression' && element.right.type === 'Identifier' && styleVariables.has(element.right.name) ) { element.right = j.memberExpression( j.identifier('styles'), j.identifier(getCssMapKey(element.right.name)), ); } }); } } }); // replace style keys with their corresponding values from the map source.find(j.ObjectProperty).forEach((path) => { const key = path.node.key; const value = path.node.value; if (key.type === 'Identifier' && value.type === 'StringLiteral') { const styleKey = value.value as keyof typeof styleMaps; if (styleMaps[styleKey]) { const mapValue = styleMaps[styleKey]; if (typeof mapValue === 'string' && mapValue.startsWith('var(')) { // token call j(path).replaceWith( j.objectProperty( key, j.callExpression(j.identifier('token'), [j.stringLiteral(styleKey)]), ), ); } else { // non-token call, just use the value j(path).replaceWith(j.objectProperty(key, j.literal(mapValue))); } } else { // copy the original object property and value when not in styleMaps // e.g. color: 'red' j(path).replaceWith(j.objectProperty(key, value)); } } }); } // this only accounts for if the selector starts with a colon // e.g. { ':hover': { ... } } -> { '&:hover': { ... } } function ensureSelectorAmpersand(j: core.JSCodeshift, objectExpression: core.ObjectExpression) { objectExpression.properties.forEach((property) => { if (property.type === 'ObjectProperty' && property.key.type === 'StringLiteral') { const key = property.key.value; if (key.startsWith(':')) { property.key = j.stringLiteral(`&${key}`); } } }); return objectExpression; } function updateImports(j: core.JSCodeshift, source: ReturnType) { // remove xcss import and collect primitives to import const primitivesToImport = new Set(); source .find(j.ImportDeclaration) .filter((path: ASTPath) => { const importSource = path.node.source.value as string; return ( importSource === '@atlaskit/primitives' || importSource.startsWith('@atlaskit/primitives/') ); }) .forEach((path) => { if (path.node.specifiers) { path.node.specifiers.forEach((specifier) => { if (specifier.type === 'ImportSpecifier' && specifier.imported) { const importedName = specifier.imported.name; if (importedName !== 'xcss') { primitivesToImport.add(importedName); } } else if (specifier.type === 'ImportDefaultSpecifier') { // handle deep imports like `import Anchor from '@atlaskit/primitives/anchor'` if (specifier.local) { primitivesToImport.add(specifier.local.name); } } }); } // remove the import declaration j(path).remove(); }); const importsNeeded = { cssMap: false, jsx: false, token: false, }; // check existing imports source.find(j.ImportDeclaration).forEach((path) => { const importSource = path.node.source.value as string; switch (importSource) { case '@atlaskit/css': path.node.specifiers?.forEach((specifier) => { if (specifier.local?.name === 'cssMap') { importsNeeded.cssMap = true; } if (specifier.local?.name === 'jsx') { importsNeeded.jsx = true; } }); break; case '@atlaskit/tokens': importsNeeded.token = true; break; case '@emotion/react': // remove the jsx import from @emotion/react path.node.specifiers = path.node.specifiers?.filter( (specifier) => !(j.ImportSpecifier.check(specifier) && specifier.imported.name === 'jsx'), ); if (path.node.specifiers?.length === 0) { j(path).remove(); } break; case 'react': // remove default import React from 'react' if not needed path.node.specifiers = path.node.specifiers?.filter( (specifier) => !(specifier.type === 'ImportDefaultSpecifier' && specifier.local?.name === 'React'), ); if (path.node.specifiers?.length === 0) { j(path).remove(); } break; } }); const newImports: ImportDeclaration[] = []; // add grouped import for primitives // e.g. import { Anchor, Box } from '@atlaskit/primitives/compiled'; if (primitivesToImport.size > 0) { const primitivesImport = j.importDeclaration( Array.from(primitivesToImport) .sort() .map((name) => j.importSpecifier(j.identifier(name))), j.literal('@atlaskit/primitives/compiled'), ); newImports.push(primitivesImport); } // add cssMap and jsx import if needed if (!importsNeeded.cssMap || !importsNeeded.jsx) { const existingCssImports = source .find(j.ImportDeclaration) .filter((path) => path.node.source.value === '@atlaskit/css'); const newSpecifiers: core.ImportSpecifier[] = []; if (!importsNeeded.cssMap) { newSpecifiers.push(j.importSpecifier(j.identifier('cssMap'))); } if (!importsNeeded.jsx) { newSpecifiers.push(j.importSpecifier(j.identifier('jsx'))); } if (existingCssImports.size() > 0) { // existing import from '@atlaskit/css' const existingCssImport = existingCssImports.at(0).get(); if (existingCssImport && existingCssImport.node.specifiers) { existingCssImport.node.specifiers.push(...newSpecifiers); } } else { const cssMapImport = j.importDeclaration(newSpecifiers, j.literal('@atlaskit/css')); newImports.push(cssMapImport); } } // add token import if (!importsNeeded.token) { let tokenUsed = false; // check if token is used in the transformed code source .find(j.CallExpression, { callee: { type: 'Identifier', name: 'token', }, }) .forEach(() => { tokenUsed = true; }); if (tokenUsed) { const tokenImport = j.importDeclaration( [j.importSpecifier(j.identifier('token'))], j.literal('@atlaskit/tokens'), ); newImports.push(tokenImport); } } // add new imports after any existing comments const rootNode = source.get().node; const firstNonCommentIndex = rootNode.program.body.findIndex( (node: core.Node) => node.type !== 'ImportDeclaration' && node.type !== 'CommentBlock' && node.type !== 'CommentLine', ); rootNode.program.body.splice(firstNonCommentIndex, 0, ...newImports); } // look for xcss import function getImportSpecifier(j: core.JSCodeshift, source: any, specifier: string) { const specifiers = source .find(j.ImportDeclaration) .filter((path: ASTPath) => path.node.source.value === '@atlaskit/primitives') .find(j.ImportSpecifier) .filter((path: any) => path.node.imported.name === specifier); if (!specifiers.length) { return null; } return specifiers.nodes()[0]!.local!.name; } /* * Logic to determine the key for the cssMap object * e.g. styles -> root, buttonStyles -> button * We might want nicer logic in the future with some smarts/context * about the element it's being applied to * e.g. if the element just has one style, we should just use the key 'root' */ function getCssMapKey(variableName: string): string { if (variableName.toLowerCase() === 'styles') { return 'root'; } return variableName.replace(/Styles$/i, ''); }