Skip to content

Commit

Permalink
feat: onlyMacroImports
Browse files Browse the repository at this point in the history
  • Loading branch information
astahmer committed Feb 20, 2024
1 parent 16b17f0 commit e99bf0e
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 3 deletions.
14 changes: 14 additions & 0 deletions .changeset/short-trains-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@pandabox/unplugin-panda-macro': patch
---

Allow only inlining macro imports

```ts
import { css } from '../styled-system/css' with { type: 'macro' }
// ^^^^^^^^^^^^^^^^^^^^
// without this, the plugin will not transform the `css` usage

const className = css({ display: 'flex', flexDirection: 'column', color: 'red.300' })
// -> `const className = 'd_flex flex_column text_red.300'`
```
19 changes: 18 additions & 1 deletion packages/unplugin-panda-macro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ Make your `styled-system` disappear at build-time by inlining the results as cla
- [x] cva `const xxx = cva({ ... })`
- [x] recipes `button({ ... })`
- [x] JSX styled factory `styled.div({ ... })` / `styled('div', { ... })`
- [x] any JSX pattern like `<Box />`, `<Stack />` etc
- [x] any function or JSX pattern like `box()` / `<Box />`, `stack()` / `<Stack />` etc

> ⚠️ Avoid [anything dynamic](https://panda-css.com/docs/guides/dynamic-styling) as usual, if not more, with Panda CSS
> due to static analysis limitations.
> [Runtime conditions](https://panda-css.com/docs/guides/dynamic-styling#runtime-conditions) will NOT be transformed
You can even choose to inline as `atomic` or `grouped` class names.

Expand Down Expand Up @@ -146,6 +151,18 @@ type PluginOptions = {
* Do not transform Panda recipes to `atomic` or `grouped` and instead keep their defaults BEM-like classes
*/
keepRecipeClassNames?: boolean
/**
* Only transform macro imports
* @example
* ```ts
* import { css } from '../styled-system/css' with { type: "macro" }
*
* const className = css({ display: "flex", flexDirection: "column", color: "red.300" })
* // -> `const className = 'd_flex flex_column text_red.300'`
* ```
*
*/
onlyMacroImports?: boolean
}
````

Expand Down
82 changes: 82 additions & 0 deletions packages/unplugin-panda-macro/__tests__/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,88 @@ describe('atomic', () => {
`)
})

test('transform css only when using `with` import attribute', () => {
const ctx = createMacroContext({
root: '/',
conf: createConfigResult({}),
})
const { panda } = ctx
const code = `import 'virtual:panda.css'
import { box } from '../styled-system/patterns' with { type: "macro" }
import { box as box2 } from '../styled-system/patterns' with { type: "macro" }
import { box as box3 } from '../styled-system/patterns'
import { box as box4 } from '../styled-system/patterns' with { type: "invalid-macro" }
import { box as box5 } from '../styled-system/patterns' with { invalid: "macro" }
box({ display: 'flex' });
box2({ flexDirection: 'column' });
box3({ fontWeight: 'semibold' });
box4({ color: 'green.300' });
box5({ textAlign: 'center' });
box6({ textStyle: '4xl' });`

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

const result = tranformPanda(ctx, { code, id, output, sourceFile, parserResult, onlyMacroImports: true })
expect(result?.code).toMatchInlineSnapshot(`
"import 'virtual:panda.css'
import { box } from '../styled-system/patterns' with { type: "macro" }
import { box as box2 } from '../styled-system/patterns' with { type: "macro" }
import { box as box3 } from '../styled-system/patterns'
import { box as box4 } from '../styled-system/patterns' with { type: "invalid-macro" }
import { box as box5 } from '../styled-system/patterns' with { invalid: "macro" }
"d_flex";
"flex_column";
box3({ fontWeight: 'semibold' });
box4({ color: 'green.300' });
box5({ textAlign: 'center' });
box6({ textStyle: '4xl' });"
`)
})

test('transform css only when using with', () => {
const ctx = createMacroContext({
root: '/',
conf: createConfigResult({}),
})
const { panda } = ctx
const code = `import 'virtual:panda.css'
import { css } from '../styled-system/css' with { type: "macro" }
import { css as css2 } from '../styled-system/css' with { type: "macro" }
import { css as css3 } from '../styled-system/css'
import { css as css4 } from '../styled-system/css' with { type: "invalid-macro" }
import { css as css5 } from '../styled-system/css' with { invalid: "macro" }
css({ display: 'flex' });
css2({ flexDirection: 'column' });
css3({ fontWeight: 'semibold' });
css4({ color: 'green.300' });
css5({ textAlign: 'center' });
css6({ textStyle: '4xl' });`

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

const result = tranformPanda(ctx, { code, id, output, sourceFile, parserResult, onlyMacroImports: true })
expect(result?.code).toMatchInlineSnapshot(`
"import 'virtual:panda.css'
import { css } from '../styled-system/css' with { type: "macro" }
import { css as css2 } from '../styled-system/css' with { type: "macro" }
import { css as css3 } from '../styled-system/css'
import { css as css4 } from '../styled-system/css' with { type: "invalid-macro" }
import { css as css5 } from '../styled-system/css' with { invalid: "macro" }
"d_flex";
css2({ flexDirection: 'column' });
css3({ fontWeight: 'semibold' });
css4({ color: 'green.300' });
css5({ textAlign: 'center' });
css6({ textStyle: '4xl' });"
`)
})

test('unwrap css raw', () => {
const ctx = createMacroContext({
root: '/',
Expand Down
1 change: 1 addition & 0 deletions packages/unplugin-panda-macro/src/plugin/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,6 @@ const resolveOptions = (options: PluginOptions): RequiredBy<PluginOptions, 'cwd'
output: options?.output || 'atomic',
optimizeCss: options.optimizeCss ?? true,
keepRecipeClassNames: options.keepRecipeClassNames ?? false,
onlyMacroImports: options.onlyMacroImports ?? false,
}
}
84 changes: 82 additions & 2 deletions packages/unplugin-panda-macro/src/plugin/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { box, extractCallExpressionArguments, unbox } from '@pandacss/extractor'
import { toHash } from '@pandacss/shared'
import { type ParserResultInterface, type SystemStyleObject, type RecipeConfig } from '@pandacss/types'
import MagicString from 'magic-string'
import { CallExpression, Node, SourceFile } from 'ts-morph'
import { CallExpression, ImportDeclaration, Node, SourceFile } from 'ts-morph'

import { type MacroContext } from './create-context'
import { createCva } from './create-cva'
Expand All @@ -30,6 +30,18 @@ export interface TransformOptions {
* Do not transform Panda recipes to `atomic` or `grouped` and instead keep their defaults BEM-like classes
*/
keepRecipeClassNames?: boolean
/**
* Only transform macro imports
* @example
* ```ts
* import { css } from '../styled-system/css' with { type: "macro" }
*
* const className = css({ display: "flex", flexDirection: "column", color: "red.300" })
* // -> `const className = 'd_flex flex_column text_red.300'`
* ```
*
*/
onlyMacroImports?: boolean
}

export interface TransformArgs extends TransformOptions {
Expand All @@ -40,14 +52,16 @@ export interface TransformArgs extends TransformOptions {
}

export const tranformPanda = (ctx: MacroContext, options: TransformArgs) => {
const { code, id, output = 'atomic', keepRecipeClassNames, sourceFile, parserResult } = options
const { code, id, output = 'atomic', keepRecipeClassNames, onlyMacroImports, sourceFile, parserResult } = options
if (!parserResult) return null

const { panda, css, mergeCss, sheet, styles } = ctx
const factoryName = panda.jsx.factoryName || 'styled'

const s = new MagicString(code)

const importMap = onlyMacroImports ? mapIdentifierToImport(sourceFile) : new Map<string, ImportDeclaration>()

/**
* Hash atomic styles and inline the resulting className
*/
Expand All @@ -72,6 +86,24 @@ export const tranformPanda = (ctx: MacroContext, options: TransformArgs) => {
const node = result.box.getNode()
const fnName = result.name

// Early return if we only want to transform macro imports
// and the current result is not coming from one
if (onlyMacroImports) {
if (!fnName) return

let identifier: string | undefined
if ((result.type?.includes('jsx') && Node.isJsxOpeningElement(node)) || Node.isJsxSelfClosingElement(node)) {
identifier = node.getTagNameNode().getText()
} else if (Node.isCallExpression(node)) {
identifier = node.getExpression().getText()
} else {
return
}

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

if (result.type?.includes('jsx')) {
const isJsx = Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node)
if (!isJsx) return
Expand Down Expand Up @@ -307,3 +339,51 @@ 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 e99bf0e

Please sign in to comment.