@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/dist/index.mjs CHANGED
@@ -1,92 +1,621 @@
1
- // src/vite-plugin-wsx.ts
1
+ // src/vite-plugin-wsx-babel.ts
2
2
  import { transform } from "esbuild";
3
+ import { transformSync } from "@babel/core";
4
+ import { existsSync } from "fs";
5
+ import { dirname, join, basename } from "path";
6
+
7
+ // src/babel-plugin-wsx-state.ts
8
+ import * as tModule from "@babel/types";
9
+ function babelPluginWSXState() {
10
+ const t = tModule;
11
+ return {
12
+ name: "babel-plugin-wsx-state",
13
+ visitor: {
14
+ ClassDeclaration(path) {
15
+ const classBody = path.node.body;
16
+ const stateProperties = [];
17
+ console.info(
18
+ `[Babel Plugin WSX State] Processing class ${path.node.id?.name || "anonymous"}, members: ${classBody.body.length}`
19
+ );
20
+ for (const member of classBody.body) {
21
+ console.info(
22
+ ` - Member type: ${member.type}, key: ${member.type === "ClassProperty" || member.type === "ClassPrivateProperty" ? member.key?.name : "N/A"}`
23
+ );
24
+ if ((member.type === "ClassProperty" || member.type === "ClassPrivateProperty") && member.key.type === "Identifier") {
25
+ console.info(
26
+ ` - Property: ${member.key.name}, decorators: ${member.decorators?.length || 0}, hasValue: ${!!member.value}`
27
+ );
28
+ if (member.decorators && member.decorators.length > 0) {
29
+ member.decorators.forEach((decorator) => {
30
+ if (decorator.expression.type === "Identifier") {
31
+ console.info(` Decorator: ${decorator.expression.name}`);
32
+ } else if (decorator.expression.type === "CallExpression" && decorator.expression.callee.type === "Identifier") {
33
+ console.debug(
34
+ ` Decorator: ${decorator.expression.callee.name}()`
35
+ );
36
+ }
37
+ });
38
+ }
39
+ const hasStateDecorator = member.decorators?.some(
40
+ (decorator) => {
41
+ if (decorator.expression.type === "Identifier" && decorator.expression.name === "state") {
42
+ return true;
43
+ }
44
+ if (decorator.expression.type === "CallExpression" && decorator.expression.callee.type === "Identifier" && decorator.expression.callee.name === "state") {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ );
50
+ if (hasStateDecorator && member.value) {
51
+ const key = member.key.name;
52
+ const initialValue = member.value;
53
+ const isObject = initialValue.type === "ObjectExpression" || initialValue.type === "ArrayExpression";
54
+ const isArray = initialValue.type === "ArrayExpression";
55
+ stateProperties.push({
56
+ key,
57
+ initialValue,
58
+ isObject,
59
+ isArray
60
+ // Add isArray flag
61
+ });
62
+ if (member.decorators) {
63
+ member.decorators = member.decorators.filter(
64
+ (decorator) => {
65
+ if (decorator.expression.type === "Identifier" && decorator.expression.name === "state") {
66
+ return false;
67
+ }
68
+ if (decorator.expression.type === "CallExpression" && decorator.expression.callee.type === "Identifier" && decorator.expression.callee.name === "state") {
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+ );
74
+ }
75
+ member.value = void 0;
76
+ }
77
+ }
78
+ }
79
+ if (stateProperties.length === 0) {
80
+ return;
81
+ }
82
+ let constructor = classBody.body.find(
83
+ (member) => member.type === "ClassMethod" && member.kind === "constructor"
84
+ );
85
+ if (!constructor) {
86
+ constructor = t.classMethod(
87
+ "constructor",
88
+ t.identifier("constructor"),
89
+ [],
90
+ t.blockStatement([])
91
+ );
92
+ classBody.body.unshift(constructor);
93
+ }
94
+ const statements = [];
95
+ const hasSuper = constructor.body.body.some(
96
+ (stmt) => stmt.type === "ExpressionStatement" && stmt.expression.type === "CallExpression" && stmt.expression.callee.type === "Super"
97
+ );
98
+ if (!hasSuper) {
99
+ statements.push(t.expressionStatement(t.callExpression(t.super(), [])));
100
+ }
101
+ for (const { key, initialValue, isObject } of stateProperties) {
102
+ if (isObject) {
103
+ const reactiveVarId = t.identifier(`_${key}Reactive`);
104
+ statements.push(
105
+ t.variableDeclaration("let", [
106
+ t.variableDeclarator(
107
+ reactiveVarId,
108
+ t.callExpression(
109
+ t.memberExpression(
110
+ t.thisExpression(),
111
+ t.identifier("reactive")
112
+ ),
113
+ [initialValue]
114
+ )
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
+ )
236
+ )
237
+ );
238
+ } else {
239
+ const getterId = t.identifier(`_get${key}`);
240
+ const setterId = t.identifier(`_set${key}`);
241
+ statements.push(
242
+ t.variableDeclaration("const", [
243
+ t.variableDeclarator(
244
+ t.arrayPattern([getterId, setterId]),
245
+ t.callExpression(
246
+ t.memberExpression(
247
+ t.thisExpression(),
248
+ t.identifier("useState")
249
+ ),
250
+ [t.stringLiteral(key), initialValue]
251
+ )
252
+ )
253
+ ])
254
+ );
255
+ statements.push(
256
+ t.expressionStatement(
257
+ t.callExpression(
258
+ t.memberExpression(
259
+ t.identifier("Object"),
260
+ t.identifier("defineProperty")
261
+ ),
262
+ [
263
+ t.thisExpression(),
264
+ t.stringLiteral(key),
265
+ t.objectExpression([
266
+ t.objectProperty(t.identifier("get"), getterId),
267
+ t.objectProperty(t.identifier("set"), setterId),
268
+ t.objectProperty(
269
+ t.identifier("enumerable"),
270
+ t.booleanLiteral(true)
271
+ ),
272
+ t.objectProperty(
273
+ t.identifier("configurable"),
274
+ t.booleanLiteral(true)
275
+ )
276
+ ])
277
+ ]
278
+ )
279
+ )
280
+ );
281
+ }
282
+ }
283
+ constructor.body.body.push(...statements);
284
+ }
285
+ }
286
+ };
287
+ }
288
+
289
+ // src/babel-plugin-wsx-style.ts
290
+ import * as tModule2 from "@babel/types";
291
+ function hasStylesImport(program) {
292
+ for (const node of program.body) {
293
+ if (node.type === "ImportDeclaration") {
294
+ const source = node.source.value;
295
+ if (typeof source === "string" && (source.endsWith(".css?inline") || source.endsWith(".css"))) {
296
+ const defaultSpecifier = node.specifiers.find(
297
+ (spec) => spec.type === "ImportDefaultSpecifier"
298
+ );
299
+ if (defaultSpecifier) {
300
+ return true;
301
+ }
302
+ }
303
+ }
304
+ }
305
+ return false;
306
+ }
307
+ function hasAutoStylesProperty(classBody) {
308
+ for (const member of classBody.body) {
309
+ if ((member.type === "ClassProperty" || member.type === "ClassPrivateProperty") && member.key.type === "Identifier" && member.key.name === "_autoStyles") {
310
+ return true;
311
+ }
312
+ }
313
+ return false;
314
+ }
315
+ function babelPluginWSXStyle() {
316
+ const t = tModule2;
317
+ return {
318
+ name: "babel-plugin-wsx-style",
319
+ visitor: {
320
+ Program(path, state) {
321
+ const { cssFileExists, cssFilePath, componentName } = state.opts;
322
+ if (!cssFileExists) {
323
+ return;
324
+ }
325
+ if (hasStylesImport(path.node)) {
326
+ console.info(
327
+ `[Babel Plugin WSX Style] Skipping ${componentName}: styles already manually imported`
328
+ );
329
+ return;
330
+ }
331
+ console.info(
332
+ `[Babel Plugin WSX Style] Injecting CSS import for ${componentName}: ${cssFilePath}`
333
+ );
334
+ const importStatement = t.importDeclaration(
335
+ [t.importDefaultSpecifier(t.identifier("styles"))],
336
+ t.stringLiteral(cssFilePath)
337
+ );
338
+ let insertIndex = 0;
339
+ for (let i = 0; i < path.node.body.length; i++) {
340
+ const node = path.node.body[i];
341
+ if (node.type === "ImportDeclaration") {
342
+ insertIndex = i + 1;
343
+ } else {
344
+ break;
345
+ }
346
+ }
347
+ path.node.body.splice(insertIndex, 0, importStatement);
348
+ },
349
+ ClassDeclaration(path, state) {
350
+ const { cssFileExists } = state.opts;
351
+ if (!cssFileExists) {
352
+ return;
353
+ }
354
+ const classBody = path.node.body;
355
+ if (hasAutoStylesProperty(classBody)) {
356
+ return;
357
+ }
358
+ const autoStylesProperty = t.classProperty(
359
+ t.identifier("_autoStyles"),
360
+ t.identifier("styles"),
361
+ null,
362
+ // typeAnnotation
363
+ [],
364
+ // decorators
365
+ false,
366
+ // computed
367
+ false
368
+ // static
369
+ );
370
+ classBody.body.unshift(autoStylesProperty);
371
+ }
372
+ }
373
+ };
374
+ }
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
+
494
+ // src/vite-plugin-wsx-babel.ts
3
495
  function getJSXFactoryImportPath(_options) {
4
496
  return "@wsxjs/wsx-core";
5
497
  }
6
- function vitePluginWSX(options = {}) {
498
+ function vitePluginWSXWithBabel(options = {}) {
7
499
  const {
8
500
  jsxFactory = "h",
9
501
  jsxFragment = "Fragment",
10
- debug = false,
11
- extensions = [".wsx"]
502
+ extensions = [".wsx"],
503
+ autoStyleInjection = true
12
504
  } = options;
13
505
  return {
14
- name: "vite-plugin-wsx",
506
+ name: "vite-plugin-wsx-babel",
15
507
  enforce: "pre",
16
- // 确保在 React 插件之前执行
17
- // 处理 .wsx 文件加载
18
- load(id) {
19
- const isWSXFile = extensions.some((ext) => id.endsWith(ext));
20
- if (!isWSXFile) {
21
- return null;
22
- }
23
- if (debug) {
24
- console.log(`[WSX Plugin] Loading: ${id}`);
25
- }
26
- return null;
27
- },
28
- // 在transform阶段处理文件
29
508
  async transform(code, id) {
30
509
  const isWSXFile = extensions.some((ext) => id.endsWith(ext));
31
510
  if (!isWSXFile) {
32
511
  return null;
33
512
  }
34
- if (debug) {
35
- console.log(`[WSX Plugin] Processing: ${id}`);
513
+ let cssFileExists = false;
514
+ let cssFilePath = "";
515
+ let componentName = "";
516
+ if (autoStyleInjection) {
517
+ const fileDir = dirname(id);
518
+ const fileName = basename(id, extensions.find((ext) => id.endsWith(ext)) || "");
519
+ const cssFilePathWithoutQuery = join(fileDir, `${fileName}.css`);
520
+ cssFileExists = existsSync(cssFilePathWithoutQuery);
521
+ componentName = fileName;
522
+ if (cssFileExists) {
523
+ cssFilePath = `./${fileName}.css?inline`;
524
+ }
36
525
  }
37
526
  let transformedCode = code;
38
527
  const hasWSXCoreImport = code.includes('from "@wsxjs/wsx-core"');
39
528
  const hasJSXInImport = hasWSXCoreImport && (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(code) || new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(code));
40
- if (debug) {
41
- console.log(`[WSX Plugin] Checking JSX imports for: ${id}`);
42
- console.log(` - hasWSXCoreImport: ${hasWSXCoreImport}`);
43
- console.log(` - hasJSXInImport: ${hasJSXInImport}`);
44
- console.log(` - has < character: ${code.includes("<")}`);
45
- console.log(` - has Fragment: ${code.includes("Fragment")}`);
46
- }
47
529
  if ((code.includes("<") || code.includes("Fragment")) && !hasJSXInImport) {
48
530
  const importPath = getJSXFactoryImportPath(options);
49
531
  const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";
50
532
  `;
51
533
  transformedCode = importStatement + transformedCode;
52
- if (debug) {
53
- console.log(`[WSX Plugin] Added JSX factory import to: ${id}`);
534
+ }
535
+ try {
536
+ const babelResult = transformSync(transformedCode, {
537
+ filename: id,
538
+ // Pass the actual filename so Babel knows it's .wsx
539
+ presets: [
540
+ [
541
+ "@babel/preset-typescript",
542
+ {
543
+ isTSX: true,
544
+ // Enable JSX syntax
545
+ allExtensions: true
546
+ // Process all extensions, including .wsx
547
+ }
548
+ ]
549
+ ],
550
+ plugins: [
551
+ // CRITICAL: Style injection plugin must run FIRST
552
+ // This ensures _autoStyles property exists before state transformations
553
+ ...autoStyleInjection && cssFileExists ? [
554
+ [
555
+ babelPluginWSXStyle,
556
+ {
557
+ cssFileExists,
558
+ cssFilePath,
559
+ componentName
560
+ }
561
+ ]
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,
566
+ // State decorator transformation runs after style injection
567
+ babelPluginWSXState,
568
+ [
569
+ "@babel/plugin-proposal-decorators",
570
+ {
571
+ version: "2023-05",
572
+ decoratorsBeforeExport: true
573
+ }
574
+ ],
575
+ [
576
+ "@babel/plugin-proposal-class-properties",
577
+ {
578
+ loose: false
579
+ }
580
+ ],
581
+ "@babel/plugin-transform-class-static-block"
582
+ // Support static class blocks
583
+ ]
584
+ // parserOpts not needed - @babel/preset-typescript and plugins handle it
585
+ });
586
+ if (babelResult && babelResult.code) {
587
+ transformedCode = babelResult.code;
54
588
  }
589
+ } catch {
590
+ }
591
+ const hasJSXAfterBabel = transformedCode.includes('from "@wsxjs/wsx-core"') && (new RegExp(`[{,]\\s*${jsxFactory}\\s*[},]`).test(transformedCode) || new RegExp(`[{,]\\s*${jsxFragment}\\s*[},]`).test(transformedCode));
592
+ if ((transformedCode.includes("<") || transformedCode.includes("Fragment")) && !hasJSXAfterBabel) {
593
+ const importPath = getJSXFactoryImportPath(options);
594
+ const importStatement = `import { ${jsxFactory}, ${jsxFragment} } from "${importPath}";
595
+ `;
596
+ transformedCode = importStatement + transformedCode;
55
597
  }
56
598
  try {
57
599
  const result = await transform(transformedCode, {
58
- loader: "tsx",
600
+ loader: "jsx",
601
+ // Already TypeScript-transformed by Babel
59
602
  jsx: "transform",
60
603
  jsxFactory,
61
604
  jsxFragment,
62
605
  target: "es2020",
63
606
  format: "esm"
64
- // Esbuild supports decorators natively with tsx loader
65
607
  });
66
- if (debug) {
67
- console.log(`[WSX Plugin] JSX transformed: ${id}`);
68
- }
69
608
  return {
70
609
  code: result.code,
71
610
  map: null
72
611
  };
73
612
  } catch (error) {
74
- console.error(`[WSX Plugin] Transform error for ${id}:`, error);
613
+ console.error(`[WSX Plugin Babel] Transform error for ${id}:`, error);
75
614
  throw error;
76
615
  }
77
- },
78
- // We handle JSX transformation directly in the transform hook
79
- // No need to modify global esbuild config
80
- // 构建开始时的日志
81
- buildStart() {
82
- if (debug) {
83
- console.log(`[WSX Plugin] Build started with extensions: ${extensions.join(", ")}`);
84
- console.log(`[WSX Plugin] JSX Factory: ${jsxFactory}, Fragment: ${jsxFragment}`);
85
- }
86
616
  }
87
617
  };
88
618
  }
89
- var vite_plugin_wsx_default = vitePluginWSX;
90
619
  export {
91
- vite_plugin_wsx_default as wsx
620
+ vitePluginWSXWithBabel as wsx
92
621
  };