diff --git a/.changeset/short-trains-train.md b/.changeset/short-trains-train.md new file mode 100644 index 000000000..b542fcc2e --- /dev/null +++ b/.changeset/short-trains-train.md @@ -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'` +``` diff --git a/packages/unplugin-panda-macro/README.md b/packages/unplugin-panda-macro/README.md index 586bed390..361282783 100644 --- a/packages/unplugin-panda-macro/README.md +++ b/packages/unplugin-panda-macro/README.md @@ -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 ``, `` etc +- [x] any function or JSX pattern like `box()` / ``, `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. @@ -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 } ```` diff --git a/packages/unplugin-panda-macro/__tests__/transform.test.ts b/packages/unplugin-panda-macro/__tests__/transform.test.ts index cb92bb849..d93381cc4 100644 --- a/packages/unplugin-panda-macro/__tests__/transform.test.ts +++ b/packages/unplugin-panda-macro/__tests__/transform.test.ts @@ -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: '/', diff --git a/packages/unplugin-panda-macro/src/plugin/core.ts b/packages/unplugin-panda-macro/src/plugin/core.ts index e1840219d..030925f7d 100644 --- a/packages/unplugin-panda-macro/src/plugin/core.ts +++ b/packages/unplugin-panda-macro/src/plugin/core.ts @@ -100,5 +100,6 @@ const resolveOptions = (options: PluginOptions): RequiredBy `const className = 'd_flex flex_column text_red.300'` + * ``` + * + */ + onlyMacroImports?: boolean } export interface TransformArgs extends TransformOptions { @@ -40,7 +52,7 @@ 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 @@ -48,6 +60,8 @@ export const tranformPanda = (ctx: MacroContext, options: TransformArgs) => { const s = new MagicString(code) + const importMap = onlyMacroImports ? mapIdentifierToImport(sourceFile) : new Map() + /** * Hash atomic styles and inline the resulting className */ @@ -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 @@ -307,3 +339,51 @@ const extractCvaUsages = (sourceFile: SourceFile, cvaNames: Set) => { 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() + 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 +}