From e66fcfb25b4cafeee738e54bf9bc314d2d15a23a Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Fri, 17 Sep 2021 15:19:29 +0300 Subject: [PATCH] fix: edge case fixes based on codemod application --- .../globalCssToCssModule.test.ts.snap | 78 ++++++++++--------- .../globalCssToCssModule.ts | 10 +-- .../transformFileToCssModule.test.ts | 18 +++-- .../postcss/exportNameMapPrefixes.ts | 4 +- .../postcss/postcssToCssModulePlugin.ts | 38 +++++++-- .../getClassNameNodeReplacement.test.ts | 11 ++- .../processNodesWithClassName.test.ts | 1 + .../ts/__tests__/splitClassName.test.ts | 10 ++- .../ts/getClassNameNodeReplacement.ts | 70 ++++++++++++++--- .../ts/getNodesWithClassName.ts | 6 +- .../ts/processNodesWithClassName.ts | 33 +++++--- .../globalCssToCssModule/ts/splitClassName.ts | 13 +++- .../ts/transformComponentFile.ts | 39 ++++++++++ src/utils/formatWithStylelint.ts | 8 ++ 14 files changed, 249 insertions(+), 90 deletions(-) create mode 100644 src/transforms/globalCssToCssModule/ts/transformComponentFile.ts diff --git a/src/transforms/globalCssToCssModule/__tests__/__snapshots__/globalCssToCssModule.test.ts.snap b/src/transforms/globalCssToCssModule/__tests__/__snapshots__/globalCssToCssModule.test.ts.snap index 715f6a50..134d8efb 100644 --- a/src/transforms/globalCssToCssModule/__tests__/__snapshots__/globalCssToCssModule.test.ts.snap +++ b/src/transforms/globalCssToCssModule/__tests__/__snapshots__/globalCssToCssModule.test.ts.snap @@ -48,6 +48,7 @@ exports[`globalCssToCssModule transforms correctly 1`] = ` :global(.copy-link-action) { opacity: 0; } + &:hover, &:focus-within { :global(.copy-link-action) { @@ -55,30 +56,26 @@ exports[`globalCssToCssModule transforms correctly 1`] = ` } } } -.spacer { - flex: 1 1 0; -} -/* &__icon-chevron { */ -.icon-chevron { - opacity: 0.6; - margin: auto; -} -.alert { - margin: 0 0.25rem; - padding: 0.125rem 0.25rem; - cursor: default; - user-select: none; +.logo { + display: flex; } -/* &__action { */ -.action { - margin: 0.5rem 0.625rem 0.5rem 0; - padding: 0.25rem; +/* &__action-list-item { */ +.action-list-item { + /* Have a small gap between buttons so they are visually distinct when pressed */ + /* stylelint-disable-next-line declaration-property-unit-whitelist */ + margin-left: 1px; - :global(.theme-light) & { - margin-top: 0; - margin-bottom: 0; + /* &:hover { */ + &:hover { + /* background: var(--color-bg-1); */ + background: var(--color-bg-1); + + /* .theme-light & { */ + :global(.theme-light) & { + background: inherit; + } } } @@ -94,31 +91,38 @@ exports[`globalCssToCssModule transforms correctly 1`] = ` } } -/* &__action-list-item { */ -.action-list-item { - /* Have a small gap between buttons so they are visually distinct when pressed */ - /* stylelint-disable-next-line declaration-property-unit-whitelist */ - margin-left: 1px; - - /* &:hover { */ - &:hover { - /* background: var(--color-bg-1); */ - background: var(--color-bg-1); +/* &__action { */ +.action { + margin: 0.5rem 0.625rem 0.5rem 0; + padding: 0.25rem; - /* .theme-light & { */ - :global(.theme-light) & { - background: inherit; - } + :global(.theme-light) & { + margin-top: 0; + margin-bottom: 0; } } +.alert { + margin: 0 0.25rem; + padding: 0.125rem 0.25rem; + cursor: default; + user-select: none; +} + +/* &__icon-chevron { */ +.icon-chevron { + opacity: 0.6; + margin: auto; +} + +.spacer { + flex: 1 1 0; +} + /* &__kek-pek { */ .kek-pek { color: red; } -.logo { - display: flex; -} " `; diff --git a/src/transforms/globalCssToCssModule/globalCssToCssModule.ts b/src/transforms/globalCssToCssModule/globalCssToCssModule.ts index 4eaad1f8..bc7539a7 100644 --- a/src/transforms/globalCssToCssModule/globalCssToCssModule.ts +++ b/src/transforms/globalCssToCssModule/globalCssToCssModule.ts @@ -9,8 +9,8 @@ import { addClassNamesUtilImportIfNeeded } from '../../utils/classNamesUtility' import { getCssModuleExportNameMap } from './postcss/getCssModuleExportNameMap' import { transformFileToCssModule } from './postcss/transformFileToCssModule' -import { getNodesWithClassName } from './ts/getNodesWithClassName' -import { STYLES_IDENTIFIER, processNodesWithClassName } from './ts/processNodesWithClassName' +import { STYLES_IDENTIFIER } from './ts/processNodesWithClassName' +import { transformComponentFile } from './ts/transformComponentFile' /** * Convert globally scoped stylesheet tied to the React component into a CSS Module. @@ -68,11 +68,7 @@ export async function globalCssToCssModule(options: CodemodOptions): CodemodResu sourceFilePath: cssFilePath, }) - processNodesWithClassName({ - exportNameMap, - nodesWithClassName: getNodesWithClassName(tsSourceFile), - }) - + transformComponentFile({ tsSourceFile, exportNameMap, cssModuleFileName }) addClassNamesUtilImportIfNeeded(tsSourceFile) tsSourceFile.addImportDeclaration({ defaultImport: STYLES_IDENTIFIER, diff --git a/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts b/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts index a26e6015..e0c4b9a7 100644 --- a/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts +++ b/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts @@ -32,6 +32,10 @@ describe('transformFileToCssModule', () => { white-space: nowrap; } + &:disabled &__button { + display: none; + } + @media (--xs-breakpoint-down) { border-radius: var(--border-radius); } @@ -67,21 +71,25 @@ describe('transformFileToCssModule', () => { white-space: nowrap; } + &:disabled .button { + display: none; + } + @media (--xs-breakpoint-down) { border-radius: var(--border-radius); } } - /* &__button comment*/ - .button { - margin-top: 1px; - } - :global(.theme-light) { .spacer { flex: 1 1 0; } } + + /* &__button comment*/ + .button { + margin-top: 1px; + } ` const { css, filePath } = await transformFileToCssModule({ sourceCss, sourceFilePath: 'whatever.scss' }) diff --git a/src/transforms/globalCssToCssModule/postcss/exportNameMapPrefixes.ts b/src/transforms/globalCssToCssModule/postcss/exportNameMapPrefixes.ts index a766311a..489594a0 100644 --- a/src/transforms/globalCssToCssModule/postcss/exportNameMapPrefixes.ts +++ b/src/transforms/globalCssToCssModule/postcss/exportNameMapPrefixes.ts @@ -1,3 +1,5 @@ +import camelcase from 'camelcase' + import { decapitalize, isDefined } from '../../../utils' interface RemovedPrefix { @@ -62,7 +64,7 @@ export function getPrefixesToRemove(exportNameMap: Record): Remo if (matches) { return { prefix: matches[0], - exportName: exportNameMap[matches[1]], + exportName: camelcase(matches[1]), } } diff --git a/src/transforms/globalCssToCssModule/postcss/postcssToCssModulePlugin.ts b/src/transforms/globalCssToCssModule/postcss/postcssToCssModulePlugin.ts index aa33d612..17e7fa4a 100644 --- a/src/transforms/globalCssToCssModule/postcss/postcssToCssModulePlugin.ts +++ b/src/transforms/globalCssToCssModule/postcss/postcssToCssModulePlugin.ts @@ -1,4 +1,4 @@ -import { AcceptedPlugin, Rule, ChildNode } from 'postcss' +import { AcceptedPlugin, Rule, ChildNode, Root } from 'postcss' import parser, { isRoot, Selector } from 'postcss-selector-parser' interface PostcssToCssModulePluginOptions { @@ -110,15 +110,19 @@ function updateChildSelectors(parent: Rule, child: Rule): string[] { * Some important comment about &__button selector. * .button { ... } */ - const parentOrComment = pickComment(child.prev(), parent) - parentOrComment.after(child) + pickComment(child.prev(), parent.root()) + parent.root().last?.after(child) + } + + if (parent.nodes.length === 0) { + parent.remove() } return updatedChildSelectors } function replaceSelectorNodesIfNeeded(nodes: Selector): boolean { - return nodes.reduce((shouldRemoveNesting, node) => { + return nodes.reduce((shouldRemoveNesting, node, index) => { /** * Assume that all nested classes and ids not starting with `&` are global: * @@ -167,7 +171,26 @@ function replaceSelectorNodesIfNeeded(nodes: Selector): boolean { node.replaceWith(parse('')) nextNode.replaceWith(parse(nextNodeValue.replace('__', '.'))) - return true + /** + * If its not the first node of the selector — keep nesting in place + * + * ```scss + * .menu { + * &:hover &__button { ... } + * } + * ``` + * + * Turns into: + * + * ```scss + * .menu { + * &:hover .button { ... } + * } + * ``` + */ + if (index === 0) { + return true + } } } } @@ -181,9 +204,10 @@ function wrapSelectorInGlobalKeyword(selector: string): string { } // If passed node is a comment -> attach it to the end of the file and return it, otherwise return the passed node. -function pickComment(maybeCommentNode: ChildNode | undefined, parent: Rule): Rule { +function pickComment(maybeCommentNode: ChildNode | undefined, parent: Rule | Root): Rule | Root { if (maybeCommentNode && maybeCommentNode.type === 'comment') { - parent.after(maybeCommentNode) + parent.last?.after(maybeCommentNode) + return maybeCommentNode as unknown as Rule } diff --git a/src/transforms/globalCssToCssModule/ts/__tests__/getClassNameNodeReplacement.test.ts b/src/transforms/globalCssToCssModule/ts/__tests__/getClassNameNodeReplacement.test.ts index c8780399..9213d1c0 100644 --- a/src/transforms/globalCssToCssModule/ts/__tests__/getClassNameNodeReplacement.test.ts +++ b/src/transforms/globalCssToCssModule/ts/__tests__/getClassNameNodeReplacement.test.ts @@ -52,14 +52,21 @@ describe('getClassNameNodeReplacement', () => { describe.each(testCases)('in parent kind $parentKind', ({ fileSource, replacement: expectedReplacement }) => { const parentNode = getParentFromFirstClassNameNode(fileSource) - const getReplacement = (options?: Partial) => - getClassNameNodeReplacement({ + const getReplacement = (options?: Partial) => { + const result = getClassNameNodeReplacement({ parentNode, exportNameReferences, leftOverClassName, ...options, }) + if (result.isParentTransformed) { + throw new Error('No parent transform is expected') + } + + return result.replacement + } + it('returns correct replacement with `leftOverClassName` provided', () => { const replacement = getReplacement() diff --git a/src/transforms/globalCssToCssModule/ts/__tests__/processNodesWithClassName.test.ts b/src/transforms/globalCssToCssModule/ts/__tests__/processNodesWithClassName.test.ts index 4bbfcf3d..e2504274 100644 --- a/src/transforms/globalCssToCssModule/ts/__tests__/processNodesWithClassName.test.ts +++ b/src/transforms/globalCssToCssModule/ts/__tests__/processNodesWithClassName.test.ts @@ -21,6 +21,7 @@ describe('processNodesWithClassName', () => { `) processNodesWithClassName({ + usageStats: {}, nodesWithClassName: getNodesWithClassName(sourceFile), exportNameMap: { kek: 'kek', diff --git a/src/transforms/globalCssToCssModule/ts/__tests__/splitClassName.test.ts b/src/transforms/globalCssToCssModule/ts/__tests__/splitClassName.test.ts index 385be9b1..f56a7dd9 100644 --- a/src/transforms/globalCssToCssModule/ts/__tests__/splitClassName.test.ts +++ b/src/transforms/globalCssToCssModule/ts/__tests__/splitClassName.test.ts @@ -2,9 +2,13 @@ import { splitClassName } from '../splitClassName' describe('splitClassName', () => { it('splits correctly', () => { - const { exportNames, leftOverClassnames } = splitClassName('kek kek--wow d-flex mr-1', { - kek: 'kek', - 'kek--wow': 'kekWow', + const { exportNames, leftOverClassnames } = splitClassName({ + usageStats: {}, + className: 'kek kek--wow d-flex mr-1', + exportNameMap: { + kek: 'kek', + 'kek--wow': 'kekWow', + }, }) expect(exportNames).toEqual(['kek', 'kekWow']) diff --git a/src/transforms/globalCssToCssModule/ts/getClassNameNodeReplacement.ts b/src/transforms/globalCssToCssModule/ts/getClassNameNodeReplacement.ts index a65ae212..53e2f6f8 100644 --- a/src/transforms/globalCssToCssModule/ts/getClassNameNodeReplacement.ts +++ b/src/transforms/globalCssToCssModule/ts/getClassNameNodeReplacement.ts @@ -8,7 +8,7 @@ import { PropertyAccessExpression, } from 'typescript' -import { wrapIntoClassNamesUtility } from '../../../utils/classNamesUtility' +import { wrapIntoClassNamesUtility, CLASSNAMES_IDENTIFIER } from '../../../utils/classNamesUtility' export interface GetClassNameNodeReplacementOptions { parentNode: NodeParentType @@ -18,8 +18,12 @@ export interface GetClassNameNodeReplacementOptions { function getClassNameNodeReplacementWithoutBraces( options: GetClassNameNodeReplacementOptions -): PropertyAccessExpression | CallExpression { - const { leftOverClassName, exportNameReferences } = options +): PropertyAccessExpression | CallExpression | Expression[] { + const { leftOverClassName, exportNameReferences, parentNode } = options + + const isInClassnamesCall = + ts.isCallExpression(parentNode.compilerNode) && + parentNode.compilerNode.expression.getText() === CLASSNAMES_IDENTIFIER // We need to use `classNames` utility for multiple `exportNames` or for a combination of the `exportName` and `StringLiteral`. // className={classNames('d-flex mr-1 kek kek--primary')} -> className={classNames('d-flex mr-1', styles.kek, styles.kekPrimary)} @@ -30,6 +34,10 @@ function getClassNameNodeReplacementWithoutBraces( classNamesCallArguments.unshift(ts.factory.createStringLiteral(leftOverClassName)) } + if (isInClassnamesCall) { + return classNamesCallArguments + } + return wrapIntoClassNamesUtility(classNamesCallArguments) } @@ -38,11 +46,25 @@ function getClassNameNodeReplacementWithoutBraces( return exportNameReferences[0] } +type GetClassNameNodeReplacementResult = + | { + replacement: PropertyAccessExpression | JsxExpression | ComputedPropertyName | CallExpression + isParentTransformed: false + } + | { + replacement: null + isParentTransformed: true + } + +/** + * Transforms node with className. In case the parentNode + */ export function getClassNameNodeReplacement( options: GetClassNameNodeReplacementOptions -): PropertyAccessExpression | JsxExpression | ComputedPropertyName | CallExpression { +): GetClassNameNodeReplacementResult { const { parentNode, exportNameReferences } = options const parentKind = parentNode.getKind() + const parentCompilerNode = parentNode.compilerNode if (exportNameReferences.length === 0) { throw new Error('`exportNameReferences` should not be empty!') @@ -50,19 +72,43 @@ export function getClassNameNodeReplacement( const replacementWithoutBraces = getClassNameNodeReplacementWithoutBraces(options) - if (parentKind === SyntaxKind.JsxAttribute) { - return ts.factory.createJsxExpression(undefined, replacementWithoutBraces) - } + if (Array.isArray(replacementWithoutBraces)) { + if (ts.isCallExpression(parentCompilerNode)) { + // In case replacement is already in `classNames` call —> transform the parentNode + // and return `false` to restart node transform to avoid missing nested items which were unmounted during parentNode change. + parentNode.transform(() => + ts.factory.createCallExpression(parentCompilerNode.expression, undefined, [ + ...replacementWithoutBraces, + ...parentCompilerNode.arguments.filter(argument => argument.kind !== SyntaxKind.StringLiteral), + ]) + ) - if (parentKind === SyntaxKind.PropertyAssignment) { - return ts.factory.createComputedPropertyName(replacementWithoutBraces) + return { isParentTransformed: true, replacement: null } + } + + throw new Error(`Unhandled parentNode: ${parentNode.getFullText()}`) } - if (parentKind === SyntaxKind.ConditionalExpression || parentKind === SyntaxKind.CallExpression) { + if ( + parentKind === SyntaxKind.ConditionalExpression || + parentKind === SyntaxKind.CallExpression || + parentKind === SyntaxKind.BinaryExpression + ) { // Replace one class string inside of `ConditionalExpression` with the `exportName`. // className={classNames('d-flex', isActive ? 'kek' : 'pek')} -> className={classNames('d-flex', isActive ? styles.kek : 'pek')} - return replacementWithoutBraces + return { isParentTransformed: false, replacement: replacementWithoutBraces } + } + if (parentKind === SyntaxKind.JsxAttribute) { + const replacement = ts.factory.createJsxExpression(undefined, replacementWithoutBraces) + + return { isParentTransformed: false, replacement } + } + + if (parentKind === SyntaxKind.PropertyAssignment) { + const replacement = ts.factory.createComputedPropertyName(replacementWithoutBraces) + + return { isParentTransformed: false, replacement } } - throw new Error(`Unsupported 'parentNode' type: ${parentNode.getKindName()}`) + throw new Error(`Unsupported 'parentNode' type: ${parentNode.getKindName()} ${parentNode.getFullText()}`) } diff --git a/src/transforms/globalCssToCssModule/ts/getNodesWithClassName.ts b/src/transforms/globalCssToCssModule/ts/getNodesWithClassName.ts index 01a8c91a..39887e03 100644 --- a/src/transforms/globalCssToCssModule/ts/getNodesWithClassName.ts +++ b/src/transforms/globalCssToCssModule/ts/getNodesWithClassName.ts @@ -10,9 +10,7 @@ export function getNodesWithClassName(sourceFile: SourceFile): (Identifier | Str .filter(identifier => identifier.getParent().compilerNode.kind === SyntaxKind.PropertyAssignment) //
— 'kek' is a `StringLiteral` inside of the `JsxAttribute`. - const classNameStringLiterals = classNameJsxAttributes.flatMap(classNameJsxAttribute => - classNameJsxAttribute.getDescendantsOfKind(SyntaxKind.StringLiteral) - ) + const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral) - return [...classNameIdentifiers, ...classNameStringLiterals] + return [...classNameIdentifiers, ...stringLiterals] } diff --git a/src/transforms/globalCssToCssModule/ts/processNodesWithClassName.ts b/src/transforms/globalCssToCssModule/ts/processNodesWithClassName.ts index 586e641b..de8113ec 100644 --- a/src/transforms/globalCssToCssModule/ts/processNodesWithClassName.ts +++ b/src/transforms/globalCssToCssModule/ts/processNodesWithClassName.ts @@ -8,6 +8,7 @@ export const STYLES_IDENTIFIER = 'styles' interface ProcessNodesWithClassNameOptions { nodesWithClassName: (Identifier | StringLiteral)[] exportNameMap: Record + usageStats: Record } /** @@ -36,9 +37,11 @@ interface ProcessNodesWithClassNameOptions { * Nodes without matching `exportNameMap` classes will be skipped without changes. * *
-> left without changes + * + * @returns areAllNodesProcessed: boolean */ -export function processNodesWithClassName(options: ProcessNodesWithClassNameOptions): void { - const { nodesWithClassName, exportNameMap } = options +export function processNodesWithClassName(options: ProcessNodesWithClassNameOptions): boolean { + const { nodesWithClassName, exportNameMap, usageStats } = options for (const nodeWithClassName of nodesWithClassName) { const classNameStringValue = @@ -46,7 +49,11 @@ export function processNodesWithClassName(options: ProcessNodesWithClassNameOpti ? nodeWithClassName.getLiteralText() : nodeWithClassName.getText() - const { exportNames, leftOverClassnames } = splitClassName(classNameStringValue, exportNameMap) + const { exportNames, leftOverClassnames } = splitClassName({ + className: classNameStringValue, + exportNameMap, + usageStats, + }) // There's nothing to update in this `className` node. if (exportNames.length === 0) { @@ -57,12 +64,18 @@ export function processNodesWithClassName(options: ProcessNodesWithClassNameOpti ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(STYLES_IDENTIFIER), exportName) ) - nodeWithClassName.transform(() => - getClassNameNodeReplacement({ - parentNode: nodeWithClassName.getParent(), - leftOverClassName: leftOverClassnames.join(' '), - exportNameReferences, - }) - ) + const result = getClassNameNodeReplacement({ + parentNode: nodeWithClassName.getParent(), + leftOverClassName: leftOverClassnames.join(' '), + exportNameReferences, + }) + + if (result.isParentTransformed) { + return false + } + + nodeWithClassName.transform(() => result.replacement) } + + return true } diff --git a/src/transforms/globalCssToCssModule/ts/splitClassName.ts b/src/transforms/globalCssToCssModule/ts/splitClassName.ts index a9c0f714..54f1f674 100644 --- a/src/transforms/globalCssToCssModule/ts/splitClassName.ts +++ b/src/transforms/globalCssToCssModule/ts/splitClassName.ts @@ -1,22 +1,31 @@ +interface SplitClassNameOptions { + className: string + exportNameMap: Record + usageStats: Record +} + interface SplitClassNameResult { exportNames: string[] leftOverClassnames: string[] } /** - * Example: splitClassName('kek--wow d-flex mr-1', { 'kek--wow': 'kekWow' }) + * Example: splitClassName({ className: 'kek--wow d-flex mr-1', exportNameMap: { 'kek--wow': 'kekWow' }, usageStats: {}) * returns: { exportNames: ['kekWow'], leftOverClassnames: ['d-flex', 'mr-1'] } * * @param className ClassName value string which might contain replaceable parts. * @param exportNameMap Mapping between classes and exportNames. + * @param usageStats Object to collect replaced classes usage in React component. * @returns Object that contains array of exportNames found in the className and the left over value. */ -export function splitClassName(className: string, exportNameMap: Record): SplitClassNameResult { +export function splitClassName(options: SplitClassNameOptions): SplitClassNameResult { + const { className, exportNameMap, usageStats } = options const classNamesToReplace = Object.keys(exportNameMap) return className.split(' ').reduce( (accumulator, className) => { if (classNamesToReplace.includes(className)) { + usageStats[className] = true accumulator.exportNames.push(exportNameMap[className]) } else { accumulator.leftOverClassnames.push(className) diff --git a/src/transforms/globalCssToCssModule/ts/transformComponentFile.ts b/src/transforms/globalCssToCssModule/ts/transformComponentFile.ts new file mode 100644 index 00000000..9a5d3b4f --- /dev/null +++ b/src/transforms/globalCssToCssModule/ts/transformComponentFile.ts @@ -0,0 +1,39 @@ +import signale from 'signale' +import { SourceFile } from 'ts-morph' + +import { isDefined } from '../../../utils' + +import { getNodesWithClassName } from './getNodesWithClassName' +import { processNodesWithClassName } from './processNodesWithClassName' + +interface TransformComponentFileOptions { + tsSourceFile: SourceFile + exportNameMap: Record + cssModuleFileName: string +} + +export function transformComponentFile(options: TransformComponentFileOptions): void { + const { tsSourceFile, exportNameMap, cssModuleFileName } = options + + // Object to collect CSS classes usage and report unused classes after the codemod. + const usageStats = Object.fromEntries(Object.keys(exportNameMap).map(className => [className, false])) + + let areAllNodesProcessed = false + + while (!areAllNodesProcessed) { + // `processNodesWithClassName` returns `true` when there's nothing more to process. + areAllNodesProcessed = processNodesWithClassName({ + usageStats, + exportNameMap, + nodesWithClassName: getNodesWithClassName(tsSourceFile), + }) + } + + const unusedClassNames = Object.entries(usageStats) + .map(([className, isUsed]) => (isUsed ? undefined : className)) + .filter(isDefined) + + if (unusedClassNames.length > 0) { + signale.warn(`Unused CSS classes in ${cssModuleFileName}`, unusedClassNames) + } +} diff --git a/src/utils/formatWithStylelint.ts b/src/utils/formatWithStylelint.ts index a6194f57..e0b66822 100644 --- a/src/utils/formatWithStylelint.ts +++ b/src/utils/formatWithStylelint.ts @@ -9,6 +9,14 @@ export async function formatWithStylelint(sourceCode: string, filePath: string): rules: { // Hard-coded for now. Make it optional or delete it once it's not needed. indentation: 4, + 'declaration-block-trailing-semicolon': 'always', + 'rule-empty-line-before': [ + 'always-multi-line', + { + except: ['after-single-line-comment', 'first-nested'], + ignore: ['after-comment', 'first-nested'], + }, + ], }, }, })