Skip to content

Commit 300266f

Browse files
committed
split styled2panda, update workflow, add more use-case, rm flags wrapping, fix configs
1 parent c9f867a commit 300266f

17 files changed

+263
-87
lines changed

.changeset/five-carrots-speak.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@pandabox/codemods": patch
3+
"@pandabox/define-recipe": patch
4+
"@pandabox/define-theme": patch
5+
"@pandabox/utils": patch
6+
---
7+
8+
bump versions & publish new packages

.github/workflows/publish.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ jobs:
2929
run: pnpm install --frozen-lockfile --ignore-scripts
3030

3131
- name: Typecheck
32-
run: pnpm define-theme typecheck
32+
run: pnpm -r typecheck
33+
34+
- name: Tests
35+
run: pnpm -r test -- --run
3336

3437
- name: Build
35-
run: pnpm define-theme build
38+
run: pnpm -r build
3639

3740
- name: Create Release Pull Request or Publish to npm
3841
id: changesets
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { test, expect } from 'vitest'
2+
import { styled2panda } from '../src/styled2panda'
3+
import { Project } from 'ts-morph'
4+
import outdent from 'outdent'
5+
6+
const project = new Project()
7+
const createSourceFile = (content: string) => project.createSourceFile('app.tsx', content, { overwrite: true })
8+
9+
test('import transform', () => {
10+
const sourceFile = createSourceFile(outdent`
11+
import styled from 'styled-components'
12+
`)
13+
14+
expect(styled2panda({ sourceFile }).code).toMatchInlineSnapshot(`"import { styled } from '../styled-system/jsx'"`)
15+
})
16+
17+
test('component auto className references', () => {
18+
const sourceFile = createSourceFile(outdent`
19+
import styled from 'styled-components'
20+
21+
const Link = styled.a\`
22+
background: papayawhip;
23+
color: #bf4f74;
24+
\`
25+
26+
const Icon = styled.svg\`
27+
width: 48px;
28+
height: 48px;
29+
30+
\${Link}:hover & {
31+
fill: rebeccapurple;
32+
}
33+
\`
34+
`)
35+
36+
expect(styled2panda({ sourceFile }).code).toMatchInlineSnapshot(`
37+
"import { styled } from '../styled-system/jsx'
38+
39+
const Link = styled('a', { base: {
40+
"background": "papayawhip",
41+
"color": "#bf4f74"
42+
} }, { defaultProps: { className: 'Link' } })
43+
44+
const Icon = styled('svg', { base: {
45+
"width": "48px",
46+
"height": "48px",
47+
".Link:hover &": {
48+
"fill": "rebeccapurple"
49+
}
50+
} }, { defaultProps: { className: 'Icon' } })"
51+
`)
52+
})
53+
54+
test('composition', () => {
55+
const sourceFile = createSourceFile(outdent`
56+
"import styled from 'styled-components'
57+
58+
// The Button from the last section without the interpolations
59+
const Button = styled.button\`
60+
color: #BF4F74;
61+
font-size: 1em;
62+
margin: 1em;
63+
padding: 0.25em 1em;
64+
border: 2px solid #BF4F74;
65+
border-radius: 3px;
66+
\`;
67+
68+
// A new component based on Button, but with some override styles
69+
const TomatoButton = styled(Button)\`
70+
color: tomato;
71+
border-color: tomato;
72+
\`;
73+
`)
74+
75+
expect(styled2panda({ sourceFile }).code).toMatchInlineSnapshot(`
76+
""import styled from 'styled-components'
77+
78+
// The Button from the last section without the interpolations
79+
const Button = styled('button', { base: {
80+
"color": "#BF4F74",
81+
"fontSize": "1em",
82+
"margin": "1em",
83+
"padding": "0.25em 1em",
84+
"border": "2px solid #BF4F74",
85+
"borderRadius": "3px"
86+
} }, { defaultProps: { className: 'Button' } });
87+
88+
// A new component based on Button, but with some override styles
89+
const TomatoButton = styled(Button, { base: {
90+
"color": "tomato",
91+
"borderColor": "tomato"
92+
} }, { defaultProps: { className: 'TomatoButton' } });"
93+
`)
94+
})

packages/codemods/__tests__/template-to-object-syntax.test.ts

+1-38
Original file line numberDiff line numberDiff line change
@@ -33,43 +33,6 @@ test('simple', () => {
3333
"@media (min-width: 768px)": {
3434
"padding": "1rem 2rem"
3535
}
36-
} }, { defaultProps: { className: 'Button' } })"
37-
`)
38-
})
39-
40-
test('component auto className references', () => {
41-
const sourceFile = createSourceFile(outdent`
42-
import styled from 'styled-components'
43-
44-
const Link = styled.a\`
45-
background: papayawhip;
46-
color: #bf4f74;
47-
\`
48-
49-
const Icon = styled.svg\`
50-
width: 48px;
51-
height: 48px;
52-
53-
\${Link}:hover & {
54-
fill: rebeccapurple;
55-
}
56-
\`
57-
`)
58-
59-
expect(templateLiteralToObjectSyntax({ sourceFile }).code).toMatchInlineSnapshot(`
60-
"import styled from 'styled-components'
61-
62-
const Link = styled('a', { base: {
63-
"background": "papayawhip",
64-
"color": "#bf4f74"
65-
} }, { defaultProps: { className: 'Link' } })
66-
67-
const Icon = styled('svg', { base: {
68-
"width": "48px",
69-
"height": "48px",
70-
".Link:hover &": {
71-
"fill": "rebeccapurple"
72-
}
73-
} }, { defaultProps: { className: 'Icon' } })"
36+
} })"
7437
`)
7538
})

