Skip to content

Commit de955f5

Browse files
committed
feat: vue styled plugin
0 parents  commit de955f5

40 files changed

+11688
-0
lines changed

.github/workflows/release.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Release
2+
on:
3+
push:
4+
branches:
5+
- main
6+
- rc
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
permissions:
14+
contents: write
15+
id-token: write
16+
issues: write
17+
pull-requests: write
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Using pnpm
26+
uses: pnpm/action-setup@v3
27+
with:
28+
version: 10.6.2
29+
30+
- name: Setup Node
31+
uses: actions/setup-node@v4
32+
with:
33+
node-version: 22
34+
cache: pnpm
35+
36+
- name: Install dependencies
37+
run: pnpm install
38+
39+
- name: Check
40+
run: pnpm test & pnpm lint
41+
42+
- name: Build
43+
run: pnpm build
44+
45+
- name: Publish
46+
env:
47+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
49+
run: pnpm release

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
dist

.releaserc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"branches": [
3+
"+([0-9])?(.{+([0-9]),x}).x",
4+
"main",
5+
{
6+
"name": "rc",
7+
"prerelease": true
8+
}
9+
],
10+
"plugins": [
11+
"@semantic-release/commit-analyzer",
12+
"@semantic-release/release-notes-generator",
13+
"@semantic-release/changelog",
14+
[
15+
"@semantic-release/npm",
16+
{
17+
"npmPublish": true
18+
}
19+
],
20+
[
21+
"@semantic-release/git",
22+
{
23+
"assets": [
24+
"package.json",
25+
"CHANGELOG.md"
26+
],
27+
"message": "chore(release): v${nextRelease.version} \n\n${nextRelease.notes}"
28+
}
29+
],
30+
[
31+
"@semantic-release/github",
32+
{
33+
"addReleases": "top",
34+
"successComment": ":tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release](<github_release_url>)"
35+
}
36+
]
37+
]
38+
}

.vscode/settings.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"typescript.tsdk": "node_modules/typescript/lib",
3+
// Disable the default formatter
4+
"prettier.enable": false,
5+
"editor.formatOnSave": false,
6+
7+
// Auto fix
8+
"editor.codeActionsOnSave": {
9+
"source.fixAll.eslint": "explicit"
10+
},
11+
12+
// Silent the stylistic rules in you IDE, but still auto fix them
13+
"eslint.rules.customizations": [
14+
{ "rule": "@stylistic", "severity": "off" },
15+
{ "rule": "*-indent", "severity": "off" },
16+
{ "rule": "*-spacing", "severity": "off" },
17+
{ "rule": "*-spaces", "severity": "off" },
18+
{ "rule": "*-order", "severity": "off" },
19+
{ "rule": "*-dangle", "severity": "off" },
20+
{ "rule": "*-newline", "severity": "off" },
21+
{ "rule": "*quotes", "severity": "off" },
22+
{ "rule": "*semi", "severity": "off" }
23+
],
24+
25+
"eslint.validate": [
26+
"javascript",
27+
"javascriptreact",
28+
"typescript",
29+
"typescriptreact",
30+
"vue",
31+
"html",
32+
"markdown",
33+
"json",
34+
"jsonc",
35+
"yaml"
36+
]
37+
}

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Vue Styled Components TypeScript Syntax Plugin
2+
3+
A Vite plugin that provides TypeScript generic syntax support for Vue Styled Components, similar to React styled-components.
4+
5+
> [!IMPORTANT]
6+
> The current plugin is not fully mature and may not completely support complex TypeScript types.
7+
8+
## Features
9+
10+
- Allows using the `styled.tag<Props>` syntax instead of the original required `styled('tag', { props })` syntax
11+
- Automatically transforms at compile time with no runtime performance impact
12+
13+
## Installation
14+
15+
```bash
16+
# npm
17+
npm install @vue-styled-components/plugin --save-dev
18+
19+
# yarn
20+
yarn add @vue-styled-components/plugin --dev
21+
22+
# pnpm
23+
pnpm add @vue-styled-components/plugin -D
24+
```
25+
26+
## Usage
27+
28+
### Add the plugin to your Vite config
29+
30+
```ts
31+
// vite.config.ts
32+
import { defineConfig } from 'vite'
33+
import vue from '@vitejs/plugin-vue'
34+
import vueJsx from '@vitejs/plugin-vue-jsx'
35+
import vueStyled from '@vue-styled-components/plugin'
36+
37+
export default defineConfig({
38+
plugins: [
39+
// Make sure to use it before vue and vueJsx plugins
40+
vueStyled(),
41+
vue(),
42+
vueJsx(),
43+
],
44+
})
45+
```
46+
47+
### Configuration Options
48+
49+
```ts
50+
vueStyled({
51+
// Included file extensions, default is ['.vue', '.tsx', '.jsx', '.ts', '.js']
52+
include: ['.vue', '.tsx', '.jsx', '.ts', '.js'],
53+
54+
// Excluded file paths, default is ['node_modules']
55+
exclude: ['node_modules'],
56+
57+
// Enable debug mode
58+
debug: false,
59+
60+
// Log level: 'error' | 'warn' | 'info' | 'debug' | 'none'
61+
logLevel: 'error',
62+
63+
// Enable type caching for better performance
64+
enableCache: true,
65+
})
66+
```
67+
68+
## Examples
69+
70+
### Using Generic Syntax
71+
72+
```tsx
73+
import styled from '@vue-styled-components/core'
74+
75+
interface IconProps {
76+
color?: string
77+
size?: number
78+
}
79+
80+
// Using the new generic syntax
81+
const Icon = styled.span<IconProps>`
82+
color: ${props => props.color || 'currentColor'};
83+
font-size: ${props => `${props.size || 16}px`};
84+
`
85+
86+
// Equivalent to the original syntax
87+
const IconOriginal = styled('span', {
88+
color: String,
89+
size: Number,
90+
})`
91+
color: ${props => props.color || 'currentColor'};
92+
font-size: ${props => `${props.size || 16}px`};
93+
`
94+
```
95+
96+
## How It Works
97+
98+
The plugin intercepts source code during the compilation phase, uses an AST parser to analyze the code, searches for `styled.tag<Props>` patterns, and transforms them into the `styled('tag', Props)` format supported by Vue Styled Components.
99+
100+
## License
101+
102+
Alpha-2.0

