Skip to content

Commit

Permalink
fix(unplugin-macro): prevent transforming unrelated components (#45)
Browse files Browse the repository at this point in the history
* chore: add failing test

* fix(unplugin-macro): prevent transforming unrelated components
  • Loading branch information
astahmer authored Mar 28, 2024
1 parent 25fed76 commit 4af7ef7
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-yaks-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pandabox/unplugin-panda-macro": patch
---

Fix an issue where some unrelated components from Panda would be transformed due to having the same name as some Panda components (JSX Patterns like Stack)
61 changes: 61 additions & 0 deletions packages/unplugin-panda-macro/__tests__/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,67 @@ export const App = () => {
"
`)
})

test('ignore unrelated components', () => {
const ctx = createMacroContext({
root: '/',
conf: createConfigResult({}),
})
const { panda } = ctx
const code = `
import { Center, Flex as ActualFlex, styled } from './styled-system/jsx'
import 'virtual:panda.css'
const Stack = ({ children }: any) => <div data-testid="stack">stack{children}</div>
const Stack2 = ({ children }: any) => <div data-testid="stack">stack{children}</div>
export const App = () => {
return (
<Center>
<Stack fontSize="2xl">
<styled.div border="2px solid token(colors.red.300)">shouldnt be transformed</styled.div>
</Stack>
<Stack2 fontSize="2xl">
shouldnt be transformed
</Stack2>
<ActualFlex fontSize="2xl">
should be transformed
</ActualFlex>
</Center>
)
}
`

const sourceFile = panda.project.addSourceFile(id, code)
const parserResult = panda.project.parseSourceFile(id)

const result = tranformPanda(ctx, { code, id, output, sourceFile, parserResult })
expect(result?.code).toMatchInlineSnapshot(`
"
import { Center, Flex as ActualFlex, styled } from './styled-system/jsx'
import 'virtual:panda.css'
const Stack = ({ children }: any) => <div data-testid="stack">stack{children}</div>
const Stack2 = ({ children }: any) => <div data-testid="stack">stack{children}</div>
export const App = () => {
return (
<div className="d_flex items_center justify_center" >
<Stack fontSize="2xl">
<div className="border_2px_solid_token(colors.red.300)" >shouldnt be transformed</div>
</Stack>
<Stack2 fontSize="2xl">
shouldnt be transformed
</Stack2>
<div className="d_flex fs_2xl" >
should be transformed
</div>
</div>
)
}
"
`)
})
})

describe('grouped', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// https://github.com/chakra-ui/panda/blob/0bf09f214ec25ff3ea74b8e432bd10c7c9453805/packages/parser/src/get-import-declarations.ts
import { resolveTsPathPattern } from '@pandacss/config/ts-path'
import type { ImportResult, ParserOptions } from '@pandacss/core'
import type { SourceFile } from 'ts-morph'
import { getModuleSpecifierValue } from './get-module-specifier-value'
import { hasMacroAttribute } from './has-macro-attribute'

export function getImportDeclarations(context: ParserOptions, sourceFile: SourceFile, onlyMacroImports = false) {
const { imports, tsOptions } = context

const importDeclarations: ImportResult[] = []

sourceFile.getImportDeclarations().forEach((node) => {
const mod = getModuleSpecifierValue(node)
if (!mod) return
if (onlyMacroImports && !hasMacroAttribute(node)) return

// import { flex, stack } from "styled-system/patterns"
node.getNamedImports().forEach((specifier) => {
const name = specifier.getNameNode().getText()
const alias = specifier.getAliasNode()?.getText() || name

const result: ImportResult = { name, alias, mod, kind: 'named' }

const found = imports.match(result, (mod) => {
if (!tsOptions?.pathMappings) return
return resolveTsPathPattern(tsOptions.pathMappings, mod)
})

if (!found) return

importDeclarations.push(result)
})

// import * as p from "styled-system/patterns
const namespace = node.getNamespaceImport()
if (namespace) {
const name = namespace.getText()
const result: ImportResult = { name, alias: name, mod, kind: 'namespace' }

const found = imports.match(result, (mod) => {
if (!tsOptions?.pathMappings) return
return resolveTsPathPattern(tsOptions.pathMappings, mod)
})

if (!found) return

importDeclarations.push(result)
}
})

return importDeclarations
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ImportDeclaration } from 'ts-morph'

export const getModuleSpecifierValue = (node: ImportDeclaration) => {
try {
return node.getModuleSpecifierValue()
} catch {
return
}
}
23 changes: 23 additions & 0 deletions packages/unplugin-panda-macro/src/plugin/has-macro-attribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ImportDeclaration } from 'ts-morph'
import { Node } from 'ts-morph'

export const hasMacroAttribute = (node: ImportDeclaration) => {
const attrs = node.getAttributes()
if (!attrs) return

const elements = attrs.getElements()
if (!elements.length) return

return elements.some((n) => {
const name = n.getName()
if (name === 'type') {
const value = n.getValue()
if (!Node.isStringLiteral(value)) return

const type = value.getLiteralText()
if (type === 'macro') {
return true
}
}
})
}
65 changes: 14 additions & 51 deletions packages/unplugin-panda-macro/src/plugin/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type MacroContext } from './create-context'
import { createCva } from './create-cva'
import { getVariableName } from './get-cva-var-name'
import { combineResult } from './unbox-combine-result'
import { getImportDeclarations } from './get-import-declarations'

export interface TransformOptions {
/**
Expand Down Expand Up @@ -60,7 +61,12 @@ export const tranformPanda = (ctx: MacroContext, options: TransformArgs) => {

const s = new MagicString(code)

const importMap = onlyMacroImports ? mapIdentifierToImport(sourceFile) : new Map<string, ImportDeclaration>()
const importDeclarations = getImportDeclarations(panda.parserOptions, sourceFile, onlyMacroImports)
const importSet = new Set(importDeclarations.map((i) => i.alias))
const file = panda.imports.file(importDeclarations)

const jsxPatternKeys = panda.patterns.details.map((d) => d.jsxName)
const isJsxPatternImported = file['createMatch'](file['importMap'].jsx, jsxPatternKeys) as (id: string) => boolean

/**
* Hash atomic styles and inline the resulting className
Expand Down Expand Up @@ -100,8 +106,7 @@ export const tranformPanda = (ctx: MacroContext, options: TransformArgs) => {
return
}

const importNode = importMap.get(identifier)
if (!importNode) return
if (!importSet.has(identifier)) return
}

if (result.type?.includes('jsx')) {
Expand All @@ -110,6 +115,12 @@ export const tranformPanda = (ctx: MacroContext, options: TransformArgs) => {

const tagName = node.getTagNameNode().getText()

const isJsxPattern = panda.patterns.details.find((node) => node.jsxName === tagName)
if (isJsxPattern && !isJsxPatternImported(tagName)) return

const isPandaComponent = file.isPandaComponent(tagName)
if (!isPandaComponent) return

// we don't care about `xxx.div` but we do care about `styled.div`
if (result.type === 'jsx-factory' && !tagName.includes(factoryName + '.')) {
return
Expand Down Expand Up @@ -339,51 +350,3 @@ const extractCvaUsages = (sourceFile: SourceFile, cvaNames: Set<string>) => {

return cvaUsages
}

const getModuleSpecifierValue = (node: ImportDeclaration) => {
try {
return node.getModuleSpecifierValue()
} catch {
return
}
}

const hasMacroAttribute = (node: ImportDeclaration) => {
const attrs = node.getAttributes()
if (!attrs) return

const elements = attrs.getElements()
if (!elements.length) return

return elements.some((n) => {
const name = n.getName()
if (name === 'type') {
const value = n.getValue()
if (!Node.isStringLiteral(value)) return

const type = value.getLiteralText()
if (type === 'macro') {
return true
}
}
})
}

const mapIdentifierToImport = (sourceFile: SourceFile) => {
const map = new Map<string, ImportDeclaration>()
const imports = sourceFile.getImportDeclarations()

imports.forEach((node) => {
const mod = getModuleSpecifierValue(node)
if (!mod) return
if (!hasMacroAttribute(node)) return

node.getNamedImports().forEach((specifier) => {
const name = specifier.getNameNode().getText()
const alias = specifier.getAliasNode()?.getText() || name
map.set(alias, node)
})
})

return map
}

0 comments on commit 4af7ef7

Please sign in to comment.