packages/codemods/package.json

+34-8
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
"version": "0.0.0",
44
"description": "A collection of codemods for Panda CSS",
55
"exports": {
6-
"./template-to-object-syntax": {
7-
"source": "./src/template-to-object-syntax.ts",
8-
"types": "./dist/template-to-object-syntax.d.ts",
6+
".": {
7+
"source": "./src/index.ts",
8+
"types": "./dist/index.d.ts",
99
"import": {
10-
"types": "./dist/template-to-object-syntax.d.mts",
11-
"default": "./dist/template-to-object-syntax.mjs"
10+
"types": "./dist/index.d.mts",
11+
"default": "./dist/index.mjs"
1212
},
1313
"require": {
14-
"types": "./dist/template-to-object-syntax.d.ts",
15-
"default": "./dist/template-to-object-syntax.js"
14+
"types": "./dist/index.d.ts",
15+
"default": "./dist/index.js"
1616
}
1717
}
1818
},
@@ -32,5 +32,31 @@
3232
"outdent": "^0.8.0",
3333
"tsup": "^8.0.1",
3434
"vitest": "^1.2.1"
35-
}
35+
},
36+
"homepage": "https://astahmer.dev",
37+
"repository": {
38+
"type": "git",
39+
"url": "git+https://github.com/astahmer/pandabox.git",
40+
"directory": "packages/codemods"
41+
},
42+
"author": "Alexandre Stahmer",
43+
"publishConfig": {
44+
"access": "public"
45+
},
46+
"sideEffects": false,
47+
"files": [
48+
"src",
49+
"dist"
50+
],
51+
"keywords": [
52+
"pandacss",
53+
"pandabox",
54+
"panda",
55+
"kit",
56+
"codemod",
57+
"template-literal",
58+
"styled2panda",
59+
"typesafety",
60+
"typescript"
61+
]
3662
}

packages/codemods/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './styled2panda'
2+
export * from './template-to-object-syntax'

packages/codemods/src/styled2panda.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import MagicString from 'magic-string'
2+
import postcss, { CssSyntaxError } from 'postcss'
3+
import postcssJs from 'postcss-js'
4+
import { Node } from 'ts-morph'
5+
import { getTemplateText, type TemplateToObjectSyntaxOptions } from './template-to-object-syntax'
6+
7+
export interface Styled2PandaOptions extends TemplateToObjectSyntaxOptions {
8+
withClassName?: boolean
9+
moduleSpecifier?: string
10+
factoryName?: string
11+
}
12+
13+
export const styled2panda = (options: Styled2PandaOptions) => {
14+
const { sourceFile, matchTag } = options
15+
16+
const withClassName = options.withClassName ?? true
17+
const factoryName = options.factoryName ?? 'styled'
18+
const importFrom = options.moduleSpecifier ?? 'styled-components'
19+
20+
const sourceText = sourceFile.getText()
21+
const s = new MagicString(sourceText)
22+
23+
sourceFile.forEachDescendant((node) => {
24+
if (Node.isImportDeclaration(node)) {
25+
const moduleSpecifier = node.getModuleSpecifier()
26+
const importPath = moduleSpecifier.getText()
27+
if (!importPath.includes(importFrom)) return
28+
29+
s.update(node.getStart(), node.getEnd(), `import { ${factoryName} } from '../styled-system/jsx'`)
30+
return
31+
}
32+
33+
if (Node.isTaggedTemplateExpression(node)) {
34+
const tagName = node.getTag().getText()
35+
if (matchTag && !matchTag(tagName)) return
36+
37+
const templateText = getTemplateText(node)
38+
39+
try {
40+
const variableDecl = node.getParent()
41+
const obj = postcssJs.objectify(postcss.parse(templateText.slice(1, -1).trim()))
42+
const json = JSON.stringify(obj, null, 2)
43+
44+
let factory, tag
45+
// styled.div`...`
46+
if (tagName.includes('.')) {
47+
const split = tagName.split('.')
48+
factory = split[0]
49+
tag = `'${split[1]}'`
50+
} else if (tagName.includes('(')) {
51+
// styled(Button)`...`
52+
const split = tagName.split('(')
53+
factory = split[0]
54+
tag = split[1].slice(0, -1)
55+
}
56+
57+
let transform: string
58+
if (withClassName && Node.isVariableDeclaration(variableDecl)) {
59+
const identifier = variableDecl.getNameNode().getText()
60+
transform = `${factory}(${tag}, { base: ${json} }, { defaultProps: { className: '${identifier}' } })`
61+
} else {
62+
transform = `${factory}(${tag}, { base: ${json} })`
63+
}
64+
65+
s.update(node.getStart(), node.getEnd(), transform)
66+
} catch (error) {
67+
if (error instanceof CssSyntaxError) {
68+
console.error(error.showSourceCode(true))
69+
}
70+
71+
throw error
72+
}
73+
}
74+
})
75+
76+
return {
77+
code: s.toString(),
78+
map: s.generateMap({ hires: true }),
79+
}
80+
}

