refactor: goodbye ts-morph
This commit is contained in:
parent
42d6cfd252
commit
7033b4b02f
|
|
@ -1,14 +1,11 @@
|
||||||
import { promises as fs } from 'node:fs'
|
|
||||||
import { tmpdir } from 'node:os'
|
|
||||||
import path from 'pathe'
|
|
||||||
import { Project, ScriptKind, type SourceFile } from 'ts-morph'
|
|
||||||
import type * as z from 'zod'
|
import type * as z from 'zod'
|
||||||
|
import { transform as metaTransform } from 'vue-metamorph'
|
||||||
|
import { transformTwPrefix } from './transform-tw-prefix'
|
||||||
import type { Config } from '@/src/utils/get-config'
|
import type { Config } from '@/src/utils/get-config'
|
||||||
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
||||||
import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'
|
import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'
|
||||||
import { transformImport } from '@/src/utils/transformers/transform-import'
|
import { transformImport } from '@/src/utils/transformers/transform-import'
|
||||||
import { transformSFC } from '@/src/utils/transformers/transform-sfc'
|
import { transformSFC } from '@/src/utils/transformers/transform-sfc'
|
||||||
import { transformTwPrefixes } from '@/src/utils/transformers/transform-tw-prefix'
|
|
||||||
|
|
||||||
export interface TransformOpts {
|
export interface TransformOpts {
|
||||||
filename: string
|
filename: string
|
||||||
|
|
@ -17,38 +14,12 @@ export interface TransformOpts {
|
||||||
baseColor?: z.infer<typeof registryBaseColorSchema>
|
baseColor?: z.infer<typeof registryBaseColorSchema>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Transformer<Output = SourceFile> = (
|
|
||||||
opts: TransformOpts & {
|
|
||||||
sourceFile: SourceFile
|
|
||||||
}
|
|
||||||
) => Promise<Output>
|
|
||||||
|
|
||||||
const transformers: Transformer[] = [
|
|
||||||
transformCssVars,
|
|
||||||
transformImport,
|
|
||||||
transformTwPrefixes,
|
|
||||||
]
|
|
||||||
|
|
||||||
const project = new Project({
|
|
||||||
compilerOptions: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function createTempSourceFile(filename: string) {
|
|
||||||
const dir = await fs.mkdtemp(path.join(tmpdir(), 'shadcn-'))
|
|
||||||
return path.join(dir, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function transform(opts: TransformOpts) {
|
export async function transform(opts: TransformOpts) {
|
||||||
const tempFile = await createTempSourceFile(opts.filename)
|
const source = await transformSFC(opts)
|
||||||
const sourceFile = project.createSourceFile(tempFile, opts.raw, {
|
|
||||||
scriptKind: ScriptKind.Unknown,
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const transformer of transformers)
|
return metaTransform(source, opts.filename, [
|
||||||
transformer({ sourceFile, ...opts })
|
transformImport(opts),
|
||||||
|
transformCssVars(opts),
|
||||||
return await transformSFC({
|
transformTwPrefix(opts),
|
||||||
sourceFile,
|
]).code
|
||||||
...opts,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
import { transform as metaTransform } from 'vue-metamorph'
|
|
||||||
import type { AST, CodemodPlugin } from 'vue-metamorph'
|
|
||||||
import type * as z from 'zod'
|
|
||||||
import { splitClassName } from './transform-css-vars'
|
|
||||||
import type { Config } from '@/src/utils/get-config'
|
|
||||||
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
|
||||||
|
|
||||||
export interface TransformOpts {
|
|
||||||
filename: string
|
|
||||||
raw: string
|
|
||||||
config: Config
|
|
||||||
baseColor?: z.infer<typeof registryBaseColorSchema>
|
|
||||||
}
|
|
||||||
|
|
||||||
function transformTwPrefix(config: Config): CodemodPlugin {
|
|
||||||
return {
|
|
||||||
type: 'codemod',
|
|
||||||
name: 'change string literals to hello, world',
|
|
||||||
|
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
||||||
transform({ scriptASTs, sfcAST, styleASTs, filename, utils: { traverseScriptAST, traverseTemplateAST } }) {
|
|
||||||
// codemod plugins self-report the number of transforms it made
|
|
||||||
// this is only used to print the stats in CLI output
|
|
||||||
let transformCount = 0
|
|
||||||
|
|
||||||
// scriptASTs is an array of Program ASTs
|
|
||||||
// in a js/ts file, this array will only have one item
|
|
||||||
// in a vue file, this array will have one item for each <script> block
|
|
||||||
for (const scriptAST of scriptASTs) {
|
|
||||||
// traverseScriptAST is an alias for the ast-types 'visit' function
|
|
||||||
// see: https://github.com/benjamn/ast-types#ast-traversal
|
|
||||||
traverseScriptAST(scriptAST, {
|
|
||||||
visitLiteral(path) {
|
|
||||||
if (path.parent.value.type !== 'ImportDeclaration' && typeof path.node.value === 'string') {
|
|
||||||
// mutate the node
|
|
||||||
path.node.value = applyPrefix(path.node.value, config.tailwind.prefix)
|
|
||||||
transformCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.traverse(path)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sfcAST) {
|
|
||||||
// traverseTemplateAST is an alias for the vue-eslint-parser 'AST.traverseNodes' function
|
|
||||||
// see: https://github.com/vuejs/vue-eslint-parser/blob/master/src/ast/traverse.ts#L118
|
|
||||||
traverseTemplateAST(sfcAST, {
|
|
||||||
enterNode(node) {
|
|
||||||
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
||||||
if (!['BinaryExpression', 'Property'].includes(node.parent?.type ?? '')) {
|
|
||||||
node.value = applyPrefix(node.value, config.tailwind.prefix)
|
|
||||||
transformCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// handle class attribute without binding
|
|
||||||
else if (node.type === 'VLiteral' && typeof node.value === 'string') {
|
|
||||||
if (node.parent.key.name === 'class') {
|
|
||||||
node.value = `"${applyPrefix(node.value, config.tailwind.prefix)}"`
|
|
||||||
transformCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leaveNode() {
|
|
||||||
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformCount
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transform(opt: TransformOpts) {
|
|
||||||
return metaTransform(opt.raw, opt.filename, [transformTwPrefix(opt.config)]).code
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyPrefix(input: string, prefix: string = '') {
|
|
||||||
const classNames = input.split(' ')
|
|
||||||
const prefixed: string[] = []
|
|
||||||
for (const className of classNames) {
|
|
||||||
const [variant, value, modifier] = splitClassName(className)
|
|
||||||
if (variant) {
|
|
||||||
modifier
|
|
||||||
? prefixed.push(`${variant}:${prefix}${value}/${modifier}`)
|
|
||||||
: prefixed.push(`${variant}:${prefix}${value}`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
modifier
|
|
||||||
? prefixed.push(`${prefix}${value}/${modifier}`)
|
|
||||||
: prefixed.push(`${prefix}${value}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prefixed.join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyPrefixesCss(css: string, prefix: string) {
|
|
||||||
const lines = css.split('\n')
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes('@apply')) {
|
|
||||||
const originalTWCls = line.replace('@apply', '').trim()
|
|
||||||
const prefixedTwCls = applyPrefix(originalTWCls, prefix)
|
|
||||||
css = css.replace(originalTWCls, prefixedTwCls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return css
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +1,69 @@
|
||||||
import type * as z from 'zod'
|
import type * as z from 'zod'
|
||||||
import MagicString from 'magic-string'
|
import type { CodemodPlugin } from 'vue-metamorph'
|
||||||
import type { SFCTemplateBlock } from '@vue/compiler-sfc'
|
import type { TransformOpts } from '.'
|
||||||
import { parse } from '@vue/compiler-sfc'
|
|
||||||
import { SyntaxKind } from 'ts-morph'
|
|
||||||
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
||||||
import type { Transformer } from '@/src/utils/transformers'
|
|
||||||
|
|
||||||
export const transformCssVars: Transformer = async ({
|
export function transformCssVars(opts: TransformOpts): CodemodPlugin {
|
||||||
sourceFile,
|
return {
|
||||||
config,
|
type: 'codemod',
|
||||||
baseColor,
|
name: 'add prefix to tailwind classes',
|
||||||
}) => {
|
|
||||||
const isVueFile = sourceFile.getFilePath().endsWith('vue')
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
// No transform if using css variables.
|
transform({ scriptASTs, sfcAST, styleASTs, filename, utils: { traverseScriptAST, traverseTemplateAST } }) {
|
||||||
|
// codemod plugins self-report the number of transforms it made
|
||||||
|
// this is only used to print the stats in CLI output
|
||||||
|
let transformCount = 0
|
||||||
|
const { baseColor, config } = opts
|
||||||
if (config.tailwind?.cssVariables || !baseColor?.inlineColors)
|
if (config.tailwind?.cssVariables || !baseColor?.inlineColors)
|
||||||
return sourceFile
|
return transformCount
|
||||||
|
|
||||||
let template: SFCTemplateBlock | null = null
|
// scriptASTs is an array of Program ASTs
|
||||||
|
// in a js/ts file, this array will only have one item
|
||||||
if (isVueFile) {
|
// in a vue file, this array will have one item for each <script> block
|
||||||
const parsed = parse(sourceFile.getText())
|
for (const scriptAST of scriptASTs) {
|
||||||
template = parsed.descriptor.template
|
// traverseScriptAST is an alias for the ast-types 'visit' function
|
||||||
|
// see: https://github.com/benjamn/ast-types#ast-traversal
|
||||||
if (!template)
|
traverseScriptAST(scriptAST, {
|
||||||
return sourceFile
|
visitLiteral(path) {
|
||||||
|
if (path.parent.value.type !== 'ImportDeclaration' && typeof path.node.value === 'string') {
|
||||||
|
// mutate the node
|
||||||
|
path.node.value = applyColorMapping(path.node.value.replace(/"/g, ''), baseColor.inlineColors)
|
||||||
|
transformCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((node) => {
|
return this.traverse(path)
|
||||||
if (template && template.loc.start.offset >= node.getPos())
|
},
|
||||||
return sourceFile
|
|
||||||
|
|
||||||
const value = node.getText()
|
|
||||||
|
|
||||||
const hasClosingDoubleQuote = value.match(/"/g)?.length === 2
|
|
||||||
if (value.search('\'') === -1 && hasClosingDoubleQuote) {
|
|
||||||
const mapped = applyColorMapping(value.replace(/"/g, ''), baseColor.inlineColors)
|
|
||||||
node.replaceWithText(`"${mapped}"`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const s = new MagicString(value)
|
|
||||||
s.replace(/'(.*?)'/g, (substring) => {
|
|
||||||
return `'${applyColorMapping(substring.replace(/'/g, ''), baseColor.inlineColors)}'`
|
|
||||||
})
|
})
|
||||||
node.replaceWithText(s.toString())
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return sourceFile
|
if (sfcAST) {
|
||||||
|
// traverseTemplateAST is an alias for the vue-eslint-parser 'AST.traverseNodes' function
|
||||||
|
// see: https://github.com/vuejs/vue-eslint-parser/blob/master/src/ast/traverse.ts#L118
|
||||||
|
traverseTemplateAST(sfcAST, {
|
||||||
|
enterNode(node) {
|
||||||
|
if (node.type === 'Literal' && typeof node.value === 'string') {
|
||||||
|
if (!['BinaryExpression', 'Property'].includes(node.parent?.type ?? '')) {
|
||||||
|
node.value = applyColorMapping(node.value.replace(/"/g, ''), baseColor.inlineColors)
|
||||||
|
transformCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// handle class attribute without binding
|
||||||
|
else if (node.type === 'VLiteral' && typeof node.value === 'string') {
|
||||||
|
if (node.parent.key.name === 'class') {
|
||||||
|
node.value = applyColorMapping(node.value.replace(/"/g, ''), baseColor.inlineColors)
|
||||||
|
transformCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leaveNode() {
|
||||||
|
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformCount
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits a className into variant-name-alpha.
|
// Splits a className into variant-name-alpha.
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,56 @@
|
||||||
import type { Transformer } from '@/src/utils/transformers'
|
import type { CodemodPlugin } from 'vue-metamorph'
|
||||||
|
import type { TransformOpts } from '.'
|
||||||
|
import type { Config } from '@/src/utils/get-config'
|
||||||
|
|
||||||
export const transformImport: Transformer = async ({ sourceFile, config }) => {
|
export function transformImport(opts: TransformOpts): CodemodPlugin {
|
||||||
const importDeclarations = sourceFile.getImportDeclarations()
|
return {
|
||||||
|
type: 'codemod',
|
||||||
|
name: 'modify import based on user config',
|
||||||
|
|
||||||
for (const importDeclaration of importDeclarations) {
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
const moduleSpecifier = importDeclaration.getModuleSpecifierValue()
|
transform({ scriptASTs, sfcAST, styleASTs, filename, utils: { traverseScriptAST, traverseTemplateAST } }) {
|
||||||
|
// codemod plugins self-report the number of transforms it made
|
||||||
|
// this is only used to print the stats in CLI output
|
||||||
|
const transformCount = 0
|
||||||
|
const { config } = opts
|
||||||
|
|
||||||
|
// scriptASTs is an array of Program ASTs
|
||||||
|
// in a js/ts file, this array will only have one item
|
||||||
|
// in a vue file, this array will have one item for each <script> block
|
||||||
|
for (const scriptAST of scriptASTs) {
|
||||||
|
// traverseScriptAST is an alias for the ast-types 'visit' function
|
||||||
|
// see: https://github.com/benjamn/ast-types#ast-traversal
|
||||||
|
traverseScriptAST(scriptAST, {
|
||||||
|
visitImportDeclaration(path) {
|
||||||
|
if (typeof path.node.source.value === 'string') {
|
||||||
|
const sourcePath = path.node.source.value
|
||||||
|
|
||||||
// Replace @/lib/registry/[style] with the components alias.
|
// Replace @/lib/registry/[style] with the components alias.
|
||||||
if (moduleSpecifier.startsWith('@/lib/registry/')) {
|
if (sourcePath.startsWith('@/lib/registry/')) {
|
||||||
if (config.aliases.ui) {
|
if (config.aliases.ui) {
|
||||||
importDeclaration.setModuleSpecifier(
|
path.node.source.value = sourcePath.replace(/^@\/lib\/registry\/[^/]+\/ui/, config.aliases.ui)
|
||||||
moduleSpecifier.replace(/^@\/lib\/registry\/[^/]+\/ui/, config.aliases.ui),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
importDeclaration.setModuleSpecifier(
|
path.node.source.value = sourcePath.replace(/^@\/lib\/registry\/[^/]+/, config.aliases.components)
|
||||||
moduleSpecifier.replace(
|
|
||||||
/^@\/lib\/registry\/[^/]+/,
|
|
||||||
config.aliases.components,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace `import { cn } from "@/lib/utils"`
|
// Replace `import { cn } from "@/lib/utils"`
|
||||||
if (moduleSpecifier === '@/lib/utils') {
|
if (sourcePath === '@/lib/utils') {
|
||||||
const namedImports = importDeclaration.getNamedImports()
|
const namedImports = path.node.specifiers?.map(node => node.local?.name ?? '') ?? []
|
||||||
const cnImport = namedImports.find(i => i.getName() === 'cn')
|
// const namedImports = importDeclaration.getNamedImports()
|
||||||
|
const cnImport = namedImports.find(i => i === 'cn')
|
||||||
if (cnImport) {
|
if (cnImport) {
|
||||||
importDeclaration.setModuleSpecifier(
|
path.node.source.value = sourcePath.replace(/^@\/lib\/utils/, config.aliases.utils)
|
||||||
moduleSpecifier.replace(/^@\/lib\/utils/, config.aliases.utils),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return this.traverse(path)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return sourceFile
|
return transformCount
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import type { Transformer } from '@/src/utils/transformers'
|
import type { TransformOpts } from '.'
|
||||||
|
|
||||||
// required cause Error: Dynamic require of "@babel/core" is not supported
|
// required cause Error: Dynamic require of "@babel/core" is not supported
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
const { transform } = require('detype')
|
const { transform } = require('detype')
|
||||||
|
|
||||||
|
export async function transformSFC(opts: TransformOpts) {
|
||||||
|
if (opts.config?.typescript || opts.filename.includes('.ts'))
|
||||||
|
return opts.raw
|
||||||
|
|
||||||
|
return await transformByDetype(opts.raw, opts.filename).then(res => res as string)
|
||||||
|
}
|
||||||
|
|
||||||
export async function transformByDetype(content: string, filename: string) {
|
export async function transformByDetype(content: string, filename: string) {
|
||||||
return await transform(content, filename, {
|
return await transform(content, filename, {
|
||||||
removeTsComments: true,
|
removeTsComments: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const transformSFC: Transformer<string> = async ({ sourceFile, config, filename }) => {
|
|
||||||
const output = sourceFile?.getFullText()
|
|
||||||
if (config?.typescript)
|
|
||||||
return output
|
|
||||||
|
|
||||||
return await transformByDetype(output, filename)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,67 @@
|
||||||
import { SyntaxKind } from 'ts-morph'
|
import type { CodemodPlugin } from 'vue-metamorph'
|
||||||
import { MagicString, parse } from '@vue/compiler-sfc'
|
|
||||||
import type { SFCTemplateBlock } from '@vue/compiler-sfc'
|
|
||||||
import { splitClassName } from './transform-css-vars'
|
import { splitClassName } from './transform-css-vars'
|
||||||
import type { Transformer } from '@/src/utils/transformers'
|
import type { TransformOpts } from '.'
|
||||||
|
import type { Config } from '@/src/utils/get-config'
|
||||||
|
|
||||||
export const transformTwPrefixes: Transformer = async ({
|
export function transformTwPrefix(opts: TransformOpts): CodemodPlugin {
|
||||||
sourceFile,
|
return {
|
||||||
config,
|
type: 'codemod',
|
||||||
}) => {
|
name: 'add prefix to tailwind classes',
|
||||||
const isVueFile = sourceFile.getFilePath().endsWith('vue')
|
|
||||||
if (!config.tailwind?.prefix)
|
|
||||||
return sourceFile
|
|
||||||
|
|
||||||
let template: SFCTemplateBlock | null = null
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
transform({ scriptASTs, sfcAST, styleASTs, filename, utils: { traverseScriptAST, traverseTemplateAST } }) {
|
||||||
|
// codemod plugins self-report the number of transforms it made
|
||||||
|
// this is only used to print the stats in CLI output
|
||||||
|
let transformCount = 0
|
||||||
|
const { config } = opts
|
||||||
|
|
||||||
if (isVueFile) {
|
// scriptASTs is an array of Program ASTs
|
||||||
const parsed = parse(sourceFile.getText())
|
// in a js/ts file, this array will only have one item
|
||||||
template = parsed.descriptor.template
|
// in a vue file, this array will have one item for each <script> block
|
||||||
|
for (const scriptAST of scriptASTs) {
|
||||||
if (!template)
|
// traverseScriptAST is an alias for the ast-types 'visit' function
|
||||||
return sourceFile
|
// see: https://github.com/benjamn/ast-types#ast-traversal
|
||||||
|
traverseScriptAST(scriptAST, {
|
||||||
|
visitLiteral(path) {
|
||||||
|
if (path.parent.value.type !== 'ImportDeclaration' && typeof path.node.value === 'string') {
|
||||||
|
// mutate the node
|
||||||
|
path.node.value = applyPrefix(path.node.value, config.tailwind.prefix)
|
||||||
|
transformCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((node) => {
|
return this.traverse(path)
|
||||||
if (template && template.loc.start.offset >= node.getPos())
|
},
|
||||||
return sourceFile
|
|
||||||
|
|
||||||
const value = node.getText()
|
|
||||||
const attrName = sourceFile.getDescendantAtPos(node.getPos() - 2)?.getText()
|
|
||||||
if (isVueFile && attrName !== 'class')
|
|
||||||
return sourceFile
|
|
||||||
|
|
||||||
// Do not parse imported packages/files
|
|
||||||
if (node.getParent().getKind() === SyntaxKind.ImportDeclaration)
|
|
||||||
return
|
|
||||||
|
|
||||||
const hasClosingDoubleQuote = value.match(/"/g)?.length === 2
|
|
||||||
const hasFunction = value.startsWith('"cn(')
|
|
||||||
if (value.search('\'') === -1 && hasClosingDoubleQuote && !hasFunction) {
|
|
||||||
const mapped = applyPrefix(value.replace(/"/g, ''), config.tailwind.prefix)
|
|
||||||
node.replaceWithText(`"${mapped}"`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const s = new MagicString(value)
|
|
||||||
s.replace(/'(.*?)'/g, (substring) => {
|
|
||||||
return `'${applyPrefix(substring.replace(/'/g, ''), config.tailwind.prefix)}'`
|
|
||||||
})
|
})
|
||||||
node.replaceWithText(s.toString())
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return sourceFile
|
if (sfcAST) {
|
||||||
|
// traverseTemplateAST is an alias for the vue-eslint-parser 'AST.traverseNodes' function
|
||||||
|
// see: https://github.com/vuejs/vue-eslint-parser/blob/master/src/ast/traverse.ts#L118
|
||||||
|
traverseTemplateAST(sfcAST, {
|
||||||
|
enterNode(node) {
|
||||||
|
if (node.type === 'Literal' && typeof node.value === 'string') {
|
||||||
|
if (!['BinaryExpression', 'Property'].includes(node.parent?.type ?? '')) {
|
||||||
|
node.value = applyPrefix(node.value, config.tailwind.prefix)
|
||||||
|
transformCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// handle class attribute without binding
|
||||||
|
else if (node.type === 'VLiteral' && typeof node.value === 'string') {
|
||||||
|
if (node.parent.key.name === 'class') {
|
||||||
|
node.value = `"${applyPrefix(node.value, config.tailwind.prefix)}"`
|
||||||
|
transformCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leaveNode() {
|
||||||
|
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformCount
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyPrefix(input: string, prefix: string = '') {
|
export function applyPrefix(input: string, prefix: string = '') {
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,7 @@ exports[`transform css vars 3`] = `
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-white hover:bg-stone-100 dark:bg-stone-950 dark:hover:bg-stone-800',
|
'bg-white hover:bg-stone-100 dark:bg-stone-950 dark:hover:bg-stone-800',
|
||||||
true &&
|
true && 'text-stone-50 sm:focus:text-stone-900 dark:text-stone-900 dark:sm:focus:text-stone-50',
|
||||||
'text-stone-50 sm:focus:text-stone-900 dark:text-stone-900 dark:sm:focus:text-stone-50',
|
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,33 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`transform import 1`] = `
|
exports[`transform import 1`] = `
|
||||||
"import { Foo } from "bar" import { Button } from "@/components/ui/button" import
|
"import { Foo } from 'bar'
|
||||||
{ Label} from "ui/label" import { Box } from "@/components/box" import { cn }
|
import { Button } from '@/components/ui/button'
|
||||||
from "@/lib/utils"
|
import { Label} from 'ui/label'
|
||||||
|
import { Box } from '@/components/box'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`transform import 2`] = `
|
exports[`transform import 2`] = `
|
||||||
"import { Foo } from "bar" import { Button } from "~/src/components/ui/button"
|
"import { Foo } from 'bar'
|
||||||
import { Label} from "ui/label" import { Box } from "~/src/components/box"
|
import { Button } from '~/src/components/ui/button'
|
||||||
import { cn, foo, bar } from "~/lib" import { bar } from "@/lib/utils/bar"
|
import { Label} from 'ui/label'
|
||||||
|
import { Box } from '~/src/components/box'
|
||||||
|
|
||||||
|
import { cn, foo } from '~/lib'
|
||||||
|
import { bar } from '@/lib/utils/bar'
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`transform import 3`] = `
|
exports[`transform import 3`] = `
|
||||||
"import { Foo } from "bar" import { Button } from "~/src/components/ui/button"
|
"import { Foo } from 'bar'
|
||||||
import { Label} from "ui/label" import { Box } from "~/src/components/box"
|
import { Button } from '~/src/components/ui/button'
|
||||||
import { cn } from "~/src/utils" import { bar } from "@/lib/utils/bar"
|
import { Label} from 'ui/label'
|
||||||
|
import { Box } from '~/src/components/box'
|
||||||
|
|
||||||
|
import { cn } from '~/src/utils'
|
||||||
|
import { bar } from '@/lib/utils/bar'
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ export const testVariants = cva(
|
||||||
|
|
||||||
exports[`transform tailwind prefix 2`] = `
|
exports[`transform tailwind prefix 2`] = `
|
||||||
"<template>
|
"<template>
|
||||||
<div class="tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground">
|
<div
|
||||||
|
class="tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground"
|
||||||
|
>
|
||||||
foo
|
foo
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -30,7 +32,9 @@ exports[`transform tailwind prefix 2`] = `
|
||||||
|
|
||||||
exports[`transform tailwind prefix 3`] = `
|
exports[`transform tailwind prefix 3`] = `
|
||||||
"<template>
|
"<template>
|
||||||
<div class="tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground">
|
<div
|
||||||
|
class="tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground"
|
||||||
|
>
|
||||||
foo
|
foo
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -40,23 +44,28 @@ exports[`transform tailwind prefix 3`] = `
|
||||||
exports[`transform tailwind prefix 4`] = `
|
exports[`transform tailwind prefix 4`] = `
|
||||||
"<template>
|
"<template>
|
||||||
<div
|
<div
|
||||||
id="testing" v-bind="props" @click="handleSomething" :data-test="true"
|
id="testing"
|
||||||
|
v-bind="props"
|
||||||
|
@click="handleSomething"
|
||||||
|
:data-test="true"
|
||||||
class="tw-mt-4"
|
class="tw-mt-4"
|
||||||
:class="cn('tw-bg-background hover:tw-bg-muted', true && 'tw-text-primary-foreground sm:focus:tw-text-accent-foreground')"
|
|
||||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
buttonVariants({ variant: 'outline' }),
|
'tw-bg-background hover:tw-bg-muted',
|
||||||
props.class,
|
true && 'tw-text-primary-foreground sm:focus:tw-text-accent-foreground',
|
||||||
'tw-bg-background'
|
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||||
|
:class="
|
||||||
|
cn(buttonVariants({ variant: 'outline' }), props.class, 'tw-bg-background')
|
||||||
|
"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'tw-flex',
|
'tw-flex',
|
||||||
orientation === 'horizontal' ? 'tw--ml-4' : 'tw--mt-4 tw-flex-col',
|
orientation === 'horizontal' ? 'tw--ml-4' : 'tw--mt-4 tw-flex-col',
|
||||||
props.class,
|
props.class,
|
||||||
)"
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
foo
|
foo
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -4,7 +4,7 @@ import { transform } from '../../src/utils/transformers'
|
||||||
it('transform import', async () => {
|
it('transform import', async () => {
|
||||||
expect(
|
expect(
|
||||||
await transform({
|
await transform({
|
||||||
filename: 'app.vue',
|
filename: 'app.ts',
|
||||||
raw: `import { Foo } from "bar"
|
raw: `import { Foo } from "bar"
|
||||||
import { Button } from "@/lib/registry/new-york/ui/button"
|
import { Button } from "@/lib/registry/new-york/ui/button"
|
||||||
import { Label} from "ui/label"
|
import { Label} from "ui/label"
|
||||||
|
|
@ -27,13 +27,13 @@ it('transform import', async () => {
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await transform({
|
await transform({
|
||||||
filename: 'app.vue',
|
filename: 'app.ts',
|
||||||
raw: `import { Foo } from "bar"
|
raw: `import { Foo } from "bar"
|
||||||
import { Button } from "@/lib/registry/new-york/ui/button"
|
import { Button } from "@/lib/registry/new-york/ui/button"
|
||||||
import { Label} from "ui/label"
|
import { Label} from "ui/label"
|
||||||
import { Box } from "@/lib/registry/new-york/box"
|
import { Box } from "@/lib/registry/new-york/box"
|
||||||
|
|
||||||
import { cn, foo, bar } from "@/lib/utils"
|
import { cn, foo } from "@/lib/utils"
|
||||||
import { bar } from "@/lib/utils/bar"
|
import { bar } from "@/lib/utils/bar"
|
||||||
`,
|
`,
|
||||||
config: {
|
config: {
|
||||||
|
|
@ -47,7 +47,7 @@ it('transform import', async () => {
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await transform({
|
await transform({
|
||||||
filename: 'app.vue',
|
filename: 'app.ts',
|
||||||
raw: `import { Foo } from "bar"
|
raw: `import { Foo } from "bar"
|
||||||
import { Button } from "@/lib/registry/new-york/ui/button"
|
import { Button } from "@/lib/registry/new-york/ui/button"
|
||||||
import { Label} from "ui/label"
|
import { Label} from "ui/label"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { expect, it } from 'vitest'
|
import { expect, it } from 'vitest'
|
||||||
import { transform } from '../../src/utils/transformers/new'
|
import { transform } from '../../src/utils/transformers'
|
||||||
import { applyPrefixesCss } from '../../src/utils/transformers/transform-tw-prefix'
|
import { applyPrefixesCss } from '../../src/utils/transformers/transform-tw-prefix'
|
||||||
|
|
||||||
it('transform tailwind prefix', async () => {
|
it('transform tailwind prefix', async () => {
|
||||||
expect(
|
expect(
|
||||||
transform({
|
await transform({
|
||||||
filename: 'test.ts',
|
filename: 'test.ts',
|
||||||
raw: `import { cva } from "class-variance-authority"
|
raw: `import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ it('transform tailwind prefix', async () => {
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
transform({
|
await transform({
|
||||||
filename: 'app.vue',
|
filename: 'app.vue',
|
||||||
raw: `<template>
|
raw: `<template>
|
||||||
<div class="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">
|
<div class="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">
|
||||||
|
|
@ -59,7 +59,7 @@ it('transform tailwind prefix', async () => {
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
transform({
|
await transform({
|
||||||
filename: 'app.vue',
|
filename: 'app.vue',
|
||||||
raw: `<template>
|
raw: `<template>
|
||||||
<div class="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">
|
<div class="bg-background hover:bg-muted text-primary-foreground sm:focus:text-accent-foreground">
|
||||||
|
|
@ -83,7 +83,7 @@ it('transform tailwind prefix', async () => {
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
transform({
|
await transform({
|
||||||
filename: 'app.vue',
|
filename: 'app.vue',
|
||||||
raw: `<template>
|
raw: `<template>
|
||||||
<div
|
<div
|
||||||
Loading…
Reference in New Issue
Block a user