__tests__/basic.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { transformStyledSyntax } from '../src/ts-transformer'
3+
import { extractPropsFromCode } from './normalize'
4+
5+
describe('基本语法转换', () => {
6+
it('应该转换简单的 styled.tag<Props> 为 styled("tag", { primary: { type: Boolean, required: false } })', () => {
7+
const code = `
8+
import styled from '@vue-styled-components/core'
9+
10+
interface ButtonProps {
11+
primary?: boolean
12+
}
13+
14+
const Button = styled.button<ButtonProps>\`
15+
background-color: \${props => props.primary ? 'blue' : 'white'};
16+
\`
17+
`
18+
19+
const result = transformStyledSyntax(code, 'test.tsx')
20+
21+
expect(result).not.toBeNull()
22+
23+
const props = extractPropsFromCode(result?.props?.[0], 'Button')
24+
expect(props).toHaveProperty('primary')
25+
expect(props.primary.type).toBe('Boolean')
26+
expect(props.primary.required).toBe(false)
27+
})
28+
29+
it('应该转换行内泛型定义', () => {
30+
const code = `
31+
import styled from '@vue-styled-components/core'
32+
33+
const Link = styled.a<{ active: boolean; disabled?: boolean }>\`
34+
color: \${props => props.active ? 'red' : 'blue'};
35+
opacity: \${props => props.disabled ? 0.5 : 1};
36+
\`
37+
`
38+
39+
const result = transformStyledSyntax(code, 'test.tsx')
40+
41+
expect(result).not.toBeNull()
42+
43+
const props = extractPropsFromCode(result?.props?.[0], 'Link')
44+
expect(props).toHaveProperty('active')
45+
expect(props).toHaveProperty('disabled')
46+
expect(props.active.type).toBe('Boolean')
47+
expect(props.active.required).toBe(true)
48+
expect(props.disabled.type).toBe('Boolean')
49+
expect(props.disabled.required).toBe(false)
50+
})
51+
52+
it('不应该转换没有泛型的样式组件', () => {
53+
const code = `
54+
import styled from '@vue-styled-components/core'
55+
56+
const Button = styled.button\`
57+
background-color: blue;
58+
\`
59+
`
60+
61+
const result = transformStyledSyntax(code, 'test.tsx')
62+
63+
expect(result).toBeNull()
64+
})
65+
66+
it('不应该转换原始的函数调用语法', () => {
67+
const code = `
68+
import styled from '@vue-styled-components/core'
69+
70+
const Button = styled('button', {
71+
primary: Boolean,
72+
})\`
73+
background-color: \${props => props.primary ? 'blue' : 'white'};
74+
\`
75+
`
76+
77+
const result = transformStyledSyntax(code, 'test.tsx')
78+
79+
expect(result).toBeNull()
80+
})
81+
82+
it('应该处理同一文件中的多个样式组件', () => {
83+
const code = `
84+
import styled from '@vue-styled-components/core'
85+
86+
interface ButtonProps {
87+
primary?: boolean
88+
}
89+
90+
interface IconProps {
91+
size?: number
92+
}
93+
94+
const Button = styled.button<ButtonProps>\`
95+
background-color: \${props => props.primary ? 'blue' : 'white'};
96+
\`
97+
98+
const Icon = styled.span<IconProps>\`
99+
font-size: \${props => \`\${props.size || 16}px\`};
100+
\`
101+
`
102+
103+
const result = transformStyledSyntax(code, 'test.tsx')
104+
105+
expect(result).not.toBeNull()
106+
107+
// 检查第一个组件
108+
const buttonProps = extractPropsFromCode(result?.props?.[0], 'Button')
109+
expect(buttonProps).toHaveProperty('primary')
110+
expect(buttonProps.primary.type).toBe('Boolean')
111+
expect(buttonProps.primary.required).toBe(false)
112+
113+
// 检查第二个组件
114+
const iconProps = extractPropsFromCode(result?.props?.[1], 'Icon')
115+
expect(iconProps).toHaveProperty('size')
116+
expect(iconProps.size.type).toBe('Number')
117+
expect(iconProps.size.required).toBe(false)
118+
})
119+
})

0 commit comments

Comments
 (0)