Last active
May 6, 2025 10:16
-
-
Save dbrxnds/158a08df0561548a2eff01bf53f69309 to your computer and use it in GitHub Desktop.
Mantine v6 to v7 - createStyles to CSS modules basic codemod
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { API, Collection, FileInfo, JSCodeshift } from "jscodeshift" | |
import fs from "fs" | |
export default function transformer(file: FileInfo, api: API) { | |
const j = api.jscodeshift | |
const source = j(file.source) | |
const createStylesCalls = j(file.source).find(j.CallExpression, { | |
callee: { | |
name: "createStyles", | |
}, | |
}) | |
createStylesCalls.map((path) => { | |
const stylesArg = path.value.arguments[0] | |
if (!stylesArg) { | |
return | |
} | |
let styles | |
if (stylesArg.type === "ObjectExpression") { | |
styles = j(stylesArg).toSource().slice(1, -1) | |
} | |
if (stylesArg.type === "ArrowFunctionExpression" || stylesArg.type === "FunctionExpression") { | |
const fnBody = j(stylesArg.body).toSource() | |
if (fnBody.startsWith("{")) return | |
styles = j(stylesArg.body).toSource().slice(2, -2) | |
} | |
if (!styles) { | |
return | |
} | |
const cssModuleStyles = convertToCssModules(styles) | |
// Create a file with the same name except for extension .module.css | |
const newFileName = file.path.replace(".tsx", ".module.css") | |
// Write the new file | |
fs.writeFileSync(newFileName, cssModuleStyles) | |
// Add import statement to the top of the file | |
const importStatement = `import classes from '${file.path.replace(".tsx", ".module.css")}'` | |
source.find(j.ImportDeclaration).at(0).insertBefore(importStatement) | |
}) | |
removeOldMantineImprots(source, j) | |
removeCreateStylesVariablesAndReferences(source, j) | |
return source.toSource() | |
} | |
function removeCreateStylesVariablesAndReferences(source: Collection<any>, j: JSCodeshift) { | |
const createStylesVariables = source.find(j.VariableDeclarator, { | |
init: { | |
type: "CallExpression", | |
callee: { | |
name: "createStyles", | |
}, | |
}, | |
}) | |
const variableNames = createStylesVariables.nodes().map((node) => node.id.name) | |
// Step 2: Find all instances where these variable names are used and remove them. | |
variableNames.forEach((variableName) => { | |
source.find(j.Identifier, { name: variableName }).forEach((path) => { | |
if ( | |
path.parentPath.node.type === "TSTypeQuery" && | |
path.parentPath.parentPath.node.type === "TSQualifiedName" | |
) { | |
const originalSource = j(path.parentPath).toSource() | |
const modifiedSource = originalSource.replace(`typeof ${variableName}`, "") | |
j(path.parentPath).replaceWith(modifiedSource) | |
} else if (path.parentPath.node.type !== "TSTypeQuery") { | |
j(path).closest(j.Statement).remove() | |
} | |
}) | |
}) | |
// Step 3: Remove the VariableDeclarators where `createStyles` is called. | |
createStylesVariables.remove() | |
} | |
function removeOldMantineImprots(source: Collection<any>, j: JSCodeshift) { | |
const imports = source.find(j.ImportDeclaration, { | |
source: { value: "@mantine/core" }, | |
}) | |
// Iterate through each import declaration | |
imports.forEach((path) => { | |
// Filter out the named imports except 'createStyles' | |
const specifiers = path.value.specifiers?.filter( | |
(specifier) => | |
!["createStyles", "DefaultProps", "Selectors"].includes(specifier.imported.name), | |
) | |
// Update the import declaration with the filtered specifiers | |
path.value.specifiers = specifiers | |
// If there are no specifiers left, remove the import declaration altogether | |
if (specifiers.length === 0) { | |
j(path).remove() | |
} | |
}) | |
} | |
function convertToCssModules(jsCode: string): string { | |
const lines = jsCode.split("\n") | |
const result: string[] = [] | |
for (let line of lines) { | |
// if (line.includes(': {')) { | |
// // Remove the colon ":" | |
// line = line.replace(':', ''); | |
// } | |
// if (line.includes('},')) { | |
// // Remove the comma "," | |
// // result.push(line.replace(',', '')); | |
// line = line.replace(',', ''); | |
// } | |
if (line.startsWith(" ")) { | |
// Remove the leading spaces | |
line = line.slice(2) | |
} | |
const indent = line.indexOf(line.trim()) | |
line = line.replaceAll(": {", " {") | |
line = line.replaceAll("},", "}") | |
// If line starts with a letter, add a period to make it a class name | |
if (/^[a-zA-Z]/.test(line)) { | |
line = "." + line | |
} | |
if (line.includes("{")) { | |
// Remove quotes around selectors (i.e., "'&:hover'") | |
line = line.replaceAll("'", "") | |
} | |
if (line.endsWith(",")) { | |
line = line.slice(0, -1) + ";" | |
} | |
if (line.includes(":") && line.endsWith(";")) { | |
line = line.slice(0, -1) | |
const colonIndex = line.indexOf(":") | |
const key = line.substring(0, colonIndex).trim() | |
const value = line.substring(colonIndex + 1).trim() | |
// const [key, value] = line.split(':'); | |
const newKey = camelToDash(key.trim()) | |
const newValue = convertCssValue(value.trim()) | |
line = `${" ".repeat(indent)}${newKey}: ${newValue};` | |
} | |
result.push(line) | |
} | |
// return convertCssValue(jsCode); | |
return result.join("\n") | |
} | |
function camelToDash(str: string): string { | |
return str.replace(/([a-zA-Z])(?=[A-Z])/g, "$1-").toLowerCase() | |
} | |
function convertCssValue(input: string): string { | |
// + " background-color: var(--mantine-colorScheme === 'dark' ? theme-color-dark-6 : theme-colors-gray-0);n" + | |
// + " color: var(--mantine-colorScheme === 'dark' ? theme-white : theme-black);n" + | |
// - ' background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));n' + | |
// - ' color: light-dark(var(--mantine-color-black), var(--mantine-color-white));n' + | |
if ( | |
(input.startsWith("'") && input.endsWith("'")) || | |
(input.startsWith('"') && input.endsWith('"')) | |
) { | |
input = input.slice(1, -1) | |
} | |
if (input.startsWith("`") && input.endsWith("`")) { | |
input = input.slice(1, -1) | |
} | |
if (input.includes("${")) { | |
input = input.replaceAll("${", "") | |
input = input.replaceAll("}", "") | |
} | |
// Use regex to search for pattern: | |
// --mantine-colorScheme === 'dark' ? (.+) : (.+) | |
// and replace with: | |
// light-dark($1, $2) | |
input = input.replaceAll( | |
/theme.colorScheme === ['"]dark['"] \? (.+) : (.+)/g, | |
"light-dark($2, $1)", | |
) | |
input = input.replaceAll(/colorScheme === ['"]dark['"] \? (.+) : (.+)/g, "light-dark($2, $1)") | |
// theme.colors.red[5] -> var(--mantine-color-red-5) | |
input = input.replaceAll(/theme\.colors\.(\w+)\[(\d+)\]/g, "var(--mantine-color-$1-$2)") | |
input = input.replaceAll(/colors\.(\w+)\[(\d+)\]/g, "var(--mantine-color-$1-$2)") | |
// theme.white | |
// var(--mantine-color-white) | |
input = input.replaceAll("theme.white", "var(--mantine-color-white)") | |
input = input.replaceAll("white", "var(--mantine-color-white)") | |
// theme.black | |
// var(--mantine-color-black) | |
input = input.replaceAll("theme.black", "var(--mantine-color-black)") | |
input = input.replaceAll("black", "var(--mantine-color-black)") | |
input = input.replaceAll(/theme\.fontSizes\.(\w+)/g, "var(--mantine-font-size-$1)") | |
input = input.replaceAll(/fontSizes\.(\w+)/g, "var(--mantine-font-size-$1)") | |
input = input.replaceAll(/theme\.spacing\.(\w+)/g, "var(--mantine-spacing-$1)") | |
input = input.replaceAll(/spacing\.(\w+)/g, "var(--mantine-spacing-$1)") | |
input = input.replaceAll(/theme\.radius\.(\w+)/g, "var(--mantine-radius-$1)") | |
input = input.replaceAll(/radius\.(\w+)/g, "var(--mantine-radius-$1)") | |
// If the value is just an integer, add "px" | |
if (/^\d+$/.test(input)) { | |
input += "px" | |
} | |
return input | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment