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