packages/codemods/src/template-to-object-syntax.ts

+7-15
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@ import postcss, { CssSyntaxError } from 'postcss'
33
import postcssJs from 'postcss-js'
44
import { Node, SourceFile, TaggedTemplateExpression, TemplateSpan } from 'ts-morph'
55

6-
interface TemplateToObjectSyntaxOptions {
6+
export interface TemplateToObjectSyntaxOptions {
77
sourceFile: SourceFile
88
matchTag?: (tag: string) => boolean
9-
flags?: {
10-
withClassName?: boolean
11-
}
129
}
1310

1411
export const templateLiteralToObjectSyntax = (options: TemplateToObjectSyntaxOptions) => {
1512
const { sourceFile, matchTag } = options
16-
const withClassName = options.flags?.withClassName ?? true
1713

1814
const sourceText = sourceFile.getText()
1915
const s = new MagicString(sourceText)
@@ -27,18 +23,11 @@ export const templateLiteralToObjectSyntax = (options: TemplateToObjectSyntaxOpt
2723
const templateText = getTemplateText(node)
2824

2925
try {
30-
const variableDecl = node.getParent()
3126
const obj = postcssJs.objectify(postcss.parse(templateText.slice(1, -1).trim()))
3227
const json = JSON.stringify(obj, null, 2)
3328
const [factory, tag] = tagName.split('.')
3429

35-
let transform: string
36-
if (withClassName && Node.isVariableDeclaration(variableDecl)) {
37-
const identifier = variableDecl.getNameNode().getText()
38-
transform = `${factory}('${tag}', { base: ${json} }, { defaultProps: { className: '${identifier}' } })`
39-
} else {
40-
transform = `${factory}('${tag}', { base: ${json} })`
41-
}
30+
const transform = `${factory}('${tag}', { base: ${json} })`
4231

4332
s.update(node.getStart(), node.getEnd(), transform)
4433
} catch (error) {
@@ -56,7 +45,10 @@ export const templateLiteralToObjectSyntax = (options: TemplateToObjectSyntaxOpt
5645
}
5746
}
5847

59-
const getTemplateText = (node: TaggedTemplateExpression) => {
48+
/**
49+
* Get the text content of a TaggedTemplateExpression without the backticks and ${xxx} references
50+
*/
51+
export const getTemplateText = (node: TaggedTemplateExpression) => {
6052
const template = node.getTemplate()
6153
if (Node.isNoSubstitutionTemplateLiteral(template)) {
6254
return template.getText().slice(1, -1) // Remove the backticks
@@ -84,7 +76,7 @@ const getTemplateText = (node: TaggedTemplateExpression) => {
8476
* Transform TemplateSpan Identifier to className reference
8577
* e.g. `${identifier}` -> `.identifier`
8678
*/
87-
function transformTemplateSpan(span: TemplateSpan) {
79+
export const transformTemplateSpan = (span: TemplateSpan) => {
8880
const expr = span.getExpression()
8981
if (Node.isIdentifier(expr)) {
9082
const literal = span.getLiteral().getText()

packages/utils/package.json

-6
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@
1717
}
1818
},
1919
"scripts": {
20-
"trace-dir": "pnpm tsc -p tsconfig.bench.json --extendedDiagnostics --generateTrace trace-dir",
21-
"analyze": "analyze-trace trace-dir --forceMillis=50 --skipMillis=50",
22-
"simplify": "simplify-trace-types trace-dir/types.json trace-dir/simple.json",
23-
"report": "tsx ./scripts/trace-report.ts",
24-
"trace": "pnpm trace-dir && pnpm analyze && pnpm simplify && pnpm report",
25-
"bench": "tsx ./sandbox/define-theme.bench.ts",
2620
"build": "tsup",
2721
"test": "vitest",
2822
"typecheck": "tsc --noEmit"
File renamed without changes.

website/app/components/input.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ark } from '@ark-ui/react/factory'
2+
import type { ComponentProps } from 'react'
3+
import { styled } from '#styled-system/jsx'
4+
import { input } from '#styled-system/recipes'
5+
6+
export const Input = styled(ark.input, input)
7+
export type InputProps = ComponentProps<typeof Input>

website/app/playground/initial-input.ts

+9
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,12 @@ const Icon = styled.svg`
1313
fill: rebeccapurple;
1414
}
1515
`
16+
17+
const TomatoButton = styled(Link)`
18+
color: tomato;
19+
border-color: tomato;
20+
`
21+
22+
const Thingy = styled('div')`
23+
font-size: 1.5em;
24+
`

0 commit comments

Comments
 (0)