@ape-egg/vibe 1.0.0

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/link.js ADDED
@@ -0,0 +1,13 @@
1
+ const recursive = (tree, results, tagChain) => {
2
+ results[tagChain.join(".")] = tree.element;
3
+
4
+ if (tree.children && Object.keys(tree.children).length > 0) {
5
+ Object.keys(tree.children).forEach((tag) => {
6
+ results = recursive(tree.children[tag], results, [...tagChain, tag]);
7
+ });
8
+ }
9
+
10
+ return results;
11
+ };
12
+
13
+ export default (tree) => recursive(tree, {}, []);
package/llms.txt ADDED
@@ -0,0 +1,279 @@
1
+ # Vibe - Complete Documentation
2
+
3
+ > Runtime-first reactivity. No virtual DOM. No build step.
4
+
5
+ ## Overview
6
+
7
+ Vibe is a lightweight reactive library that uses Proxy-based state and MutationObserver for fine-grained DOM updates. It works directly in the browser with zero compilation required.
8
+
9
+ **Key characteristics:**
10
+ - Proxy-based reactive state (`window.$`)
11
+ - Surgical DOM updates (only affected elements re-render)
12
+ - MutationObserver for dynamic element tracking
13
+ - Works with vanilla HTML - no special file format
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @ape-egg/vibe
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```html
24
+ <script type="module">
25
+ import state from "@ape-egg/vibe";
26
+ window.$ = state({ name: "World", count: 0 });
27
+ </script>
28
+
29
+ <h1>Hello, @[name]!</h1>
30
+ <button onclick="$.count++">Clicked @[count] times</button>
31
+ ```
32
+
33
+ ## Core Syntax
34
+
35
+ ### Reactive Bindings
36
+
37
+ Use `@[property]` syntax anywhere in HTML or CSS:
38
+
39
+ ```html
40
+ <!-- Text content -->
41
+ <div>@[firstName]</div>
42
+
43
+ <!-- Expressions -->
44
+ <div>@[firstName + ' ' + lastName]</div>
45
+ <div>@[count * 2]</div>
46
+
47
+ <!-- Attributes -->
48
+ <input value="@[inputValue]">
49
+ <button disabled="@[isLoading]">Submit</button>
50
+
51
+ <!-- CSS -->
52
+ <style>
53
+ .box { background: @[themeColor]; }
54
+ </style>
55
+ ```
56
+
57
+ ⚠️ **Important**: Bindings are evaluated using `new Function()`. Do not bind untrusted user input.
58
+
59
+ ### State Access
60
+
61
+ State is accessed globally via `window.$`:
62
+
63
+ ```javascript
64
+ // Read
65
+ console.log($.firstName);
66
+
67
+ // Write (triggers re-render)
68
+ $.firstName = "John";
69
+
70
+ // Increment
71
+ $.count++;
72
+ ```
73
+
74
+ ## Control Flow
75
+
76
+ ### Iteration
77
+
78
+ ```html
79
+ <!-- each items as item -->
80
+ <li>@[item]</li>
81
+ <!-- /each -->
82
+ ```
83
+
84
+ With index:
85
+
86
+ ```html
87
+ <!-- each items as item, index -->
88
+ <li>@[index]: @[item]</li>
89
+ <!-- /each -->
90
+ ```
91
+
92
+ ### Nested Iteration
93
+
94
+ Use dot paths for nested arrays:
95
+
96
+ ```html
97
+ <!-- each categories as category -->
98
+ <h2>@[category.name]</h2>
99
+ <!-- each category.items as item -->
100
+ <span>@[item.name]</span>
101
+ <!-- /each -->
102
+ <!-- /each -->
103
+ ```
104
+
105
+ ### Conditionals
106
+
107
+ ```html
108
+ <!-- if isLoggedIn -->
109
+ <span>Welcome, @[username]!</span>
110
+ <!-- else -->
111
+ <span>Please log in</span>
112
+ <!-- /if -->
113
+ ```
114
+
115
+ Conditionals can be nested inside iterations and vice versa.
116
+
117
+ ## Special Attributes
118
+
119
+ ### Dehydrate
120
+
121
+ Skip reactive processing for an element and its children:
122
+
123
+ ```html
124
+ <code dehydrate>@[this] displays literally, not parsed</code>
125
+ ```
126
+
127
+ Use cases:
128
+ - Displaying `@[...]` syntax in documentation
129
+ - Static content that shouldn't be reactive
130
+ - Performance optimization for large static sections
131
+
132
+ ### Boolean Attributes
133
+
134
+ Attributes not in the value whitelist are removed when falsy:
135
+
136
+ ```html
137
+ <button disabled="@[isLoading]">Submit</button>
138
+ <!-- When isLoading is false, disabled attribute is removed entirely -->
139
+ ```
140
+
141
+ ## Events
142
+
143
+ Use standard inline event handlers:
144
+
145
+ ```html
146
+ <button onclick="$.count++">Increment</button>
147
+ <input oninput="$.text = this.value">
148
+ <form onsubmit="event.preventDefault(); handleSubmit()">
149
+ ```
150
+
151
+ ## Styling
152
+
153
+ ### CSS Bindings
154
+
155
+ Reactive values work inside `<style>` tags:
156
+
157
+ ```html
158
+ <style>
159
+ .box {
160
+ background: @[backgroundColor];
161
+ color: @[textColor];
162
+ width: @[width]px;
163
+ }
164
+ </style>
165
+ ```
166
+
167
+ ### Preventing FOUC
168
+
169
+ Hide content until hydration completes:
170
+
171
+ ```html
172
+ <body style="visibility: hidden;">
173
+ ```
174
+
175
+ Or use the included CSS:
176
+
177
+ ```html
178
+ <link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
179
+ <body vibe>
180
+ ```
181
+
182
+ ## Dynamic Elements
183
+
184
+ Elements added via JavaScript are automatically hydrated through MutationObserver:
185
+
186
+ ```javascript
187
+ const div = document.createElement('div');
188
+ div.innerHTML = '<span>Hello, @[name]!</span>';
189
+ document.body.appendChild(div);
190
+ // Automatically becomes reactive
191
+ ```
192
+
193
+ ## API Reference
194
+
195
+ ### `state(initialState, afterUpdate?)`
196
+
197
+ Creates reactive state and initializes the framework.
198
+
199
+ ```javascript
200
+ import state from "@ape-egg/vibe";
201
+
202
+ window.$ = state(
203
+ { count: 0, user: { name: "Alice" } },
204
+ (newState, oldState) => {
205
+ console.log("State updated:", newState);
206
+ }
207
+ );
208
+ ```
209
+
210
+ **Parameters:**
211
+ - `initialState` - Object containing initial state values
212
+ - `afterUpdate` - Optional callback after each state change (receives read-only snapshots)
213
+
214
+ **Returns:** Proxy object for reactive state access
215
+
216
+ ## Scoped Variables
217
+
218
+ Inside `<!-- each -->` blocks, these variables are available:
219
+ - `item` (or custom name) - current array element
220
+ - `index` (or custom name) - current index
221
+ - Parent state remains accessible via `$`
222
+
223
+ ```html
224
+ <!-- each users as user, i -->
225
+ <div>@[i]: @[user.name] (total: @[users.length])</div>
226
+ <!-- /each -->
227
+ ```
228
+
229
+ ## Architecture
230
+
231
+ Vibe consists of these core modules:
232
+
233
+ - **state.js** - Proxy-based reactive state container
234
+ - **parse.js** - DOM parser that finds `@[...]` bindings
235
+ - **link.js** - Maps elements to parsed tree nodes
236
+ - **hydrate.js** - Updates DOM with current state values
237
+ - **affected.js** - Determines which elements need updating
238
+ - **iterate.js** - Array rendering with efficient diffing
239
+ - **conditionals.js** - Conditional block rendering
240
+
241
+ ## How It Works
242
+
243
+ ```
244
+ 1. state() initializes the Proxy and framework
245
+ 2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->
246
+ 3. link.js maps elements to the parsed tree
247
+ 4. hydrate.js replaces bindings with values
248
+ 5. iterate.js renders <!-- each --> loops
249
+ 6. conditionals.js renders <!-- if --> blocks
250
+ 7. MutationObserver watches for new elements
251
+ 8. On state change: affected.js finds changed elements → hydrate.js updates them
252
+ ```
253
+
254
+ ## Current Limitations
255
+
256
+ - **Top-level reactivity only**: `$.nested.prop = value` doesn't trigger updates (must replace parent object)
257
+ - **No computed values**: Derived state must be calculated manually
258
+ - **No two-way binding sugar**: Must wire input events manually
259
+ - **Expression security**: `new Function()` evaluation - don't bind untrusted input
260
+
261
+ ## Best Practices
262
+
263
+ 1. **Initialize state before DOM**: Place `<script>` in `<head>` or before reactive elements
264
+ 2. **Use dehydrate for docs**: When showing `@[...]` syntax examples
265
+ 3. **Prevent FOUC**: Use `visibility: hidden` on body until hydration
266
+ 4. **Keep expressions simple**: Complex logic belongs in JavaScript, not templates
267
+ 5. **Replace objects for deep updates**: `$.user = { ...$.user, name: "New" }`
268
+
269
+ ## Browser Support
270
+
271
+ Modern browsers with:
272
+ - Proxy (ES6)
273
+ - MutationObserver
274
+ - ES Modules
275
+
276
+ ## Resources
277
+
278
+ - **Homepage**: https://vibe.korte.kim
279
+ - **npm**: https://www.npmjs.com/package/@ape-egg/vibe
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@ape-egg/vibe",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Runtime-first reactivity",
6
+ "main": "index.js",
7
+ "homepage": "https://vibe.korte.kim",
8
+ "keywords": [
9
+ "reactive",
10
+ "framework",
11
+ "frontend",
12
+ "ui",
13
+ "mutation-observer",
14
+ "proxy",
15
+ "minimalistic"
16
+ ],
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ },
20
+ "author": "Kim Korte",
21
+ "license": "ISC",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {}
26
+ }
package/parse.js ADDED
@@ -0,0 +1,225 @@
1
+ import { hash } from './utils.js'
2
+ import { findEndComment, findConditionalEnd } from './iteration-utils.js'
3
+ import { NON_REACTIVE_ELEMENTS, BINDING_REGEX, ITERATION_REGEX, CONDITIONAL_REGEX } from './constants.js'
4
+
5
+ const parseHTML = (children, rootKey = undefined) =>
6
+ children.reduce((s, element, i) => {
7
+ const { nodeName, textContent } = element
8
+ if (['#comment'].includes(nodeName)) {
9
+ return `${s}${nodeName === '#comment' ? `asd` : textContent}`
10
+ }
11
+ const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName
12
+ const innerNodeIdentifier = `${name}_${i}`.toLowerCase()
13
+
14
+ return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}`
15
+ }, '')
16
+
17
+ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
18
+ let result = {};
19
+
20
+ for (let i = 0; i < children.length; i++) {
21
+ // Skip if this index was part of an iteration template
22
+ if (skipIndices.has(i)) continue;
23
+
24
+ const element = children[i];
25
+ const { nodeName, childNodes, outerHTML, innerHTML, textContent } = element;
26
+
27
+ // Skip non-reactive elements and dehydrated elements
28
+ if (NON_REACTIVE_ELEMENTS.includes(nodeName)) continue;
29
+ if (element.hasAttribute?.('dehydrate')) continue;
30
+
31
+ // Handle iteration comments
32
+ if (nodeName === '#comment') {
33
+ const iterationMatch = textContent.trim().match(ITERATION_REGEX);
34
+
35
+ if (iterationMatch) {
36
+ const [_, arrayPath, itemAlias, indexAlias] = iterationMatch;
37
+
38
+ try {
39
+ // Find matching end comment
40
+ const endIndex = findEndComment(children, i + 1);
41
+
42
+ // Extract template nodes (between start and end comments)
43
+ const templateNodes = Array.from(children).slice(i + 1, endIndex);
44
+
45
+ // Create a temporary container for the template
46
+ const templateContainer = document.createElement('div');
47
+ templateNodes.forEach(node => {
48
+ templateContainer.appendChild(node.cloneNode(true));
49
+ });
50
+
51
+ // Parse the template recursively
52
+ const templateParsed = recursive([...templateContainer.childNodes]);
53
+
54
+ // Store iteration metadata
55
+ const iterationKey = `iteration_${hash()}`;
56
+ result[iterationKey] = {
57
+ type: 'iteration',
58
+ meta: {
59
+ arrayPath,
60
+ itemAlias,
61
+ indexAlias: indexAlias || 'index',
62
+ startComment: element,
63
+ endComment: children[endIndex],
64
+ template: {
65
+ parsed: parseHTML([...templateContainer.childNodes]),
66
+ element: templateContainer,
67
+ children: templateParsed
68
+ }
69
+ },
70
+ runtime: {
71
+ instances: [],
72
+ templateRemoved: false
73
+ },
74
+ children: {}
75
+ };
76
+
77
+ // Mark template indices as processed
78
+ for (let j = i + 1; j < endIndex; j++) {
79
+ skipIndices.add(j);
80
+ }
81
+ skipIndices.add(endIndex); // Also skip the end comment
82
+
83
+ // Skip past this iteration block
84
+ i = endIndex;
85
+ continue;
86
+ } catch (e) {
87
+ console.error('Error parsing iteration block:', e);
88
+ continue;
89
+ }
90
+ }
91
+
92
+ // Handle conditional comments
93
+ const conditionalMatch = textContent.trim().match(CONDITIONAL_REGEX);
94
+
95
+ if (conditionalMatch) {
96
+ const [_, expression] = conditionalMatch;
97
+
98
+ try {
99
+ // Find matching end comment and optional else
100
+ const { elseIndex, endIndex } = findConditionalEnd(children, i + 1);
101
+
102
+ // Extract true branch nodes (between if and else/endif)
103
+ const trueBranchEnd = elseIndex !== null ? elseIndex : endIndex;
104
+ const trueBranchNodes = Array.from(children).slice(i + 1, trueBranchEnd);
105
+
106
+ // Create temporary container for true branch
107
+ const trueBranchContainer = document.createElement('div');
108
+ trueBranchNodes.forEach(node => {
109
+ trueBranchContainer.appendChild(node.cloneNode(true));
110
+ });
111
+
112
+ // Parse the true branch recursively
113
+ const trueBranchParsed = recursive([...trueBranchContainer.childNodes]);
114
+
115
+ // Extract false branch nodes if else exists
116
+ let falseBranchParsed = null;
117
+ let falseBranchContainer = null;
118
+ if (elseIndex !== null) {
119
+ const falseBranchNodes = Array.from(children).slice(elseIndex + 1, endIndex);
120
+ falseBranchContainer = document.createElement('div');
121
+ falseBranchNodes.forEach(node => {
122
+ falseBranchContainer.appendChild(node.cloneNode(true));
123
+ });
124
+ falseBranchParsed = recursive([...falseBranchContainer.childNodes]);
125
+ }
126
+
127
+ // Store conditional metadata
128
+ const conditionalKey = `conditional_${hash()}`;
129
+ result[conditionalKey] = {
130
+ type: 'conditional',
131
+ meta: {
132
+ expression,
133
+ startComment: element,
134
+ elseComment: elseIndex !== null ? children[elseIndex] : null,
135
+ endComment: children[endIndex],
136
+ branches: {
137
+ if: {
138
+ parsed: parseHTML([...trueBranchContainer.childNodes]),
139
+ element: trueBranchContainer,
140
+ children: trueBranchParsed
141
+ },
142
+ else: falseBranchContainer ? {
143
+ parsed: parseHTML([...falseBranchContainer.childNodes]),
144
+ element: falseBranchContainer,
145
+ children: falseBranchParsed
146
+ } : null
147
+ }
148
+ },
149
+ runtime: {
150
+ activeBranch: undefined,
151
+ activeInstance: null,
152
+ templateRemoved: false
153
+ },
154
+ children: {}
155
+ };
156
+
157
+ // Mark template indices as processed
158
+ for (let j = i + 1; j <= endIndex; j++) {
159
+ skipIndices.add(j);
160
+ }
161
+
162
+ // Skip past this conditional block
163
+ i = endIndex;
164
+ continue;
165
+ } catch (e) {
166
+ console.error('Error parsing conditional block:', e);
167
+ continue;
168
+ }
169
+ }
170
+
171
+ // Skip other comments
172
+ continue;
173
+ }
174
+
175
+ const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
176
+ const nodeIdentifier = `${name}_${i}`.toLowerCase();
177
+
178
+ // Check for attribute bindings
179
+ const attributes = {};
180
+ if (element.attributes) {
181
+ for (let j = 0; j < element.attributes.length; j++) {
182
+ const attr = element.attributes[j];
183
+ // Reset lastIndex before test - BINDING_REGEX has 'g' flag which persists state
184
+ BINDING_REGEX.lastIndex = 0;
185
+ if (BINDING_REGEX.test(attr.value)) {
186
+ attributes[attr.name] = attr.value;
187
+ }
188
+ }
189
+ }
190
+ const hasAttributeBindings = Object.keys(attributes).length > 0;
191
+
192
+ const hasChildren = childNodes.length;
193
+
194
+ if (hasChildren) {
195
+ const iteratableChildren = [...childNodes];
196
+ const parsed = parseHTML(iteratableChildren);
197
+
198
+ result[rootKey || nodeIdentifier] = {
199
+ parsed,
200
+ element,
201
+ children: recursive(iteratableChildren),
202
+ ...(hasAttributeBindings && { attributes })
203
+ };
204
+ } else {
205
+ result[nodeIdentifier] = {
206
+ parsed: innerHTML || textContent,
207
+ element,
208
+ children: {},
209
+ ...(hasAttributeBindings && { attributes })
210
+ };
211
+ }
212
+ }
213
+
214
+ return result;
215
+ }
216
+
217
+ export default (root, rootKey = undefined) => {
218
+ const { childNodes } = root
219
+ return {
220
+ // html: root.outerHTML,
221
+ parsed: parseHTML([...childNodes], rootKey),
222
+ element: root,
223
+ children: recursive(Array.from(childNodes), rootKey)
224
+ }
225
+ }
package/state.js ADDED
@@ -0,0 +1,26 @@
1
+ export default (state, rerender) =>
2
+ new Proxy(state, {
3
+ set(obj, prop, value) {
4
+ const ref = Reflect.set(...arguments)
5
+ rerender({
6
+ [prop]: value,
7
+ })
8
+ // console.info("state change", { obj, prop, value });
9
+ return ref
10
+ },
11
+ get(target, prop) {
12
+ // if (typeof target[prop] === 'function') {
13
+ // const inputString = target[prop].toString();
14
+ // const regex = /\$\.\w+/g;
15
+ // let match;
16
+ // while ((match = regex.exec(inputString))) {
17
+ // const varName = match[0].split('.')[1];
18
+ // console.log(varName, state[varName]);
19
+ // }
20
+ // console.log(arguments)
21
+
22
+ // }
23
+
24
+ return Reflect.get(...arguments)
25
+ },
26
+ })
package/utils.js ADDED
@@ -0,0 +1,45 @@
1
+ // Fast incrementing counter instead of expensive random hash
2
+ let hashCounter = 0;
3
+ export const hash = () => `_${hashCounter++}`;
4
+
5
+ // Instead of using lodash-es as a dependency, we run our own deepMerge (mergeWith in lodash)
6
+ export const deepMerge = (target, source) => {
7
+ // Handle null/undefined
8
+ if (source == null) return target;
9
+ if (target == null) return source;
10
+
11
+ // If source is not an object, return it
12
+ if (typeof source !== "object") return source;
13
+
14
+ // If source is an array, replace target array (don't merge arrays)
15
+ if (Array.isArray(source)) return source;
16
+
17
+ // Clone target to avoid mutation
18
+ const result = { ...target };
19
+
20
+ // Merge each property from source
21
+ for (const key in source) {
22
+ if (source.hasOwnProperty(key)) {
23
+ const targetValue = result[key];
24
+ const sourceValue = source[key];
25
+
26
+ // If both are objects (but not arrays), merge recursively
27
+ if (
28
+ targetValue != null &&
29
+ sourceValue != null &&
30
+ typeof targetValue === "object" &&
31
+ typeof sourceValue === "object" &&
32
+ !Array.isArray(targetValue) &&
33
+ !Array.isArray(sourceValue)
34
+ ) {
35
+ result[key] = deepMerge(targetValue, sourceValue);
36
+ } else {
37
+ // Otherwise, replace with source value (handles arrays and primitives)
38
+ result[key] = sourceValue;
39
+ }
40
+ }
41
+ }
42
+
43
+ return result;
44
+ };
45
+
package/vibe.css ADDED
@@ -0,0 +1,11 @@
1
+ /* Vibe Framework - Hydration Styles
2
+ * Elements with [vibe] attribute are hidden until framework removes it after hydration.
3
+ * This prevents flash of unprocessed content and disables transitions during init.
4
+ */
5
+ [vibe] {
6
+ visibility: hidden;
7
+ }
8
+
9
+ [vibe] * {
10
+ transition: none !important;
11
+ }