Skip to content

Instantly share code, notes, and snippets.

@dbrxnds
Last active May 6, 2025 10:16
Show Gist options
  • Save dbrxnds/158a08df0561548a2eff01bf53f69309 to your computer and use it in GitHub Desktop.
Save dbrxnds/158a08df0561548a2eff01bf53f69309 to your computer and use it in GitHub Desktop.
Mantine v6 to v7 - createStyles to CSS modules basic codemod
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