Skip to content

Commit 31a4920

Browse files
chore: add option to write files after the codemod (#14)
1 parent 7d2d449 commit 31a4920

File tree

7 files changed

+98
-25
lines changed

7 files changed

+98
-25
lines changed

src/cli.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ import { Project } from 'ts-morph'
44

55
import { globalCssToCssModule } from './transforms/globalCssToCssModule/globalCssToCssModule'
66

7-
// TODO: add interactive CLI support
87
const TARGET_FILE = path.resolve(__dirname, './transforms/globalCssToCssModule/__tests__/fixtures/Kek.tsx')
98

109
async function main(): Promise<void> {
1110
const project = new Project()
1211
project.addSourceFilesAtPaths(TARGET_FILE)
1312

14-
const result = await globalCssToCssModule(project)
13+
const result = await globalCssToCssModule({ project, shouldWriteFiles: true })
1514
console.log(result)
1615
}
1716

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Global CSS to CSS Module codemod
2+
3+
Status: WIP
4+
5+
## Usage
6+
7+
```sh
8+
echo "Hello world"
9+
```
10+
11+
## TODO
12+
13+
[] Handle @import statements in SCSS
14+
[] Run code formatters on the updated files
15+
[] Add interactive CLI support

src/transforms/globalCssToCssModule/__tests__/globalCssToCssModule.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('globalCssToCssModule', () => {
1010
it('transforms correctly', async () => {
1111
const project = new Project()
1212
project.addSourceFilesAtPaths(TARGET_FILE)
13-
const [transformResult] = await globalCssToCssModule(project)
13+
const [transformResult] = await globalCssToCssModule({ project })
1414

1515
expect(transformResult.css.source).toMatchSnapshot()
1616
expect(transformResult.ts.source).toMatchSnapshot()
Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { readFileSync } from 'fs'
1+
import { existsSync, readFileSync, promises as fsPromises } from 'fs'
22
import path from 'path'
33

44
import { Project } from 'ts-morph'
55

6+
import { isDefined } from '../../utils'
7+
68
import { getCssModuleExportNameMap } from './postcss/getCssModuleExportNameMap'
79
import { transformFileToCssModule } from './postcss/transformFileToCssModule'
810
import { addClassNamesUtilImportIfNeeded } from './ts/classNamesUtility'
911
import { getNodesWithClassName } from './ts/getNodesWithClassName'
1012
import { STYLES_IDENTIFIER, processNodesWithClassName } from './ts/processNodesWithClassName'
1113

14+
interface GlobalCssToCssModuleOptions {
15+
project: Project
16+
/** If `true` persist changes made by the codemod to the filesystem. */
17+
shouldWriteFiles?: boolean
18+
}
19+
1220
interface CodemodResult {
1321
css: {
1422
source: string
@@ -18,6 +26,7 @@ interface CodemodResult {
1826
source: string
1927
path: string
2028
}
29+
fsWritePromise?: Promise<void[]>
2130
}
2231

2332
/**
@@ -32,44 +41,86 @@ interface CodemodResult {
3241
* 7) Add `.module.scss` import to the `.tsx` file.
3342
*
3443
*/
35-
export function globalCssToCssModule(project: Project): Promise<CodemodResult[]> {
36-
const codemodResults = project.getSourceFiles().map(async sourceFile => {
37-
const filePath = sourceFile.getFilePath()
44+
export async function globalCssToCssModule(options: GlobalCssToCssModuleOptions): Promise<CodemodResult[]> {
45+
const { project, shouldWriteFiles } = options
46+
/**
47+
* Find `.tsx` files with co-located `.scss` file.
48+
* For example `RepoHeader.tsx` should have matching `RepoHeader.scss` in the same folder.
49+
*/
50+
const itemsToProcess = project
51+
.getSourceFiles()
52+
.map(tsSourceFile => {
53+
const tsFilePath = tsSourceFile.getFilePath()
54+
55+
const parsedTsFilePath = path.parse(tsFilePath)
56+
const cssFilePath = path.resolve(parsedTsFilePath.dir, `${parsedTsFilePath.name}.scss`)
57+
58+
if (existsSync(cssFilePath)) {
59+
return {
60+
tsSourceFile,
61+
cssFilePath,
62+
}
63+
}
64+
65+
return undefined
66+
})
67+
.filter(isDefined)
3868

39-
const parsedTsFilePath = path.parse(filePath)
40-
const cssFilePath = path.resolve(parsedTsFilePath.dir, `${parsedTsFilePath.name}.scss`)
69+
const codemodResultPromises = itemsToProcess.map(async ({ tsSourceFile, cssFilePath }) => {
70+
const tsFilePath = tsSourceFile.getFilePath()
71+
const parsedTsFilePath = path.parse(tsFilePath)
4172

42-
// TODO: add check if SCSS file doesn't exist and exit if it's not found.
4373
const sourceCss = readFileSync(cssFilePath, 'utf8')
44-
const { css: cssModuleSource, filePath: cssModuleFileName } = await transformFileToCssModule(
74+
const { css: cssModuleSource, filePath: cssModuleFileName } = await transformFileToCssModule({
4575
sourceCss,
46-
cssFilePath
47-
)
76+
sourceFilePath: cssFilePath,
77+
})
4878
const exportNameMap = await getCssModuleExportNameMap(cssModuleSource)
4979

5080
processNodesWithClassName({
5181
exportNameMap,
52-
nodesWithClassName: getNodesWithClassName(sourceFile),
82+
nodesWithClassName: getNodesWithClassName(tsSourceFile),
5383
})
5484

55-
addClassNamesUtilImportIfNeeded(sourceFile)
56-
sourceFile.addImportDeclaration({
85+
addClassNamesUtilImportIfNeeded(tsSourceFile)
86+
tsSourceFile.addImportDeclaration({
5787
defaultImport: STYLES_IDENTIFIER,
5888
moduleSpecifier: `./${path.parse(cssModuleFileName).base}`,
5989
})
6090

61-
// TODO: run prettier and eslint --fix over updated files.
91+
/**
92+
* If `shouldWriteFiles` is true:
93+
*
94+
* 1. Update TS file with a new source that uses CSS module.
95+
* 2. Create a new CSS module file.
96+
* 3. Delete redundant SCSS file that's replaced with CSS module.
97+
*/
98+
const fsWritePromise = shouldWriteFiles
99+
? Promise.all([
100+
tsSourceFile.save(),
101+
fsPromises.writeFile(cssModuleFileName, cssModuleSource, { encoding: 'utf-8' }),
102+
fsPromises.rm(cssFilePath),
103+
])
104+
: undefined
105+
62106
return {
107+
fsWritePromise,
63108
css: {
64109
source: cssModuleSource,
65110
path: path.resolve(parsedTsFilePath.dir, cssModuleFileName),
66111
},
67112
ts: {
68-
source: sourceFile.getFullText(),
69-
path: sourceFile.getFilePath(),
113+
source: tsSourceFile.getFullText(),
114+
path: tsSourceFile.getFilePath(),
70115
},
71116
}
72117
})
73118

74-
return Promise.all(codemodResults)
119+
const codemodResults = await Promise.all(codemodResultPromises)
120+
121+
if (shouldWriteFiles) {
122+
await Promise.all(codemodResults.map(result => result.fsWritePromise))
123+
}
124+
125+
return codemodResults
75126
}

src/transforms/globalCssToCssModule/postcss/__tests__/transformFileToCssModule.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const replaceWhitespace = (value: string) => value.replace(/\s+/g, ' ').trim()
44

55
describe('transformFileToCssModule', () => {
66
it('correctly transforms provided CSS to CSS module', async () => {
7-
const cssSource = `
7+
const sourceCss = `
88
// .repo-header comment
99
.repo-header {
1010
flex: none;
@@ -72,7 +72,7 @@ describe('transformFileToCssModule', () => {
7272
}
7373
`
7474

75-
const { css, filePath } = await transformFileToCssModule(cssSource, 'whatever.scss')
75+
const { css, filePath } = await transformFileToCssModule({ sourceCss, sourceFilePath: 'whatever.scss' })
7676

7777
expect(replaceWhitespace(css)).toEqual(replaceWhitespace(expectedCssModuleSource))
7878
expect(filePath).toEqual('whatever.module.scss')

src/transforms/globalCssToCssModule/postcss/transformFileToCssModule.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@ import path from 'path'
33
import { createCssProcessor } from './createCssProcessor'
44
import { postcssToCssModulePlugin } from './postcssToCssModulePlugin'
55

6+
interface TransformFileToCssModuleOptions {
7+
sourceCss: string
8+
sourceFilePath: string
9+
}
10+
611
interface TransformFileToCssModuleResult {
712
css: string
813
filePath: string
914
}
1015

1116
export async function transformFileToCssModule(
12-
sourceCss: string,
13-
sourceFilePath: string
17+
options: TransformFileToCssModuleOptions
1418
): Promise<TransformFileToCssModuleResult> {
19+
const { sourceCss, sourceFilePath } = options
20+
1521
const transformFileToCssModuleProcessor = createCssProcessor(postcssToCssModulePlugin())
1622
const transformedResult = await transformFileToCssModuleProcessor(sourceCss)
1723

1824
const { dir, name } = path.parse(sourceFilePath)
1925
const newFilePath = path.join(dir, `${name}.module.scss`)
2026

21-
// TODO: add option to write files.
2227
return {
2328
css: transformedResult.css,
2429
filePath: newFilePath,

src/utils/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isDefined<T>(argument: T | undefined): argument is T {
2+
return argument !== undefined
3+
}

0 commit comments

Comments
 (0)