@ape-egg/vibe 4.0.1 → 4.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vibe
2
2
 
3
- **Version 4.0.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`).
3
+ **Version 4.1.3** — 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
- // Boot with merged state
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;
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.0.1",
3
+ "version": "4.1.3",
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(/&lt;/g, '<')
35
5
  .replace(/&gt;/g, '>')
@@ -37,19 +7,12 @@ const decodeEntities = (s) => s
37
7
  .replace(/&#39;/g, "'")
38
8
  .replace(/&amp;/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++) {