Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transformer): add JS runtime transformer for @pandabox/unplugin #21

Merged
merged 3 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/node_modules
/dist
/app
/apps
/coverage
47 changes: 39 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -84,7 +84,7 @@ export default defineConfig({
},
},
text: {
value: 'gray.100',
value: '#111',
},
},
}),
Expand All @@ -102,14 +102,45 @@ export default defineConfig({
Produces:

```html
<div class="bg_red.500 lg:bg_blue.500 text_gray.100" />
<div class="bg_red.500 lg:bg_blue.500 text_#111" />
```

---

### 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
8 changes: 4 additions & 4 deletions src/__tests__/codegen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand Down
4 changes: 3 additions & 1 deletion src/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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: {
Expand Down
81 changes: 81 additions & 0 deletions src/__tests__/transformer.test.ts
Original file line number Diff line number Diff line change
@@ -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 (<div
className={
css({
bg: ct('foo.200'),
color: ct('bar.100')
})}
/>);
`,
}),
).toMatchInlineSnapshot(`
"import { css, ct, cva } from '@/styled-system/css';

const styles = cva({
base: {
// background: ct('foo.200'),
color: 'blue',
},
});

export const Component = () => {
return (<div
className={
css({
bg: {"base":"#000","lg":"#111"},
color: 'red'
})}
/>);
"
`);
});

it('skips without imports, expressions, content', () => {
expect(
transform(tokens)({
configure: () => {},
filePath: 'test.tsx',
content: `<div className={css({ bg: ct("foo.200") })/>`,
}),
).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();
});
});
8 changes: 4 additions & 4 deletions src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -27,4 +30,4 @@ const pluginComponentTokens = (tokens: ComponentTokens): PandaPlugin => {
};
};

export { pluginComponentTokens, ComponentTokens };
export { pluginComponentTokens, transform, type ComponentTokens };
21 changes: 21 additions & 0 deletions src/transform.ts
Original file line number Diff line number Diff line change
@@ -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);
};
};