@wsxjs/wsx-vite-plugin 0.0.8 → 0.0.10
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 +250 -35
- package/dist/index.mjs +250 -35
- package/package.json +2 -2
- package/src/babel-plugin-wsx-focus.ts +219 -0
- package/src/babel-plugin-wsx-state.ts +138 -4
- package/src/vite-plugin-wsx-babel.ts +6 -42
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.
|
|
138
|
-
t.
|
|
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
|
|
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.
|
|
102
|
-
t.
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.0.10",
|
|
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.
|
|
34
|
+
"@wsxjs/wsx-core": "0.0.10"
|
|
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.
|
|
189
|
-
t.
|
|
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
|
|
170
|
-
|
|
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
|