Skip to content

Commit a6de4c0

Browse files
committed
feat: basic generic plugin
1 parent fae497e commit a6de4c0

24 files changed

+4576
-43
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# TypeScript 泛型语法插件
2+
3+
Vue Styled Components 提供了一个 Vite 插件,使得开发者可以使用类似 React styled-components 的 TypeScript 泛型语法。
4+
5+
## 安装
6+
7+
```bash
8+
# npm
9+
npm install @vue-styled-components/plugin-typescript-syntax --save-dev
10+
11+
# yarn
12+
yarn add @vue-styled-components/plugin-typescript-syntax --dev
13+
14+
# pnpm
15+
pnpm add @vue-styled-components/plugin-typescript-syntax -D
16+
```
17+
18+
## 配置
19+
20+
在 Vite 配置文件中添加插件:
21+
22+
```ts
23+
// vite.config.ts
24+
import { defineConfig } from 'vite'
25+
import vue from '@vitejs/plugin-vue'
26+
import vueJsx from '@vitejs/plugin-vue-jsx'
27+
import vueStyledComponentsTypescriptSyntax from '@vue-styled-components/plugin-typescript-syntax'
28+
29+
export default defineConfig({
30+
plugins: [
31+
// 确保在 vue 和 vueJsx 插件之前使用
32+
vueStyledComponentsTypescriptSyntax(),
33+
vue(),
34+
vueJsx(),
35+
],
36+
})
37+
```
38+
39+
## 使用方法
40+
41+
### 传统语法
42+
43+
在没有插件的情况下,你需要使用以下语法定义带有 props 的样式化组件:
44+
45+
```tsx
46+
import styled from '@vue-styled-components/core'
47+
48+
// 定义 props
49+
const buttonProps = {
50+
primary: Boolean,
51+
size: String,
52+
}
53+
54+
// 创建样式化组件
55+
const Button = styled('button', buttonProps)`
56+
background-color: ${props => props.primary ? 'blue' : 'white'};
57+
padding: ${props => props.size === 'large' ? '12px 24px' : '8px 16px'};
58+
`
59+
```
60+
61+
### 泛型语法
62+
63+
使用插件后,你可以使用更简洁的泛型语法:
64+
65+
```tsx
66+
import styled from '@vue-styled-components/core'
67+
68+
// 使用 TypeScript 接口定义 props
69+
interface ButtonProps {
70+
primary?: boolean
71+
size?: 'small' | 'medium' | 'large'
72+
}
73+
74+
// 使用泛型语法创建样式化组件
75+
const Button = styled.button<ButtonProps>`
76+
background-color: ${props => props.primary ? 'blue' : 'white'};
77+
padding: ${props => {
78+
switch (props.size) {
79+
case 'small': return '4px 8px'
80+
case 'large': return '12px 24px'
81+
default: return '8px 16px'
82+
}
83+
}};
84+
`
85+
```
86+
87+
## 支持的高级用法
88+
89+
插件支持各种复杂的泛型用例,包括:
90+
91+
### 行内类型定义
92+
93+
```tsx
94+
const Link = styled.a<{ active: boolean; disabled?: boolean }>`
95+
color: ${props => props.active ? 'red' : 'blue'};
96+
opacity: ${props => props.disabled ? 0.5 : 1};
97+
`
98+
```
99+
100+
### 嵌套泛型
101+
102+
```tsx
103+
interface BaseProps<T> {
104+
value?: T
105+
onChange?: (value: T) => void
106+
}
107+
108+
interface InputProps<T = string> extends BaseProps<T> {
109+
placeholder?: string
110+
disabled?: boolean
111+
}
112+
113+
const Input = styled.input<InputProps<string>>`
114+
border: 1px solid #ccc;
115+
opacity: ${props => props.disabled ? 0.5 : 1};
116+
`
117+
```
118+
119+
### 多行泛型定义
120+
121+
```tsx
122+
const ComplexButton = styled.button<
123+
BaseProps &
124+
SizeProps & {
125+
fullWidth?: boolean
126+
outlined?: boolean
127+
}
128+
>`
129+
width: ${props => props.fullWidth ? '100%' : 'auto'};
130+
padding: 8px 16px;
131+
`
132+
```
133+
134+
### 复杂类型结构
135+
136+
```tsx
137+
type ButtonVariant<T extends Theme> = T extends 'dark'
138+
? { background: 'black'; color: 'white' }
139+
: { background: 'white'; color: 'black' }
140+
141+
interface ComplexButtonProps<T extends Theme = 'light'> {
142+
variant?: T
143+
style?: ButtonVariant<T>
144+
}
145+
146+
const ThemeButton = styled.button<ComplexButtonProps<'dark'>>`
147+
background-color: black;
148+
color: white;
149+
`
150+
```
151+
152+
### 特殊字符和模板字符串
153+
154+
```tsx
155+
const Button = styled.button<{
156+
color: '#fff' | '#000' | `rgba(${number}, ${number}, ${number}, ${number})`;
157+
'data-id'?: string;
158+
shadow: `${number}px ${number}px`;
159+
}>`
160+
color: ${props => props.color};
161+
`
162+
```
163+
164+
## 工作原理
165+
166+
插件在编译阶段拦截源代码,通过以下步骤处理:
167+
168+
1. 使用正则表达式匹配 `styled.tag<Props>` 模式的代码
169+
2. 解析泛型参数,确保类型定义中嵌套的尖括号和模板字符串被正确处理
170+
3. 将匹配到的代码转换为 Vue Styled Components 支持的 `styled('tag', Props)` 格式
171+
4. 转换后的代码保留了完整的 TypeScript 类型检查功能
172+
173+
这种转换完全在编译时进行,不会影响运行时性能,同时保持了完整的 TypeScript 类型检查支持。
174+
175+
## 支持的文件类型
176+
177+
默认情况下,插件会处理以下文件类型:
178+
- `.vue`
179+
- `.tsx`
180+
- `.jsx`
181+
- `.ts`
182+
- `.js`
183+
184+
你可以通过配置选项自定义包含和排除的文件:
185+
186+
```ts
187+
vueStyledComponentsTypescriptSyntax({
188+
include: ['.vue', '.tsx', '.jsx', '.ts'],
189+
exclude: ['node_modules', 'dist'],
190+
})
191+
```
192+
193+
## 注意事项
194+
195+
- 插件需要在 Vue 和 JSX 插件之前使用,以确保转换在 Vue 编译之前完成
196+
- 泛型语法仅在编译时转换,不会影响运行时行为
197+
- 确保你的 TypeScript 配置正确,以获得完整的类型检查支持
198+
- 在复杂泛型定义中,确保尖括号和引号匹配正确,避免语法错误
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Vue Styled Components TypeScript 语法糖插件
2+
3+
这个 Vite 插件为 Vue Styled Components 提供了类似 React styled-components 的 TypeScript 泛型语法支持。
4+
5+
## 功能
6+
7+
- 允许使用 `styled.tag<Props>` 的语法,而不是原来必须使用的 `styled('tag', { props })` 语法
8+
- 在编译时自动转换,不影响运行时性能
9+
- 完全兼容 TypeScript 类型系统
10+
11+
## 安装
12+
13+
```bash
14+
# npm
15+
npm install @vue-styled-components/plugin-typescript-syntax --save-dev
16+
17+
# yarn
18+
yarn add @vue-styled-components/plugin-typescript-syntax --dev
19+
20+
# pnpm
21+
pnpm add @vue-styled-components/plugin-typescript-syntax -D
22+
```
23+
24+
## 使用方法
25+
26+
### 在 Vite 配置中添加插件
27+
28+
```ts
29+
// vite.config.ts
30+
import { defineConfig } from 'vite'
31+
import vue from '@vitejs/plugin-vue'
32+
import vueJsx from '@vitejs/plugin-vue-jsx'
33+
import vueStyledComponentsTypescriptSyntax from '@vue-styled-components/plugin-typescript-syntax'
34+
35+
export default defineConfig({
36+
plugins: [
37+
// 确保在 vue 和 vueJsx 插件之前使用
38+
vueStyledComponentsTypescriptSyntax(),
39+
vue(),
40+
vueJsx(),
41+
],
42+
})
43+
```
44+
45+
### 配置选项
46+
47+
```ts
48+
vueStyledComponentsTypescriptSyntax({
49+
// 包含的文件扩展名,默认为 ['.vue', '.tsx', '.jsx', '.ts', '.js']
50+
include: ['.vue', '.tsx', '.jsx', '.ts', '.js'],
51+
52+
// 排除的文件路径,默认为 ['node_modules']
53+
exclude: ['node_modules'],
54+
})
55+
```
56+
57+
## 示例
58+
59+
### 使用泛型语法
60+
61+
```tsx
62+
import styled from '@vue-styled-components/core'
63+
64+
interface IconProps {
65+
color?: string
66+
size?: number
67+
}
68+
69+
// 使用新的泛型语法
70+
const Icon = styled.span<IconProps>`
71+
color: ${props => props.color || 'currentColor'};
72+
font-size: ${props => `${props.size || 16}px`};
73+
`
74+
75+
// 等同于原来的语法
76+
const IconOriginal = styled('span', {
77+
color: String,
78+
size: Number,
79+
})`
80+
color: ${props => props.color || 'currentColor'};
81+
font-size: ${props => `${props.size || 16}px`};
82+
`
83+
```
84+
85+
## 工作原理
86+
87+
插件在编译阶段拦截源代码,使用 AST 解析器分析代码,查找 `styled.tag<Props>` 模式的代码,并将其转换为 Vue Styled Components 支持的 `styled('tag', Props)` 格式。
88+
89+
## 许可证
90+
91+
Apache-2.0
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { transformStyledSyntax } from '../src/transform'
3+
import { normalizeString } 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+
expect(result?.code).toContain(`styled('button', { primary: { type: Boolean, required: false } })`)
23+
})
24+
25+
it('应该转换行内泛型定义', () => {
26+
const code = `
27+
import styled from '@vue-styled-components/core'
28+
29+
const Link = styled.a<{ active: boolean; disabled?: boolean }>\`
30+
color: \${props => props.active ? 'red' : 'blue'};
31+
opacity: \${props => props.disabled ? 0.5 : 1};
32+
\`
33+
`
34+
35+
const result = transformStyledSyntax(code, 'test.tsx')
36+
37+
expect(result).not.toBeNull()
38+
/* console.log(result?.code) */
39+
expect(normalizeString(result?.code)).toContain(normalizeString(`styled('a', { active: { type: Boolean, required: true }, disabled: { type: Boolean, required: false } })`))
40+
})
41+
42+
it('不应该转换没有泛型的样式组件', () => {
43+
const code = `
44+
import styled from '@vue-styled-components/core'
45+
46+
const Button = styled.button\`
47+
background-color: blue;
48+
\`
49+
`
50+
51+
const result = transformStyledSyntax(code, 'test.tsx')
52+
53+
expect(result).toBeNull()
54+
})
55+
56+
it('不应该转换原始的函数调用语法', () => {
57+
const code = `
58+
import styled from '@vue-styled-components/core'
59+
60+
const Button = styled('button', {
61+
primary: Boolean,
62+
})\`
63+
background-color: \${props => props.primary ? 'blue' : 'white'};
64+
\`
65+
`
66+
67+
const result = transformStyledSyntax(code, 'test.tsx')
68+
69+
expect(result).toBeNull()
70+
})
71+
72+
it('应该处理同一文件中的多个样式组件', () => {
73+
const code = `
74+
import styled from '@vue-styled-components/core'
75+
76+
interface ButtonProps {
77+
primary?: boolean
78+
}
79+
80+
interface IconProps {
81+
size?: number
82+
}
83+
84+
const Button = styled.button<ButtonProps>\`
85+
background-color: \${props => props.primary ? 'blue' : 'white'};
86+
\`
87+
88+
const Icon = styled.span<IconProps>\`
89+
font-size: \${props => \`\${props.size || 16}px\`};
90+
\`
91+
`
92+
93+
const result = transformStyledSyntax(code, 'test.tsx')
94+
95+
expect(result).not.toBeNull()
96+
expect(result?.code).toContain(`styled('button', { primary: { type: Boolean, required: false } })`)
97+
expect(result?.code).toContain(`styled('span', { size: { type: Number, required: false } })`)
98+
})
99+
})

0 commit comments

Comments
 (0)