Requires node 16.
Don't forgot to run yarn prettier
after the script finishes.
Examples
$ node scripts/migrateComponentClasses.mjs
$ node scripts/migrateComponentClasses.mjs --dry --grep "AccordionSummary"
import * as fs from 'node:fs/promises'; | |
import * as path from 'node:path'; | |
import * as process from 'node:process'; | |
import glob from 'fast-glob'; | |
const dry = process.argv.slice(2).includes('--dry'); | |
const grep = process.argv.slice(2).includes('--grep') | |
? new RegExp(process.argv.slice(2)[process.argv.slice(2).indexOf('--grep') + 1]) | |
: null; | |
function lowerCaseFirst(s) { | |
return `${s[0].toLowerCase()}${s.slice(1)}`; | |
} | |
function upperCaseFirst(s) { | |
return `${s[0].toUpperCase()}${s.slice(1)}`; | |
} | |
async function migrateDeclaration(code, componentName, partialClasses) { | |
let migratedCode = code; | |
migratedCode = migratedCode.replace(partialClasses, `Partial<${componentName}Classes>`); | |
const declarationImportsMatch = code.match(/^import(.+?) from '.+?';/gm); | |
if (declarationImportsMatch === null) { | |
throw new Error('You need to implement this first :('); | |
} else { | |
const lastImport = declarationImportsMatch[declarationImportsMatch.length - 1]; | |
migratedCode = migratedCode.replace( | |
lastImport, | |
`${lastImport}\nimport { ${componentName}Classes } from './${lowerCaseFirst( | |
componentName, | |
)}Classes';`, | |
); | |
} | |
migratedCode = migratedCode.replace( | |
new RegExp(`export type ${componentName}ClassKey = .+?;\n\n`, 's'), | |
'', | |
); | |
return migratedCode; | |
} | |
async function migrateClassesToTypeScript(code, componentName, partialClasses) { | |
let migratedCode = code; | |
const requiredClasses = partialClasses.replace(/\?:/g, ':'); | |
const newClassesTypes = [ | |
`export interface ${componentName}Classes ${requiredClasses}`, | |
`export type ${componentName}ClassKey = keyof ${componentName}Classes;`, | |
].join('\n\n'); | |
const importsMatch = migratedCode.match(/^import(.+?) from '.+?';/gm); | |
if (importsMatch === null) { | |
throw new Error('You need to implement this first :('); | |
} else { | |
const lastImport = importsMatch[importsMatch.length - 1]; | |
migratedCode = migratedCode.replace(lastImport, `${lastImport}\n\n${newClassesTypes}`); | |
} | |
migratedCode = migratedCode.replace( | |
`get${componentName}UtilityClass(slot) {`, | |
`get${componentName}UtilityClass(slot: string): string {`, | |
); | |
migratedCode = migratedCode.replace( | |
`const ${lowerCaseFirst(componentName)}Classes = generateUtilityClasses`, | |
`const ${lowerCaseFirst( | |
componentName, | |
)}Classes: ${componentName}Classes = generateUtilityClasses`, | |
); | |
return migratedCode; | |
} | |
let files = await glob('packages/**/src/**/*Classes.js'); | |
files = files.filter((filePath) => { | |
const isMaterialUIStylesModule = /\/material-ui-styles\//.test(filePath); | |
return !isMaterialUIStylesModule; | |
}); | |
if (grep !== null) { | |
files = files.filter((filePath) => grep.test(filePath)); | |
} | |
const results = await Promise.allSettled( | |
files.map(async (classesPath) => { | |
const componentName = upperCaseFirst(path.parse(classesPath).name.replace(/Classes$/, '')); | |
const declarationPath = path.resolve(path.dirname(classesPath), `./${componentName}.d.ts`); | |
const [classesCode, declarationCode] = await Promise.all([ | |
fs.readFile(classesPath, { encoding: 'utf-8' }), | |
fs.readFile(declarationPath, { encoding: 'utf-8' }), | |
]); | |
let [migratedClasses, migratedDeclaration] = [classesCode, declarationCode]; | |
const classesPropMatch = declarationCode.match(/classes\?: ({\n(.+?)});/ms); | |
if (classesPropMatch === null) { | |
// already migrated | |
// console.warn(`Could not find classes in \n${declarationCode}`); | |
return false; | |
} | |
const partialClasses = classesPropMatch[1]; | |
migratedDeclaration = await migrateDeclaration( | |
migratedDeclaration, | |
componentName, | |
partialClasses, | |
); | |
migratedClasses = await migrateClassesToTypeScript( | |
migratedClasses, | |
componentName, | |
partialClasses, | |
); | |
if (dry) { | |
console.log(`-----<${componentName}>-----`); | |
console.log(migratedClasses); | |
console.log(migratedDeclaration); | |
console.log(`-----</${componentName}>-----`); | |
} else { | |
await Promise.all([ | |
fs.writeFile( | |
path.join(path.dirname(classesPath), `${path.basename(classesPath, '.js')}.ts`), | |
migratedClasses, | |
), | |
fs.unlink(classesPath), | |
fs | |
.unlink(path.join(path.dirname(classesPath), `${path.basename(classesPath, '.js')}.d.ts`)) | |
.catch(() => { | |
console.warn(`${componentName}: Unable to remove declaration for ${classesPath}`); | |
}), | |
fs.writeFile(declarationPath, migratedDeclaration), | |
]); | |
} | |
// Great Success! | |
return true; | |
}), | |
); | |
const errors = results | |
.map((result) => { | |
return result.reason; | |
}) | |
.filter((reason) => reason !== undefined); | |
// node 16 doesn't log AggregateError.prototype.errors | |
errors.forEach((error) => { | |
console.error(error); | |
}); | |
if (errors.length > 0) { | |
throw new AggregateError(errors, 'Failed at least one migration'); | |
} | |
const migratedComponents = results.filter((result) => { | |
return result.status === 'fulfilled' && result.value === true; | |
}); | |
console.log(`Migrated ${migratedComponents.length} components`); |