@ape-egg/vibe 4.0.0 → 4.1.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.
- package/README.md +3 -1
- package/boot.js +10 -9
- package/component.js +0 -29
- package/hot-module-refresh.js +0 -0
- package/index.js +0 -28
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +0 -56
- package/runtime/affected.js +6 -189
- package/runtime/cleanup.js +4 -35
- package/runtime/component-cache.js +0 -53
- package/runtime/component.js +7 -401
- package/runtime/conditionals.js +1 -143
- package/runtime/constants.js +19 -82
- package/runtime/debug.js +22 -47
- package/runtime/dispatch.js +0 -46
- package/runtime/hydrate.js +10 -129
- package/runtime/index.js +7 -365
- package/runtime/iterate.js +21 -579
- package/runtime/iteration-utils.js +18 -71
- package/runtime/loop-scope.js +0 -58
- package/runtime/manifest.js +0 -27
- package/runtime/parse.js +2 -116
- package/runtime/pre-compiled-iterations.js +3 -51
- package/runtime/pre-compiled-manifest.js +6 -169
- package/runtime/raw-html.js +0 -5
- package/runtime/reconcile.js +4 -159
- package/runtime/staging.js +0 -57
- package/runtime/state.js +0 -61
- package/runtime/this-scope.js +0 -17
- package/runtime/tracking.js +0 -65
- package/runtime/utils.js +1 -144
- package/runtime/vibe-css.js +54 -0
- package/spa.js +0 -76
- package/vibe.css +22 -44
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vibe
|
|
2
2
|
|
|
3
|
-
**Version 4.
|
|
3
|
+
**Version 4.1.1** — Runtime-first reactivity for plain HTML. Drop a script tag into any page and get reactive bindings, control flow, and URL-loaded components — no build step required. Compile later if you want; the compiler is a separate, optional package (`@ape-egg/vibe-compiler`).
|
|
4
4
|
|
|
5
5
|
## Security model & CSP
|
|
6
6
|
|
|
@@ -70,6 +70,8 @@ To prevent a flash of unstyled content while Vibe hydrates:
|
|
|
70
70
|
|
|
71
71
|
`vibe.css` hides `[vibe-fouc]` and `.vibe-fouc` until hydration completes; Vibe removes the attribute/class once it's done.
|
|
72
72
|
|
|
73
|
+
The cover is more than cosmetic: authored `on*` handlers (`onclick="this.flip()"`) are native inline handlers until boot rewrites them, so a click that lands while the page is uncovered runs one raw and throws. Vibe injects the cover itself, so it holds whether or not you link `vibe.css` — but it can only do that once its own JavaScript evaluates. **Link `vibe.css` (step 1) to be covered from first paint.** Without it, the stretch between first paint and Vibe's modules arriving is uncovered, which on a slow connection is long enough for an early click, or for a CSS animation to run and deliver `animationend` to a raw handler. The compiler and the Vite plugin write the cover into `<head>` at build/serve time, so compiled output and dev both close that stretch too.
|
|
74
|
+
|
|
73
75
|
### Reactive Bindings
|
|
74
76
|
|
|
75
77
|
Vibe supports bindings in three positions:
|
package/boot.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
// Shared boot mechanism for vibe
|
|
2
|
-
// Used by both index.js (global state) and component.js (component state)
|
|
3
|
-
|
|
4
1
|
import main from './runtime/index.js';
|
|
5
2
|
import { getPendingListeners, chainInstanceReady } from './index.js';
|
|
3
|
+
import { injectVibeCss, removeLayoutCss, warnIfCoverDefeated } from './runtime/vibe-css.js';
|
|
4
|
+
|
|
5
|
+
if (typeof document !== 'undefined') {
|
|
6
|
+
injectVibeCss(document);
|
|
7
|
+
warnIfCoverDefeated(document);
|
|
8
|
+
}
|
|
6
9
|
|
|
7
10
|
let bootQueued = false;
|
|
8
11
|
let booted = false;
|
|
@@ -24,7 +27,6 @@ export const boot = () => {
|
|
|
24
27
|
}
|
|
25
28
|
booted = true;
|
|
26
29
|
|
|
27
|
-
// Merge all state: global + components
|
|
28
30
|
const globalState = window.__vibe?.state || {};
|
|
29
31
|
const componentStates = window.__vibe?.components || {};
|
|
30
32
|
|
|
@@ -33,18 +35,17 @@ export const boot = () => {
|
|
|
33
35
|
...componentStates,
|
|
34
36
|
};
|
|
35
37
|
|
|
36
|
-
// Get config and targetSelector (first caller wins)
|
|
37
38
|
const config = window.__vibe?.config || {};
|
|
38
39
|
const targetSelector = config.target || '';
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
if (config.disableVibeCss && typeof document !== 'undefined') {
|
|
42
|
+
removeLayoutCss(document);
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
window.$ = main(mergedState, config, targetSelector);
|
|
42
46
|
|
|
43
|
-
// Forward real $.ready to the pre-boot placeholder's ready promise so
|
|
44
|
-
// any consumer that captured the placeholder can still await readiness.
|
|
45
47
|
chainInstanceReady(window.$.ready);
|
|
46
48
|
|
|
47
|
-
// Apply pending listeners from vibe instance
|
|
48
49
|
const pendingListeners = getPendingListeners();
|
|
49
50
|
if (pendingListeners) {
|
|
50
51
|
Object.keys(pendingListeners).forEach(event => {
|
package/component.js
CHANGED
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
// Component state entry point
|
|
2
|
-
// Usage:
|
|
3
|
-
// <component>
|
|
4
|
-
// <script type="module">
|
|
5
|
-
// import component from 'vibe/component.js';
|
|
6
|
-
// component({ count: 0 }, { debug: false, target: 'body' });
|
|
7
|
-
// </script>
|
|
8
|
-
// <div>@[this.count]</div>
|
|
9
|
-
// </component>
|
|
10
|
-
//
|
|
11
|
-
// Or with class:
|
|
12
|
-
// <div class="component">...</div>
|
|
13
|
-
|
|
14
1
|
import { generateComponentId } from './runtime/component.js';
|
|
15
2
|
import { ensureBoot } from './boot.js';
|
|
16
3
|
|
|
17
4
|
const component = (state = {}, config) => {
|
|
18
|
-
// Initialize component registry
|
|
19
5
|
const ns = (window.__vibe ??= {});
|
|
20
6
|
if (!ns.components) ns.components = {};
|
|
21
7
|
|
|
22
|
-
// Pair this call with its wrapper. Module scripts execute in document
|
|
23
|
-
// order, so the K-th component() call belongs to the K-th wrapper whose own
|
|
24
|
-
// direct <script type="module"> calls component( — the runtime mirror of
|
|
25
|
-
// the build tagger's registers_state predicate. Wrappers without such a
|
|
26
|
-
// script (no local state, or a compiler-neutered vibe-module script that
|
|
27
|
-
// registers through boot) can never claim a call, so they're excluded —
|
|
28
|
-
// otherwise a stateless wrapper earlier in the document absorbs a later
|
|
29
|
-
// section's claim and every pairing after it is cross-wired. In compiled
|
|
30
|
-
// pages the claimed wrapper is already build-tagged: register under its id.
|
|
31
|
-
// Supports: <component> or <div class="component">; <component src> mounts
|
|
32
|
-
// register through the fetch pipeline instead.
|
|
33
8
|
const allWrappers = Array.from(document.querySelectorAll('component:not([src]), div.component:not([src])'));
|
|
34
9
|
const wrapper = allWrappers.find((el) => {
|
|
35
10
|
const ownsCall = Array.from(el.children).some(
|
|
@@ -45,22 +20,18 @@ const component = (state = {}, config) => {
|
|
|
45
20
|
return;
|
|
46
21
|
}
|
|
47
22
|
|
|
48
|
-
// Get or generate component ID
|
|
49
23
|
let componentId = wrapper.getAttribute('data-vibe-component-id');
|
|
50
24
|
if (!componentId) {
|
|
51
25
|
componentId = generateComponentId();
|
|
52
26
|
wrapper.setAttribute('data-vibe-component-id', componentId);
|
|
53
27
|
}
|
|
54
28
|
|
|
55
|
-
// Register component state in shared registry
|
|
56
29
|
ns.components[componentId] = state;
|
|
57
30
|
|
|
58
|
-
// Store config (first caller wins); config.target scopes the boot.
|
|
59
31
|
if (config && !ns.config) {
|
|
60
32
|
ns.config = config;
|
|
61
33
|
}
|
|
62
34
|
|
|
63
|
-
// Ensure boot happens
|
|
64
35
|
ensureBoot();
|
|
65
36
|
|
|
66
37
|
return componentId;
|
package/hot-module-refresh.js
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
|
-
// Universal entry point for Vibe
|
|
2
|
-
// Usage: import vibe from 'vibe/index.js'; vibe({ initialState }, { debug: true }, 'body');
|
|
3
|
-
|
|
4
1
|
import { boot, isBooted, ensureBoot } from './boot.js';
|
|
5
2
|
|
|
6
|
-
// Shared instance for queueing listeners before boot
|
|
7
3
|
let vibeInstance = null;
|
|
8
4
|
let resolveInstanceReady = null;
|
|
9
5
|
|
|
10
6
|
const createVibeInstance = () => ({
|
|
11
7
|
_pendingListeners: { afterUpdate: [], afterDomMutation: [], ready: [], unmount: [] },
|
|
12
|
-
// Promise that resolves when the real $.ready resolves post-boot. Lets
|
|
13
|
-
// consumers holding the pre-boot placeholder (e.g. tests awaiting
|
|
14
|
-
// `window.$.ready` before boot has replaced $ with the reactive proxy)
|
|
15
|
-
// wait for readiness without polling. Non-enumerable so it stays out of
|
|
16
|
-
// state snapshots.
|
|
17
8
|
ready: new Promise((resolve) => { resolveInstanceReady = resolve; }),
|
|
18
9
|
on(event, callback) {
|
|
19
|
-
// If booted, delegate to window.$
|
|
20
10
|
if (isBooted() && window.$) {
|
|
21
11
|
return window.$.on(event, callback);
|
|
22
12
|
}
|
|
23
13
|
|
|
24
|
-
// Otherwise queue for later
|
|
25
14
|
if (this._pendingListeners[event]) {
|
|
26
15
|
this._pendingListeners[event].push(callback);
|
|
27
16
|
}
|
|
@@ -31,11 +20,6 @@ const createVibeInstance = () => ({
|
|
|
31
20
|
}
|
|
32
21
|
});
|
|
33
22
|
|
|
34
|
-
// Shallow, key-level defaults: set only keys `target` does not have yet.
|
|
35
|
-
// This is what "initial state, declared again" means once the app is live —
|
|
36
|
-
// a re-mounted SPA page fragment's vibe({ ... }) seeds on first mount and
|
|
37
|
-
// never clobbers live state after (vibe() state is app-lifetime; per-visit
|
|
38
|
-
// state belongs in a component()).
|
|
39
23
|
export const applyDefaults = (target, state) => {
|
|
40
24
|
for (const key in state) {
|
|
41
25
|
if (!(key in target)) target[key] = state[key];
|
|
@@ -45,45 +29,33 @@ export const applyDefaults = (target, state) => {
|
|
|
45
29
|
|
|
46
30
|
const vibe = (state = {}, config) => {
|
|
47
31
|
if (isBooted()) {
|
|
48
|
-
// Already booted: initial state declared late seeds missing keys only —
|
|
49
|
-
// on a fresh document load this branch never runs, so MPA behavior is
|
|
50
|
-
// byte-identical.
|
|
51
32
|
applyDefaults(window.$, state);
|
|
52
33
|
return window.$;
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
// Create shared instance on first call
|
|
56
36
|
if (!vibeInstance) {
|
|
57
37
|
vibeInstance = createVibeInstance();
|
|
58
38
|
}
|
|
59
39
|
|
|
60
|
-
// Not booted yet - accumulate in the reserved namespace's state registry
|
|
61
40
|
const ns = (window.__vibe ??= {});
|
|
62
41
|
if (!ns.state) ns.state = {};
|
|
63
42
|
Object.assign(ns.state, state);
|
|
64
43
|
|
|
65
|
-
// Store config (first caller wins). `config.target` scopes the boot to a
|
|
66
|
-
// selector (the old third positional argument, folded into config at 3.0.0).
|
|
67
44
|
if (config && !ns.config) {
|
|
68
45
|
ns.config = config;
|
|
69
46
|
}
|
|
70
47
|
|
|
71
|
-
// Explicit boot call (no state passed means "boot now with accumulated state")
|
|
72
48
|
if (Object.keys(state).length === 0 && Object.keys(ns.state).length > 0) {
|
|
73
49
|
return boot();
|
|
74
50
|
}
|
|
75
51
|
|
|
76
|
-
// Queue boot in microtask to allow all component scripts to register
|
|
77
52
|
ensureBoot();
|
|
78
53
|
|
|
79
54
|
return vibeInstance;
|
|
80
55
|
};
|
|
81
56
|
|
|
82
|
-
// Export function to get pending listeners (used by boot.js)
|
|
83
57
|
export const getPendingListeners = () => vibeInstance?._pendingListeners || null;
|
|
84
58
|
|
|
85
|
-
// Forward post-boot $.ready to the pre-boot placeholder's ready promise.
|
|
86
|
-
// Called by boot.js after main() creates the real reactive proxy.
|
|
87
59
|
export const chainInstanceReady = (realReadyPromise) => {
|
|
88
60
|
if (resolveInstanceReady && realReadyPromise) {
|
|
89
61
|
realReadyPromise.then(() => resolveInstanceReady());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ape-egg/vibe",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime-first reactivity for plain HTML — no build step, no virtual DOM, no new syntax to learn",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
".": "./index.js",
|
|
14
14
|
"./component": "./component.js",
|
|
15
15
|
"./spa": "./spa.js",
|
|
16
|
-
"./hot-module-refresh": "./hot-module-refresh.js"
|
|
16
|
+
"./hot-module-refresh": "./hot-module-refresh.js",
|
|
17
|
+
"./vibe.css": "./vibe.css"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"index.js",
|
|
@@ -1,35 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* _vibe-compiled-iteration-batch.js
|
|
3
|
-
*
|
|
4
|
-
* EXPERIMENTAL: Fast path for iteration rendering using compiled batch functions.
|
|
5
|
-
*
|
|
6
|
-
* This is a preview/prototype of what Vibe Compiled (Phase 2) will do automatically.
|
|
7
|
-
* Currently opt-in via: window.__VIBE_FAST_ITERATION__ = true
|
|
8
|
-
*
|
|
9
|
-
* HOW IT WORKS:
|
|
10
|
-
* Instead of cloning DOM nodes and hydrating each item individually (slow),
|
|
11
|
-
* this compiles the template to a JavaScript function that builds HTML strings
|
|
12
|
-
* in a loop, then parses once with innerHTML (fast).
|
|
13
|
-
*
|
|
14
|
-
* Template: <item-card><item-text>@[item.name]</item-text></item-card>
|
|
15
|
-
* Compiles to: (arr) => { let html=''; for(...) html += `<item-card>...${item.name}...`; return html; }
|
|
16
|
-
*
|
|
17
|
-
* LIMITATIONS:
|
|
18
|
-
* - Cannot handle nested <!-- each --> or <!-- if --> (those need DOM-based rendering)
|
|
19
|
-
* - Cannot do incremental updates for small changes (rebuilds entire list)
|
|
20
|
-
* - Template structure is "frozen" at compile time
|
|
21
|
-
*
|
|
22
|
-
* WHEN VIBE COMPILED EXISTS:
|
|
23
|
-
* This logic will move to build-time compilation, producing optimized JavaScript
|
|
24
|
-
* that gets shipped to the browser. The runtime will just execute the compiled code.
|
|
25
|
-
*
|
|
26
|
-
* See: .claude/phase-2-compiler.md for full compiler plans
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
1
|
import { BINDING_REGEX } from './constants.js';
|
|
30
2
|
|
|
31
|
-
// innerHTML serialization encodes <, >, &, ", ' inside attribute values.
|
|
32
|
-
// Decode them back before wrapping @[expr] in ${...} for the template literal.
|
|
33
3
|
const decodeEntities = (s) => s
|
|
34
4
|
.replace(/</g, '<')
|
|
35
5
|
.replace(/>/g, '>')
|
|
@@ -37,19 +7,12 @@ const decodeEntities = (s) => s
|
|
|
37
7
|
.replace(/'/g, "'")
|
|
38
8
|
.replace(/&/g, '&');
|
|
39
9
|
|
|
40
|
-
// Reusable template element for HTML parsing
|
|
41
10
|
const parseTemplate = document.createElement('template');
|
|
42
11
|
|
|
43
|
-
/**
|
|
44
|
-
* Check if a template can use the fast path (no nested iterations/conditionals)
|
|
45
|
-
*/
|
|
46
12
|
export const canUseFastPath = (template) => {
|
|
47
13
|
return !hasNestedStructures(template);
|
|
48
14
|
};
|
|
49
15
|
|
|
50
|
-
/**
|
|
51
|
-
* Recursively check if tree has nested iterations or conditionals
|
|
52
|
-
*/
|
|
53
16
|
const hasNestedStructures = (tree) => {
|
|
54
17
|
if (!tree || !tree.children) return false;
|
|
55
18
|
for (const key in tree.children) {
|
|
@@ -61,10 +24,6 @@ const hasNestedStructures = (tree) => {
|
|
|
61
24
|
return false;
|
|
62
25
|
};
|
|
63
26
|
|
|
64
|
-
/**
|
|
65
|
-
* Compile a batch function for an iteration template.
|
|
66
|
-
* Returns a function: (array, ...stateValues) => htmlString
|
|
67
|
-
*/
|
|
68
27
|
export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
|
|
69
28
|
const templateHtml = template.element.innerHTML.trim();
|
|
70
29
|
const escaped = templateHtml.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
@@ -85,13 +44,9 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
|
|
|
85
44
|
);
|
|
86
45
|
};
|
|
87
46
|
|
|
88
|
-
/**
|
|
89
|
-
* Fast render: compile template to batch function and render via innerHTML
|
|
90
|
-
*/
|
|
91
47
|
export const renderFast = (iterationNode, array, state, parent, endComment) => {
|
|
92
48
|
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
93
49
|
|
|
94
|
-
// Compile batch function if not cached
|
|
95
50
|
if (!iterationNode.runtime.batchFn) {
|
|
96
51
|
const stateKeys = Object.keys(state);
|
|
97
52
|
iterationNode.runtime.batchFn = compileBatchFn(template, itemAlias, indexAlias, stateKeys);
|
|
@@ -101,16 +56,13 @@ export const renderFast = (iterationNode, array, state, parent, endComment) => {
|
|
|
101
56
|
const batchFn = iterationNode.runtime.batchFn;
|
|
102
57
|
const stateKeys = iterationNode.runtime.stateKeys;
|
|
103
58
|
|
|
104
|
-
// Build HTML string using batch function
|
|
105
59
|
const stateValues = stateKeys.map((k) => state[k]);
|
|
106
60
|
const html = batchFn(array, ...stateValues);
|
|
107
61
|
|
|
108
|
-
// Parse with reusable template element
|
|
109
62
|
parseTemplate.innerHTML = html;
|
|
110
63
|
const frag = parseTemplate.content;
|
|
111
64
|
const kids = frag.children;
|
|
112
65
|
|
|
113
|
-
// Track instances
|
|
114
66
|
const arrayLen = array.length;
|
|
115
67
|
const instances = new Array(arrayLen);
|
|
116
68
|
for (let i = 0; i < arrayLen; i++) {
|
|
@@ -121,14 +73,10 @@ export const renderFast = (iterationNode, array, state, parent, endComment) => {
|
|
|
121
73
|
iterationNode.runtime.instances = instances;
|
|
122
74
|
};
|
|
123
75
|
|
|
124
|
-
/**
|
|
125
|
-
* Fast update: clear and rebuild for bulk operations
|
|
126
|
-
*/
|
|
127
76
|
export const updateFast = (iterationNode, newArray, state, startComment, endComment) => {
|
|
128
77
|
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
129
78
|
const parent = startComment.parentNode;
|
|
130
79
|
|
|
131
|
-
// Fast clear using Range
|
|
132
80
|
if (iterationNode.runtime.instances.length > 0) {
|
|
133
81
|
const range = document.createRange();
|
|
134
82
|
range.setStartAfter(startComment);
|
|
@@ -141,25 +89,21 @@ export const updateFast = (iterationNode, newArray, state, startComment, endComm
|
|
|
141
89
|
return;
|
|
142
90
|
}
|
|
143
91
|
|
|
144
|
-
// Compile batch function if not cached (e.g., initial array was empty)
|
|
145
92
|
if (!iterationNode.runtime.batchFn) {
|
|
146
93
|
const stateKeys = Object.keys(state);
|
|
147
94
|
iterationNode.runtime.batchFn = compileBatchFn(template, itemAlias, indexAlias, stateKeys);
|
|
148
95
|
iterationNode.runtime.stateKeys = stateKeys;
|
|
149
96
|
}
|
|
150
97
|
|
|
151
|
-
// Build HTML string using cached batch function
|
|
152
98
|
const batchFn = iterationNode.runtime.batchFn;
|
|
153
99
|
const stateKeys = iterationNode.runtime.stateKeys;
|
|
154
100
|
const stateValues = stateKeys.map((k) => state[k]);
|
|
155
101
|
const html = batchFn(newArray, ...stateValues);
|
|
156
102
|
|
|
157
|
-
// Parse and insert
|
|
158
103
|
parseTemplate.innerHTML = html;
|
|
159
104
|
const frag = parseTemplate.content;
|
|
160
105
|
const kids = frag.children;
|
|
161
106
|
|
|
162
|
-
// Build instance tracking
|
|
163
107
|
const arrayLen = newArray.length;
|
|
164
108
|
const instances = new Array(arrayLen);
|
|
165
109
|
for (let i = 0; i < arrayLen; i++) {
|