@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/wsx-vite-plugin",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Vite plugin for WSX Framework",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -25,9 +25,17 @@
25
25
  "web-components"
26
26
  ],
27
27
  "dependencies": {
28
- "@wsxjs/wsx-core": "0.0.7"
28
+ "@babel/core": "^7.28.0",
29
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
30
+ "@babel/plugin-proposal-decorators": "^7.28.0",
31
+ "@babel/plugin-transform-class-static-block": "^7.28.0",
32
+ "@babel/preset-typescript": "^7.28.5",
33
+ "@babel/types": "^7.28.1",
34
+ "@wsxjs/wsx-core": "0.0.9"
29
35
  },
30
36
  "devDependencies": {
37
+ "@babel/traverse": "^7.28.5",
38
+ "@types/babel__core": "^7.20.5",
31
39
  "@types/jest": "^29.0.0",
32
40
  "@types/node": "^20.0.0",
33
41
  "jest": "^29.0.0",
@@ -37,12 +45,12 @@
37
45
  "vite": "^5.0.0"
38
46
  },
39
47
  "peerDependencies": {
40
- "vite": ">=4.0.0",
41
- "esbuild": ">=0.25.0"
48
+ "esbuild": ">=0.25.0",
49
+ "vite": ">=4.0.0"
42
50
  },
