@wsxjs/wsx-vite-plugin 0.0.7 → 0.0.9

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Babel plugin to automatically inject CSS styles for WSX components
3
+ *
4
+ * Transforms:
5
+ * // Button.wsx (if Button.css exists)
6
+ * export default class Button extends WebComponent { ... }
7
+ *
8
+ * To:
9
+ * import styles from "./Button.css?inline";
10
+ * export default class Button extends WebComponent {
11
+ * private _autoStyles = styles;
12
+ * ...
13
+ * }
14
+ */
15
+
16
+ import type { PluginObj, PluginPass } from "@babel/core";
17
+ import type * as t from "@babel/types";
18
+ import * as tModule from "@babel/types";
19
+
20
+ interface WSXStylePluginPass extends PluginPass {
21
+ cssFileExists: boolean;
22
+ cssFilePath: string;
23
+ componentName: string;
24
+ }
25
+
26
+ /**
27
+ * Check if styles are already imported in the file
28
+ */
29
+ function hasStylesImport(program: t.Program): boolean {
30
+ for (const node of program.body) {
31
+ if (node.type === "ImportDeclaration") {
32
+ const source = node.source.value;
33
+ // Check if it's a CSS import with ?inline
34
+ if (
35
+ typeof source === "string" &&
36
+ (source.endsWith(".css?inline") || source.endsWith(".css"))
37
+ ) {
38
+ // Check if it's imported as "styles"
39
+ const defaultSpecifier = node.specifiers.find(
40
+ (spec) => spec.type === "ImportDefaultSpecifier"
41
+ );
42
+ if (defaultSpecifier) {
43
+ return true;
44
+ }
45
+ }
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Check if _autoStyles property already exists in the class
53
+ */
54
+ function hasAutoStylesProperty(classBody: t.ClassBody): boolean {
55
+ for (const member of classBody.body) {
56
+ if (
57
+ (member.type === "ClassProperty" || member.type === "ClassPrivateProperty") &&
58
+ member.key.type === "Identifier" &&
59
+ member.key.name === "_autoStyles"
60
+ ) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ export default function babelPluginWSXStyle(): PluginObj<WSXStylePluginPass> {
68
+ const t = tModule;
69
+ return {
70
+ name: "babel-plugin-wsx-style",
71
+ visitor: {
72
+ Program(path, state) {
73
+ const { cssFileExists, cssFilePath, componentName } =
74
+ state.opts as WSXStylePluginPass;
75
+
76
+ // Skip if CSS file doesn't exist
77
+ if (!cssFileExists) {
78
+ return;
79
+ }
80
+
81
+ // Check if styles are already manually imported
82
+ if (hasStylesImport(path.node)) {
83
+ console.info(
84
+ `[Babel Plugin WSX Style] Skipping ${componentName}: styles already manually imported`
85
+ );
86
+ return; // Skip auto-injection if manual import exists
87
+ }
88
+
89
+ console.info(
90
+ `[Babel Plugin WSX Style] Injecting CSS import for ${componentName}: ${cssFilePath}`
91
+ );
92
+
93
+ // Add CSS import at the top of the file
94
+ const importStatement = t.importDeclaration(
95
+ [t.importDefaultSpecifier(t.identifier("styles"))],
96
+ t.stringLiteral(cssFilePath)
97
+ );
98
+
99
+ // Insert after existing imports (if any)
100
+ let insertIndex = 0;
101
+ for (let i = 0; i < path.node.body.length; i++) {
102
+ const node = path.node.body[i];
103
+ if (node.type === "ImportDeclaration") {
104
+ insertIndex = i + 1;
105
+ } else {
106
+ break;
107
+ }
108
+ }
109
+
110
+ path.node.body.splice(insertIndex, 0, importStatement);
111
+ },
112
+ ClassDeclaration(path, state) {
113
+ const { cssFileExists } = state.opts as WSXStylePluginPass;
114
+
115
+ // Skip if CSS file doesn't exist
116
+ if (!cssFileExists) {
117
+ return;
118
+ }
119
+
120
+ const classBody = path.node.body;
121
+
122
+ // Check if _autoStyles property already exists
123
+ if (hasAutoStylesProperty(classBody)) {
124
+ return; // Skip if already exists
125
+ }
126
+
127
+ // Note: We don't check for manual imports here because:
128
+ // 1. Program visitor already handled that and skipped if manual import exists
129
+ // 2. If we reach here, it means Program visitor added the import (or it was already there)
130
+ // 3. We should add the class property regardless
131
+
132
+ // Add class property: private _autoStyles = styles;
133
+ // Use classProperty (not classPrivateProperty) to create TypeScript private property
134
+ // TypeScript private is compile-time only and becomes a regular property at runtime
135
+ const autoStylesProperty = t.classProperty(
136
+ t.identifier("_autoStyles"),
137
+ t.identifier("styles"),
138
+ null, // typeAnnotation
139
+ [], // decorators
140
+ false, // computed
141
+ false // static
142
+ );
143
+
144
+ // Insert at the beginning of class body (before methods and other properties)
145
+ classBody.body.unshift(autoStylesProperty);
146
+ },
147
+ },
148
+ };
149
+ }
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
- export { default as wsx } from "./vite-plugin-wsx";
1
+ // Export the Babel-based plugin (with compile-time decorator support)
2
+ export { vitePluginWSXWithBabel as wsx } from "./vite-plugin-wsx-babel";
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Vite Plugin for WSX with Babel decorator support
3
+ *
4
+ * Uses Babel to preprocess decorators before esbuild transformation
5
+ * This ensures decorators work correctly even with esbuild's limitations
6
+ */
7
+
8
+ import type { Plugin } from "vite";
9
+ import { transform } from "esbuild";
10
+ import { transformSync } from "@babel/core";
11
+ import { existsSync } from "fs";
12
+ import { dirname, join, basename } from "path";
13
+ import babelPluginWSXState from "./babel-plugin-wsx-state";
14
+ import babelPluginWSXStyle from "./babel-plugin-wsx-style";
15
+ import babelPluginWSXFocus from "./babel-plugin-wsx-focus";
16
+
17
+ export interface WSXPluginOptions {
18
+ jsxFactory?: string;
19
+ jsxFragment?: string;
20
+ debug?: boolean;
21
+ extensions?: string[];
22
+ autoStyleInjection?: boolean; // Default: true
23
+ }
24
+
25
+ function getJSXFactoryImportPath(_options: WSXPluginOptions): string {
26
+ return "@wsxjs/wsx-core";
27
+ }
28
+
29
+ export function vitePluginWSXWithBabel(options: WSXPluginOptions = {}): Plugin {
30
+ const {
31
+ jsxFactory = "h",
32
+ jsxFragment = "Fragment",
33
+ extensions = [".wsx"],
34
+ autoStyleInjection = true,
35
+ } = options;
36
+
37
+ return {
38
+ name: "vite-plugin-wsx-babel",
39
+ enforce: "pre",
40
+
41
+ async transform(code: string, id: string) {
42
+ const isWSXFile = extensions.some((ext) => id.endsWith(ext));
43
+
44
+ if (!isWSXFile) {
45
+ return null;
46
+ }
47
+
48
+ // Check if corresponding CSS file exists (for auto style injection)
49
+ let cssFileExists = false;
50
+ let cssFilePath = "";
51
+ let componentName = "";
52
+
53
+ if (autoStyleInjection) {
54
+ const fileDir = dirname(id);
55
+ const fileName = basename(id, extensions.find((ext) => id.endsWith(ext)) || "");
56
+ const cssFilePathWithoutQuery = join(fileDir, `${fileName}.css`);
57
+ cssFileExists = existsSync(cssFilePathWithoutQuery);
58
+ componentName = fileName;
59
+
60
+ // Generate relative path for import (e.g., "./Button.css?inline")
61
+ // Use relative path from the file's directory
62
+ if (cssFileExists) {
63
+ // For import statement, use relative path with ?inline query
64
+ cssFilePath = `./${fileName}.css?inline`;
65
+ }
66
+ }
67
+
68
+ let transformedCode = code;
69
+
70
+ // 1. Add JSX imports if needed
71
+ // Check for actual import statement
72
+ const hasWSXCoreImport = code.includes('from "@wsxjs/wsx-core"');
73
+ // Check if JSX factory functions are already imported
74
+ const hasJSXInImport =
75
+ hasWSXCoreImport &&
76
+ (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(code) ||
77
+ new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(code));
78
+
79
+ // If file has JSX syntax but no import, add it
80
+ // Note: @jsxImportSource pragma is just a hint, we need actual import for esbuild
81
+ if ((code.includes("<") || code.includes("Fragment")) && !hasJSXInImport) {
82
+ const importPath = getJSXFactoryImportPath(options);
83
+ const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";\n`;
84
+ transformedCode = importStatement + transformedCode;
85
+ }
86
+
87
+ // 2. Use Babel to preprocess decorators
88
+ try {
89
+ const babelResult = transformSync(transformedCode, {
90
+ filename: id, // Pass the actual filename so Babel knows it's .wsx
91
+ presets: [
92
+ [
93
+ "@babel/preset-typescript",
94
+ {
95
+ isTSX: true, // Enable JSX syntax
96
+ allExtensions: true, // Process all extensions, including .wsx
97
+ },
98
+ ],
99
+ ],
100
+ plugins: [
101
+ // CRITICAL: Style injection plugin must run FIRST
102
+ // This ensures _autoStyles property exists before state transformations
103
+ ...(autoStyleInjection && cssFileExists
104
+ ? [
105
+ [
106
+ babelPluginWSXStyle,
107
+ {
108
+ cssFileExists,
109
+ cssFilePath,
110
+ componentName,
111
+ },
112
+ ],
113
+ ]
114
+ : []),
115
+ // Focus key generation plugin runs early to add data-wsx-key attributes
116
+ // This must run before JSX is transformed to h() calls
117
+ babelPluginWSXFocus,
118
+ // State decorator transformation runs after style injection
119
+ babelPluginWSXState,
120
+ [
121
+ "@babel/plugin-proposal-decorators",
122
+ {
123
+ version: "2023-05",
124
+ decoratorsBeforeExport: true,
125
+ },
126
+ ],
127
+ [
128
+ "@babel/plugin-proposal-class-properties",
129
+ {
130
+ loose: false,
131
+ },
132
+ ],
133
+ "@babel/plugin-transform-class-static-block", // Support static class blocks
134
+ ],
135
+ // parserOpts not needed - @babel/preset-typescript and plugins handle it
136
+ });
137
+
138
+ if (babelResult && babelResult.code) {
139
+ transformedCode = babelResult.code;
140
+ }
141
+ } catch {
142
+ // Babel transform failed, fallback to esbuild only
143
+ }
144
+
145
+ // 2.5. Ensure JSX imports still exist after Babel transformation
146
+ // Babel might have removed or modified imports, so we check again
147
+ const hasJSXAfterBabel =
148
+ transformedCode.includes('from "@wsxjs/wsx-core"') &&
149
+ (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(transformedCode) ||
150
+ new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(transformedCode));
151
+
152
+ if (
153
+ (transformedCode.includes("<") || transformedCode.includes("Fragment")) &&
154
+ !hasJSXAfterBabel
155
+ ) {
156
+ const importPath = getJSXFactoryImportPath(options);
157
+ const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";\n`;
158
+ transformedCode = importStatement + transformedCode;
159
+ }
160
+
161
+ // 3. Use esbuild for JSX transformation
162
+ try {
163
+ const result = await transform(transformedCode, {
164
+ loader: "jsx", // Already TypeScript-transformed by Babel
165
+ jsx: "transform",
166
+ jsxFactory: jsxFactory,
167
+ jsxFragment: jsxFragment,
168
+ target: "es2020",
169
+ format: "esm",
170
+ });
171
+
172
+ return {
173
+ code: result.code,
174
+ map: null,
175
+ };
176
+ } catch (error) {
177
+ console.error(`[WSX Plugin Babel] Transform error for ${id}:`, error);
178
+ throw error;
179
+ }
180
+ },
181
+ };
182
+ }
@@ -1,166 +0,0 @@
1
- /* eslint-disable no-console */
2
- /**
3
- * Vite Plugin for WSX (Web Component JSX)
4
- *
5
- * 专门处理.wsx文件:
6
- * - 自动添加JSX pragma
7
- * - 支持TypeScript编译
8
- * - 完全隔离,不影响主项目React配置
9
- */
10
-
11
- import type { Plugin } from "vite";
12
- import { transform } from "esbuild";
13
-
14
- export interface WSXPluginOptions {
15
- /**
16
- * JSX工厂函数名
17
- * @default 'h'
18
- */
19
- jsxFactory?: string;
20
-
21
- /**
22
- * JSX Fragment函数名
23
- * @default 'Fragment'
24
- */
25
- jsxFragment?: string;
26
-
27
- /**
28
- * 是否启用调试日志
29
- * @default false
30
- */
31
- debug?: boolean;
32
-
33
- /**
34
- * 文件扩展名
35
- * @default ['.wsx']
36
- */
37
- extensions?: string[];
38
- }
39
-
40
- /**
41
- * 获取 JSX 工厂函数的导入路径
42
- */
43
- function getJSXFactoryImportPath(_options: WSXPluginOptions): string {
44
- // 使用 @wsxjs/wsx-core 包中的 JSX 工厂
45
- return "@wsxjs/wsx-core";
46
- }
47
-
48
- /**
49
- * WSX Vite插件
50
- */
51
- export function vitePluginWSX(options: WSXPluginOptions = {}): Plugin {
52
- const {
53
- jsxFactory = "h",
54
- jsxFragment = "Fragment",
55
- debug = false,
56
- extensions = [".wsx"],
57
- } = options;
58
-
59
- return {
60
- name: "vite-plugin-wsx",
61
- enforce: "pre", // 确保在 React 插件之前执行
62
-
63
- // 处理 .wsx 文件加载
64
- load(id: string) {
65
- const isWSXFile = extensions.some((ext) => id.endsWith(ext));
66
-
67
- if (!isWSXFile) {
68
- return null;
69
- }
70
-
71
- if (debug) {
72
- console.log(`[WSX Plugin] Loading: ${id}`);
73
- }
74
-
75
- // 返回 null 让 Vite 继续处理文件
76
- return null;
77
- },
78
-
79
- // 在transform阶段处理文件
80
- async transform(code: string, id: string) {
81
- // 检查是否是WSX文件
82
- const isWSXFile = extensions.some((ext) => id.endsWith(ext));
83
-
84
- if (!isWSXFile) {
85
- return null;
86
- }
87
-
88
- if (debug) {
89
- console.log(`[WSX Plugin] Processing: ${id}`);
90
- }
91
-
92
- let transformedCode = code;
93
-
94
- // 1. 检查是否已经有JSX工厂导入
95
- const hasWSXCoreImport = code.includes('from "@wsxjs/wsx-core"');
96
- // 更精确的检测:使用正则表达式检查 JSX 工厂函数是否在导入中
97
- const hasJSXInImport =
98
- hasWSXCoreImport &&
99
- (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(code) ||
100
- new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(code));
101
-
102
- // 调试信息
103
- if (debug) {
104
- console.log(`[WSX Plugin] Checking JSX imports for: ${id}`);
105
- console.log(` - hasWSXCoreImport: ${hasWSXCoreImport}`);
106
- console.log(` - hasJSXInImport: ${hasJSXInImport}`);
107
- console.log(` - has < character: ${code.includes("<")}`);
108
- console.log(` - has Fragment: ${code.includes("Fragment")}`);
109
- }
110
-
111
- // 如果有JSX语法但没有JSX工厂导入,则需要注入
112
- if ((code.includes("<") || code.includes("Fragment")) && !hasJSXInImport) {
113
- // 使用标准的包导入
114
- const importPath = getJSXFactoryImportPath(options);
115
- const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";\n`;
116
- transformedCode = importStatement + transformedCode;
117
-
118
- if (debug) {
119
- console.log(`[WSX Plugin] Added JSX factory import to: ${id}`);
120
- }
121
- }
122
-
123
- // 2. 添加JSX pragma - 不添加,让esbuild使用jsxFactory config
124
- // JSX transformation will be handled by esbuild with our custom config
125
-
126
- // 3. 使用 esbuild 进行 JSX 转换
127
-
128
- try {
129
- const result = await transform(transformedCode, {
130
- loader: "tsx",
131
- jsx: "transform",
132
- jsxFactory: jsxFactory,
133
- jsxFragment: jsxFragment,
134
- target: "es2020",
135
- format: "esm",
136
- // Esbuild supports decorators natively with tsx loader
137
- });
138
-
139
- if (debug) {
140
- console.log(`[WSX Plugin] JSX transformed: ${id}`);
141
- }
142
-
143
- return {
144
- code: result.code,
145
- map: null,
146
- };
147
- } catch (error) {
148
- console.error(`[WSX Plugin] Transform error for ${id}:`, error);
149
- throw error;
150
- }
151
- },
152
-
153
- // We handle JSX transformation directly in the transform hook
154
- // No need to modify global esbuild config
155
-
156
- // 构建开始时的日志
157
- buildStart() {
158
- if (debug) {
159
- console.log(`[WSX Plugin] Build started with extensions: ${extensions.join(", ")}`);
160
- console.log(`[WSX Plugin] JSX Factory: ${jsxFactory}, Fragment: ${jsxFragment}`);
161
- }
162
- },
163
- };
164
- }
165
-
166
- export default vitePluginWSX;