From 3bb81a451b16f6d8442b95d2bd010e1e0743bb7c Mon Sep 17 00:00:00 2001 From: Jon Ambas Date: Tue, 9 Apr 2024 09:45:54 -0400 Subject: [PATCH] feat(transformer): add JS runtime transformer for @pandabox/unplugin (#21) --- .gitignore | 2 +- README.md | 47 +++++++++++++++--- src/__tests__/codegen.test.ts | 8 +-- src/__tests__/parser.test.ts | 4 +- src/__tests__/transformer.test.ts | 81 +++++++++++++++++++++++++++++++ src/codegen.ts | 8 +-- src/index.ts | 5 +- src/transform.ts | 21 ++++++++ 8 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 src/__tests__/transformer.test.ts create mode 100644 src/transform.ts diff --git a/.gitignore b/.gitignore index ef8291e..a85b3e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /node_modules /dist -/app +/apps /coverage \ No newline at end of file diff --git a/README.md b/README.md index e6ee41a..1a17f2f 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ Which will produce: --- -### Supported Syntax +### Supported syntax -This plugin supports aliasing to Panda's object syntax via a `value` key, just as you would define semantic tokens in Panda's theme. +This plugin supports aliasing to Panda's object syntax via a `value` key, just as you would define semantic tokens in Panda's theme. Anything Panda supports will work, including raw values. ```ts export default defineConfig({ @@ -84,7 +84,7 @@ export default defineConfig({ }, }, text: { - value: 'gray.100', + value: '#111', }, }, }), @@ -102,14 +102,45 @@ export default defineConfig({ Produces: ```html -
+
``` --- -### Alternatives +### Further optimization -There are alternatives to achieve the same result. +This plugin generates a performant JS runtime to map paths to their respective class names. This runtime can be completely removed using [@pandabox/unplugin](https://github.com/astahmer/pandabox/tree/main/packages/unplugin), with a transformer exported from this package. Your bundler's config will need to be modified to achieve this. -- Use Panda's `importMap` in config to reference your own alias to token mapping. -- Use `@pandabox/unplugin` to strip out and remove your own alias mapping at build time. +Example Next.js config: + +```js +import unplugin from '@pandabox/unplugin'; +import { transform } from 'panda-plugin-ct'; + +// Your token object +// This should be the same as the object you supplied to the Panda plugin +const tokens = {}; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + webpack: (config) => { + config.plugins.push( + unplugin.webpack({ + transform: transform(tokens), + optimizeJs: true, // Optional, this will replace other Panda runtime functions (css, cva, etc) + }), + ); + return config; + }, +}; + +export default nextConfig; +``` + +--- + +### Acknowledgement + +- [Jimmy](https://github.com/jimmymorris) – for the idea and motivation behind the plugin +- [Alex](https://github.com/astahmer) – for providing feedback with the plugin's internals and functionality diff --git a/src/__tests__/codegen.test.ts b/src/__tests__/codegen.test.ts index 9f3544e..8e1401a 100644 --- a/src/__tests__/codegen.test.ts +++ b/src/__tests__/codegen.test.ts @@ -30,10 +30,10 @@ describe('codegen', () => { "code": " const pluginCtMap = new Map([["foo.100","#fff"],["foo.200",{"base":"#000","lg":"#111"}],["bar.100","red"],["bar.200","blue"]]); - export const ct = (path) => { - if (!pluginCtMap.has(path)) return 'panda-plugin-ct_alias-not-found'; - return pluginCtMap.get(path); - };", + export const ct = (path) => { + if (!pluginCtMap.has(path)) return 'panda-plugin-ct_alias-not-found'; + return pluginCtMap.get(path); + };", "file": "ct.mjs", }, { diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index 2311d65..19e6326 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -15,6 +15,7 @@ export const makeParser = (content: string) => { describe('parser', () => { it('parses', () => { const res = makeParser(` + import foo from 'bar'; import { css, ct, cva } from '@/styled-system/css'; const styles = cva({ @@ -36,7 +37,8 @@ describe('parser', () => { expect(res).toMatchInlineSnapshot( ` - "import { css, ct, cva } from '@/styled-system/css'; + "import foo from 'bar'; + import { css, ct, cva } from '@/styled-system/css'; const styles = cva({ base: { diff --git a/src/__tests__/transformer.test.ts b/src/__tests__/transformer.test.ts new file mode 100644 index 0000000..1ef5cc2 --- /dev/null +++ b/src/__tests__/transformer.test.ts @@ -0,0 +1,81 @@ +import { transform } from '../transform'; +import { tokens } from './fixtures'; + +describe('transform', () => { + it('returns a function', () => { + expect(transform(tokens)).toBeTypeOf('function'); + }); + + it('replaces ct', () => { + expect( + transform(tokens)({ + configure: () => {}, + filePath: 'test.tsx', + content: ` + import { css, ct, cva } from '@/styled-system/css'; + + const styles = cva({ + base: { + // background: ct('foo.200'), + color: ct('bar.200'), + }, + }); + + export const Component = () => { + return (
); + `, + }), + ).toMatchInlineSnapshot(` + "import { css, ct, cva } from '@/styled-system/css'; + + const styles = cva({ + base: { + // background: ct('foo.200'), + color: 'blue', + }, + }); + + export const Component = () => { + return (
); + " + `); + }); + + it('skips without imports, expressions, content', () => { + expect( + transform(tokens)({ + configure: () => {}, + filePath: 'test.tsx', + content: `
`, + }), + ).toBeUndefined(); + + expect( + transform(tokens)({ + configure: () => {}, + filePath: 'test.tsx', + content: `import { ct } from '@/styled-system/css`, + }), + ).toBeUndefined(); + + expect( + transform(tokens)({ + configure: () => {}, + filePath: 'test.tsx', + content: ``, + }), + ).toBeUndefined(); + }); +}); diff --git a/src/codegen.ts b/src/codegen.ts index 7c13a0e..014ba82 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -26,10 +26,10 @@ export const codegen = ( const ctFile: ArtifactContent = { file: `ct.${ext}`, code: `${mapTemplate(map)} - export const ct = (path) => { - if (!pluginCtMap.has(path)) return 'panda-plugin-ct_alias-not-found'; - return pluginCtMap.get(path); - };`, + export const ct = (path) => { + if (!pluginCtMap.has(path)) return 'panda-plugin-ct_alias-not-found'; + return pluginCtMap.get(path); + };`, }; const ctDtsFile: ArtifactContent = { diff --git a/src/index.ts b/src/index.ts index 91c7afa..a9384c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,11 @@ import { codegen } from './codegen'; import { createContext } from './context'; import type { ComponentTokens } from './types'; import { makeMap } from './map'; +import { transform } from './transform'; /** + * 🐼 A Panda CSS plugin for design token aliases + * * @see https://github.com/jonambas/panda-plugin-ct */ const pluginComponentTokens = (tokens: ComponentTokens): PandaPlugin => { @@ -27,4 +30,4 @@ const pluginComponentTokens = (tokens: ComponentTokens): PandaPlugin => { }; }; -export { pluginComponentTokens, ComponentTokens }; +export { pluginComponentTokens, transform, type ComponentTokens }; diff --git a/src/transform.ts b/src/transform.ts new file mode 100644 index 0000000..55d8f7a --- /dev/null +++ b/src/transform.ts @@ -0,0 +1,21 @@ +import type { ParserResultBeforeHookArgs } from '@pandacss/types'; +import { createContext } from './context'; +import type { ComponentTokens } from './types'; +import { parser } from './parser'; + +/** + * Transformer for @pandabox/unplugin. + * Replaces JS runtime calls to `ct` with their resulting class names. + * + * @see https://github.com/jonambas/panda-plugin-ct + * @see https://github.com/astahmer/pandabox/tree/main/packages/unplugin + */ +export const transform = (tokens: ComponentTokens) => { + const context = createContext(tokens); + return (args: ParserResultBeforeHookArgs) => { + // This doesn't have `args.configure` + + if (!args.content) return; + return parser(args, context); + }; +};