43
51
  "scripts": {
44
- "build": "tsup src/index.ts --format cjs,esm --dts --external esbuild",
45
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch --external esbuild",
52
+ "build": "tsup src/index.ts --format cjs,esm --dts --external esbuild --external @babel/core --external @babel/preset-typescript --external @babel/plugin-proposal-decorators --external @babel/plugin-proposal-class-properties --external @babel/plugin-transform-class-static-block --external @babel/types",
53
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch --external esbuild --external @babel/core --external @babel/preset-typescript --external @babel/plugin-proposal-decorators --external @babel/plugin-proposal-class-properties --external @babel/plugin-transform-class-static-block --external @babel/types",
46
54
  "test": "jest",
47
55
  "test:watch": "jest --watch",
48
56
  "test:coverage": "jest --coverage",
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Babel plugin to automatically add data-wsx-key attributes to focusable elements
3
+ *
4
+ * Transforms:
5
+ * <input value={this.name} onInput={this.handleInput} />
6
+ *
7
+ * To:
8
+ * <input
9
+ * data-wsx-key="MyComponent-input-text-0-0"
10
+ * value={this.name}
11
+ * onInput={this.handleInput}
12
+ * />
13
+ *
14
+ * This enables automatic focus preservation during component rerenders.
15
+ */
16
+
17
+ import type { PluginObj, NodePath } from "@babel/core";
18
+ import type * as t from "@babel/types";
19
+ import * as tModule from "@babel/types";
20
+
21
+ /**
22
+ * Focusable HTML elements that need keys
23
+ */
24
+ const FOCUSABLE_ELEMENTS = new Set([
25
+ "input",
26
+ "textarea",
27
+ "select",
28
+ "button", // Also focusable
29
+ ]);
30
+
31
+ /**
32
+ * Check if an element is focusable
33
+ */
34
+ function isFocusableElement(tagName: string, hasContentEditable: boolean): boolean {
35
+ const lowerTag = tagName.toLowerCase();
36
+ return FOCUSABLE_ELEMENTS.has(lowerTag) || hasContentEditable;
37
+ }
38
+
39
+ /**
40
+ * Extract props from JSX attributes for key generation
41
+ */
42
+ function extractPropsFromJSXAttributes(attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): {
43
+ id?: string;
44
+ name?: string;
45
+ type?: string;
46
+ } {
47
+ const props: { id?: string; name?: string; type?: string } = {};
48
+
49
+ for (const attr of attributes) {
50
+ if (tModule.isJSXAttribute(attr) && tModule.isJSXIdentifier(attr.name)) {
51
+ const keyName = attr.name.name;
52
+
53
+ if (keyName === "id" || keyName === "name" || keyName === "type") {
54
+ if (tModule.isStringLiteral(attr.value)) {
55
+ props[keyName as "id" | "name" | "type"] = attr.value.value;
56
+ } else if (
57
+ tModule.isJSXExpressionContainer(attr.value) &&
58
+ tModule.isStringLiteral(attr.value.expression)
59
+ ) {
60
+ props[keyName as "id" | "name" | "type"] = attr.value.expression.value;
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ return props;
67
+ }
68
+
69
+ /**
70
+ * Generate stable key for an element
71
+ * @param tagName - HTML tag name
72
+ * @param componentName - Component class name
73
+ * @param path - Path from root (array of sibling indices)
74
+ * @param props - Element properties (for id, name, type)
75
+ * @returns Stable key string
76
+ */
77
+ function generateStableKey(
78
+ tagName: string,
79
+ componentName: string,
80
+ path: number[],
81
+ props: { id?: string; name?: string; type?: string }
82
+ ): string {
83
+ const pathStr = path.join("-");
84
+ const lowerTag = tagName.toLowerCase();
85
+
86
+ // Priority: id > name > type + path
87
+ if (props.id) {
88
+ return `${componentName}-${props.id}`;
89
+ }
90
+
91
+ if (props.name) {
92
+ return `${componentName}-${props.name}`;
93
+ }
94
+
95
+ // Default: component-tag-type-path
96
+ const typeStr = props.type || "text";
97
+ return `${componentName}-${lowerTag}-${typeStr}-${pathStr}`;
98
+ }
99
+
100
+ /**
101
+ * Calculate path from root JSX element
102
+ */
103
+ function calculateJSXPath(path: NodePath<t.JSXOpeningElement>): number[] {
104
+ const pathArray: number[] = [];
105
+ let currentPath: NodePath | null = path.parentPath; // JSXElement
106
+
107
+ // Walk up to find siblings
108
+ while (currentPath) {
109
+ if (currentPath.isJSXElement()) {
110
+ const parent = currentPath.parentPath;
111
+ if (parent && parent.isJSXElement()) {
112
+ // Find index in parent's children
113
+ const parentNode = parent.node;
114
+ let index = 0;
115
+ for (let i = 0; i < parentNode.children.length; i++) {
116
+ const child = parentNode.children[i];
117
+ if (child === currentPath.node) {
118
+ index = i;
119
+ break;
120
+ }
121
+ }
122
+ pathArray.unshift(index);
123
+ } else if (parent && parent.isReturnStatement()) {
124
+ // At root level
125
+ break;
126
+ }
127
+ } else if (currentPath.isReturnStatement()) {
128
+ // At root level
129
+ break;
130
+ }
131
+ currentPath = currentPath.parentPath;
132
+ }
133
+
134
+ return pathArray.length > 0 ? pathArray : [0];
135
+ }
136
+
137
+ /**
138
+ * Find component name from class declaration
139
+ */
140
+ function findComponentName(path: NodePath<t.JSXOpeningElement>): string {
141
+ let classPath = path;
142
+
143
+ // Find parent class declaration
144
+ while (classPath) {
145
+ if (classPath.isClassDeclaration()) {
146
+ if (classPath.node.id && tModule.isIdentifier(classPath.node.id)) {
147
+ return classPath.node.id.name;
148
+ }
149
+ break;
150
+ }
151
+ classPath = classPath.parentPath;
152
+ }
153
+
154
+ return "Component";
155
+ }
156
+
157
+ export default function babelPluginWSXFocus(): PluginObj {
158
+ const t = tModule;
159
+ return {
160
+ name: "babel-plugin-wsx-focus",
161
+ visitor: {
162
+ JSXOpeningElement(path) {
163
+ const element = path.node;
164
+
165
+ if (!t.isJSXIdentifier(element.name)) {
166
+ return;
167
+ }
168
+
169
+ const elementName = element.name.name;
170
+
171
+ // Check if already has data-wsx-key
172
+ const hasKey = element.attributes.some(
173
+ (attr) =>
174
+ t.isJSXAttribute(attr) &&
175
+ t.isJSXIdentifier(attr.name) &&
176
+ attr.name.name === "data-wsx-key"
177
+ );
178
+
179
+ if (hasKey) {
180
+ return; // Skip if already has key
181
+ }
182
+
183
+ // Extract props
184
+ const props = extractPropsFromJSXAttributes(element.attributes);
185
+
186
+ // Check for contenteditable attribute
187
+ const hasContentEditable = element.attributes.some(
188
+ (attr) =>
189
+ t.isJSXAttribute(attr) &&
190
+ t.isJSXIdentifier(attr.name) &&
191
+ (attr.name.name === "contenteditable" ||
192
+ attr.name.name === "contentEditable")
193
+ );
194
+
195
+ // Check if element is focusable
196
+ if (!isFocusableElement(elementName, hasContentEditable)) {
197
+ return; // Skip non-focusable elements
198
+ }
199
+
200
+ // Get component name
201
+ const componentName = findComponentName(path);
202
+
203
+ // Calculate path from root
204
+ const pathArray = calculateJSXPath(path);
205
+
206
+ // Generate key
207
+ const key = generateStableKey(elementName, componentName, pathArray, props);
208
+
209
+ // Add data-wsx-key attribute
210
+ const keyAttr = t.jsxAttribute(
211
+ t.jsxIdentifier("data-wsx-key"),
212
+ t.stringLiteral(key)
213
+ );
214
+
215
+ element.attributes.push(keyAttr);
216
+ },
217
+ },
218
+ };
219
+ }
@@ -0,0 +1,394 @@
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
+ isArray?: boolean; // Add isArray flag
25
+ }>;
26
+ reactiveMethodName: string;
27
+ }
28
+
29
+ export default function babelPluginWSXState(): PluginObj<WSXStatePluginPass> {
30
+ const t = tModule;
31
+ return {
32
+ name: "babel-plugin-wsx-state",
33
+ visitor: {
34
+ ClassDeclaration(path) {
35
+ const classBody = path.node.body;
36
+ const stateProperties: Array<{
37
+ key: string;
38
+ initialValue: t.Expression;
39
+ isObject: boolean;
40
+ isArray?: boolean;
41
+ }> = [];
42
+
43
+ // Find all @state decorated properties
44
+ // Debug: log all class members
45
+ console.info(
46
+ `[Babel Plugin WSX State] Processing class ${path.node.id?.name || "anonymous"}, members: ${classBody.body.length}`
47
+ );
48
+
49
+ for (const member of classBody.body) {
50
+ // Debug: log member type
51
+ console.info(
52
+ ` - Member type: ${member.type}, key: ${member.type === "ClassProperty" || member.type === "ClassPrivateProperty" ? (member.key as any)?.name : "N/A"}`
53
+ );
54
+
55
+ // Check both ClassProperty and ClassPrivateProperty
56
+ // @babel/plugin-proposal-class-properties might convert them
57
+ if (
58
+ (member.type === "ClassProperty" ||
59
+ member.type === "ClassPrivateProperty") &&
60
+ member.key.type === "Identifier"
61
+ ) {
62
+ // Debug: log all class properties
63
+ console.info(
64
+ ` - Property: ${member.key.name}, decorators: ${member.decorators?.length || 0}, hasValue: ${!!member.value}`
65
+ );
66
+
67
+ if (member.decorators && member.decorators.length > 0) {
68
+ // Debug: log decorator names
69
+ member.decorators.forEach((decorator) => {
70
+ if (decorator.expression.type === "Identifier") {
71
+ console.info(` Decorator: ${decorator.expression.name}`);
72
+ } else if (
73
+ decorator.expression.type === "CallExpression" &&
74
+ decorator.expression.callee.type === "Identifier"
75
+ ) {
76
+ console.debug(
77
+ ` Decorator: ${decorator.expression.callee.name}()`
78
+ );
79
+ }
80
+ });
81
+ }
82
+
83
+ // Check if has @state decorator
84
+ const hasStateDecorator = member.decorators?.some(
85
+ (decorator: t.Decorator) => {
86
+ if (
87
+ decorator.expression.type === "Identifier" &&
88
+ decorator.expression.name === "state"
89
+ ) {
90
+ return true;
91
+ }
92
+ if (
93
+ decorator.expression.type === "CallExpression" &&
94
+ decorator.expression.callee.type === "Identifier" &&
95
+ decorator.expression.callee.name === "state"
96
+ ) {
97
+ return true;
98
+ }
99
+ return false;
100
+ }
101
+ );
102
+
103
+ if (hasStateDecorator && member.value) {
104
+ const key = member.key.name;
105
+ const initialValue = member.value as t.Expression;
106
+
107
+ // Determine if it's an object/array
108
+ const isObject =
109
+ initialValue.type === "ObjectExpression" ||
110
+ initialValue.type === "ArrayExpression";
111
+
112
+ // Check if it's specifically an array
113
+ const isArray = initialValue.type === "ArrayExpression";
114
+
115
+ stateProperties.push({
116
+ key,
117
+ initialValue,
118
+ isObject,
119
+ isArray, // Add isArray flag
120
+ });
121
+
122
+ // Remove @state decorator - but keep other decorators
123
+ if (member.decorators) {
124
+ member.decorators = member.decorators.filter(
125
+ (decorator: t.Decorator) => {
126
+ if (
127
+ decorator.expression.type === "Identifier" &&
128
+ decorator.expression.name === "state"
129
+ ) {
130
+ return false; // Remove @state decorator
131
+ }
132
+ if (
133
+ decorator.expression.type === "CallExpression" &&
134
+ decorator.expression.callee.type === "Identifier" &&
135
+ decorator.expression.callee.name === "state"
136
+ ) {
137
+ return false; // Remove @state() decorator
138
+ }
139
+ return true; // Keep other decorators
140
+ }
141
+ );
142
+ }
143
+
144
+ // Remove initial value - it will be set in constructor via this.reactive()
145
+ // Keep the property declaration but without initial value
146
+ member.value = undefined;
147
+ }
148
+ }
149
+ }
150
+
151
+ if (stateProperties.length === 0) {
152
+ return; // No @state properties found
153
+ }
154
+
155
+ // Find or create constructor
156
+ let constructor = classBody.body.find(
157
+ (member: t.ClassBody["body"][number]): member is t.ClassMethod =>
158
+ member.type === "ClassMethod" && member.kind === "constructor"
159
+ ) as t.ClassMethod | undefined;
160
+
161
+ if (!constructor) {
162
+ // Create constructor if it doesn't exist
163
+ constructor = t.classMethod(
164
+ "constructor",
165
+ t.identifier("constructor"),
166
+ [],
167
+ t.blockStatement([])
168
+ );
169
+ classBody.body.unshift(constructor);
170
+ }
171
+
172
+ // Add initialization code to constructor
173
+ const statements: t.Statement[] = [];
174
+
175
+ // Add super() call if not present
176
+ const hasSuper = constructor.body.body.some(
177
+ (stmt: t.Statement) =>
178
+ stmt.type === "ExpressionStatement" &&
179
+ stmt.expression.type === "CallExpression" &&
180
+ stmt.expression.callee.type === "Super"
181
+ );
182
+
183
+ if (!hasSuper) {
184
+ statements.push(t.expressionStatement(t.callExpression(t.super(), [])));
185
+ }
186
+
187
+ // CRITICAL: Add state property initialization AFTER all existing constructor code
188
+ // WebComponent already has reactive() and useState() methods
189
+ // We'll insert these statements at the END of constructor, not right after super()
190
+ for (const { key, initialValue, isObject } of stateProperties) {
191
+ if (isObject) {
192
+ // For objects/arrays: this.state = this.reactive({ count: 0 });
193
+ // Store the initial reactive value in a private variable
194
+ const reactiveVarId = t.identifier(`_${key}Reactive`);
195
+
196
+ // Create variable to store reactive value
197
+ statements.push(
198
+ t.variableDeclaration("let", [
199
+ t.variableDeclarator(
200
+ reactiveVarId,
201
+ t.callExpression(
202
+ t.memberExpression(
203
+ t.thisExpression(),
204
+ t.identifier("reactive")
205
+ ),
206
+ [initialValue]
207
+ )
208
+ ),
209
+ ])
210
+ );
211
+
212
+ // For both arrays and objects, create a getter/setter that automatically wraps new values in reactive()
213
+ // This ensures that when you do `this.state = { ... }` or `this.todos = [...]`,
214
+ // the new value is automatically wrapped in reactive()
215
+ // Create getter/setter using Object.defineProperty
216
+ statements.push(
217
+ t.expressionStatement(
218
+ t.callExpression(
219
+ t.memberExpression(
220
+ t.identifier("Object"),
221
+ t.identifier("defineProperty")
222
+ ),
223
+ [
224
+ t.thisExpression(),
225
+ t.stringLiteral(key),
226
+ t.objectExpression([
227
+ t.objectProperty(
228
+ t.identifier("get"),
229
+ t.arrowFunctionExpression([], reactiveVarId)
230
+ ),
231
+ t.objectProperty(
232
+ t.identifier("set"),
233
+ t.arrowFunctionExpression(
234
+ [t.identifier("newValue")],
235
+ t.blockStatement([
236
+ t.expressionStatement(
237
+ t.assignmentExpression(
238
+ "=",
239
+ reactiveVarId,
240
+ t.conditionalExpression(
241
+ // Check if newValue is an object or array
242
+ t.logicalExpression(
243
+ "&&",
244
+ t.binaryExpression(
245
+ "!==",
246
+ t.identifier(
247
+ "newValue"
248
+ ),
249
+ t.nullLiteral()
250
+ ),
251
+ t.logicalExpression(
252
+ "&&",
253
+ t.binaryExpression(
254
+ "!==",
255
+ t.unaryExpression(
256
+ "typeof",
257
+ t.identifier(
258
+ "newValue"
259
+ )
260
+ ),
261
+ t.stringLiteral(
262
+ "undefined"
263
+ )
264
+ ),
265
+ t.logicalExpression(
266
+ "||",
267
+ t.callExpression(
268
+ t.memberExpression(
269
+ t.identifier(
270
+ "Array"
271
+ ),
272
+ t.identifier(
273
+ "isArray"
274
+ )
275
+ ),
276
+ [
277
+ t.identifier(
278
+ "newValue"
279
+ ),
280
+ ]
281
+ ),
282
+ t.binaryExpression(
283
+ "===",
284
+ t.unaryExpression(
285
+ "typeof",
286
+ t.identifier(
287
+ "newValue"
288
+ )
289
+ ),
290
+ t.stringLiteral(
291
+ "object"
292
+ )
293
+ )
294
+ )
295
+ )
296
+ ),
297
+ // If object/array, wrap in reactive
298
+ t.callExpression(
299
+ t.memberExpression(
300
+ t.thisExpression(),
301
+ t.identifier("reactive")
302
+ ),
303
+ [t.identifier("newValue")]
304
+ ),
305
+ // Otherwise, just assign (for primitives)
306
+ t.identifier("newValue")
307
+ )
308
+ )
309
+ ),
310
+ // Trigger rerender when value is replaced
311
+ t.expressionStatement(
312
+ t.callExpression(
313
+ t.memberExpression(
314
+ t.thisExpression(),
315
+ t.identifier("scheduleRerender")
316
+ ),
317
+ []
318
+ )
319
+ ),
320
+ ])
321
+ )
322
+ ),
323
+ t.objectProperty(
324
+ t.identifier("enumerable"),
325
+ t.booleanLiteral(true)
326
+ ),
327
+ t.objectProperty(
328
+ t.identifier("configurable"),
329
+ t.booleanLiteral(true)
330
+ ),
331
+ ]),
332
+ ]
333
+ )
334
+ )
335
+ );
336
+ } else {
337
+ // For primitives: use useState
338
+ // const [getState, setState] = this.useState("state", initialValue);
339
+ // Object.defineProperty(this, "state", { get: getState, set: setState });
340
+ const getterId = t.identifier(`_get${key}`);
341
+ const setterId = t.identifier(`_set${key}`);
342
+
343
+ statements.push(
344
+ t.variableDeclaration("const", [
345
+ t.variableDeclarator(
346
+ t.arrayPattern([getterId, setterId]),
347
+ t.callExpression(
348
+ t.memberExpression(
349
+ t.thisExpression(),
350
+ t.identifier("useState")
351
+ ),
352
+ [t.stringLiteral(key), initialValue]
353
+ )
354
+ ),
355
+ ])
356
+ );
357
+
358
+ statements.push(
359
+ t.expressionStatement(
360
+ t.callExpression(
361
+ t.memberExpression(
362
+ t.identifier("Object"),
363
+ t.identifier("defineProperty")
364
+ ),
365
+ [
366
+ t.thisExpression(),
367
+ t.stringLiteral(key),
368
+ t.objectExpression([
369
+ t.objectProperty(t.identifier("get"), getterId),
370
+ t.objectProperty(t.identifier("set"), setterId),
371
+ t.objectProperty(
372
+ t.identifier("enumerable"),
373
+ t.booleanLiteral(true)
374
+ ),
375
+ t.objectProperty(
376
+ t.identifier("configurable"),
377
+ t.booleanLiteral(true)
378
+ ),
379
+ ]),
380
+ ]
381
+ )
382
+ )
383
+ );
384
+ }
385
+ }
386
+
387
+ // CRITICAL: Insert statements at the END of constructor
388
+ // WebComponent already has reactive() and useState() methods
389
+ // Inserting at the end ensures all constructor code has run
390
+ constructor.body.body.push(...statements);
391
+ },
392
+ },
393
+ };
394
+ }