@wsxjs/wsx-vite-plugin 0.0.7 → 0.0.8

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,260 @@
1
+ /**
2
+ * Babel plugin to transform @state decorator to compile-time Proxy creation
3
+ *
4
+ * Transforms:
5
+ * @state private state = { count: 0 };
6
+ *
7
+ * To:
8
+ * private state;
9
+ * constructor() {
10
+ * super();
11
+ * this.state = this.reactive({ count: 0 });
12
+ * }
13
+ */
14
+
15
+ import type { PluginObj, PluginPass } from "@babel/core";
16
+ import type * as t from "@babel/types";
17
+ import * as tModule from "@babel/types";
18
+
19
+ interface WSXStatePluginPass extends PluginPass {
20
+ stateProperties: Array<{
21
+ key: string;
22
+ initialValue: t.Expression;
23
+ isObject: boolean;
24
+ }>;
25
+ reactiveMethodName: string;
26
+ }
27
+
28
+ export default function babelPluginWSXState(): PluginObj<WSXStatePluginPass> {
29
+ const t = tModule;
30
+ return {
31
+ name: "babel-plugin-wsx-state",
32
+ visitor: {
33
+ ClassDeclaration(path) {
34
+ const classBody = path.node.body;
35
+ const stateProperties: Array<{
36
+ key: string;
37
+ initialValue: t.Expression;
38
+ isObject: boolean;
39
+ }> = [];
40
+
41
+ // Find all @state decorated properties
42
+ // Debug: log all class members
43
+ console.info(
44
+ `[Babel Plugin WSX State] Processing class ${path.node.id?.name || "anonymous"}, members: ${classBody.body.length}`
45
+ );
46
+
47
+ for (const member of classBody.body) {
48
+ // Debug: log member type
49
+ console.info(
50
+ ` - Member type: ${member.type}, key: ${member.type === "ClassProperty" || member.type === "ClassPrivateProperty" ? (member.key as any)?.name : "N/A"}`
51
+ );
52
+
53
+ // Check both ClassProperty and ClassPrivateProperty
54
+ // @babel/plugin-proposal-class-properties might convert them
55
+ if (
56
+ (member.type === "ClassProperty" ||
57
+ member.type === "ClassPrivateProperty") &&
58
+ member.key.type === "Identifier"
59
+ ) {
60
+ // Debug: log all class properties
61
+ console.info(
62
+ ` - Property: ${member.key.name}, decorators: ${member.decorators?.length || 0}, hasValue: ${!!member.value}`
63
+ );
64
+
65
+ if (member.decorators && member.decorators.length > 0) {
66
+ // Debug: log decorator names
67
+ member.decorators.forEach((decorator) => {
68
+ if (decorator.expression.type === "Identifier") {
69
+ console.info(` Decorator: ${decorator.expression.name}`);
70
+ } else if (
71
+ decorator.expression.type === "CallExpression" &&
72
+ decorator.expression.callee.type === "Identifier"
73
+ ) {
74
+ console.debug(
75
+ ` Decorator: ${decorator.expression.callee.name}()`
76
+ );
77
+ }
78
+ });
79
+ }
80
+
81
+ // Check if has @state decorator
82
+ const hasStateDecorator = member.decorators?.some(
83
+ (decorator: t.Decorator) => {
84
+ if (
85
+ decorator.expression.type === "Identifier" &&
86
+ decorator.expression.name === "state"
87
+ ) {
88
+ return true;
89
+ }
90
+ if (
91
+ decorator.expression.type === "CallExpression" &&
92
+ decorator.expression.callee.type === "Identifier" &&
93
+ decorator.expression.callee.name === "state"
94
+ ) {
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ );
100
+
101
+ if (hasStateDecorator && member.value) {
102
+ const key = member.key.name;
103
+ const initialValue = member.value as t.Expression;
104
+
105
+ // Determine if it's an object/array
106
+ const isObject =
107
+ initialValue.type === "ObjectExpression" ||
108
+ initialValue.type === "ArrayExpression";
109
+
110
+ stateProperties.push({
111
+ key,
112
+ initialValue,
113
+ isObject,
114
+ });
115
+
116
+ // Remove @state decorator - but keep other decorators
117
+ if (member.decorators) {
118
+ member.decorators = member.decorators.filter(
119
+ (decorator: t.Decorator) => {
120
+ if (
121
+ decorator.expression.type === "Identifier" &&
122
+ decorator.expression.name === "state"
123
+ ) {
124
+ return false; // Remove @state decorator
125
+ }
126
+ if (
127
+ decorator.expression.type === "CallExpression" &&
128
+ decorator.expression.callee.type === "Identifier" &&
129
+ decorator.expression.callee.name === "state"
130
+ ) {
131
+ return false; // Remove @state() decorator
132
+ }
133
+ return true; // Keep other decorators
134
+ }
135
+ );
136
+ }
137
+
138
+ // Remove initial value - it will be set in constructor via this.reactive()
139
+ // Keep the property declaration but without initial value
140
+ member.value = undefined;
141
+ }
142
+ }
143
+ }
144
+
145
+ if (stateProperties.length === 0) {
146
+ return; // No @state properties found
147
+ }
148
+
149
+ // Find or create constructor
150
+ let constructor = classBody.body.find(
151
+ (member: t.ClassBody["body"][number]): member is t.ClassMethod =>
152
+ member.type === "ClassMethod" && member.kind === "constructor"
153
+ ) as t.ClassMethod | undefined;
154
+
155
+ if (!constructor) {
156
+ // Create constructor if it doesn't exist
157
+ constructor = t.classMethod(
158
+ "constructor",
159
+ t.identifier("constructor"),
160
+ [],
161
+ t.blockStatement([])
162
+ );
163
+ classBody.body.unshift(constructor);
164
+ }
165
+
166
+ // Add initialization code to constructor
167
+ const statements: t.Statement[] = [];
168
+
169
+ // Add super() call if not present
170
+ const hasSuper = constructor.body.body.some(
171
+ (stmt: t.Statement) =>
172
+ stmt.type === "ExpressionStatement" &&
173
+ stmt.expression.type === "CallExpression" &&
174
+ stmt.expression.callee.type === "Super"
175
+ );
176
+
177
+ if (!hasSuper) {
178
+ statements.push(t.expressionStatement(t.callExpression(t.super(), [])));
179
+ }
180
+
181
+ // CRITICAL: Add state property initialization AFTER all existing constructor code
182
+ // WebComponent already has reactive() and useState() methods
183
+ // We'll insert these statements at the END of constructor, not right after super()
184
+ for (const { key, initialValue, isObject } of stateProperties) {
185
+ if (isObject) {
186
+ // For objects/arrays: this.state = this.reactive({ count: 0 });
187
+ statements.push(
188
+ t.expressionStatement(
189
+ t.assignmentExpression(
190
+ "=",
191
+ t.memberExpression(t.thisExpression(), t.identifier(key)),
192
+ t.callExpression(
193
+ t.memberExpression(
194
+ t.thisExpression(),
195
+ t.identifier("reactive")
196
+ ),
197
+ [initialValue]
198
+ )
199
+ )
200
+ )
201
+ );
202
+ } else {
203
+ // For primitives: use useState
204
+ // const [getState, setState] = this.useState("state", initialValue);
205
+ // Object.defineProperty(this, "state", { get: getState, set: setState });
206
+ const getterId = t.identifier(`_get${key}`);
207
+ const setterId = t.identifier(`_set${key}`);
208
+
209
+ statements.push(
210
+ t.variableDeclaration("const", [
211
+ t.variableDeclarator(
212
+ t.arrayPattern([getterId, setterId]),
213
+ t.callExpression(
214
+ t.memberExpression(
215
+ t.thisExpression(),
216
+ t.identifier("useState")
217
+ ),
218
+ [t.stringLiteral(key), initialValue]
219
+ )
220
+ ),
221
+ ])
222
+ );
223
+
224
+ statements.push(
225
+ t.expressionStatement(
226
+ t.callExpression(
227
+ t.memberExpression(
228
+ t.identifier("Object"),
229
+ t.identifier("defineProperty")
230
+ ),
231
+ [
232
+ t.thisExpression(),
233
+ t.stringLiteral(key),
234
+ t.objectExpression([
235
+ t.objectProperty(t.identifier("get"), getterId),
236
+ t.objectProperty(t.identifier("set"), setterId),
237
+ t.objectProperty(
238
+ t.identifier("enumerable"),
239
+ t.booleanLiteral(true)
240
+ ),
241
+ t.objectProperty(
242
+ t.identifier("configurable"),
243
+ t.booleanLiteral(true)
244
+ ),
245
+ ]),
246
+ ]
247
+ )
248
+ )
249
+ );
250
+ }
251
+ }
252
+
253
+ // CRITICAL: Insert statements at the END of constructor
254
+ // WebComponent already has reactive() and useState() methods
255
+ // Inserting at the end ensures all constructor code has run
256
+ constructor.body.body.push(...statements);
257
+ },
258
+ },
259
+ };
260
+ }
@@ -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,218 @@
1
+ /* eslint-disable no-console */
2
+ /**
3
+ * Vite Plugin for WSX with Babel decorator support
4
+ *
5
+ * Uses Babel to preprocess decorators before esbuild transformation
6
+ * This ensures decorators work correctly even with esbuild's limitations
7
+ */
8
+
9
+ import type { Plugin } from "vite";
10
+ import { transform } from "esbuild";
11
+ import { transformSync } from "@babel/core";
12
+ import { existsSync } from "fs";
13
+ import { dirname, join, basename } from "path";
14
+ import babelPluginWSXState from "./babel-plugin-wsx-state";
15
+ import babelPluginWSXStyle from "./babel-plugin-wsx-style";
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
+ debug = false,
34
+ extensions = [".wsx"],
35
+ autoStyleInjection = true,
36
+ } = options;
37
+
38
+ return {
39
+ name: "vite-plugin-wsx-babel",
40
+ enforce: "pre",
41
+
42
+ async transform(code: string, id: string) {
43
+ const isWSXFile = extensions.some((ext) => id.endsWith(ext));
44
+
45
+ if (!isWSXFile) {
46
+ return null;
47
+ }
48
+
49
+ if (debug) {
50
+ console.log(`[WSX Plugin Babel] Processing: ${id}`);
51
+ }
52
+
53
+ // Check if corresponding CSS file exists (for auto style injection)
54
+ let cssFileExists = false;
55
+ let cssFilePath = "";
56
+ let componentName = "";
57
+
58
+ if (autoStyleInjection) {
59
+ const fileDir = dirname(id);
60
+ const fileName = basename(id, extensions.find((ext) => id.endsWith(ext)) || "");
61
+ const cssFilePathWithoutQuery = join(fileDir, `${fileName}.css`);
62
+ cssFileExists = existsSync(cssFilePathWithoutQuery);
63
+ componentName = fileName;
64
+
65
+ // Generate relative path for import (e.g., "./Button.css?inline")
66
+ // Use relative path from the file's directory
67
+ if (cssFileExists) {
68
+ // For import statement, use relative path with ?inline query
69
+ cssFilePath = `./${fileName}.css?inline`;
70
+ }
71
+
72
+ if (cssFileExists) {
73
+ console.log(
74
+ `[WSX Plugin Babel] Found CSS file for auto-injection: ${cssFilePathWithoutQuery}, will inject: ${cssFilePath}`
75
+ );
76
+ }
77
+ }
78
+
79
+ let transformedCode = code;
80
+
81
+ // 1. Add JSX imports if needed
82
+ // Check for actual import statement
83
+ const hasWSXCoreImport = code.includes('from "@wsxjs/wsx-core"');
84
+ // Check if JSX factory functions are already imported
85
+ const hasJSXInImport =
86
+ hasWSXCoreImport &&
87
+ (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(code) ||
88
+ new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(code));
89
+
90
+ // If file has JSX syntax but no import, add it
91
+ // Note: @jsxImportSource pragma is just a hint, we need actual import for esbuild
92
+ if ((code.includes("<") || code.includes("Fragment")) && !hasJSXInImport) {
93
+ const importPath = getJSXFactoryImportPath(options);
94
+ const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";\n`;
95
+ transformedCode = importStatement + transformedCode;
96
+ }
97
+
98
+ // 2. Use Babel to preprocess decorators
99
+ try {
100
+ const babelResult = transformSync(transformedCode, {
101
+ filename: id, // Pass the actual filename so Babel knows it's .wsx
102
+ presets: [
103
+ [
104
+ "@babel/preset-typescript",
105
+ {
106
+ isTSX: true, // Enable JSX syntax
107
+ allExtensions: true, // Process all extensions, including .wsx
108
+ },
109
+ ],
110
+ ],
111
+ plugins: [
112
+ // CRITICAL: Style injection plugin must run FIRST
113
+ // This ensures _autoStyles property exists before state transformations
114
+ ...(autoStyleInjection && cssFileExists
115
+ ? [
116
+ [
117
+ babelPluginWSXStyle,
118
+ {
119
+ cssFileExists,
120
+ cssFilePath,
121
+ componentName,
122
+ },
123
+ ],
124
+ ]
125
+ : []),
126
+ // State decorator transformation runs after style injection
127
+ babelPluginWSXState,
128
+ [
129
+ "@babel/plugin-proposal-decorators",
130
+ {
131
+ version: "2023-05",
132
+ decoratorsBeforeExport: true,
133
+ },
134
+ ],
135
+ [
136
+ "@babel/plugin-proposal-class-properties",
137
+ {
138
+ loose: false,
139
+ },
140
+ ],
141
+ "@babel/plugin-transform-class-static-block", // Support static class blocks
142
+ ],
143
+ // parserOpts not needed - @babel/preset-typescript and plugins handle it
144
+ });
145
+
146
+ if (babelResult && babelResult.code) {
147
+ transformedCode = babelResult.code;
148
+ if (debug) {
149
+ console.log(`[WSX Plugin Babel] Decorators preprocessed: ${id}`);
150
+ // Log generated code for debugging
151
+ if (
152
+ transformedCode.includes("this.reactive") ||
153
+ transformedCode.includes("this.useState")
154
+ ) {
155
+ console.log(
156
+ `[WSX Plugin Babel] Generated reactive code found in: ${id}\n` +
157
+ transformedCode
158
+ .split("\n")
159
+ .filter(
160
+ (line) =>
161
+ line.includes("this.reactive") ||
162
+ line.includes("this.useState")
163
+ )
164
+ .join("\n")
165
+ );
166
+ }
167
+ }
168
+ }
169
+ } catch (error) {
170
+ console.warn(
171
+ `[WSX Plugin Babel] Babel transform failed for ${id}, falling back to esbuild only:`,
172
+ error
173
+ );
174
+ }
175
+
176
+ // 2.5. Ensure JSX imports still exist after Babel transformation
177
+ // Babel might have removed or modified imports, so we check again
178
+ const hasJSXAfterBabel =
179
+ transformedCode.includes('from "@wsxjs/wsx-core"') &&
180
+ (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(transformedCode) ||
181
+ new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(transformedCode));
182
+
183
+ if (
184
+ (transformedCode.includes("<") || transformedCode.includes("Fragment")) &&
185
+ !hasJSXAfterBabel
186
+ ) {
187
+ const importPath = getJSXFactoryImportPath(options);
188
+ const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";\n`;
189
+ transformedCode = importStatement + transformedCode;
190
+ if (debug) {
191
+ console.log(
192
+ `[WSX Plugin Babel] Re-added JSX imports after Babel transform: ${id}`
193
+ );
194
+ }
195
+ }
196
+
197
+ // 3. Use esbuild for JSX transformation
198
+ try {
199
+ const result = await transform(transformedCode, {
200
+ loader: "jsx", // Already TypeScript-transformed by Babel
201
+ jsx: "transform",
202
+ jsxFactory: jsxFactory,
203
+ jsxFragment: jsxFragment,
204
+ target: "es2020",
205
+ format: "esm",
206
+ });
207
+
208
+ return {
209
+ code: result.code,
210
+ map: null,
211
+ };
212
+ } catch (error) {
213
+ console.error(`[WSX Plugin Babel] Transform error for ${id}:`, error);
214
+ throw error;
215
+ }
216
+ },
217
+ };
218
+ }