@wsxjs/wsx-vite-plugin 0.0.8 → 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/dist/index.js CHANGED
@@ -87,10 +87,13 @@ function babelPluginWSXState() {
87
87
  const key = member.key.name;
88
88
  const initialValue = member.value;
89
89
  const isObject = initialValue.type === "ObjectExpression" || initialValue.type === "ArrayExpression";
90
+ const isArray = initialValue.type === "ArrayExpression";
90
91
  stateProperties.push({
91
92
  key,
92
93
  initialValue,
93
- isObject
94
+ isObject,
95
+ isArray
96
+ // Add isArray flag
94
97
  });
95
98
  if (member.decorators) {
96
99
  member.decorators = member.decorators.filter(
@@ -133,11 +136,11 @@ function babelPluginWSXState() {
133
136
  }
134
137
  for (const { key, initialValue, isObject } of stateProperties) {
135
138
  if (isObject) {
139
+ const reactiveVarId = t.identifier(`_${key}Reactive`);
136
140
  statements.push(
137
- t.expressionStatement(
138
- t.assignmentExpression(
139
- "=",
140
- t.memberExpression(t.thisExpression(), t.identifier(key)),
141
+ t.variableDeclaration("let", [
142
+ t.variableDeclarator(
143
+ reactiveVarId,
141
144
  t.callExpression(
142
145
  t.memberExpression(
143
146
  t.thisExpression(),
@@ -146,6 +149,126 @@ function babelPluginWSXState() {
146
149
  [initialValue]
147
150
  )
148
151
  )
152
+ ])
153
+ );
154
+ statements.push(
155
+ t.expressionStatement(
156
+ t.callExpression(
157
+ t.memberExpression(
158
+ t.identifier("Object"),
159
+ t.identifier("defineProperty")
160
+ ),
161
+ [
162
+ t.thisExpression(),
163
+ t.stringLiteral(key),
164
+ t.objectExpression([
165
+ t.objectProperty(
166
+ t.identifier("get"),
167
+ t.arrowFunctionExpression([], reactiveVarId)
168
+ ),
169
+ t.objectProperty(
170
+ t.identifier("set"),
171
+ t.arrowFunctionExpression(
172
+ [t.identifier("newValue")],
173
+ t.blockStatement([
174
+ t.expressionStatement(
175
+ t.assignmentExpression(
176
+ "=",
177
+ reactiveVarId,
178
+ t.conditionalExpression(
179
+ // Check if newValue is an object or array
180
+ t.logicalExpression(
181
+ "&&",
182
+ t.binaryExpression(
183
+ "!==",
184
+ t.identifier(
185
+ "newValue"
186
+ ),
187
+ t.nullLiteral()
188
+ ),
189
+ t.logicalExpression(
190
+ "&&",
191
+ t.binaryExpression(
192
+ "!==",
193
+ t.unaryExpression(
194
+ "typeof",
195
+ t.identifier(
196
+ "newValue"
197
+ )
198
+ ),
199
+ t.stringLiteral(
200
+ "undefined"
201
+ )
202
+ ),
203
+ t.logicalExpression(
204
+ "||",
205
+ t.callExpression(
206
+ t.memberExpression(
207
+ t.identifier(
208
+ "Array"
209
+ ),
210
+ t.identifier(
211
+ "isArray"
212
+ )
213
+ ),
214
+ [
215
+ t.identifier(
216
+ "newValue"
217
+ )
218
+ ]
219
+ ),
220
+ t.binaryExpression(
221
+ "===",
222
+ t.unaryExpression(
223
+ "typeof",
224
+ t.identifier(
225
+ "newValue"
226
+ )
227
+ ),
228
+ t.stringLiteral(
229
+ "object"
230
+ )
231
+ )
232
+ )
233
+ )
234
+ ),
235
+ // If object/array, wrap in reactive
236
+ t.callExpression(
237
+ t.memberExpression(
238
+ t.thisExpression(),
239
+ t.identifier("reactive")
240
+ ),
241
+ [t.identifier("newValue")]
242
+ ),
243
+ // Otherwise, just assign (for primitives)
244
+ t.identifier("newValue")
245
+ )
246
+ )
247
+ ),
248
+ // Trigger rerender when value is replaced
249
+ t.expressionStatement(
250
+ t.callExpression(
251
+ t.memberExpression(
252
+ t.thisExpression(),
253
+ t.identifier("scheduleRerender")
254
+ ),
255
+ []
256
+ )
257
+ )
258
+ ])
259
+ )
260
+ ),
261
+ t.objectProperty(
262
+ t.identifier("enumerable"),
263
+ t.booleanLiteral(true)
264
+ ),
265
+ t.objectProperty(
266
+ t.identifier("configurable"),
267
+ t.booleanLiteral(true)
268
+ )
269
+ ])
270
+ ]
271
+ )
149
272
  )
150
273
  );
151
274
  } else {
@@ -286,6 +409,124 @@ function babelPluginWSXStyle() {
286
409
  };
287
410
  }
288
411
 
412
+ // src/babel-plugin-wsx-focus.ts
413
+ var tModule3 = __toESM(require("@babel/types"));
414
+ var FOCUSABLE_ELEMENTS = /* @__PURE__ */ new Set([
415
+ "input",
416
+ "textarea",
417
+ "select",
418
+ "button"
419
+ // Also focusable
420
+ ]);
421
+ function isFocusableElement(tagName, hasContentEditable) {
422
+ const lowerTag = tagName.toLowerCase();
423
+ return FOCUSABLE_ELEMENTS.has(lowerTag) || hasContentEditable;
424
+ }
425
+ function extractPropsFromJSXAttributes(attributes) {
426
+ const props = {};
427
+ for (const attr of attributes) {
428
+ if (tModule3.isJSXAttribute(attr) && tModule3.isJSXIdentifier(attr.name)) {
429
+ const keyName = attr.name.name;
430
+ if (keyName === "id" || keyName === "name" || keyName === "type") {
431
+ if (tModule3.isStringLiteral(attr.value)) {
432
+ props[keyName] = attr.value.value;
433
+ } else if (tModule3.isJSXExpressionContainer(attr.value) && tModule3.isStringLiteral(attr.value.expression)) {
434
+ props[keyName] = attr.value.expression.value;
435
+ }
436
+ }
437
+ }
438
+ }
439
+ return props;
440
+ }
441
+ function generateStableKey(tagName, componentName, path, props) {
442
+ const pathStr = path.join("-");
443
+ const lowerTag = tagName.toLowerCase();
444
+ if (props.id) {
445
+ return `${componentName}-${props.id}`;
446
+ }
447
+ if (props.name) {
448
+ return `${componentName}-${props.name}`;
449
+ }
450
+ const typeStr = props.type || "text";
451
+ return `${componentName}-${lowerTag}-${typeStr}-${pathStr}`;
452
+ }
453
+ function calculateJSXPath(path) {
454
+ const pathArray = [];
455
+ let currentPath = path.parentPath;
456
+ while (currentPath) {
457
+ if (currentPath.isJSXElement()) {
458
+ const parent = currentPath.parentPath;
459
+ if (parent && parent.isJSXElement()) {
460
+ const parentNode = parent.node;
461
+ let index = 0;
462
+ for (let i = 0; i < parentNode.children.length; i++) {
463
+ const child = parentNode.children[i];
464
+ if (child === currentPath.node) {
465
+ index = i;
466
+ break;
467
+ }
468
+ }
469
+ pathArray.unshift(index);
470
+ } else if (parent && parent.isReturnStatement()) {
471
+ break;
472
+ }
473
+ } else if (currentPath.isReturnStatement()) {
474
+ break;
475
+ }
476
+ currentPath = currentPath.parentPath;
477
+ }
478
+ return pathArray.length > 0 ? pathArray : [0];
479
+ }
480
+ function findComponentName(path) {
481
+ let classPath = path;
482
+ while (classPath) {
483
+ if (classPath.isClassDeclaration()) {
484
+ if (classPath.node.id && tModule3.isIdentifier(classPath.node.id)) {
485
+ return classPath.node.id.name;
486
+ }
487
+ break;
488
+ }
489
+ classPath = classPath.parentPath;
490
+ }
491
+ return "Component";
492
+ }
493
+ function babelPluginWSXFocus() {
494
+ const t = tModule3;
495
+ return {
496
+ name: "babel-plugin-wsx-focus",
497
+ visitor: {
498
+ JSXOpeningElement(path) {
499
+ const element = path.node;
500
+ if (!t.isJSXIdentifier(element.name)) {
501
+ return;
502
+ }
503
+ const elementName = element.name.name;
504
+ const hasKey = element.attributes.some(
505
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-wsx-key"
506
+ );
507
+ if (hasKey) {
508
+ return;
509
+ }
510
+ const props = extractPropsFromJSXAttributes(element.attributes);
511
+ const hasContentEditable = element.attributes.some(
512
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && (attr.name.name === "contenteditable" || attr.name.name === "contentEditable")
513
+ );
514
+ if (!isFocusableElement(elementName, hasContentEditable)) {
515
+ return;
516
+ }
517
+ const componentName = findComponentName(path);
518
+ const pathArray = calculateJSXPath(path);
519
+ const key = generateStableKey(elementName, componentName, pathArray, props);
520
+ const keyAttr = t.jsxAttribute(
521
+ t.jsxIdentifier("data-wsx-key"),
522
+ t.stringLiteral(key)
523
+ );
524
+ element.attributes.push(keyAttr);
525
+ }
526
+ }
527
+ };
528
+ }
529
+
289
530
  // src/vite-plugin-wsx-babel.ts
290
531
  function getJSXFactoryImportPath(_options) {
291
532
  return "@wsxjs/wsx-core";
@@ -294,7 +535,6 @@ function vitePluginWSXWithBabel(options = {}) {
294
535
  const {
295
536
  jsxFactory = "h",
296
537
  jsxFragment = "Fragment",
297
- debug = false,
298
538
  extensions = [".wsx"],
299
539
  autoStyleInjection = true
300
540
  } = options;
@@ -306,9 +546,6 @@ function vitePluginWSXWithBabel(options = {}) {
306
546
  if (!isWSXFile) {
307
547
  return null;
308
548
  }
309
- if (debug) {
310
- console.log(`[WSX Plugin Babel] Processing: ${id}`);
311
- }
312
549
  let cssFileExists = false;
313
550
  let cssFilePath = "";
314
551
  let componentName = "";
@@ -321,11 +558,6 @@ function vitePluginWSXWithBabel(options = {}) {
321
558
  if (cssFileExists) {
322
559
  cssFilePath = `./${fileName}.css?inline`;
323
560
  }
324
- if (cssFileExists) {
325
- console.log(
326
- `[WSX Plugin Babel] Found CSS file for auto-injection: ${cssFilePathWithoutQuery}, will inject: ${cssFilePath}`
327
- );
328
- }
329
561
  }
330
562
  let transformedCode = code;
331
563
  const hasWSXCoreImport = code.includes('from "@wsxjs/wsx-core"');
@@ -364,6 +596,9 @@ function vitePluginWSXWithBabel(options = {}) {
364
596
  }
365
597
  ]
366
598
  ] : [],
599
+ // Focus key generation plugin runs early to add data-wsx-key attributes
600
+ // This must run before JSX is transformed to h() calls
601
+ babelPluginWSXFocus,
367
602
  // State decorator transformation runs after style injection
368
603
  babelPluginWSXState,
369
604
  [
@@ -386,23 +621,8 @@ function vitePluginWSXWithBabel(options = {}) {
386
621
  });
387
622
  if (babelResult && babelResult.code) {
388
623
  transformedCode = babelResult.code;
389
- if (debug) {
390
- console.log(`[WSX Plugin Babel] Decorators preprocessed: ${id}`);
391
- if (transformedCode.includes("this.reactive") || transformedCode.includes("this.useState")) {
392
- console.log(
393
- `[WSX Plugin Babel] Generated reactive code found in: ${id}
394
- ` + transformedCode.split("\n").filter(
395
- (line) => line.includes("this.reactive") || line.includes("this.useState")
396
- ).join("\n")
397
- );
398
- }
399
- }
400
624
  }
401
- } catch (error) {
402
- console.warn(
403
- `[WSX Plugin Babel] Babel transform failed for ${id}, falling back to esbuild only:`,
404
- error
405
- );
625
+ } catch {
406
626
  }
407
627
  const hasJSXAfterBabel = transformedCode.includes('from "@wsxjs/wsx-core"') && (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(transformedCode) || new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(transformedCode));
408
628
  if ((transformedCode.includes("<") || transformedCode.includes("Fragment")) && !hasJSXAfterBabel) {
@@ -410,11 +630,6 @@ function vitePluginWSXWithBabel(options = {}) {
410
630
  const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";
411
631
  `;
412
632
  transformedCode = importStatement + transformedCode;
413
- if (debug) {
414
- console.log(
415
- `[WSX Plugin Babel] Re-added JSX imports after Babel transform: ${id}`
416
- );
417
- }
418
633
  }
419
634
  try {
420
635
  const result = await (0, import_esbuild.transform)(transformedCode, {
package/dist/index.mjs CHANGED
@@ -51,10 +51,13 @@ function babelPluginWSXState() {
51
51
  const key = member.key.name;
52
52
  const initialValue = member.value;
53
53
  const isObject = initialValue.type === "ObjectExpression" || initialValue.type === "ArrayExpression";
54
+ const isArray = initialValue.type === "ArrayExpression";
54
55
  stateProperties.push({
55
56
  key,
56
57
  initialValue,
57
- isObject
58
+ isObject,
59
+ isArray
60
+ // Add isArray flag
58
61
  });
59
62
  if (member.decorators) {
60
63
  member.decorators = member.decorators.filter(
@@ -97,11 +100,11 @@ function babelPluginWSXState() {
97
100
  }
98
101
  for (const { key, initialValue, isObject } of stateProperties) {
99
102
  if (isObject) {
103
+ const reactiveVarId = t.identifier(`_${key}Reactive`);
100
104
  statements.push(
101
- t.expressionStatement(
102
- t.assignmentExpression(
103
- "=",
104
- t.memberExpression(t.thisExpression(), t.identifier(key)),
105
+ t.variableDeclaration("let", [
106
+ t.variableDeclarator(
107
+ reactiveVarId,
105
108
  t.callExpression(
106
109
  t.memberExpression(
107
110
  t.thisExpression(),
@@ -110,6 +113,126 @@ function babelPluginWSXState() {
110
113
  [initialValue]
111
114
  )
112
115
  )
116
+ ])
117
+ );
118
+ statements.push(
119
+ t.expressionStatement(
120
+ t.callExpression(
121
+ t.memberExpression(
122
+ t.identifier("Object"),
123
+ t.identifier("defineProperty")
124
+ ),
125
+ [
126
+ t.thisExpression(),
127
+ t.stringLiteral(key),
128
+ t.objectExpression([
129
+ t.objectProperty(
130
+ t.identifier("get"),
131
+ t.arrowFunctionExpression([], reactiveVarId)
132
+ ),
133
+ t.objectProperty(
134
+ t.identifier("set"),
135
+ t.arrowFunctionExpression(
136
+ [t.identifier("newValue")],
137
+ t.blockStatement([
138
+ t.expressionStatement(
139
+ t.assignmentExpression(
140
+ "=",
141
+ reactiveVarId,
142
+ t.conditionalExpression(
143
+ // Check if newValue is an object or array
144
+ t.logicalExpression(
145
+ "&&",
146
+ t.binaryExpression(
147
+ "!==",
148
+ t.identifier(
149
+ "newValue"
150
+ ),
151
+ t.nullLiteral()
152
+ ),
153
+ t.logicalExpression(
154
+ "&&",
155
+ t.binaryExpression(
156
+ "!==",
157
+ t.unaryExpression(
158
+ "typeof",
159
+ t.identifier(
160
+ "newValue"
161
+ )
162
+ ),
163
+ t.stringLiteral(
164
+ "undefined"
165
+ )
166
+ ),
167
+ t.logicalExpression(
168
+ "||",
169
+ t.callExpression(
170
+ t.memberExpression(
171
+ t.identifier(
172
+ "Array"
173
+ ),
174
+ t.identifier(
175
+ "isArray"
176
+ )
177
+ ),
178
+ [
179
+ t.identifier(
180
+ "newValue"
181
+ )
182
+ ]
183
+ ),
184
+ t.binaryExpression(
185
+ "===",
186
+ t.unaryExpression(
187
+ "typeof",
188
+ t.identifier(
189
+ "newValue"
190
+ )
191
+ ),
192
+ t.stringLiteral(
193
+ "object"
194
+ )
195
+ )
196
+ )
197
+ )
198
+ ),
199
+ // If object/array, wrap in reactive
200
+ t.callExpression(
201
+ t.memberExpression(
202
+ t.thisExpression(),
203
+ t.identifier("reactive")
204
+ ),
205
+ [t.identifier("newValue")]
206
+ ),
207
+ // Otherwise, just assign (for primitives)
208
+ t.identifier("newValue")
209
+ )
210
+ )
211
+ ),
212
+ // Trigger rerender when value is replaced
213
+ t.expressionStatement(
214
+ t.callExpression(
215
+ t.memberExpression(
216
+ t.thisExpression(),
217
+ t.identifier("scheduleRerender")
218
+ ),
219
+ []
220
+ )
221
+ )
222
+ ])
223
+ )
224
+ ),
225
+ t.objectProperty(
226
+ t.identifier("enumerable"),
227
+ t.booleanLiteral(true)
228
+ ),
229
+ t.objectProperty(
230
+ t.identifier("configurable"),
231
+ t.booleanLiteral(true)
232
+ )
233
+ ])
234
+ ]
235
+ )
113
236
  )
114
237
  );
115
238
  } else {
@@ -250,6 +373,124 @@ function babelPluginWSXStyle() {
250
373
  };
251
374
  }
252
375
 
376
+ // src/babel-plugin-wsx-focus.ts
377
+ import * as tModule3 from "@babel/types";
378
+ var FOCUSABLE_ELEMENTS = /* @__PURE__ */ new Set([
379
+ "input",
380
+ "textarea",
381
+ "select",
382
+ "button"
383
+ // Also focusable
384
+ ]);
385
+ function isFocusableElement(tagName, hasContentEditable) {
386
+ const lowerTag = tagName.toLowerCase();
387
+ return FOCUSABLE_ELEMENTS.has(lowerTag) || hasContentEditable;
388
+ }
389
+ function extractPropsFromJSXAttributes(attributes) {
390
+ const props = {};
391
+ for (const attr of attributes) {
392
+ if (tModule3.isJSXAttribute(attr) && tModule3.isJSXIdentifier(attr.name)) {
393
+ const keyName = attr.name.name;
394
+ if (keyName === "id" || keyName === "name" || keyName === "type") {
395
+ if (tModule3.isStringLiteral(attr.value)) {
396
+ props[keyName] = attr.value.value;
397
+ } else if (tModule3.isJSXExpressionContainer(attr.value) && tModule3.isStringLiteral(attr.value.expression)) {
398
+ props[keyName] = attr.value.expression.value;
399
+ }
400
+ }
401
+ }
402
+ }
403
+ return props;
404
+ }
405
+ function generateStableKey(tagName, componentName, path, props) {
406
+ const pathStr = path.join("-");
407
+ const lowerTag = tagName.toLowerCase();
408
+ if (props.id) {
409
+ return `${componentName}-${props.id}`;
410
+ }
411
+ if (props.name) {
412
+ return `${componentName}-${props.name}`;
413
+ }
414
+ const typeStr = props.type || "text";
415
+ return `${componentName}-${lowerTag}-${typeStr}-${pathStr}`;
416
+ }
417
+ function calculateJSXPath(path) {
418
+ const pathArray = [];
419
+ let currentPath = path.parentPath;
420
+ while (currentPath) {
421
+ if (currentPath.isJSXElement()) {
422
+ const parent = currentPath.parentPath;
423
+ if (parent && parent.isJSXElement()) {
424
+ const parentNode = parent.node;
425
+ let index = 0;
426
+ for (let i = 0; i < parentNode.children.length; i++) {
427
+ const child = parentNode.children[i];
428
+ if (child === currentPath.node) {
429
+ index = i;
430
+ break;
431
+ }
432
+ }
433
+ pathArray.unshift(index);
434
+ } else if (parent && parent.isReturnStatement()) {
435
+ break;
436
+ }
437
+ } else if (currentPath.isReturnStatement()) {
438
+ break;
439
+ }
440
+ currentPath = currentPath.parentPath;
441
+ }
442
+ return pathArray.length > 0 ? pathArray : [0];
443
+ }
444
+ function findComponentName(path) {
445
+ let classPath = path;
446
+ while (classPath) {
447
+ if (classPath.isClassDeclaration()) {
448
+ if (classPath.node.id && tModule3.isIdentifier(classPath.node.id)) {
449
+ return classPath.node.id.name;
450
+ }
451
+ break;
452
+ }
453
+ classPath = classPath.parentPath;
454
+ }
455
+ return "Component";
456
+ }
457
+ function babelPluginWSXFocus() {
458
+ const t = tModule3;
459
+ return {
460
+ name: "babel-plugin-wsx-focus",
461
+ visitor: {
462
+ JSXOpeningElement(path) {
463
+ const element = path.node;
464
+ if (!t.isJSXIdentifier(element.name)) {
465
+ return;
466
+ }
467
+ const elementName = element.name.name;
468
+ const hasKey = element.attributes.some(
469
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-wsx-key"
470
+ );
471
+ if (hasKey) {
472
+ return;
473
+ }
474
+ const props = extractPropsFromJSXAttributes(element.attributes);
475
+ const hasContentEditable = element.attributes.some(
476
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && (attr.name.name === "contenteditable" || attr.name.name === "contentEditable")
477
+ );
478
+ if (!isFocusableElement(elementName, hasContentEditable)) {
479
+ return;
480
+ }
481
+ const componentName = findComponentName(path);
482
+ const pathArray = calculateJSXPath(path);
483
+ const key = generateStableKey(elementName, componentName, pathArray, props);
484
+ const keyAttr = t.jsxAttribute(
485
+ t.jsxIdentifier("data-wsx-key"),
486
+ t.stringLiteral(key)
487
+ );
488
+ element.attributes.push(keyAttr);
489
+ }
490
+ }
491
+ };
492
+ }
493
+
253
494
  // src/vite-plugin-wsx-babel.ts
254
495
  function getJSXFactoryImportPath(_options) {
255
496
  return "@wsxjs/wsx-core";
@@ -258,7 +499,6 @@ function vitePluginWSXWithBabel(options = {}) {
258
499
  const {
259
500
  jsxFactory = "h",
260
501
  jsxFragment = "Fragment",
261
- debug = false,
262
502
  extensions = [".wsx"],
263
503
  autoStyleInjection = true
264
504
  } = options;
@@ -270,9 +510,6 @@ function vitePluginWSXWithBabel(options = {}) {
270
510
  if (!isWSXFile) {
271
511
  return null;
272
512
  }
273
- if (debug) {
274
- console.log(`[WSX Plugin Babel] Processing: ${id}`);
275
- }
276
513
  let cssFileExists = false;
277
514
  let cssFilePath = "";
278
515
  let componentName = "";
@@ -285,11 +522,6 @@ function vitePluginWSXWithBabel(options = {}) {
285
522
  if (cssFileExists) {
286
523
  cssFilePath = `./${fileName}.css?inline`;
287
524
  }
288
- if (cssFileExists) {
289
- console.log(
290
- `[WSX Plugin Babel] Found CSS file for auto-injection: ${cssFilePathWithoutQuery}, will inject: ${cssFilePath}`
291
- );
292
- }
293
525
  }
294
526
  let transformedCode = code;
295
527
  const hasWSXCoreImport = code.includes('from "@wsxjs/wsx-core"');
@@ -328,6 +560,9 @@ function vitePluginWSXWithBabel(options = {}) {
328
560
  }
329
561
  ]
330
562
  ] : [],
563
+ // Focus key generation plugin runs early to add data-wsx-key attributes
564
+ // This must run before JSX is transformed to h() calls
565
+ babelPluginWSXFocus,
331
566
  // State decorator transformation runs after style injection
332
567
  babelPluginWSXState,
333
568
  [
@@ -350,23 +585,8 @@ function vitePluginWSXWithBabel(options = {}) {
350
585
  });
351
586
  if (babelResult && babelResult.code) {
352
587
  transformedCode = babelResult.code;
353
- if (debug) {
354
- console.log(`[WSX Plugin Babel] Decorators preprocessed: ${id}`);
355
- if (transformedCode.includes("this.reactive") || transformedCode.includes("this.useState")) {
356
- console.log(
357
- `[WSX Plugin Babel] Generated reactive code found in: ${id}
358
- ` + transformedCode.split("\n").filter(
359
- (line) => line.includes("this.reactive") || line.includes("this.useState")
360
- ).join("\n")
361
- );
362
- }
363
- }
364
588
  }
365
- } catch (error) {
366
- console.warn(
367
- `[WSX Plugin Babel] Babel transform failed for ${id}, falling back to esbuild only:`,
368
- error
369
- );
589
+ } catch {
370
590
  }
371
591
  const hasJSXAfterBabel = transformedCode.includes('from "@wsxjs/wsx-core"') && (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(transformedCode) || new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(transformedCode));
372
592
  if ((transformedCode.includes("<") || transformedCode.includes("Fragment")) && !hasJSXAfterBabel) {
@@ -374,11 +594,6 @@ function vitePluginWSXWithBabel(options = {}) {
374
594
  const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";
375
595
  `;
376
596
  transformedCode = importStatement + transformedCode;
377
- if (debug) {
378
- console.log(
379
- `[WSX Plugin Babel] Re-added JSX imports after Babel transform: ${id}`
380
- );
381
- }
382
597
  }
383
598
  try {
384
599
  const result = await transform(transformedCode, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/wsx-vite-plugin",
3
- "version": "0.0.8",
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",
@@ -31,7 +31,7 @@
31
31
  "@babel/plugin-transform-class-static-block": "^7.28.0",
32
32
  "@babel/preset-typescript": "^7.28.5",
33
33
  "@babel/types": "^7.28.1",
34
- "@wsxjs/wsx-core": "0.0.8"
34
+ "@wsxjs/wsx-core": "0.0.9"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@babel/traverse": "^7.28.5",
@@ -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
+ }
@@ -21,6 +21,7 @@ interface WSXStatePluginPass extends PluginPass {
21
21
  key: string;
22
22
  initialValue: t.Expression;
23
23
  isObject: boolean;
24
+ isArray?: boolean; // Add isArray flag
24
25
  }>;
25
26
  reactiveMethodName: string;
26
27
  }
@@ -36,6 +37,7 @@ export default function babelPluginWSXState(): PluginObj<WSXStatePluginPass> {
36
37
  key: string;
37
38
  initialValue: t.Expression;
38
39
  isObject: boolean;
40
+ isArray?: boolean;
39
41
  }> = [];
40
42
 
41
43
  // Find all @state decorated properties
@@ -107,10 +109,14 @@ export default function babelPluginWSXState(): PluginObj<WSXStatePluginPass> {
107
109
  initialValue.type === "ObjectExpression" ||
108
110
  initialValue.type === "ArrayExpression";
109
111
 
112
+ // Check if it's specifically an array
113
+ const isArray = initialValue.type === "ArrayExpression";
114
+
110
115
  stateProperties.push({
111
116
  key,
112
117
  initialValue,
113
118
  isObject,
119
+ isArray, // Add isArray flag
114
120
  });
115
121
 
116
122
  // Remove @state decorator - but keep other decorators
@@ -184,11 +190,14 @@ export default function babelPluginWSXState(): PluginObj<WSXStatePluginPass> {
184
190
  for (const { key, initialValue, isObject } of stateProperties) {
185
191
  if (isObject) {
186
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
187
197
  statements.push(
188
- t.expressionStatement(
189
- t.assignmentExpression(
190
- "=",
191
- t.memberExpression(t.thisExpression(), t.identifier(key)),
198
+ t.variableDeclaration("let", [
199
+ t.variableDeclarator(
200
+ reactiveVarId,
192
201
  t.callExpression(
193
202
  t.memberExpression(
194
203
  t.thisExpression(),
@@ -196,6 +205,131 @@ export default function babelPluginWSXState(): PluginObj<WSXStatePluginPass> {
196
205
  ),
197
206
  [initialValue]
198
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
+ ]
199
333
  )
200
334
  )
201
335
  );
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-console */
2
1
  /**
3
2
  * Vite Plugin for WSX with Babel decorator support
4
3
  *
@@ -13,6 +12,7 @@ import { existsSync } from "fs";
13
12
  import { dirname, join, basename } from "path";
14
13
  import babelPluginWSXState from "./babel-plugin-wsx-state";
15
14
  import babelPluginWSXStyle from "./babel-plugin-wsx-style";
15
+ import babelPluginWSXFocus from "./babel-plugin-wsx-focus";
16
16
 
17
17
  export interface WSXPluginOptions {
18
18
  jsxFactory?: string;
@@ -30,7 +30,6 @@ export function vitePluginWSXWithBabel(options: WSXPluginOptions = {}): Plugin {
30
30
  const {
31
31
  jsxFactory = "h",
32
32
  jsxFragment = "Fragment",
33
- debug = false,
34
33
  extensions = [".wsx"],
35
34
  autoStyleInjection = true,
36
35
  } = options;
@@ -46,10 +45,6 @@ export function vitePluginWSXWithBabel(options: WSXPluginOptions = {}): Plugin {
46
45
  return null;
47
46
  }
48
47
 
49
- if (debug) {
50
- console.log(`[WSX Plugin Babel] Processing: ${id}`);
51
- }
52
-
53
48
  // Check if corresponding CSS file exists (for auto style injection)
54
49
  let cssFileExists = false;
55
50
  let cssFilePath = "";
@@ -68,12 +63,6 @@ export function vitePluginWSXWithBabel(options: WSXPluginOptions = {}): Plugin {
68
63
  // For import statement, use relative path with ?inline query
69
64
  cssFilePath = `./${fileName}.css?inline`;
70
65
  }
71
-
72
- if (cssFileExists) {
73
- console.log(
74
- `[WSX Plugin Babel] Found CSS file for auto-injection: ${cssFilePathWithoutQuery}, will inject: ${cssFilePath}`
75
- );
76
- }
77
66
  }
78
67
 
79
68
  let transformedCode = code;
@@ -123,6 +112,9 @@ export function vitePluginWSXWithBabel(options: WSXPluginOptions = {}): Plugin {
123
112
  ],
124
113
  ]
125
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,
126
118
  // State decorator transformation runs after style injection
127
119
  babelPluginWSXState,
128
120
  [
@@ -145,32 +137,9 @@ export function vitePluginWSXWithBabel(options: WSXPluginOptions = {}): Plugin {
145
137
 
146
138
  if (babelResult && babelResult.code) {
147
139
  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
140
  }
169
- } catch (error) {
170
- console.warn(
171
- `[WSX Plugin Babel] Babel transform failed for ${id}, falling back to esbuild only:`,
172
- error
173
- );
141
+ } catch {
142
+ // Babel transform failed, fallback to esbuild only
174
143
  }
175
144
 
176
145
  // 2.5. Ensure JSX imports still exist after Babel transformation
@@ -187,11 +156,6 @@ export function vitePluginWSXWithBabel(options: WSXPluginOptions = {}): Plugin {
187
156
  const importPath = getJSXFactoryImportPath(options);
188
157
  const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";\n`;
189
158
  transformedCode = importStatement + transformedCode;
190
- if (debug) {
191
- console.log(
192
- `[WSX Plugin Babel] Re-added JSX imports after Babel transform: ${id}`
193
- );
194
- }
195
159
  }
196
160
 
197
161
  // 3. Use esbuild for JSX transformation