Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add clsx as option for className #189

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const program = new Command()
interface CodemodCliOptions extends TransformOptions {
write: boolean
format: boolean
transform: string
transform: string,
classname: 'classnames' | 'clsx'
}

const PROJECT_ROOT = path.resolve(__dirname, '../../../')
Expand All @@ -26,6 +27,7 @@ program
// TODO: make it `true` by default after switching to fast bulk format.
.option('-f, --format [format]', 'Format Typescript source files with ESLint', false)
.option('-w, --write [write]', 'Persist codemod changes to the filesystem', false)
.option('-c, --classname <classname>', 'Select className wrapper', 'classnames')
.option('-t, --transform <transform>', 'Absolute or relative to project root path to a transform module')
.argument('<fileGlob>', 'Absolute or relative to project root file glob to change files based on')
.allowUnknownOption(true)
Expand All @@ -40,7 +42,8 @@ program
)
.action(async (commandArgument: string, options: CodemodCliOptions) => {
const { fileGlob, transformOptions } = parseOptions(commandArgument)
const { write: shouldWriteFiles, format: shouldFormat, transform } = options
const { write: shouldWriteFiles, format: shouldFormat, transform, classname: rawClassname } = options
const classname = ['classnames', 'clsx'].includes(rawClassname) ? rawClassname : 'classnames';

const projectGlob = path.isAbsolute(fileGlob) ? fileGlob : path.join(PROJECT_ROOT, fileGlob)
const transformPath = path.isAbsolute(transform) ? transform : path.join(PROJECT_ROOT, transform)
Expand All @@ -61,6 +64,7 @@ program
shouldWriteFiles,
shouldFormat,
transformOptions,
classname
}

const results = await (codemod as Codemod)(codemodContext)
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface CodemodContext<T extends TransformOptions = TransformOptions> {
shouldWriteFiles?: boolean
/** If `true` format Typescript source files with `prettier-eslint`. */
shouldFormat?: boolean
classname: 'classnames' | 'clsx'
}

export interface CodemodResultFile {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { createSourceFile } from '@sourcegraph/codemod-toolkit-ts'
import {
CLASSNAMES_IDENTIFIER,
CLASSNAMES_MODULE_SPECIFIER,
CLSX_IDENTIFIER,
wrapIntoClassNamesUtility,
addClassNamesUtilImportIfNeeded,
addClsxUtilImportIfNeeded,
wrapIntoClsxUtility,
} from '..'

describe('`classNames` helpers', () => {
Expand All @@ -20,6 +23,15 @@ describe('`classNames` helpers', () => {
})
})

describe('wrapIntoClsxUtility', () => {
it('wraps arguments into `clsx` function call', () => {
const classNamesArguments = [factory.createStringLiteral('first'), factory.createStringLiteral('second')]
const classNamesCall = wrapIntoClsxUtility(classNamesArguments)

expect(printNode(classNamesCall)).toEqual(`${CLSX_IDENTIFIER}("first", "second")`)
})
})

describe('addClassNamesUtilImportIfNeeded', () => {
const classNamesImport = `import ${CLASSNAMES_IDENTIFIER} from '${CLASSNAMES_MODULE_SPECIFIER}'`

Expand Down Expand Up @@ -52,4 +64,38 @@ describe('`classNames` helpers', () => {
expect(sourceFile.getText().includes(CLASSNAMES_MODULE_SPECIFIER)).toBe(false)
})
})

describe('addClsxUtilImportIfNeeded', () => {
const clsxImport = `import ${CLSX_IDENTIFIER} from '${CLSX_IDENTIFIER}'`

it('adds `clsx` import if needed', () => {
const { sourceFile } = createSourceFile('<div className={clsx("wow")} />')
const hasClsxImtmport = () => {
console.log(sourceFile.getText());
return sourceFile.getText().includes(clsxImport)
}

expect(hasClsxImtmport()).toBe(false)
addClsxUtilImportIfNeeded(sourceFile)
expect(hasClsxImtmport()).toBe(true)
})

it("doesn't duplicate `clsx` import", () => {
const { sourceFile } = createSourceFile('<div className={clsx("wow")} />')

addClsxUtilImportIfNeeded(sourceFile)
addClsxUtilImportIfNeeded(sourceFile)

const clsxMatches = sourceFile.getText().match(new RegExp(clsxImport, 'g'))
expect(clsxMatches?.length).toEqual(1)
})

it("doesn't add `clsx` import if `clsx` util is not used", () => {
const { sourceFile } = createSourceFile('<div className="wow" />')

addClsxUtilImportIfNeeded(sourceFile)

expect(sourceFile.getText().includes(CLSX_IDENTIFIER)).toBe(false)
})
})
})
37 changes: 37 additions & 0 deletions packages/toolkit-packages/src/classNames/classNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,40 @@ export function addClassNamesUtilImportIfNeeded(sourceFile: SourceFile): void {
export function isClassNamesCallExpression(node?: Node): node is CallExpression {
return Node.isCallExpression(node) && isImportedFromModule(node.getExpression(), CLASSNAMES_MODULE_SPECIFIER)
}

export const CLSX_IDENTIFIER = 'clsx'

// Wraps an array of arguments in a `clsx` function call.
export function wrapIntoClsxUtility(classNames: ts.Expression[]): ts.CallExpression {
return ts.factory.createCallExpression(ts.factory.createIdentifier(CLSX_IDENTIFIER), undefined, classNames)
}

// Adds `clsx` import to the `sourceFile` if `classNames` util is used and import doesn't exist.
export function addClsxUtilImportIfNeeded(sourceFile: SourceFile): void {
addOrUpdateImportIfIdentifierIsUsed({
sourceFile,
importStructure: {
defaultImport: CLSX_IDENTIFIER,
moduleSpecifier: CLSX_IDENTIFIER,
},
})
}

interface Utility {
wrapper: (classNames: ts.Expression[]) => ts.CallExpression,
importer: (sourceFile: SourceFile) => void,
identifier: string
}

export const utilities: Record<'clsx' | 'classnames', Utility> = {
clsx: {
wrapper: wrapIntoClsxUtility,
importer: addClsxUtilImportIfNeeded,
identifier: CLSX_IDENTIFIER
},
classnames: {
wrapper: wrapIntoClassNamesUtility,
importer: addClassNamesUtilImportIfNeeded,
identifier: CLASSNAMES_IDENTIFIER
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('globalCssToCssModule', () => {
it('transforms correctly', async () => {
const project = new Project()
project.addSourceFilesAtPaths(TARGET_FILE)
const [{ files }] = await globalCssToCssModule({ project, shouldFormat: true })
const [{ files }] = await globalCssToCssModule({ project, shouldFormat: true, classname: 'classnames' })

expect(files).toBeTruthy()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import signale from 'signale'
import { Codemod } from '@sourcegraph/codemod-cli'
import { isDefined } from '@sourcegraph/codemod-common'
import { formatWithStylelint } from '@sourcegraph/codemod-toolkit-css'
import { addClassNamesUtilImportIfNeeded } from '@sourcegraph/codemod-toolkit-packages'
import { utilities } from '@sourcegraph/codemod-toolkit-packages'
import { formatWithPrettierEslint } from '@sourcegraph/codemod-toolkit-ts'

import { getCssModuleExportNameMap } from './postcss/getCssModuleExportNameMap'
Expand All @@ -26,7 +26,7 @@ import { transformComponentFile } from './ts/transformComponentFile'
*
*/
export const globalCssToCssModule: Codemod = context => {
const { project, shouldWriteFiles, shouldFormat } = context
const { project, shouldWriteFiles, shouldFormat, classname } = context
const fs = project.getFileSystem()

/**
Expand Down Expand Up @@ -76,8 +76,10 @@ export const globalCssToCssModule: Codemod = context => {
sourceFilePath: cssFilePath,
})

transformComponentFile({ tsSourceFile, exportNameMap, cssModuleFileName })
addClassNamesUtilImportIfNeeded(tsSourceFile)
transformComponentFile({ tsSourceFile, exportNameMap, cssModuleFileName, classname })

utilities[classname].importer(tsSourceFile)

tsSourceFile.addImportDeclaration({
defaultImport: STYLES_IDENTIFIER,
moduleSpecifier: `./${path.parse(cssModuleFileName).base}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('getClassNameNodeReplacement', () => {
parentNode,
exportNameReferences,
leftOverClassName,
classname: 'classnames',
...options,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('processNodesWithClassName', () => {
processNodesWithClassName({
usageStats: {},
nodesWithClassName: getNodesWithClassName(sourceFile),
classname: 'classnames',
exportNameMap: {
kek: 'kek',
'kek--wow': 'kekWow',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { ts, NodeParentType, SyntaxKind } from 'ts-morph'

import { wrapIntoClassNamesUtility, CLASSNAMES_IDENTIFIER } from '@sourcegraph/codemod-toolkit-packages'
import { utilities } from '@sourcegraph/codemod-toolkit-packages'

export interface GetClassNameNodeReplacementOptions {
parentNode: NodeParentType<ts.StringLiteral>
leftOverClassName: string
exportNameReferences: ts.PropertyAccessExpression[]
exportNameReferences: ts.PropertyAccessExpression[],
classname: 'classnames' | 'clsx'
}

function getClassNameNodeReplacementWithoutBraces(
options: GetClassNameNodeReplacementOptions
): ts.PropertyAccessExpression | ts.CallExpression | ts.Expression[] {
const { leftOverClassName, exportNameReferences, parentNode } = options
const { leftOverClassName, exportNameReferences, parentNode, classname } = options

const isInClassnamesCall =
ts.isCallExpression(parentNode.compilerNode) &&
parentNode.compilerNode.expression.getText() === CLASSNAMES_IDENTIFIER
parentNode.compilerNode.expression.getText() === utilities[classname].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)}
Expand All @@ -30,7 +31,7 @@ function getClassNameNodeReplacementWithoutBraces(
return classNamesCallArguments
}

return wrapIntoClassNamesUtility(classNamesCallArguments)
return utilities[classname].wrapper(classNamesCallArguments)
}

// Replace one class with the `exportName`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ProcessNodesWithClassNameOptions {
nodesWithClassName: (Identifier | StringLiteral)[]
exportNameMap: Record<string, string>
usageStats: Record<string, boolean>
classname: 'classnames' | 'clsx'
}

/**
Expand Down Expand Up @@ -41,7 +42,7 @@ interface ProcessNodesWithClassNameOptions {
* @returns areAllNodesProcessed: boolean
*/
export function processNodesWithClassName(options: ProcessNodesWithClassNameOptions): boolean {
const { nodesWithClassName, exportNameMap, usageStats } = options
const { nodesWithClassName, exportNameMap, usageStats, classname } = options

for (const nodeWithClassName of nodesWithClassName) {
const classNameStringValue =
Expand All @@ -68,6 +69,7 @@ export function processNodesWithClassName(options: ProcessNodesWithClassNameOpti
parentNode: nodeWithClassName.getParent(),
leftOverClassName: leftOverClassnames.join(' '),
exportNameReferences,
classname
})

if (result.isParentTransformed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ interface TransformComponentFileOptions {
tsSourceFile: SourceFile
exportNameMap: Record<string, string>
cssModuleFileName: string
classname: 'classnames' | 'clsx'
}

export function transformComponentFile(options: TransformComponentFileOptions): void {
const { tsSourceFile, exportNameMap, cssModuleFileName } = options
const { tsSourceFile, exportNameMap, cssModuleFileName, classname } = options

// Object to collect CSS classes usage and report unused classes after the codemod.
const usageStats = Object.fromEntries(
Expand All @@ -30,6 +31,7 @@ export function transformComponentFile(options: TransformComponentFileOptions):
usageStats,
exportNameMap,
nodesWithClassName: getNodesWithClassName(tsSourceFile),
classname
})
}

Expand Down