From 31a4920b39a6713bc9be91809ec81c321faae039 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Tue, 31 Aug 2021 22:39:45 +0800 Subject: [PATCH] chore: add option to write files after the codemod (#14) --- src/cli.ts | 3 +- src/transforms/globalCssToCssModule/README.md | 15 ++++ .../__tests__/globalCssToCssModule.test.ts | 2 +- .../globalCssToCssModule.ts | 85 +++++++++++++++---- .../transformFileToCssModule.test.ts | 4 +- .../postcss/transformFileToCssModule.ts | 11 ++- src/utils/index.ts | 3 + 7 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 src/transforms/globalCssToCssModule/README.md create mode 100644 src/utils/index.ts diff --git a/src/cli.ts b/src/cli.ts index 9be3d537..ec68be93 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,14 +4,13 @@ import { Project } from 'ts-morph' import { globalCssToCssModule } from './transforms/globalCssToCssModule/globalCssToCssModule' -// TODO: add interactive CLI support const TARGET_FILE = path.resolve(__dirname, './transforms/globalCssToCssModule/__tests__/fixtures/Kek.tsx') async function main(): Promise { const project = new Project() project.addSourceFilesAtPaths(TARGET_FILE) - const result = await globalCssToCssModule(project) + const result = await globalCssToCssModule({ project, shouldWriteFiles: true }) console.log(result) } diff --git a/src/transforms/globalCssToCssModule/README.md b/src/transforms/globalCssToCssModule/README.md new file mode 100644 index 00000000..014ed563 --- /dev/null +++ b/src/transforms/globalCssToCssModule/README.md @@ -0,0 +1,15 @@ +# Global CSS to CSS Module codemod + +Status: WIP + +## Usage + +```sh +echo "Hello world" +``` + +## TODO + +[] Handle @import statements in SCSS +[] Run code formatters on the updated files +[] Add interactive CLI support diff --git a/src/transforms/globalCssToCssModule/__tests__/globalCssToCssModule.test.ts b/src/transforms/globalCssToCssModule/__tests__/globalCssToCssModule.test.ts index d789c061..d97d6ebe 100644 --- a/src/transforms/globalCssToCssModule/__tests__/globalCssToCssModule.test.ts +++ b/src/transforms/globalCssToCssModule/__tests__/globalCssToCssModule.test.ts @@ -10,7 +10,7 @@ describe('globalCssToCssModule', () => { it('transforms correctly', async () => { const project = new Project() project.addSourceFilesAtPaths(TARGET_FILE) - const [transformResult] = await globalCssToCssModule(project) + const [transformResult] = await globalCssToCssModule({ project }) expect(transformResult.css.source).toMatchSnapshot() expect(transformResult.ts.source).toMatchSnapshot() diff --git a/src/transforms/globalCssToCssModule/globalCssToCssModule.ts b/src/transforms/globalCssToCssModule/globalCssToCssModule.ts index 0062850c..5a66dcb9 100644 --- a/src/transforms/globalCssToCssModule/globalCssToCssModule.ts +++ b/src/transforms/globalCssToCssModule/globalCssToCssModule.ts @@ -1,14 +1,22 @@ -import { readFileSync } from 'fs' +import { existsSync, readFileSync, promises as fsPromises } from 'fs' import path from 'path' import { Project } from 'ts-morph' +import { isDefined } from '../../utils' + import { getCssModuleExportNameMap } from './postcss/getCssModuleExportNameMap' import { transformFileToCssModule } from './postcss/transformFileToCssModule' import { addClassNamesUtilImportIfNeeded } from './ts/classNamesUtility' import { getNodesWithClassName } from './ts/getNodesWithClassName' import { STYLES_IDENTIFIER, processNodesWithClassName } from './ts/processNodesWithClassName' +interface GlobalCssToCssModuleOptions { + project: Project + /** If `true` persist changes made by the codemod to the filesystem. */ + shouldWriteFiles?: boolean +} + interface CodemodResult { css: { source: string @@ -18,6 +26,7 @@ interface CodemodResult { source: string path: string } + fsWritePromise?: Promise } /** @@ -32,44 +41,86 @@ interface CodemodResult { * 7) Add `.module.scss` import to the `.tsx` file. * */ -export function globalCssToCssModule(project: Project): Promise { - const codemodResults = project.getSourceFiles().map(async sourceFile => { - const filePath = sourceFile.getFilePath() +export async function globalCssToCssModule(options: GlobalCssToCssModuleOptions): Promise { + const { project, shouldWriteFiles } = options + /** + * Find `.tsx` files with co-located `.scss` file. + * For example `RepoHeader.tsx` should have matching `RepoHeader.scss` in the same folder. + */ + const itemsToProcess = project + .getSourceFiles() + .map(tsSourceFile => { + const tsFilePath = tsSourceFile.getFilePath() + + const parsedTsFilePath = path.parse(tsFilePath) + const cssFilePath = path.resolve(parsedTsFilePath.dir, `${parsedTsFilePath.name}.scss`) + + if (existsSync(cssFilePath)) { + return { + tsSourceFile, + cssFilePath, + } + } + + return undefined + }) + .filter(isDefined) - const parsedTsFilePath = path.parse(filePath) - const cssFilePath = path.resolve(parsedTsFilePath.dir, `${parsedTsFilePath.name}.scss`) + const codemodResultPromises = itemsToProcess.map(async ({ tsSourceFile, cssFilePath }) => { + const tsFilePath = tsSourceFile.getFilePath() + const parsedTsFilePath = path.parse(tsFilePath) - // TODO: add check if SCSS file doesn't exist and exit if it's not found. const sourceCss = readFileSync(cssFilePath, 'utf8') - const { css: cssModuleSource, filePath: cssModuleFileName } = await transformFileToCssModule( + const { css: cssModuleSource, filePath: cssModuleFileName } = await transformFileToCssModule({ sourceCss, - cssFilePath - ) + sourceFilePath: cssFilePath, + }) const exportNameMap = await getCssModuleExportNameMap(cssModuleSource) processNodesWithClassName({ exportNameMap, - nodesWithClassName: getNodesWithClassName(sourceFile), + nodesWithClassName: getNodesWithClassName(tsSourceFile), }) - addClassNamesUtilImportIfNeeded(sourceFile) - sourceFile.addImportDeclaration({ + addClassNamesUtilImportIfNeeded(tsSourceFile) + tsSourceFile.addImportDeclaration({ defaultImport: STYLES_IDENTIFIER, moduleSpecifier: `./${path.parse(cssModuleFileName).base}`, }) - // TODO: run prettier and eslint --fix over updated files. + /** + * If `shouldWriteFiles` is true: + * + * 1. Update TS file with a new source that uses CSS module. + * 2. Create a new CSS module file. + * 3. Delete redundant SCSS file that's replaced with CSS module. + */ + const fsWritePromise = shouldWriteFiles + ? Promise.all([ + tsSourceFile.save(), + fsPromises.writeFile(cssModuleFileName, cssModuleSource, { encoding: 'utf-8' }), + fsPromises.rm(cssFilePath), + ]) + : undefined + return { + fsWritePromise, css: { source: cssModuleSource, path: path.resolve(parsedTsFilePath.dir, cssModuleFileName), }, ts: { - source: sourceFile.getFullText(), - path: sourceFile.getFilePath(), + source: tsSourceFile.getFullText(), + path: tsSourceFile.getFilePath(), }, } }) - return Promise.all(codemodResults) + const codemodResults = await Promise.all(codemodResultPromises) + + if (shouldWriteFiles) { + await Promise.all(codemodResults.map(result => result.fsWritePromise)) + } + + return codemodResults } diff --git a/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts b/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts index f54816b9..f19fce29 100644 --- a/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts +++ b/src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts @@ -4,7 +4,7 @@ const replaceWhitespace = (value: string) => value.replace(/\s+/g, ' ').trim() describe('transformFileToCssModule', () => { it('correctly transforms provided CSS to CSS module', async () => { - const cssSource = ` + const sourceCss = ` // .repo-header comment .repo-header { flex: none; @@ -72,7 +72,7 @@ describe('transformFileToCssModule', () => { } ` - const { css, filePath } = await transformFileToCssModule(cssSource, 'whatever.scss') + const { css, filePath } = await transformFileToCssModule({ sourceCss, sourceFilePath: 'whatever.scss' }) expect(replaceWhitespace(css)).toEqual(replaceWhitespace(expectedCssModuleSource)) expect(filePath).toEqual('whatever.module.scss') diff --git a/src/transforms/globalCssToCssModule/postcss/transformFileToCssModule.ts b/src/transforms/globalCssToCssModule/postcss/transformFileToCssModule.ts index 13e54fcd..8e0e6515 100644 --- a/src/transforms/globalCssToCssModule/postcss/transformFileToCssModule.ts +++ b/src/transforms/globalCssToCssModule/postcss/transformFileToCssModule.ts @@ -3,22 +3,27 @@ import path from 'path' import { createCssProcessor } from './createCssProcessor' import { postcssToCssModulePlugin } from './postcssToCssModulePlugin' +interface TransformFileToCssModuleOptions { + sourceCss: string + sourceFilePath: string +} + interface TransformFileToCssModuleResult { css: string filePath: string } export async function transformFileToCssModule( - sourceCss: string, - sourceFilePath: string + options: TransformFileToCssModuleOptions ): Promise { + const { sourceCss, sourceFilePath } = options + const transformFileToCssModuleProcessor = createCssProcessor(postcssToCssModulePlugin()) const transformedResult = await transformFileToCssModuleProcessor(sourceCss) const { dir, name } = path.parse(sourceFilePath) const newFilePath = path.join(dir, `${name}.module.scss`) - // TODO: add option to write files. return { css: transformedResult.css, filePath: newFilePath, diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..1ad82263 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export function isDefined(argument: T | undefined): argument is T { + return argument !== undefined +}