UNPKG

ember-codemod-remove-ember-css-modules

Version:

Codemod to replace ember-css-modules with embroider-css-modules

368 lines (367 loc) 16.5 kB
/* eslint-disable @typescript-eslint/ban-ts-comment */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { AST } from '@codemod-utils/ast-template'; import { createFiles } from '@codemod-utils/files'; function sanitizeClassAndLocalClassAttributes(file) { function removeAttributeWithoutValue(attributeName, attributes) { const attributeIndex = attributes.findIndex( // @ts-ignore: Assume that types from external packages are correct (attribute) => attribute.name === attributeName); if (attributeIndex === -1) { return; } const attribute = attributes[attributeIndex]; // @ts-ignore: Assume that types from external packages are correct if (attribute.isValueless) { attributes.splice(attributeIndex, 1); return; } // @ts-ignore: Assume that types from external packages are correct if (attribute.value.type !== 'TextNode') { return; } // @ts-ignore: Assume that types from external packages are correct const attributeValue = attribute.value.chars.trim(); if (attributeValue === '') { attributes.splice(attributeIndex, 1); return; } } const traverse = AST.traverse(); const ast = traverse(file, { ElementNode(node) { const { attributes } = node; removeAttributeWithoutValue('class', attributes); removeAttributeWithoutValue('local-class', attributes); }, }); return AST.print(ast); } function mergeClassAndLocalClassAttributes(file) { const traverse = AST.traverse(); const ast = traverse(file, { ElementNode(node) { // Check that both class and local-class attributes exist const { attributes } = node; const localClassAttributeIndex = attributes.findIndex((attribute) => attribute.name === 'local-class'); const classAttributeIndex = attributes.findIndex((attribute) => attribute.name === 'class'); if (localClassAttributeIndex === -1 || classAttributeIndex === -1) { return; } // Merge attributes only when both have TextNode values const localClassAttribute = attributes[localClassAttributeIndex]; const classAttribute = attributes[classAttributeIndex]; if (localClassAttribute.value.type !== 'TextNode' || classAttribute.value.type !== 'TextNode') { return; } const localClassAttributeValue = localClassAttribute.value.chars.trim(); const classAttributeValue = classAttribute.value.chars.trim(); // Update the class attribute const localClassNames = localClassAttributeValue.split(/\s+/); let params; if (localClassNames.length === 1) { params = [ AST.builders.path(`this.styles.${localClassNames[0]}`), AST.builders.string(` ${classAttributeValue}`), ]; } else { params = [ AST.builders.sexpr('local', [ AST.builders.path('this.styles'), ...localClassNames.map(AST.builders.string), ]), AST.builders.string(` ${classAttributeValue}`), ]; } attributes[classAttributeIndex].value = AST.builders.mustache(AST.builders.path('concat'), params); // Remove the local-class attribute attributes.splice(localClassAttributeIndex, 1); }, }); return AST.print(ast); } function removeLocalClassHelpers(file) { /* The {{local-class}} helper from ember-css-modules allows 1 positional argument. The argument's value is presumed to be a concatenated string or `undefined`. */ function canRemoveLocalClassHelper(path) { // @ts-ignore: Assume that types from external packages are correct const hasFromArgument = path.hash.pairs.some((pair) => pair.key === 'from'); if (hasFromArgument) { throw new RangeError( // @ts-ignore: Assume that types from external packages are correct `Unable to handle the {{local-class}} helper's \`from\` key. See lines ${path.loc.start.line}-${path.loc.end.line}.`); } // @ts-ignore: Assume that types from external packages are correct const param = path.params[0]; if (param === undefined) { return true; } if (param.type !== 'StringLiteral') { return false; } const value = param.value.trim(); return value === ''; } const traverse = AST.traverse(); const ast = traverse(file, { // @ts-ignore: Assume that types from external packages are correct AttrNode(node) { if (node.name !== 'class') { return; } const hasLocalClassHelper = node.value.type === 'MustacheStatement' && // @ts-ignore: Assume that types from external packages are correct node.value.path.original === 'local-class'; if (!hasLocalClassHelper) { return; } if (canRemoveLocalClassHelper(node.value)) { return null; } // @ts-ignore: Assume that types from external packages are correct const param = node.value.params[0]; if (param.type !== 'StringLiteral') { return; } const localClassNames = param.value.trim().split(/\s+/); if (localClassNames.length === 1) { node.value = AST.builders.mustache(AST.builders.path(`this.styles.${localClassNames[0]}`)); return; } node.value = AST.builders.mustache(AST.builders.path('local'), [ AST.builders.path('this.styles'), ...localClassNames.map(AST.builders.string), ]); }, MustacheStatement(node) { if (node.path.type !== 'PathExpression' || node.path.original !== 'local-class') { return; } const param = node.params[0]; if (param.type !== 'StringLiteral') { return; } const localClassNames = param.value.trim().split(/\s+/); if (localClassNames.length === 1) { return AST.builders.mustache(AST.builders.path(`this.styles.${localClassNames[0]}`)); } return AST.builders.mustache(AST.builders.path('local'), [ AST.builders.path('this.styles'), ...localClassNames.map(AST.builders.string), ]); }, SubExpression(node) { // @ts-ignore: Assume that types from external packages are correct const hasLocalClassHelper = node.path.original === 'local-class'; if (!hasLocalClassHelper) { return; } if (canRemoveLocalClassHelper(node)) { return AST.builders.string(''); } const param = node.params[0]; if (param.type !== 'StringLiteral') { return node; } const localClassNames = param.value.trim().split(/\s+/); if (localClassNames.length === 1) { return AST.builders.path(`this.styles.${localClassNames[0]}`); } return AST.builders.sexpr(AST.builders.path('local'), [ AST.builders.path('this.styles'), ...localClassNames.map(AST.builders.string), ]); }, }); return AST.print(ast); } function removeLocalClassAttributes(file) { function transformParam(param) { // @ts-ignore: Assume that types from external packages are correct switch (param.type) { case 'StringLiteral': { // @ts-ignore: Assume that types from external packages are correct const localClassNames = param.value.trim().split(/\s+/); if (localClassNames.length === 1) { // @ts-ignore: Assume that types from external packages are correct param.value = localClassNames[0]; } else { param = AST.builders.sexpr('array', localClassNames.map(AST.builders.string)); } break; } case 'SubExpression': { // @ts-ignore: Assume that types from external packages are correct switch (param.path.original) { case 'if': case 'unless': { // @ts-ignore: Assume that types from external packages are correct const subparams = param.params.map(transformParam); // @ts-ignore: Assume that types from external packages are correct param = AST.builders.sexpr(param.path.original, subparams); break; } } break; } } return param; } // @ts-ignore: Assume that types from external packages are correct function transformPart(part) { // @ts-ignore: Assume that types from external packages are correct switch (part.type) { case 'MustacheStatement': { // @ts-ignore: Assume that types from external packages are correct switch (part.path.original) { case 'concat': { function hasPathExpression(params) { // @ts-ignore: Assume that types from external packages are correct return params.some((param) => param.type === 'PathExpression'); } // @ts-ignore: Assume that types from external packages are correct if (hasPathExpression(part.params)) { return AST.builders.mustache(AST.builders.path('get'), [ AST.builders.path('this.styles'), // @ts-ignore: Assume that types from external packages are correct AST.builders.sexpr(part.path.original, part.params), ]); } // @ts-ignore: Assume that types from external packages are correct const params = part.params.map(transformParam); return AST.builders.mustache('local', [ AST.builders.path('this.styles'), ...params, ]); } case 'if': case 'unless': { // @ts-ignore: Assume that types from external packages are correct const params = part.params.map(transformParam); return AST.builders.mustache('local', [ AST.builders.path('this.styles'), // @ts-ignore: Assume that types from external packages are correct AST.builders.sexpr(part.path.original, params), ]); } default: { return AST.builders.mustache(AST.builders.path('get'), [ AST.builders.path('this.styles'), // @ts-ignore: Assume that types from external packages are correct part.path, ]); } } } case 'TextNode': { // @ts-ignore: Assume that types from external packages are correct const value = part.chars.trim(); if (value === '') { return part; } const localClassNames = value.split(/\s+/); if (localClassNames.length === 1) { return AST.builders.mustache(AST.builders.path(`this.styles.${localClassNames[0]}`)); } return AST.builders.mustache(AST.builders.path('local'), [ AST.builders.path('this.styles'), ...localClassNames.map(AST.builders.string), ]); } } } function transformParts(parts) { const numParts = parts.length; return parts.reduce((accumulator, part, index) => { // @ts-ignore: Assume that types from external packages are correct accumulator.push(transformPart(part)); if (index < numParts - 1) { // @ts-ignore: Assume that types from external packages are correct accumulator.push(AST.builders.text(' ')); } return accumulator; }, []); } const traverse = AST.traverse(); const ast = traverse(file, { ElementNode(node) { // Check if the local-class attribute (still) exists const { attributes } = node; const localClassAttributeIndex = attributes.findIndex((attribute) => attribute.name === 'local-class'); if (localClassAttributeIndex === -1) { return; } // Change the local-class attribute to class const localClassAttribute = attributes[localClassAttributeIndex]; switch (localClassAttribute.value.type) { case 'ConcatStatement': { localClassAttribute.name = 'class'; // @ts-ignore: Assume that types from external packages are correct localClassAttribute.value.parts = transformParts(localClassAttribute.value.parts); break; } case 'MustacheStatement': { localClassAttribute.name = 'class'; // @ts-ignore: Assume that types from external packages are correct localClassAttribute.value = transformPart(localClassAttribute.value); break; } case 'TextNode': { localClassAttribute.name = 'class'; // @ts-ignore: Assume that types from external packages are correct localClassAttribute.value = transformPart(localClassAttribute.value); break; } } }, HashPair(node) { if (node.key !== 'local-class') { return; } node.key = 'class'; const newValue = transformParam(node.value); // @ts-ignore: Assume that types from external packages are correct if (newValue.type === 'StringLiteral') { node.value = AST.builders.path( // @ts-ignore: Assume that types from external packages are correct `this.styles.${newValue.value}`); return; } node.value = AST.builders.sexpr('local', [ AST.builders.path('this.styles'), // @ts-ignore: Assume that types from external packages are correct newValue, ]); }, }); return AST.print(ast); } export function updateTemplate(entityName, { customizations, options }) { const { getFilePath } = customizations; const { projectRoot } = options; const filePath = getFilePath(entityName); try { let file = readFileSync(join(projectRoot, filePath), 'utf8'); file = sanitizeClassAndLocalClassAttributes(file); file = mergeClassAndLocalClassAttributes(file); file = removeLocalClassHelpers(file); file = removeLocalClassAttributes(file); const fileMap = new Map([[filePath, file]]); createFiles(fileMap, options); } catch (error) { let message = `WARNING: updateTemplate could not update \`${filePath}\`. Please update the file manually.`; if (error instanceof Error) { message += ` (${error.message})`; } console.warn(`${message}\n`); } }