pulse-js-framework 1.7.4 → 1.7.6
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/README.md +78 -392
- package/cli/analyze.js +127 -46
- package/cli/build.js +51 -13
- package/cli/dev.js +14 -0
- package/cli/docs-test.js +633 -0
- package/cli/format.js +64 -8
- package/cli/index.js +313 -31
- package/cli/lint.js +121 -27
- package/cli/logger.js +32 -4
- package/cli/release.js +50 -20
- package/cli/utils/cli-ui.js +452 -0
- package/compiler/parser.js +19 -2
- package/core/errors.js +2 -297
- package/package.json +16 -4
- package/runtime/async.js +282 -14
- package/runtime/dom-adapter.js +920 -0
- package/runtime/dom-advanced.js +357 -0
- package/runtime/dom-binding.js +230 -0
- package/runtime/dom-conditional.js +133 -0
- package/runtime/dom-element.js +142 -0
- package/runtime/dom-lifecycle.js +178 -0
- package/runtime/dom-list.js +267 -0
- package/runtime/dom-selector.js +267 -0
- package/runtime/dom.js +131 -1122
- package/runtime/errors.js +575 -0
- package/runtime/form.js +417 -22
- package/runtime/logger.js +144 -69
- package/runtime/logger.prod.js +43 -18
- package/runtime/native.js +398 -52
- package/runtime/pulse.js +202 -80
- package/runtime/router.js +31 -42
- package/runtime/store.js +90 -12
- package/runtime/utils.js +279 -18
- package/types/async.d.ts +310 -0
- package/types/form.d.ts +378 -0
- package/types/index.d.ts +44 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse DOM Conditional Module
|
|
3
|
+
* Conditional rendering primitives (when, match, show)
|
|
4
|
+
*
|
|
5
|
+
* @module dom-conditional
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { effect } from './pulse.js';
|
|
9
|
+
import { getAdapter } from './dom-adapter.js';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// CONDITIONAL RENDERING
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Conditional rendering - renders content based on condition
|
|
17
|
+
*
|
|
18
|
+
* @param {Function|Pulse} condition - Condition source (reactive)
|
|
19
|
+
* @param {Function|Node} thenTemplate - Template to render when true
|
|
20
|
+
* @param {Function|Node|null} elseTemplate - Template to render when false
|
|
21
|
+
* @returns {DocumentFragment} Container fragment with conditional content
|
|
22
|
+
*/
|
|
23
|
+
export function when(condition, thenTemplate, elseTemplate = null) {
|
|
24
|
+
const dom = getAdapter();
|
|
25
|
+
const container = dom.createDocumentFragment();
|
|
26
|
+
const marker = dom.createComment('when');
|
|
27
|
+
dom.appendChild(container, marker);
|
|
28
|
+
|
|
29
|
+
let currentNodes = [];
|
|
30
|
+
let currentCleanup = null;
|
|
31
|
+
|
|
32
|
+
effect(() => {
|
|
33
|
+
const show = typeof condition === 'function' ? condition() : condition.get();
|
|
34
|
+
|
|
35
|
+
// Cleanup previous
|
|
36
|
+
for (const node of currentNodes) {
|
|
37
|
+
dom.removeNode(node);
|
|
38
|
+
}
|
|
39
|
+
if (currentCleanup) currentCleanup();
|
|
40
|
+
currentNodes = [];
|
|
41
|
+
currentCleanup = null;
|
|
42
|
+
|
|
43
|
+
// Render new
|
|
44
|
+
const template = show ? thenTemplate : elseTemplate;
|
|
45
|
+
if (template) {
|
|
46
|
+
const result = typeof template === 'function' ? template() : template;
|
|
47
|
+
if (result) {
|
|
48
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
49
|
+
const fragment = dom.createDocumentFragment();
|
|
50
|
+
for (const node of nodes) {
|
|
51
|
+
if (dom.isNode(node)) {
|
|
52
|
+
dom.appendChild(fragment, node);
|
|
53
|
+
currentNodes.push(node);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const markerParent = dom.getParentNode(marker);
|
|
57
|
+
if (markerParent) {
|
|
58
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return container;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Switch/case rendering - renders content based on matching value
|
|
69
|
+
*
|
|
70
|
+
* @param {Function|Pulse} getValue - Value source (reactive)
|
|
71
|
+
* @param {Object} cases - Map of value -> template, with optional 'default' key
|
|
72
|
+
* @returns {Comment} Marker node for position tracking
|
|
73
|
+
*/
|
|
74
|
+
export function match(getValue, cases) {
|
|
75
|
+
const dom = getAdapter();
|
|
76
|
+
const marker = dom.createComment('match');
|
|
77
|
+
let currentNodes = [];
|
|
78
|
+
|
|
79
|
+
effect(() => {
|
|
80
|
+
const value = typeof getValue === 'function' ? getValue() : getValue.get();
|
|
81
|
+
|
|
82
|
+
// Remove old nodes
|
|
83
|
+
for (const node of currentNodes) {
|
|
84
|
+
dom.removeNode(node);
|
|
85
|
+
}
|
|
86
|
+
currentNodes = [];
|
|
87
|
+
|
|
88
|
+
// Find matching case
|
|
89
|
+
const template = cases[value] ?? cases.default;
|
|
90
|
+
if (template) {
|
|
91
|
+
const result = typeof template === 'function' ? template() : template;
|
|
92
|
+
if (result) {
|
|
93
|
+
const nodes = Array.isArray(result) ? result : [result];
|
|
94
|
+
const fragment = dom.createDocumentFragment();
|
|
95
|
+
for (const node of nodes) {
|
|
96
|
+
if (dom.isNode(node)) {
|
|
97
|
+
dom.appendChild(fragment, node);
|
|
98
|
+
currentNodes.push(node);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const markerParent = dom.getParentNode(marker);
|
|
102
|
+
if (markerParent) {
|
|
103
|
+
dom.insertBefore(markerParent, fragment, dom.getNextSibling(marker));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return marker;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Toggle element visibility without removing from DOM
|
|
114
|
+
* Unlike when(), this keeps the element in the DOM but hides it
|
|
115
|
+
*
|
|
116
|
+
* @param {Function|Pulse} condition - Condition source (reactive)
|
|
117
|
+
* @param {HTMLElement} element - Element to show/hide
|
|
118
|
+
* @returns {HTMLElement} The element for chaining
|
|
119
|
+
*/
|
|
120
|
+
export function show(condition, element) {
|
|
121
|
+
const dom = getAdapter();
|
|
122
|
+
effect(() => {
|
|
123
|
+
const shouldShow = typeof condition === 'function' ? condition() : condition.get();
|
|
124
|
+
dom.setStyle(element, 'display', shouldShow ? '' : 'none');
|
|
125
|
+
});
|
|
126
|
+
return element;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default {
|
|
130
|
+
when,
|
|
131
|
+
match,
|
|
132
|
+
show
|
|
133
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse DOM Element Module
|
|
3
|
+
* Core element creation and text node utilities
|
|
4
|
+
*
|
|
5
|
+
* @module dom-element
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { effect } from './pulse.js';
|
|
9
|
+
import { loggers } from './logger.js';
|
|
10
|
+
import { safeSetAttribute } from './utils.js';
|
|
11
|
+
import { getAdapter } from './dom-adapter.js';
|
|
12
|
+
import { parseSelector } from './dom-selector.js';
|
|
13
|
+
|
|
14
|
+
const log = loggers.dom;
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// ELEMENT CREATION
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a DOM element from a CSS selector-like string
|
|
22
|
+
*
|
|
23
|
+
* @param {string} selector - CSS selector-like string (e.g., 'div.container#main')
|
|
24
|
+
* @param {...*} children - Child elements, text, or reactive functions
|
|
25
|
+
* @returns {HTMLElement} Created DOM element
|
|
26
|
+
*/
|
|
27
|
+
export function el(selector, ...children) {
|
|
28
|
+
const dom = getAdapter();
|
|
29
|
+
const config = parseSelector(selector);
|
|
30
|
+
const element = dom.createElement(config.tag);
|
|
31
|
+
|
|
32
|
+
if (config.id) {
|
|
33
|
+
dom.setProperty(element, 'id', config.id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (config.classes.length > 0) {
|
|
37
|
+
dom.setProperty(element, 'className', config.classes.join(' '));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(config.attrs)) {
|
|
41
|
+
// Use safeSetAttribute to prevent XSS via event handlers and javascript: URLs
|
|
42
|
+
safeSetAttribute(element, key, value, {}, dom);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Process children
|
|
46
|
+
for (const child of children) {
|
|
47
|
+
appendChild(element, child);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return element;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Append a child to an element, handling various types
|
|
55
|
+
*
|
|
56
|
+
* @private
|
|
57
|
+
* @param {HTMLElement} parent - Parent element
|
|
58
|
+
* @param {*} child - Child to append (string, number, Node, array, or function)
|
|
59
|
+
*/
|
|
60
|
+
function appendChild(parent, child) {
|
|
61
|
+
const dom = getAdapter();
|
|
62
|
+
|
|
63
|
+
if (child == null || child === false) return;
|
|
64
|
+
|
|
65
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
66
|
+
dom.appendChild(parent, dom.createTextNode(String(child)));
|
|
67
|
+
} else if (dom.isNode(child)) {
|
|
68
|
+
dom.appendChild(parent, child);
|
|
69
|
+
} else if (Array.isArray(child)) {
|
|
70
|
+
for (const c of child) {
|
|
71
|
+
appendChild(parent, c);
|
|
72
|
+
}
|
|
73
|
+
} else if (typeof child === 'function') {
|
|
74
|
+
// Reactive child - create a placeholder and update it
|
|
75
|
+
const placeholder = dom.createComment('pulse');
|
|
76
|
+
dom.appendChild(parent, placeholder);
|
|
77
|
+
let currentNodes = [];
|
|
78
|
+
|
|
79
|
+
effect(() => {
|
|
80
|
+
const result = child();
|
|
81
|
+
|
|
82
|
+
// Remove old nodes
|
|
83
|
+
for (const node of currentNodes) {
|
|
84
|
+
dom.removeNode(node);
|
|
85
|
+
}
|
|
86
|
+
currentNodes = [];
|
|
87
|
+
|
|
88
|
+
// Add new nodes
|
|
89
|
+
if (result != null && result !== false) {
|
|
90
|
+
const fragment = dom.createDocumentFragment();
|
|
91
|
+
if (typeof result === 'string' || typeof result === 'number') {
|
|
92
|
+
const textNode = dom.createTextNode(String(result));
|
|
93
|
+
dom.appendChild(fragment, textNode);
|
|
94
|
+
currentNodes.push(textNode);
|
|
95
|
+
} else if (dom.isNode(result)) {
|
|
96
|
+
dom.appendChild(fragment, result);
|
|
97
|
+
currentNodes.push(result);
|
|
98
|
+
} else if (Array.isArray(result)) {
|
|
99
|
+
for (const r of result) {
|
|
100
|
+
if (dom.isNode(r)) {
|
|
101
|
+
dom.appendChild(fragment, r);
|
|
102
|
+
currentNodes.push(r);
|
|
103
|
+
} else if (r != null && r !== false) {
|
|
104
|
+
const textNode = dom.createTextNode(String(r));
|
|
105
|
+
dom.appendChild(fragment, textNode);
|
|
106
|
+
currentNodes.push(textNode);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const placeholderParent = dom.getParentNode(placeholder);
|
|
111
|
+
if (placeholderParent) {
|
|
112
|
+
dom.insertBefore(placeholderParent, fragment, dom.getNextSibling(placeholder));
|
|
113
|
+
} else {
|
|
114
|
+
log.warn('Cannot insert reactive children: placeholder has no parent node');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a reactive text node
|
|
123
|
+
*
|
|
124
|
+
* @param {*|Function} getValue - Text value or function returning text
|
|
125
|
+
* @returns {Text} Text node
|
|
126
|
+
*/
|
|
127
|
+
export function text(getValue) {
|
|
128
|
+
const dom = getAdapter();
|
|
129
|
+
if (typeof getValue === 'function') {
|
|
130
|
+
const node = dom.createTextNode('');
|
|
131
|
+
effect(() => {
|
|
132
|
+
dom.setTextContent(node, String(getValue()));
|
|
133
|
+
});
|
|
134
|
+
return node;
|
|
135
|
+
}
|
|
136
|
+
return dom.createTextNode(String(getValue));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default {
|
|
140
|
+
el,
|
|
141
|
+
text
|
|
142
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse DOM Lifecycle Module
|
|
3
|
+
* Component lifecycle hooks and mounting utilities
|
|
4
|
+
*
|
|
5
|
+
* @module dom-lifecycle
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pulse, onCleanup } from './pulse.js';
|
|
9
|
+
import { loggers } from './logger.js';
|
|
10
|
+
import { getAdapter } from './dom-adapter.js';
|
|
11
|
+
import { Errors } from './errors.js';
|
|
12
|
+
import { resolveSelector } from './dom-selector.js';
|
|
13
|
+
|
|
14
|
+
const log = loggers.dom;
|
|
15
|
+
|
|
16
|
+
// Context utilities are injected by dom.js to avoid circular dependencies
|
|
17
|
+
let _contextUtils = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set context utilities for component factory (called by dom.js)
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
export function _setContextUtils(utils) {
|
|
24
|
+
_contextUtils = utils;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// LIFECYCLE TRACKING
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
let currentMountContext = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register a callback to run when component mounts
|
|
35
|
+
*
|
|
36
|
+
* @param {Function} fn - Callback to run on mount
|
|
37
|
+
*/
|
|
38
|
+
export function onMount(fn) {
|
|
39
|
+
if (currentMountContext) {
|
|
40
|
+
currentMountContext.mountCallbacks.push(fn);
|
|
41
|
+
} else {
|
|
42
|
+
// Defer to next microtask if no context
|
|
43
|
+
const dom = getAdapter();
|
|
44
|
+
dom.queueMicrotask(fn);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register a callback to run when component unmounts
|
|
50
|
+
*
|
|
51
|
+
* @param {Function} fn - Callback to run on unmount
|
|
52
|
+
*/
|
|
53
|
+
export function onUnmount(fn) {
|
|
54
|
+
if (currentMountContext) {
|
|
55
|
+
currentMountContext.unmountCallbacks.push(fn);
|
|
56
|
+
}
|
|
57
|
+
// Also register with effect cleanup if in an effect
|
|
58
|
+
onCleanup(fn);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get current mount context (for internal use)
|
|
63
|
+
* @returns {Object|null} Current mount context
|
|
64
|
+
*/
|
|
65
|
+
export function getMountContext() {
|
|
66
|
+
return currentMountContext;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set current mount context (for internal use)
|
|
71
|
+
* @param {Object|null} context - Mount context to set
|
|
72
|
+
* @returns {Object|null} Previous mount context
|
|
73
|
+
*/
|
|
74
|
+
export function setMountContext(context) {
|
|
75
|
+
const prev = currentMountContext;
|
|
76
|
+
currentMountContext = context;
|
|
77
|
+
return prev;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// MOUNTING
|
|
82
|
+
// =============================================================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mount an element to a target
|
|
86
|
+
*
|
|
87
|
+
* @param {string|HTMLElement} target - CSS selector or DOM element
|
|
88
|
+
* @param {Node} element - Element to mount
|
|
89
|
+
* @returns {Function} Unmount function
|
|
90
|
+
* @throws {Error} If target element is not found
|
|
91
|
+
*/
|
|
92
|
+
export function mount(target, element) {
|
|
93
|
+
const dom = getAdapter();
|
|
94
|
+
const { element: resolved, selector } = resolveSelector(target, 'mount');
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
throw Errors.mountNotFound(selector);
|
|
97
|
+
}
|
|
98
|
+
dom.appendChild(resolved, element);
|
|
99
|
+
return () => {
|
|
100
|
+
dom.removeNode(element);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// COMPONENT FACTORY
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a component factory with lifecycle support
|
|
110
|
+
*
|
|
111
|
+
* @param {Function} setup - Setup function that receives context
|
|
112
|
+
* @returns {Function} Component factory function
|
|
113
|
+
*/
|
|
114
|
+
export function component(setup) {
|
|
115
|
+
return (props = {}) => {
|
|
116
|
+
const dom = getAdapter();
|
|
117
|
+
const state = {};
|
|
118
|
+
const methods = {};
|
|
119
|
+
|
|
120
|
+
// Create mount context for lifecycle hooks
|
|
121
|
+
const mountContext = {
|
|
122
|
+
mountCallbacks: [],
|
|
123
|
+
unmountCallbacks: []
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const prevContext = currentMountContext;
|
|
127
|
+
currentMountContext = mountContext;
|
|
128
|
+
|
|
129
|
+
// Build context with injected utilities
|
|
130
|
+
const ctx = {
|
|
131
|
+
state,
|
|
132
|
+
methods,
|
|
133
|
+
props,
|
|
134
|
+
pulse,
|
|
135
|
+
onMount,
|
|
136
|
+
onUnmount,
|
|
137
|
+
// Spread utilities injected from dom.js
|
|
138
|
+
...(_contextUtils || {})
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let result;
|
|
142
|
+
try {
|
|
143
|
+
result = setup(ctx);
|
|
144
|
+
} finally {
|
|
145
|
+
currentMountContext = prevContext;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Schedule mount callbacks after DOM insertion
|
|
149
|
+
if (mountContext.mountCallbacks.length > 0) {
|
|
150
|
+
dom.queueMicrotask(() => {
|
|
151
|
+
for (const cb of mountContext.mountCallbacks) {
|
|
152
|
+
try {
|
|
153
|
+
cb();
|
|
154
|
+
} catch (e) {
|
|
155
|
+
log.error('Mount callback error:', e);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Store unmount callbacks on the element for later cleanup
|
|
162
|
+
if (dom.isNode(result) && mountContext.unmountCallbacks.length > 0) {
|
|
163
|
+
result._pulseUnmount = mountContext.unmountCallbacks;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default {
|
|
171
|
+
onMount,
|
|
172
|
+
onUnmount,
|
|
173
|
+
mount,
|
|
174
|
+
component,
|
|
175
|
+
getMountContext,
|
|
176
|
+
setMountContext,
|
|
177
|
+
_setContextUtils
|
|
178
|
+
};
|