@vitarx/plugin-vite 0.0.1-beta.4 → 0.0.1-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -249,10 +249,42 @@ import.meta.hot.accept(mod => {
249
249
 
250
250
  ```typescript
251
251
  interface VitePluginVitarxOptions {
252
- // 暂无配置选项
252
+ /**
253
+ * 是否将 className 属性转换为 class 属性
254
+ * 仅对原生 HTML 元素生效,组件不生效
255
+ * @default false
256
+ */
257
+ transformClassNameToClass?: boolean
253
258
  }
254
259
  ```
255
260
 
261
+ ### className 转 class
262
+
263
+ 启用 `transformClassNameToClass` 选项后,原生 HTML 元素的 `className` 属性会自动转换为 `class`:
264
+
265
+ ```typescript
266
+ // vite.config.ts
267
+ import { defineConfig } from 'vite'
268
+ import vitarx from '@vitarx/plugin-vite'
269
+
270
+ export default defineConfig({
271
+ plugins: [vitarx({ transformClassNameToClass: true })]
272
+ })
273
+ ```
274
+
275
+ ```jsx
276
+ // 编译前
277
+ <div className="container">Hello</div>
278
+
279
+ // 编译后
280
+ createView('div', { class: 'container', children: 'Hello' })
281
+ ```
282
+
283
+ **注意事项:**
284
+ - 仅对原生 HTML 元素(小写字母开头)生效,组件不转换
285
+ - `class` 和 `className` 不能同时存在,否则会抛出 `E016` 错误
286
+ - `class` 属性优先级更高
287
+
256
288
  ## 目录结构
257
289
 
258
290
  ```
@@ -292,6 +324,7 @@ src/
292
324
  | E013 | Match 组件必须包含子元素 |
293
325
  | E014 | IfBlock 组件必须包含子元素 |
294
326
  | E015 | Switch 组件必须包含至少一个 Match 子元素 |
327
+ | E016 | class 和 className 不能同时存在 |
295
328
 
296
329
  ## License
297
330
 
package/dist/error.d.ts CHANGED
@@ -71,6 +71,8 @@ export declare const ErrorCodes: {
71
71
  readonly E014: "E014";
72
72
  /** Switch 组件必须有 Match 子元素 */
73
73
  readonly E015: "E015";
74
+ /** class 和 className 不能同时存在 */
75
+ readonly E016: "E016";
74
76
  };
75
77
  /**
76
78
  * 警告代码枚举
package/dist/error.js CHANGED
@@ -61,6 +61,8 @@ export const ErrorCodes = {
61
61
  E014: 'E014',
62
62
  /** Switch 组件必须有 Match 子元素 */
63
63
  E015: 'E015',
64
+ /** class 和 className 不能同时存在 */
65
+ E016: 'E016',
64
66
  };
65
67
  /**
66
68
  * 警告代码枚举
@@ -88,6 +90,7 @@ export const ErrorMessages = {
88
90
  E013: 'Match component must have children',
89
91
  E014: 'IfBlock component must have children',
90
92
  E015: 'Switch component must have at least one Match child',
93
+ E016: 'Cannot use both "class" and "className" attributes on the same element. When transformClassNameToClass is enabled, "class" takes priority',
91
94
  };
92
95
  /**
93
96
  * 警告消息映射
@@ -27,7 +27,8 @@ export default class HMRManager {
27
27
  /**
28
28
  * 置换新模块
29
29
  *
30
- * 此方法提供给`jsxDev`函数调用,保持每次创建组件实例都是最新的模块!
30
+ * 此方法提供给`createView`函数调用,保持每次创建组件实例都是最新的模块!
31
+ *
31
32
  * @param component
32
33
  */
33
34
  resolveComponent(component: Component): Component;
@@ -51,7 +51,8 @@ class HMRManager {
51
51
  /**
52
52
  * 置换新模块
53
53
  *
54
- * 此方法提供给`jsxDev`函数调用,保持每次创建组件实例都是最新的模块!
54
+ * 此方法提供给`createView`函数调用,保持每次创建组件实例都是最新的模块!
55
+ *
55
56
  * @param component
56
57
  */
57
58
  resolveComponent(component) {
@@ -21,7 +21,9 @@ export function processUpdate(view, newComponent) {
21
21
  view.component = newComponent;
22
22
  // 如果逻辑有变化,则完全重新挂载组件
23
23
  if (logic) {
24
+ const ctx = view.ctx;
24
25
  view.dispose();
26
+ view.init(ctx);
25
27
  view.mount(placeholder, 'replace');
26
28
  return;
27
29
  }
package/dist/index.d.ts CHANGED
@@ -2,10 +2,19 @@ import { type Plugin } from 'vite';
2
2
  export type * from './types.js';
3
3
  /**
4
4
  * vite-plugin-vitarx 配置选项
5
- *
6
- * 暂无配置选项
7
5
  */
8
6
  export interface VitePluginVitarxOptions {
7
+ /**
8
+ * 是否将 className 属性转换为 class 属性
9
+ * 仅对原生 HTML 元素生效,组件不生效
10
+ *
11
+ * 当启用时:
12
+ * - `<div className="test" />` 转换为 `<div class="test" />`
13
+ * - 如果同时存在 class 和 className,会抛出错误(class 优先级更高)
14
+ *
15
+ * @default false
16
+ */
17
+ transformClassNameToClass?: boolean;
9
18
  }
10
19
  /**
11
20
  * vite-plugin-vitarx
@@ -15,8 +24,9 @@ export interface VitePluginVitarxOptions {
15
24
  * - 支持 v-if、v-else-if、v-else 、v-model 等编译宏指令
16
25
  * - 支持 Switch , IfBlock 等编译宏组件
17
26
  * - 开发时 HMR 热更新相关代码注入与功能支持
27
+ * - 支持 className 转 class(仅原生 HTML 元素)
18
28
  *
19
- * @param _options - 暂无可选配置。
29
+ * @param options - 插件配置选项。
20
30
  * @returns - vite插件对象。
21
31
  */
22
- export default function vitarx(_options?: VitePluginVitarxOptions): Plugin;
32
+ export default function vitarx(options?: VitePluginVitarxOptions): Plugin;
package/dist/index.js CHANGED
@@ -20,26 +20,24 @@ else {
20
20
  * - 支持 v-if、v-else-if、v-else 、v-model 等编译宏指令
21
21
  * - 支持 Switch , IfBlock 等编译宏组件
22
22
  * - 开发时 HMR 热更新相关代码注入与功能支持
23
+ * - 支持 className 转 class(仅原生 HTML 元素)
23
24
  *
24
- * @param _options - 暂无可选配置。
25
+ * @param options - 插件配置选项。
25
26
  * @returns - vite插件对象。
26
27
  */
27
- export default function vitarx(_options) {
28
+ export default function vitarx(options) {
28
29
  let compileOptions;
29
30
  let isDEV = false;
30
31
  let isSSR = false;
31
32
  let viteConfig;
32
33
  return {
33
34
  name: 'vite-plugin-vitarx',
35
+ enforce: 'pre',
34
36
  config(config, env) {
35
37
  isDEV = env.command === 'serve' && !env.isPreview;
36
38
  const configSSR = !!config.build?.ssr;
37
39
  isSSR = env.isSsrBuild === true || configSSR;
38
- const buildConfig = {
39
- jsx: 'preserve',
40
- exclude: /\.[jt]sx$/
41
- };
42
- const _config = {
40
+ return {
43
41
  define: {
44
42
  __VITARX_DEV__: JSON.stringify(isDEV),
45
43
  __VITARX_SSR__: JSON.stringify(isSSR)
@@ -48,10 +46,11 @@ export default function vitarx(_options) {
48
46
  alias: {
49
47
  '@vitarx/vite-plugin/hmr-client': path.join(__dirname, 'hmr-client/index.js')
50
48
  }
49
+ },
50
+ [IS_V8 ? 'oxc' : 'esbuild']: {
51
+ jsx: 'preserve'
51
52
  }
52
53
  };
53
- _config[IS_V8 ? 'oxc' : 'esbuild'] = buildConfig;
54
- return _config;
55
54
  },
56
55
  configResolved(config) {
57
56
  viteConfig = config;
@@ -60,7 +59,8 @@ export default function vitarx(_options) {
60
59
  ssr: isSSR,
61
60
  hmr: isDEV && !isSSR,
62
61
  runtimeModule: 'vitarx',
63
- sourceMap: false
62
+ sourceMap: false,
63
+ transformClassNameToClass: options?.transformClassNameToClass ?? false
64
64
  };
65
65
  },
66
66
  async transform(code, id) {
@@ -19,12 +19,33 @@ export declare function processSpreadAttribute(attr: t.JSXSpreadAttribute, hasVB
19
19
  property: t.ObjectProperty;
20
20
  hasVBind: boolean;
21
21
  } | null;
22
+ /**
23
+ * 获取属性值表达式
24
+ * 处理不同类型的属性值,统一返回表达式
25
+ * @param value - JSX 属性值
26
+ * @returns 表达式节点
27
+ */
28
+ export declare function getAttributeValue(value: t.JSXAttribute['value']): t.Expression;
29
+ /**
30
+ * 创建对象属性节点
31
+ * 静态值直接赋值,动态值生成 getter 方法
32
+ *
33
+ * 特殊处理:
34
+ * - children 属性不使用 unref,因为 ref 作为 children 是合法的可变渲染源
35
+ * - 其他标识符属性使用 unref 解包
36
+ *
37
+ * @param key - 属性名
38
+ * @param value - 属性值表达式
39
+ * @param ctx - 转换上下文
40
+ * @returns 对象属性或方法节点
41
+ */
42
+ export declare function createProperty(key: string, value: t.Expression, ctx: TransformContext): t.ObjectProperty | t.ObjectMethod;
22
43
  /**
23
44
  * 处理单个 JSX 属性
24
45
  * 根据属性名类型分发到不同的处理逻辑
25
46
  * @param attr - JSX 属性节点
26
47
  * @param existingPropNames - 已存在的属性名集合
27
48
  * @param ctx - 转换上下文
28
- * @returns 属性处理结果
49
+ * @returns - 属性处理结果
29
50
  */
30
51
  export declare function processAttribute(attr: t.JSXAttribute, existingPropNames: Set<string>, ctx: TransformContext): AttributeResult;
@@ -27,48 +27,6 @@ export function processSpreadAttribute(attr, hasVBind, node) {
27
27
  hasVBind: true
28
28
  };
29
29
  }
30
- /**
31
- * 处理单个 JSX 属性
32
- * 根据属性名类型分发到不同的处理逻辑
33
- * @param attr - JSX 属性节点
34
- * @param existingPropNames - 已存在的属性名集合
35
- * @param ctx - 转换上下文
36
- * @returns 属性处理结果
37
- */
38
- export function processAttribute(attr, existingPropNames, ctx) {
39
- const attrName = attr.name;
40
- // 处理命名空间属性 (v:xxx)
41
- if (attrName.type === 'JSXNamespacedName') {
42
- return processNamespacedAttribute(attr, attrName, existingPropNames, ctx);
43
- }
44
- // 处理标识符属性
45
- if (attrName.type === 'JSXIdentifier') {
46
- const name = attrName.name;
47
- // v-bind 指令
48
- if (name === 'v-bind') {
49
- const value = getAttributeValue(attr.value);
50
- return { type: 'directive', name: 'v-bind', value, isVBind: true, isVModel: false };
51
- }
52
- // v-model 指令
53
- if (name === 'v-model') {
54
- const value = getAttributeValue(attr.value);
55
- return { type: 'directive', name: 'v-model', value, isVBind: false, isVModel: true };
56
- }
57
- // 其他 v- 指令(排除 v-if 系列,它们在单独的处理流程中)
58
- if (name.startsWith('v-') && !['v-if', 'v-else-if', 'v-else'].includes(name)) {
59
- const value = getAttributeValue(attr.value);
60
- return { type: 'directive', name, value, isVBind: false, isVModel: false };
61
- }
62
- // 普通属性
63
- existingPropNames.add(name);
64
- const value = getAttributeValue(attr.value);
65
- const property = createProperty(name, value, ctx);
66
- return { type: 'property', property };
67
- }
68
- // 未知属性类型,作为普通属性处理
69
- const value = getAttributeValue(attr.value);
70
- return { type: 'property', property: t.objectProperty(t.identifier('unknown'), value) };
71
- }
72
30
  /**
73
31
  * 处理命名空间属性
74
32
  *
@@ -109,18 +67,14 @@ function processNamespacedAttribute(attr, attrName, existingPropNames, ctx) {
109
67
  * @param value - JSX 属性值
110
68
  * @returns 表达式节点
111
69
  */
112
- function getAttributeValue(value) {
113
- // 无值属性默认为 true
70
+ export function getAttributeValue(value) {
114
71
  if (!value) {
115
72
  return t.booleanLiteral(true);
116
73
  }
117
- // 字符串字面量直接返回
118
74
  if (isStringLiteral(value)) {
119
75
  return value;
120
76
  }
121
- // JSX 表达式容器
122
77
  if (isJSXExpressionContainer(value)) {
123
- // 空表达式 {} 视为 true
124
78
  if (value.expression.type === 'JSXEmptyExpression') {
125
79
  return t.booleanLiteral(true);
126
80
  }
@@ -141,7 +95,7 @@ function getAttributeValue(value) {
141
95
  * @param ctx - 转换上下文
142
96
  * @returns 对象属性或方法节点
143
97
  */
144
- function createProperty(key, value, ctx) {
98
+ export function createProperty(key, value, ctx) {
145
99
  const keyNode = t.stringLiteral(key);
146
100
  if (isStringLiteral(value) || isNumericLiteral(value) || isBooleanLiteral(value)) {
147
101
  return t.objectProperty(keyNode, value);
@@ -159,3 +113,45 @@ function createProperty(key, value, ctx) {
159
113
  }
160
114
  return t.objectMethod('get', keyNode, [], t.blockStatement([t.returnStatement(value)]));
161
115
  }
116
+ /**
117
+ * 处理单个 JSX 属性
118
+ * 根据属性名类型分发到不同的处理逻辑
119
+ * @param attr - JSX 属性节点
120
+ * @param existingPropNames - 已存在的属性名集合
121
+ * @param ctx - 转换上下文
122
+ * @returns - 属性处理结果
123
+ */
124
+ export function processAttribute(attr, existingPropNames, ctx) {
125
+ const attrName = attr.name;
126
+ // 处理命名空间属性 (v:xxx)
127
+ if (attrName.type === 'JSXNamespacedName') {
128
+ return processNamespacedAttribute(attr, attrName, existingPropNames, ctx);
129
+ }
130
+ // 处理标识符属性
131
+ if (attrName.type === 'JSXIdentifier') {
132
+ const name = attrName.name;
133
+ // v-bind 指令
134
+ if (name === 'v-bind') {
135
+ const value = getAttributeValue(attr.value);
136
+ return { type: 'directive', name: 'v-bind', value, isVBind: true, isVModel: false };
137
+ }
138
+ // v-model 指令
139
+ if (name === 'v-model') {
140
+ const value = getAttributeValue(attr.value);
141
+ return { type: 'directive', name: 'v-model', value, isVBind: false, isVModel: true };
142
+ }
143
+ // 其他 v- 指令(排除 v-if 系列,它们在单独的处理流程中)
144
+ if (name.startsWith('v-') && !['v-if', 'v-else-if', 'v-else'].includes(name)) {
145
+ const value = getAttributeValue(attr.value);
146
+ return { type: 'directive', name, value, isVBind: false, isVModel: false };
147
+ }
148
+ // 普通属性
149
+ existingPropNames.add(name);
150
+ const value = getAttributeValue(attr.value);
151
+ const property = createProperty(name, value, ctx);
152
+ return { type: 'property', property };
153
+ }
154
+ // 未知属性类型,作为普通属性处理
155
+ const value = getAttributeValue(attr.value);
156
+ return { type: 'property', property: t.objectProperty(t.identifier('unknown'), value) };
157
+ }
@@ -6,7 +6,7 @@
6
6
  import * as t from '@babel/types';
7
7
  import { TransformContext } from '../../context.js';
8
8
  import type { PropsResult } from './types.js';
9
- export type { PropsResult, VModelState, AttributeResult } from './types.js';
9
+ export type { AttributeResult, PropsResult, VModelState } from './types.js';
10
10
  /**
11
11
  * 处理 JSX 元素的属性
12
12
  * 将 JSX 属性转换为运行时 props 对象,并提取指令
@@ -17,6 +17,7 @@ export type { PropsResult, VModelState, AttributeResult } from './types.js';
17
17
  * 3. 普通属性转换为对象属性或 getter
18
18
  * 4. 指令属性提取到 directives Map
19
19
  * 5. v-model 特殊处理,生成 modelValue 和 onUpdate:modelValue
20
+ * 6. className 转 class(仅原生元素,启用 transformClassNameToClass 时)
20
21
  *
21
22
  * @param node - JSX 元素节点
22
23
  * @param ctx - 转换上下文
@@ -4,8 +4,39 @@
4
4
  * @module passes/props
5
5
  */
6
6
  import * as t from '@babel/types';
7
- import { processAttribute, processSpreadAttribute } from './attribute.js';
7
+ import { createError } from '../../error.js';
8
+ import { isNativeElement } from '../../utils/index.js';
9
+ import { createProperty, getAttributeValue, processAttribute, processSpreadAttribute } from './attribute.js';
8
10
  import { createVModelProps, extractVModelState } from './vmodel.js';
11
+ /**
12
+ * 获取 JSX 元素的名称
13
+ */
14
+ function getJSXElementName(node) {
15
+ const nameNode = node.openingElement.name;
16
+ return nameNode.type === 'JSXIdentifier' ? nameNode.name : null;
17
+ }
18
+ /**
19
+ * 校验 class 和 className 不能同时存在
20
+ */
21
+ function validateClassAndClassName(attributes) {
22
+ let hasClass = false;
23
+ let hasClassName = false;
24
+ let classNameAttr;
25
+ for (const attr of attributes) {
26
+ if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
27
+ if (attr.name.name === 'class') {
28
+ hasClass = true;
29
+ }
30
+ else if (attr.name.name === 'className') {
31
+ hasClassName = true;
32
+ classNameAttr = attr;
33
+ }
34
+ }
35
+ }
36
+ if (hasClass && hasClassName) {
37
+ throw createError('E016', classNameAttr);
38
+ }
39
+ }
9
40
  /**
10
41
  * 处理 JSX 元素的属性
11
42
  * 将 JSX 属性转换为运行时 props 对象,并提取指令
@@ -16,6 +47,7 @@ import { createVModelProps, extractVModelState } from './vmodel.js';
16
47
  * 3. 普通属性转换为对象属性或 getter
17
48
  * 4. 指令属性提取到 directives Map
18
49
  * 5. v-model 特殊处理,生成 modelValue 和 onUpdate:modelValue
50
+ * 6. className 转 class(仅原生元素,启用 transformClassNameToClass 时)
19
51
  *
20
52
  * @param node - JSX 元素节点
21
53
  * @param ctx - 转换上下文
@@ -45,6 +77,14 @@ export function processProps(node, ctx, hasChildren = false) {
45
77
  const existingPropNames = new Set();
46
78
  // 提取初始 v-model 状态
47
79
  let vModelState = extractVModelState(attributes);
80
+ // 获取元素名称,用于判断是否为原生元素
81
+ const elementName = getJSXElementName(node);
82
+ const isNative = elementName ? isNativeElement(elementName) : false;
83
+ // 检查是否启用 className 转 class 且为原生元素
84
+ const shouldTransformClassName = ctx.options.transformClassNameToClass && isNative;
85
+ // 如果启用 className 转 class,先检测 class 和 className 冲突
86
+ if (shouldTransformClassName)
87
+ validateClassAndClassName(attributes);
48
88
  // 遍历处理所有属性
49
89
  for (const attr of attributes) {
50
90
  // 处理展开属性 {...props}
@@ -62,6 +102,16 @@ export function processProps(node, ctx, hasChildren = false) {
62
102
  if (hasChildren && attr.name.type === 'JSXIdentifier' && attr.name.name === 'children') {
63
103
  continue;
64
104
  }
105
+ // 如果启用 className 转 class,处理 className 属性
106
+ if (shouldTransformClassName &&
107
+ attr.name.type === 'JSXIdentifier' &&
108
+ attr.name.name === 'className') {
109
+ const value = getAttributeValue(attr.value);
110
+ const property = createProperty('class', value, ctx);
111
+ properties.push(property);
112
+ existingPropNames.add('class');
113
+ continue;
114
+ }
65
115
  const result = processAttribute(attr, existingPropNames, ctx);
66
116
  if (result.type === 'directive') {
67
117
  // v-model 指令:更新状态
@@ -8,6 +8,7 @@ export interface CompileOptions {
8
8
  ssr: boolean;
9
9
  runtimeModule: string;
10
10
  sourceMap: boolean | 'inline' | 'both';
11
+ transformClassNameToClass: boolean;
11
12
  }
12
13
  /**
13
14
  * 转换 JSX/TSX 代码
package/dist/transform.js CHANGED
@@ -15,8 +15,6 @@ const processedNodes = new WeakSet();
15
15
  * 检查文件是否需要转换
16
16
  */
17
17
  function shouldTransform(id) {
18
- if (id.includes('node_modules'))
19
- return false;
20
18
  const ext = id.split('?')[0].split('.').pop()?.toLowerCase();
21
19
  return ext === 'jsx' || ext === 'tsx';
22
20
  }
package/dist/types.d.ts CHANGED
@@ -32,6 +32,50 @@ export interface MatchProps {
32
32
  children: RenderUnit;
33
33
  }
34
34
  declare global {
35
+ namespace Vitarx {
36
+ interface IntrinsicAttributes {
37
+ /**
38
+ * 条件渲染指令 - v-if
39
+ *
40
+ * 用于条件性地渲染元素。
41
+ * 当表达式的值为真时,元素将被渲染;否则,元素不会被渲染到DOM中。
42
+ *
43
+ * ```tsx
44
+ * const show = ref(true)
45
+ * // v-if指令不支持 ref 自动解包,必须使用.value
46
+ * <div v-if={show.value}></div>
47
+ * ```
48
+ */
49
+ 'v-if'?: unknown;
50
+ /**
51
+ * 条件渲染指令 - v-else
52
+ *
53
+ * 用于表示else块。
54
+ * 必须紧跟在v-if或v-else-if元素后面。
55
+ *
56
+ * ```tsx
57
+ * const show = ref(false)
58
+ * <div v-if={show.value}></div>
59
+ * <div v-else></div>
60
+ * ```
61
+ */
62
+ 'v-else'?: unknown;
63
+ /**
64
+ * 条件渲染指令 - v-else-if
65
+ *
66
+ * 用于表示else-if块。
67
+ * 必须紧跟在v-if或v-else-if元素后面。
68
+ *
69
+ * ```tsx
70
+ * const show = ref(1)
71
+ * <div v-if={show.value === 0}></div>
72
+ * <div v-else-if={show.value === 1}></div>
73
+ * <div v-else></div>
74
+ * ```
75
+ */
76
+ 'v-else-if'?: unknown;
77
+ }
78
+ }
35
79
  /**
36
80
  * IfBlock - 编译宏组件,无需import导入
37
81
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitarx/plugin-vite",
3
- "version": "0.0.1-beta.4",
3
+ "version": "0.0.1-beta.6",
4
4
  "description": "The official plugin for Vitarx support in Vite.",
5
5
  "author": "ZhuChongLin <8210856@qq.com>",
6
6
  "license": "MIT",
@@ -52,7 +52,8 @@
52
52
  "node": "^25.7.0",
53
53
  "prettier": "^3.8.1",
54
54
  "rimraf": "^6.1.3",
55
- "vitest": "^4.0.18"
55
+ "vitest": "^4.0.18",
56
+ "vite": "^8.0.2"
56
57
  },
57
58
  "scripts": {
58
59
  "build": "rimraf dist && tsc",