Skip to content

Instantly share code, notes, and snippets.

@eps1lon
Last active May 11, 2021 08:44
Show Gist options
  • Save eps1lon/4326159c3968e74548b757cb9c69ccd2 to your computer and use it in GitHub Desktop.
Save eps1lon/4326159c3968e74548b757cb9c69ccd2 to your computer and use it in GitHub Desktop.
ensure component classes exist

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`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment