apprun 3.36.0 → 3.36.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +133 -20
  3. package/WHATSNEW.md +20 -12
  4. package/apprun-book.jpg +0 -0
  5. package/apprun.d.ts +5 -2
  6. package/cli/app.js +29 -0
  7. package/cli/index.html +13 -0
  8. package/{apprun-cli.js → cli/index.js} +8 -14
  9. package/dist/apprun-dev-tools.js +1 -2
  10. package/dist/apprun-dev-tools.js.map +1 -1
  11. package/dist/apprun-html.esm.js +7 -7
  12. package/dist/apprun-html.esm.js.map +1 -1
  13. package/dist/apprun-html.js +1 -1
  14. package/dist/apprun-html.js.map +1 -1
  15. package/dist/apprun-play-html.esm.js +7 -7
  16. package/dist/apprun-play-html.esm.js.map +1 -1
  17. package/dist/apprun-play.js +1 -1
  18. package/dist/apprun-play.js.map +1 -1
  19. package/dist/apprun.esm.js +1 -1
  20. package/dist/apprun.esm.js.map +1 -1
  21. package/dist/apprun.js +1 -1
  22. package/dist/apprun.js.map +1 -1
  23. package/esm/app.js +10 -6
  24. package/esm/app.js.map +1 -1
  25. package/esm/apprun-dev-tools.js +1 -7
  26. package/esm/apprun-dev-tools.js.map +1 -1
  27. package/esm/apprun.js +18 -4
  28. package/esm/apprun.js.map +1 -1
  29. package/esm/component.js +4 -0
  30. package/esm/component.js.map +1 -1
  31. package/esm/router.js +241 -18
  32. package/esm/router.js.map +1 -1
  33. package/esm/vdom-my-new.js +329 -0
  34. package/esm/vdom-my-new.js.map +1 -0
  35. package/esm/vdom-my-prop-attr.js +227 -0
  36. package/esm/vdom-my-prop-attr.js.map +1 -0
  37. package/esm/vdom-my.js +109 -143
  38. package/esm/vdom-my.js.map +1 -1
  39. package/esm/version.js +1 -1
  40. package/index.html +1 -1
  41. package/jest.config.js +2 -1
  42. package/jest.setup.js +29 -3
  43. package/jsx-runtime.js +1 -1
  44. package/jsx-runtime.js.map +1 -1
  45. package/package.json +7 -7
  46. package/src/app.ts +11 -6
  47. package/src/apprun-dev-tools.tsx +1 -7
  48. package/src/apprun.ts +22 -4
  49. package/src/component.ts +4 -0
  50. package/src/router.ts +259 -15
  51. package/src/vdom-my-new.ts +311 -0
  52. package/src/vdom-my-prop-attr.ts +241 -0
  53. package/src/vdom-my.ts +109 -134
  54. package/src/version.ts +1 -1
  55. package/cli/export.js +0 -92
  56. package/cli/import.js +0 -68
  57. package/plan/plan-apprun-bugfixes.md +0 -207
@@ -0,0 +1,241 @@
1
+ /**
2
+ * VDOM Property and Attribute Handler
3
+ * Standards-compliant property/attribute handling with caching and special element support
4
+ * Features: Skip logic for preserving user interactions during VDOM reconciliation
5
+ * Exports: updateProps - Main function for DOM element property updates
6
+ * Updated: 2025-01-14 - Added skip logic for focus-sensitive, scroll, and media properties
7
+ */
8
+
9
+ import { find, html, svg } from 'property-information';
10
+
11
+ const ATTR_PROPS = '_props';
12
+ const propertyInfoCache = new Map<string, any>();
13
+
14
+ // Merge old and new props, handling className -> class conversion and null cleanup
15
+ function mergeProps(oldProps: { [key: string]: any }, newProps: { [key: string]: any }): { [key: string]: any } {
16
+ if (newProps) {
17
+ newProps['class'] = newProps['class'] || newProps['className'];
18
+ delete newProps['className'];
19
+ }
20
+
21
+ if (!oldProps || Object.keys(oldProps).length === 0) return newProps || {};
22
+ if (!newProps || Object.keys(newProps).length === 0) {
23
+ const props: { [key: string]: any } = {};
24
+ Object.keys(oldProps).forEach(p => props[p] = null);
25
+ return props;
26
+ }
27
+
28
+ const props: { [key: string]: any } = {};
29
+ Object.keys(oldProps).forEach(p => {
30
+ if (!(p in newProps)) props[p] = null;
31
+ });
32
+ Object.keys(newProps).forEach(p => props[p] = newProps[p]);
33
+ return props;
34
+ }
35
+
36
+ // Convert kebab-case to camelCase for dataset keys
37
+ function convertKebabToCamelCase(str: string): string {
38
+ if (str.length <= 1) return str.toLowerCase();
39
+ return str.split('-').map((word, index) =>
40
+ index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
41
+ ).join('');
42
+ }
43
+
44
+ // Cached property information lookup
45
+ function getPropertyInfo(name: string, isSvg: boolean) {
46
+ const cacheKey = `${name}:${isSvg}`;
47
+ let info = propertyInfoCache.get(cacheKey);
48
+ if (info === undefined) {
49
+ info = find(isSvg ? svg : html, name) || null;
50
+ propertyInfoCache.set(cacheKey, info);
51
+ }
52
+ return info;
53
+ }
54
+
55
+
56
+ // Specialized handlers for different attribute types
57
+ function setDatasetAttribute(element: Element, attributeName: string, value: any): void {
58
+ const camelKey = convertKebabToCamelCase(attributeName.slice(5));
59
+ if (value == null) {
60
+ delete (element as HTMLElement).dataset[camelKey];
61
+ } else {
62
+ (element as HTMLElement).dataset[camelKey] = String(value);
63
+ }
64
+ }
65
+
66
+ function setEventHandler(element: Element, name: string, value: any): void {
67
+ if (!name.startsWith('on')) return;
68
+
69
+ if (!value || typeof value === 'function') {
70
+ (element as any)[name] = value;
71
+ } else if (typeof value === 'string') {
72
+ if (value) element.setAttribute(name, value);
73
+ else element.removeAttribute(name);
74
+ }
75
+ }
76
+
77
+ function setBooleanAttribute(element: Element, attributeName: string, value: any): void {
78
+ if (shouldSetBooleanAttribute(value)) {
79
+ element.setAttribute(attributeName, attributeName);
80
+ } else {
81
+ element.removeAttribute(attributeName);
82
+ }
83
+ }
84
+
85
+ function setElementProperty(element: Element, propertyName: string, value: any): void {
86
+ try {
87
+ (element as any)[propertyName] = value;
88
+ } catch (error) {
89
+ setElementAttribute(element, propertyName, value, false);
90
+ }
91
+ }
92
+
93
+ function setElementAttribute(element: Element, attributeName: string, value: any, isSvg: boolean): void {
94
+ if (value == null) {
95
+ element.removeAttribute(attributeName);
96
+ return;
97
+ }
98
+
99
+ const stringValue = String(value);
100
+ if (isSvg && attributeName.includes(':')) {
101
+ const [prefix] = attributeName.split(':');
102
+ if (prefix === 'xlink') {
103
+ element.setAttributeNS('http://www.w3.org/1999/xlink', attributeName, stringValue);
104
+ } else {
105
+ element.setAttribute(attributeName, stringValue);
106
+ }
107
+ } else {
108
+ element.setAttribute(attributeName, stringValue);
109
+ }
110
+ }
111
+
112
+ function shouldSetBooleanAttribute(value: any): boolean {
113
+ if (value == null || value === false || value === '') return false;
114
+ if (value === true) return true;
115
+ if (typeof value === 'string') {
116
+ const lowerValue = value.toLowerCase();
117
+ return lowerValue !== 'false' && value !== '0';
118
+ }
119
+ return Boolean(value);
120
+ }
121
+
122
+ // Core property/attribute setting function - handles all property types with skip logic
123
+ function setAttributeOrProperty(element: Element, name: string, value: any, isSvg: boolean): void {
124
+ // Check if we should skip this property update to preserve user interaction
125
+ if (shouldSkipPatch(element as HTMLElement, name)) {
126
+ return;
127
+ }
128
+
129
+ // Special cases with dedicated handling
130
+ if (name === 'style') {
131
+ if ((element as HTMLElement).style.cssText) (element as HTMLElement).style.cssText = '';
132
+ if (typeof value === 'string') (element as HTMLElement).style.cssText = value;
133
+ else if (value && typeof value === 'object') {
134
+ for (const s in value) {
135
+ if ((element as HTMLElement).style[s] !== value[s]) (element as HTMLElement).style[s] = value[s];
136
+ }
137
+ }
138
+ return;
139
+ }
140
+
141
+ if (name === 'key') {
142
+ if (value !== undefined && value !== null) (element as any).key = value;
143
+ return;
144
+ }
145
+
146
+ if (name.startsWith('data-')) {
147
+ setDatasetAttribute(element, name, value);
148
+ return;
149
+ }
150
+
151
+ if (name.startsWith('on')) {
152
+ setEventHandler(element, name, value);
153
+ return;
154
+ }
155
+
156
+ // Form elements with special property requirements
157
+ if ((element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') &&
158
+ (name === 'value' || name === 'selected' || name === 'selectedIndex')) {
159
+ setElementProperty(element, name, value);
160
+ return;
161
+ }
162
+
163
+ // Input checked needs both property and attribute
164
+ if (element.tagName === 'INPUT' && name === 'checked') {
165
+ setElementProperty(element, name, value);
166
+ setBooleanAttribute(element, name, value);
167
+ return;
168
+ }
169
+
170
+ // Use property information for standard attributes
171
+ const info = getPropertyInfo(name, isSvg);
172
+ if (info) {
173
+ if (info.boolean || info.overloadedBoolean) {
174
+ setBooleanAttribute(element, info.attribute, value);
175
+ } else if (info.mustUseProperty && !isSvg) {
176
+ setElementProperty(element, info.property, value);
177
+ } else {
178
+ setElementAttribute(element, info.attribute, value, isSvg);
179
+ }
180
+ } else {
181
+ // Fallback handling for unknown attributes
182
+ if (name.startsWith('aria-') || name === 'role') {
183
+ setElementAttribute(element, name, value, isSvg);
184
+ } else if (name in element || (element as any)[name] !== undefined) {
185
+ setElementProperty(element, name, value);
186
+ } else {
187
+ setElementAttribute(element, name, value, isSvg);
188
+ }
189
+ }
190
+ }
191
+
192
+ // Skip logic for preventing user interaction disruption during VDOM reconciliation
193
+ function shouldSkipPatch(dom: HTMLElement, prop: string): boolean {
194
+ if (document.activeElement === dom) {
195
+ return ['value', 'selectionStart', 'selectionEnd', 'selectionDirection']
196
+ .includes(prop);
197
+ }
198
+ if (prop === 'scrollTop' || prop === 'scrollLeft') {
199
+ return true;
200
+ }
201
+ if (dom instanceof HTMLMediaElement &&
202
+ ['currentTime', 'paused', 'playbackRate', 'volume'].includes(prop)) {
203
+ return true;
204
+ }
205
+ return false; // default: allow normal diff logic
206
+ }
207
+
208
+ function isValidAttributeName(name: string): boolean {
209
+ return /^[a-zA-Z_:][\w\-:.]*$/.test(name) &&
210
+ !name.includes('<') && !name.includes('>') && !name.includes('"') && !name.includes("'");
211
+ }
212
+
213
+
214
+ // Main exported function for updating element properties
215
+ export function updateProps(element: Element, props: { [key: string]: any }, isSvg: boolean): void {
216
+ // console.assert(!!element);
217
+ const cached = element[ATTR_PROPS] || {};
218
+ const merged = mergeProps(cached, props);
219
+ element[ATTR_PROPS] = props || {};
220
+
221
+ applyPropertiesToElement(element, merged, props, isSvg);
222
+ }
223
+
224
+ // Apply merged properties to element with ref handling
225
+ function applyPropertiesToElement(
226
+ element: Element,
227
+ mergedProps: { [key: string]: any },
228
+ originalProps: { [key: string]: any } | null,
229
+ isSvg: boolean
230
+ ): void {
231
+ for (const name in mergedProps) {
232
+ if (isValidAttributeName(name)) {
233
+ setAttributeOrProperty(element, name, mergedProps[name], isSvg);
234
+ }
235
+ }
236
+
237
+ // Handle ref callback
238
+ if (mergedProps && typeof mergedProps['ref'] === 'function') {
239
+ window.requestAnimationFrame(() => mergedProps['ref'](element));
240
+ }
241
+ }
package/src/vdom-my.ts CHANGED
@@ -1,13 +1,56 @@
1
+ /** * VDOM Implementation for AppRun
2
+ *
3
+ * Notes for AppRun’s key prop
4
+ * Use the key prop only if you need to preserve browser-side DOM state (such as cursor
5
+ * position in an <input>, focus, or maintaining state in inline components).
6
+ *
7
+ * For most use cases, especially with stateless or purely data-driven UIs, key is not
8
+ * needed and may decrease performance by forcing DOM moves.
9
+ *
10
+ * AppRun aggressively updates DOM to match your vdom, so DOM node preservation is
11
+ * only valuable when you intentionally depend on browser-managed state.
12
+ *
13
+ * | Use Case | Should I use `key`? | Why |
14
+ * | ----------------------------- | ------------------- | --------------------- |
15
+ * | Preserve input cursor/focus | ✅ Yes | Keeps browser state |
16
+ * | Inline stateful component | ✅ Yes | Preserves component |
17
+ * | Regular data list (stateless) | 🚫 No | Unnecessary DOM moves |
18
+ * | Purely visual updates | 🚫 No | No state to preserve |
19
+ *
20
+ * Features:
21
+ * - Virtual DOM rendering and diffing for efficient DOM updates
22
+ * - JSX Fragment support for both Babel and TypeScript
23
+ * - Element creation with props, children, and event handling
24
+ * - SVG element support with proper namespace handling
25
+ * - Component lifecycle management and caching
26
+ * - Keyed element optimization with automatic cleanup for memory management
27
+ * - Safe HTML insertion and text node creation
28
+ * - Directive processing integration
29
+ *
30
+ * Implementation:
31
+ * - Uses plain JavaScript object for keyCache instead of Map for better performance
32
+ * - Implements automatic cleanup of disconnected elements from keyCache
33
+ * - Supports both string and function-based tags
34
+ * - Handles component mounting and state management
35
+ * - Optimized children updating with minimal DOM operations
36
+ * - Memory-efficient caching with configurable thresholds (500 ops, 1000 max size)
37
+ *
38
+ * Recent Changes:
39
+ * - 2025-07-15: Converted keyCache from Map to plain object ({}) for improved performance
40
+ * - Updated cleanup functions to use object property deletion instead of Map methods
41
+ * - Enhanced memory management with automatic cleanup of disconnected elements
42
+ * - Added comprehensive key prop usage documentation and guidelines
43
+ */
44
+
1
45
  import { VDOM, VNode } from './types';
2
46
  import directive from './directive';
47
+ import { updateProps } from './vdom-my-prop-attr';
3
48
  export type Element = any; //HTMLElement | SVGSVGElement | SVGElement;
4
49
 
5
50
  export function Fragment(props, ...children): any[] {
6
51
  return collect(children);
7
52
  }
8
53
 
9
- const ATTR_PROPS = '_props';
10
-
11
54
  function collect(children) {
12
55
  const ch = [];
13
56
  const push = (c) => {
@@ -25,6 +68,30 @@ function collect(children) {
25
68
  return ch;
26
69
  }
27
70
 
71
+ const keyCache: { [key: string]: Element } = {};
72
+ let cleanupCounter = 0;
73
+ const CLEANUP_THRESHOLD = 500; // Cleanup every 500 operations
74
+ const MAX_CACHE_SIZE = 1000;
75
+
76
+ // Lightweight cleanup function - only runs when needed
77
+ function cleanupKeyCache() {
78
+ if (Object.keys(keyCache).length <= MAX_CACHE_SIZE) return; // Skip if under limit
79
+
80
+ for (const [key, element] of Object.entries(keyCache)) {
81
+ if (!element.isConnected) {
82
+ delete keyCache[key];
83
+ }
84
+ }
85
+ }
86
+
87
+ // Export cleanup function for manual cleanup if needed
88
+ export function clearKeyCache() {
89
+ for (const key in keyCache) {
90
+ delete keyCache[key];
91
+ }
92
+ cleanupCounter = 0;
93
+ }
94
+
28
95
  export function createElement(tag: string | Function | [], props?: {}, ...children) {
29
96
  const ch = collect(children);
30
97
  if (typeof tag === 'string') return { tag, props, children: ch };
@@ -35,8 +102,6 @@ export function createElement(tag: string | Function | [], props?: {}, ...childr
35
102
  else throw new Error(`Unknown tag in vdom ${tag}`);
36
103
  };
37
104
 
38
- const keyCache = {};
39
-
40
105
  export const updateElement = (element: Element | string, nodes: VDOM, component = {}) => {
41
106
  // tslint:disable-next-line
42
107
  if (nodes == null || nodes === false) return;
@@ -77,86 +142,56 @@ function update(element: Element, node: VNode, isSvg: boolean) {
77
142
  updateProps(element, node.props, isSvg);
78
143
  }
79
144
 
80
- function updateChildren(element: Element, children: any[], isSvg: boolean) {
145
+ function updateChildren(element, children, isSvg: boolean) {
81
146
  const old_len = element.childNodes?.length || 0;
82
147
  const new_len = children?.length || 0;
83
-
84
- // Handle key-based reordering first if any children have keys
85
- const hasKeysInNewChildren = children?.some(child =>
86
- child && typeof child === 'object' && child.props && child.props.key !== undefined
87
- );
88
-
89
- if (hasKeysInNewChildren) {
90
- // Create a map of existing keyed elements
91
- const existingKeyedElements = new Map();
92
- for (let i = 0; i < old_len; i++) {
93
- const el = element.childNodes[i];
94
- if (el && el.key) {
95
- existingKeyedElements.set(el.key, el);
96
- }
97
- }
98
-
99
- // Build new DOM structure
100
- const fragment = document.createDocumentFragment();
101
- for (let i = 0; i < new_len; i++) {
102
- const child = children[i];
103
- if (child == null) continue;
104
-
105
- const key = child.props && child.props['key'];
106
- if (key && existingKeyedElements.has(key)) {
107
- // Reuse existing element
108
- const existingEl = existingKeyedElements.get(key);
109
- update(existingEl, child as VNode, isSvg);
110
- fragment.appendChild(existingEl);
111
- existingKeyedElements.delete(key); // Mark as used
112
- } else {
113
- // Create new element
114
- fragment.appendChild(create(child, isSvg));
115
- }
116
- }
117
-
118
- // Clear current children and append new structure
119
- while (element.firstChild) {
120
- element.removeChild(element.firstChild);
121
- }
122
- element.appendChild(fragment);
123
- return;
124
- }
125
-
126
- // Original non-keyed logic
127
148
  const len = Math.min(old_len, new_len);
128
149
  for (let i = 0; i < len; i++) {
129
150
  const child = children[i];
130
- if (child == null) continue;
131
151
  const el = element.childNodes[i];
132
- if (!el) continue; // Safety check for undefined childNodes
133
152
  if (typeof child === 'string') {
134
- if (el.nodeType === 3) {
135
- if (el.nodeValue !== child) {
136
- el.nodeValue = child;
153
+ if (el.textContent !== child) {
154
+ if (el.nodeType === 3) {
155
+ el.nodeValue = child
156
+ } else {
157
+ element.replaceChild(createText(child), el);
137
158
  }
138
- } else {
139
- element.replaceChild(createText(child), el);
140
159
  }
141
160
  } else if (child instanceof HTMLElement || child instanceof SVGElement) {
142
- element.replaceChild(child, el);
143
- } else if (child && typeof child === 'object') {
144
- update(element.childNodes[i], child as VNode, isSvg);
161
+ element.insertBefore(child, el);
162
+ } else {
163
+ const key = child.props && child.props['key'];
164
+ if (key) {
165
+ if (el.key === key) {
166
+ update(element.childNodes[i], child, isSvg);
167
+ } else {
168
+ // console.log(el.key, key);
169
+ const old = keyCache[key];
170
+ if (old) {
171
+ // const temp = old.nextSibling;
172
+ element.insertBefore(old, el);
173
+ // temp ? element.insertBefore(el, temp) : element.appendChild(el);
174
+ update(element.childNodes[i], child, isSvg);
175
+ } else {
176
+ element.replaceChild(create(child, isSvg), el);
177
+ }
178
+ }
179
+ } else {
180
+ update(element.childNodes[i], child, isSvg);
181
+ }
145
182
  }
146
183
  }
147
184
 
148
- // Remove extra old nodes
149
- while (element.childNodes.length > len) {
185
+ let n = element.childNodes?.length || 0;
186
+ while (n > len) {
150
187
  element.removeChild(element.lastChild);
188
+ n--;
151
189
  }
152
190
 
153
191
  if (new_len > len) {
154
192
  const d = document.createDocumentFragment();
155
193
  for (let i = len; i < children.length; i++) {
156
- const child = children[i];
157
- if (child != null) {
158
- d.appendChild(create(child, isSvg));
159
- }
194
+ d.appendChild(create(children[i], isSvg));
160
195
  }
161
196
  element.appendChild(d);
162
197
  }
@@ -182,12 +217,7 @@ function create(node: VNode | string | HTMLElement | SVGElement, isSvg: boolean)
182
217
  // console.assert(node !== null && node !== undefined);
183
218
  if ((node instanceof HTMLElement) || (node instanceof SVGElement)) return node;
184
219
  if (typeof node === "string") return createText(node);
185
-
186
- // Type guard for VNode objects - handle invalid node types gracefully
187
- if (!node || typeof node !== 'object' || !node.tag || (typeof node.tag === 'function')) {
188
- return createText(typeof node === 'object' ? JSON.stringify(node) : String(node ?? ''));
189
- }
190
-
220
+ if (!node.tag || (typeof node.tag === 'function')) return createText(JSON.stringify(node));
191
221
  isSvg = isSvg || node.tag === "svg";
192
222
  const element = isSvg
193
223
  ? document.createElementNS("http://www.w3.org/2000/svg", node.tag)
@@ -195,73 +225,18 @@ function create(node: VNode | string | HTMLElement | SVGElement, isSvg: boolean)
195
225
 
196
226
  updateProps(element, node.props, isSvg);
197
227
  if (node.children) node.children.forEach(child => element.appendChild(create(child, isSvg)));
198
- return element
199
- }
200
228
 
201
- function mergeProps(oldProps: {}, newProps: {}): {} {
202
- newProps['class'] = newProps['class'] || newProps['className'];
203
- delete newProps['className'];
204
- const props = {};
205
- if (oldProps) Object.keys(oldProps).forEach(p => props[p] = null);
206
- if (newProps) Object.keys(newProps).forEach(p => props[p] = newProps[p]);
207
- return props;
208
- }
209
-
210
- export function updateProps(element: Element, props: {}, isSvg) {
211
- // console.assert(!!element);
212
- const cached = element[ATTR_PROPS] || {};
213
- props = mergeProps(cached, props || {});
214
- element[ATTR_PROPS] = props;
229
+ if (node.props && (node.props as any).key !== undefined) {
230
+ (element as any).key = (node.props as any).key;
231
+ keyCache[(node.props as any).key] = element;
215
232
 
216
- for (const name in props) {
217
- const value = props[name];
218
- // if (cached[name] === value) continue;
219
- // console.log('updateProps', name, value);
220
- if (name.startsWith('data-')) {
221
- const dname = name.substring(5);
222
- const cname = dname.replace(/-(\w)/g, (match) => match[1].toUpperCase());
223
- if (element.dataset[cname] !== value) {
224
- if (value || value === "") element.dataset[cname] = value;
225
- else delete element.dataset[cname];
226
- }
227
- } else if (name === 'style') {
228
- if (element.style.cssText) element.style.cssText = '';
229
- if (typeof value === 'string') element.style.cssText = value;
230
- else {
231
- for (const s in value) {
232
- if (element.style[s] !== value[s]) element.style[s] = value[s];
233
- }
234
- }
235
- } else if (name.startsWith('xlink')) {
236
- const xname = name.replace('xlink', '').toLowerCase();
237
- if (value == null || value === false) {
238
- element.removeAttributeNS('http://www.w3.org/1999/xlink', xname);
239
- } else {
240
- element.setAttributeNS('http://www.w3.org/1999/xlink', xname, value);
241
- }
242
- } else if (name.startsWith('on')) {
243
- if (!value || typeof value === 'function') {
244
- element[name] = value;
245
- } else if (typeof value === 'string') {
246
- if (value) element.setAttribute(name, value);
247
- else element.removeAttribute(name);
248
- }
249
- } else if (/^id$|^class$|^list$|^readonly$|^contenteditable$|^role|-|^for$/g.test(name) || isSvg) {
250
- if (element.getAttribute(name) !== value) {
251
- if (value) element.setAttribute(name, value);
252
- else element.removeAttribute(name);
253
- }
254
- } else if (element[name] !== value) {
255
- element[name] = value;
233
+ // Lightweight cleanup - only when counter reaches threshold
234
+ if (++cleanupCounter >= CLEANUP_THRESHOLD) {
235
+ cleanupKeyCache();
236
+ cleanupCounter = 0;
256
237
  }
257
- if (name === 'key' && value !== undefined) {
258
- keyCache[value] = element;
259
- element.key = value; // Set key property on the DOM element
260
- }
261
- }
262
- if (props && typeof props['ref'] === 'function') {
263
- window.requestAnimationFrame(() => props['ref'](element));
264
238
  }
239
+ return element
265
240
  }
266
241
 
267
242
  function render_component(node, parent, idx) {
package/src/version.ts CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  // Import version from package.json to maintain single source of truth
12
12
  // This version string is used across the framework
13
- export const APPRUN_VERSION = '3.36.0';
13
+ export const APPRUN_VERSION = '3.36.1';
14
14
 
15
15
  // Version string with prefix for global tracking
16
16
  export const APPRUN_VERSION_GLOBAL = `AppRun-${APPRUN_VERSION}`;
package/cli/export.js DELETED
@@ -1,92 +0,0 @@
1
- import { exec } from 'child_process';
2
- import yaml from 'js-yaml';
3
- import { unlinkSync } from 'fs';
4
-
5
- function replacer(key, value) {
6
- if (typeof value === 'function') return value.toString();
7
- return ['', null].includes(value) || (typeof value === 'object' && (value.length === 0 || Object.keys(value).length === 0)) ? undefined : value;
8
- }
9
-
10
- function createProxy(obj) {
11
- const handler = {
12
- get(target, property, receiver) {
13
-
14
- // Get the property value
15
- const value = Reflect.get(target, property, receiver);
16
-
17
- // If the value is an object (including arrays), proxy it
18
- if (typeof value === 'object' && value !== null) {
19
- if (Array.isArray(value)) {
20
- // Proxy each element of the array if it's an object
21
- return value.map(item => createProxy(item));
22
- } else {
23
- // Recursively proxy the object
24
- return createProxy(value);
25
- }
26
- }
27
-
28
- return `{${property}}`
29
- },
30
-
31
- };
32
-
33
- return Array.isArray(obj) ?
34
- obj.map(item => createProxy(item)) : new Proxy(obj, handler);
35
- }
36
-
37
- function getVDOM(component) {
38
- let view;
39
- if (typeof component.state === 'object') {
40
- const proxy = createProxy(component.state);
41
- view = component.view(proxy);
42
- } else {
43
- view = component.view(component.state);
44
- }
45
- return view;
46
- }
47
-
48
- export default function export_yaml(fn) {
49
-
50
- const tmp = process.cwd() + '/_tmp.js';
51
- exec(`npx esbuild ${fn} --outfile=${tmp} --format=esm --bundle`, (error, stdout, stderr) => {
52
- if (error) {
53
- console.error(`exec error: ${error}`);
54
- return;
55
- }
56
-
57
- import(tmp).then((m) => {
58
-
59
- const defaul_export = m.default
60
- let component;
61
- if (typeof defaul_export === 'function' &&
62
- /^class\s/.test(Function.prototype.toString.call(defaul_export))) {
63
- component = new m.default();
64
- component = component.mount();
65
- } else if (typeof defaul_export === 'function') {
66
- component = defaul_export();
67
- } else {
68
- component = defaul_export;
69
- }
70
-
71
- const vdom = getVDOM(component);
72
- const events = component['_actions'].map(a => a.name);
73
-
74
- const component_def = {
75
- name: component.constructor.name,
76
- file: fn,
77
- state: component.state,
78
- view: vdom,
79
- actions: events,
80
- update: component.update,
81
- };
82
-
83
- const yaml_def = yaml.dump(component_def, { replacer });
84
- console.log(yaml_def);
85
-
86
- unlinkSync(tmp);
87
- });
88
- });
89
- }
90
-
91
-
92
-