@ape-egg/vibe 2.3.0 → 3.0.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/README.md +14 -4
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +10 -15
  6. package/llms.txt +8 -6
  7. package/package.json +19 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +312 -99
  11. package/runtime/conditionals.js +111 -14
  12. package/runtime/debug.js +24 -0
  13. package/runtime/dispatch.js +172 -0
  14. package/runtime/hydrate.js +251 -111
  15. package/runtime/index.js +180 -71
  16. package/runtime/iterate.js +125 -50
  17. package/runtime/iteration-utils.js +59 -8
  18. package/runtime/manifest.js +77 -2
  19. package/runtime/parse.js +69 -5
  20. package/runtime/pre-compiled-iterations.js +19 -6
  21. package/runtime/pre-compiled-manifest.js +13 -4
  22. package/runtime/staging.js +153 -0
  23. package/runtime/state.js +31 -0
  24. package/runtime/tracking.js +173 -0
  25. package/runtime/utils.js +155 -78
  26. package/spa.js +77 -14
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1196
  29. package/ROADMAP.md +0 -397
  30. package/compiler/bin/vibe-compile.js +0 -121
  31. package/compiler/native/.gitkeep +0 -0
  32. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  33. package/compiler/native/vibe-compiler-linux-x64 +0 -0
  34. package/compiler/src/Cargo.lock +0 -2023
  35. package/compiler/src/Cargo.toml +0 -38
  36. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
  37. package/compiler/src/compiler/binding_case.rs +0 -88
  38. package/compiler/src/compiler/compile.rs +0 -2880
  39. package/compiler/src/compiler/component_tagger.rs +0 -469
  40. package/compiler/src/compiler/iteration_optimizer.rs +0 -455
  41. package/compiler/src/compiler/js_analyzer.rs +0 -715
  42. package/compiler/src/compiler/manifest_builder.rs +0 -693
  43. package/compiler/src/compiler/mod.rs +0 -16
  44. package/compiler/src/compiler/name_binding_protect.rs +0 -207
  45. package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
  46. package/compiler/src/compiler/spa.rs +0 -477
  47. package/compiler/src/compiler/state_extractor.rs +0 -263
  48. package/compiler/src/compiler/value_stamper.rs +0 -921
  49. package/compiler/src/compiler/watcher.rs +0 -1278
  50. package/compiler/src/config.rs +0 -279
  51. package/compiler/src/main.rs +0 -358
  52. package/compiler/src/parser/element.rs +0 -96
  53. package/compiler/src/parser/html.rs +0 -1004
  54. package/compiler/src/parser/mod.rs +0 -8
  55. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  56. package/runtime/scope.js +0 -50
  57. package/test-results/.last-run.json +0 -4
package/README.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # Vibe
2
2
 
3
- **Version 2.0.2 (Beta)** — A runtime-first reactive framework with optional compilation.
3
+ **Version 3.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`).
4
+
5
+ ## Security model & CSP
6
+
7
+ Vibe escapes by default: `@[expr]` renders as text, never markup. The one deliberate exception is `@[$.unsafe(trustedHtml)]`, which sets `innerHTML` and is for trusted input only.
8
+
9
+ What you must know before adopting: **Vibe's engine evaluates binding expressions with `new Function` and uses inline `on*` attributes as its event model.** A site running Vibe therefore needs a Content-Security-Policy that allows `'unsafe-eval'` and inline event handlers — i.e. it cannot deploy a strict CSP. Vibe itself is not an XSS vector, but strict CSP is a browser-level safety net against *other* injection bugs anywhere on a page, and Vibe requires that net loosened.
10
+
11
+ If you ship to an environment that mandates strict CSP (banks, healthcare, government, security-reviewed enterprise), Vibe is currently the wrong tool. For everything else — personal sites, games, dashboards, internal tools, most product work — this is the same posture as running Vue's in-browser template compiler or Alpine.js, and it is a documented trade-off, not an accident.
4
12
 
5
13
  No virtual DOM. No build step required. Just modern JavaScript. When you need production optimizations, add the optional Rust-based compiler.
6
14
 
@@ -212,7 +220,7 @@ Multi-segment paths (`@[this.user.profile.name]`), conditionals (`<!-- if this.e
212
220
  How it works:
213
221
 
214
222
  1. `component({...})` claims the nearest unprocessed `<component>` (or `<div class="component">`) wrapper, registers state at `$[id]`, and tags the wrapper with `data-vibe-component-id`
215
- 2. Internally it calls `ensureBoot()` (from `@ape-egg/vibe/boot`), which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once, even if multiple `<component>` blocks call `component()`
223
+ 2. Internally it triggers the boot pipeline, which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once, even if multiple `<component>` blocks call `component()`
216
224
  3. From there, `@[this.X]`, `onclick="this.fn()"`, and `<!-- if this.X -->` work as documented
217
225
 
218
226
  Multiple drop-in blocks on the same page each get their own state bucket. They can read each other's state via global `$['_c0'].count` if they need to coordinate, but in most drop-in cases they're independent.
@@ -350,9 +358,11 @@ No need for immutable update patterns or spread operators. Just mutate and Vibe
350
358
  1. **Proxy-based state** — `window.$` intercepts property changes
351
359
  2. **Deep reactivity** — Nested mutations trigger updates automatically (`$.obj.nested.prop = x`)
352
360
  3. **DOM parsing** — Finds all `@[...]` bindings on load
353
- 4. **Surgical updates** — Only affected elements re-render
361
+ 4. **Auto-tracked subscriptions** — Every binding, conditional, and iteration records which state keys its expression read during evaluation; a write dispatches exactly its subscribers — O(what changed), no tree walk, no virtual DOM
354
362
  5. **MutationObserver** — Tracks dynamically added elements
355
363
 
364
+ One contract follows from #4: **an expression that should react must read reactive state.** `@[items.map(format)]` re-renders when `$.items` changes because `items` was read from `$` — but if `format` is a window global that you later *reassign*, nothing re-renders, because assigning a global is not a state write. Values that change over time belong in `$`. (Helpers defined by component scripts are safe: mounts re-settle their directives once their scripts have run.)
365
+
356
366
  ---
357
367
 
358
368
  ## Vibe Compiler
@@ -729,7 +739,7 @@ ISC
729
739
  - `runtime/affected.js:~122` — `shouldAffect = noMatch || ...` fallback for unmatched expressions
730
740
  - `runtime/affected.js:~248` — same fallback for name bindings
731
741
 
732
- Every time `matchesKey` can't prove an expression is unrelated to a state change, that binding re-hydrates. A proper AST-based dependency extraction at parse time would eliminate the fallback entirely: each binding stores its exact dependency set, and runtime just checks if any dep key changed.
742
+ As of 3.0.0 this is solved not by static extraction (which lies for helper calls) but by the subscription engine: every binding records the state keys it actually reads during evaluation, live at the proxy's get trap, so a write notifies exactly its subscribers. Update cost is O(change), and it costs zero authoring syntax.
733
743
 
734
744
  ### HTML lowercases attribute names — breaks name binding matching
735
745
 
package/boot.js CHANGED
@@ -25,8 +25,8 @@ export const boot = () => {
25
25
  booted = true;
26
26
 
27
27
  // Merge all state: global + components
28
- const globalState = window.__vibeGlobalState || {};
29
- const componentStates = window.__vibeComponents || {};
28
+ const globalState = window.__vibe?.state || {};
29
+ const componentStates = window.__vibe?.components || {};
30
30
 
31
31
  const mergedState = {
32
32
  ...globalState,
@@ -34,8 +34,8 @@ export const boot = () => {
34
34
  };
35
35
 
36
36
  // Get config and targetSelector (first caller wins)
37
- const config = window.__vibeConfig || {};
38
- const targetSelector = window.__vibeTargetSelector || '';
37
+ const config = window.__vibe?.config || {};
38
+ const targetSelector = config.target || '';
39
39
 
40
40
  // Boot with merged state
41
41
  window.$ = main(mergedState, config, targetSelector);
package/component.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // <component>
4
4
  // <script type="module">
5
5
  // import component from 'vibe/component.js';
6
- // component({ count: 0 }, { debug: false }, 'body');
6
+ // component({ count: 0 }, { debug: false, target: 'body' });
7
7
  // </script>
8
8
  // <div>@[this.count]</div>
9
9
  // </component>
@@ -14,28 +14,31 @@
14
14
  import { generateComponentId } from './runtime/component.js';
15
15
  import { ensureBoot } from './boot.js';
16
16
 
17
- const component = (state = {}, config, targetSelector) => {
17
+ const component = (state = {}, config) => {
18
18
  // Initialize component registry
19
- if (!window.__vibeComponents) {
20
- window.__vibeComponents = {};
21
- }
19
+ const ns = (window.__vibe ??= {});
20
+ if (!ns.components) ns.components = {};
22
21
 
23
- // Find the first unprocessed INLINE component wrapper
24
- // Skip <component src=""> (fetched components) - they don't need state tagging
25
- // Scripts execute in DOM order, so we claim wrappers in DOM order too
26
- // Supports: <component> or <div class="component">
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.
27
33
  const allWrappers = Array.from(document.querySelectorAll('component:not([src]), div.component:not([src])'));
28
-
29
- // Try to find unprocessed component (runtime mode)
30
- let wrapper = allWrappers.find(el => !el.hasAttribute('data-vibe-component-id'));
31
-
32
- // If not found, try to find pre-compiled component that hasn't registered state yet (compiled mode)
33
- if (!wrapper) {
34
- wrapper = allWrappers.find(el => {
35
- const existingId = el.getAttribute('data-vibe-component-id');
36
- return existingId && !window.__vibeComponents[existingId];
37
- });
38
- }
34
+ const wrapper = allWrappers.find((el) => {
35
+ const ownsCall = Array.from(el.children).some(
36
+ (child) => child.matches?.('script[type="module"]') && /component\s*\(/.test(child.textContent),
37
+ );
38
+ if (!ownsCall) return false;
39
+ const existingId = el.getAttribute('data-vibe-component-id');
40
+ return !existingId || !ns.components[existingId];
41
+ });
39
42
 
40
43
  if (!wrapper) {
41
44
  console.warn('[vibe] component() must be called inside <component> or <div class="component">');
@@ -50,16 +53,11 @@ const component = (state = {}, config, targetSelector) => {
50
53
  }
51
54
 
52
55
  // Register component state in shared registry
53
- window.__vibeComponents[componentId] = state;
54
-
55
- // Store config (first caller wins)
56
- if (config && !window.__vibeConfig) {
57
- window.__vibeConfig = config;
58
- }
56
+ ns.components[componentId] = state;
59
57
 
60
- // Store targetSelector (first caller wins)
61
- if (targetSelector && !window.__vibeTargetSelector) {
62
- window.__vibeTargetSelector = targetSelector;
58
+ // Store config (first caller wins); config.target scopes the boot.
59
+ if (config && !ns.config) {
60
+ ns.config = config;
63
61
  }
64
62
 
65
63
  // Ensure boot happens
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // SOFT DEPENDENCY: nothing in Vibe's runtime imports this, it imports nothing
8
8
  // from Vibe, and deleting it leaves a working framework. It depends solely on
9
- // the runtime global `window.$` (reconcile / renderComponent /
9
+ // the runtime global `window.$` (reconcile / _renderComponent /
10
10
  // clearComponentCache), so it ships as a flat module a plain static server can
11
11
  // serve as-is — no bundler, no Vite.
12
12
  //
@@ -267,7 +267,7 @@ export const setupHotModuleRefresh = ({ debug = false, subscribe }) => {
267
267
  // Component file changed. Strategy:
268
268
  // 1. Fetch the raw template once per update; hash its <script type="module">
269
269
  // contents. Script hash unchanged + not inside an iteration → surgical
270
- // path: $.renderComponent produces the processed HTML (props + slot
270
+ // path: $._renderComponent produces the processed HTML (props + slot
271
271
  // substituted, componentIds reused) and $.reconcile diffs it against
272
272
  // the live wrapper's children. DOM identity, focus, and component
273
273
  // state are preserved.
@@ -295,7 +295,7 @@ export const setupHotModuleRefresh = ({ debug = false, subscribe }) => {
295
295
  const rawUrl = path + (path.includes('?') ? '&' : '?') + '_t=' + Date.now();
296
296
  const rawHtml = await fetch(rawUrl, { cache: 'no-store' }).then((r) => r.text());
297
297
  const scriptHash = hashScripts(rawHtml);
298
- const canSurgical = typeof window.$?.renderComponent === 'function'
298
+ const canSurgical = typeof window.$?._renderComponent === 'function'
299
299
  && typeof window.$?.reconcile === 'function';
300
300
 
301
301
  let surgical = 0;
@@ -314,7 +314,7 @@ export const setupHotModuleRefresh = ({ debug = false, subscribe }) => {
314
314
  ? el._vibePluginSlot
315
315
  : (el._vibeSlotContent || '');
316
316
  const props = el._vibeProps || {};
317
- const processedHtml = window.$.renderComponent(rawHtml, props, slot, { componentIds });
317
+ const processedHtml = window.$._renderComponent(rawHtml, props, slot, { componentIds });
318
318
  const summary = await window.$.reconcile(el, processedHtml);
319
319
  el._vibeScriptHash = scriptHash;
320
320
  el._vibeRawSource = rawHtml;
package/index.js CHANGED
@@ -43,7 +43,7 @@ export const applyDefaults = (target, state) => {
43
43
  return target;
44
44
  };
45
45
 
46
- const vibe = (state = {}, config, targetSelector) => {
46
+ const vibe = (state = {}, config) => {
47
47
  if (isBooted()) {
48
48
  // Already booted: initial state declared late seeds missing keys only —
49
49
  // on a fresh document load this branch never runs, so MPA behavior is
@@ -57,24 +57,19 @@ const vibe = (state = {}, config, targetSelector) => {
57
57
  vibeInstance = createVibeInstance();
58
58
  }
59
59
 
60
- // Not booted yet - accumulate in global state registry
61
- if (!window.__vibeGlobalState) {
62
- window.__vibeGlobalState = {};
63
- }
64
- Object.assign(window.__vibeGlobalState, state);
65
-
66
- // Store config (first caller wins)
67
- if (config && !window.__vibeConfig) {
68
- window.__vibeConfig = config;
69
- }
60
+ // Not booted yet - accumulate in the reserved namespace's state registry
61
+ const ns = (window.__vibe ??= {});
62
+ if (!ns.state) ns.state = {};
63
+ Object.assign(ns.state, state);
70
64
 
71
- // Store targetSelector (first caller wins)
72
- if (targetSelector && !window.__vibeTargetSelector) {
73
- window.__vibeTargetSelector = targetSelector;
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
+ if (config && !ns.config) {
68
+ ns.config = config;
74
69
  }
75
70
 
76
71
  // Explicit boot call (no state passed means "boot now with accumulated state")
77
- if (Object.keys(state).length === 0 && Object.keys(window.__vibeGlobalState).length > 0) {
72
+ if (Object.keys(state).length === 0 && Object.keys(ns.state).length > 0) {
78
73
  return boot();
79
74
  }
80
75
 
package/llms.txt CHANGED
@@ -88,6 +88,8 @@ $.config.theme.colors.primary = "#007bff";
88
88
 
89
89
  Vibe uses recursive proxies to detect changes at any nesting level automatically.
90
90
 
91
+ **Reactivity model (3.0.0):** every binding, conditional, and iteration subscribes to the state keys its expression read during evaluation; a state write dispatches exactly its subscribers. Consequence: an expression only re-evaluates when reactive state it read changes. Window globals, module values, and other non-state sources are read once — reassigning them re-renders nothing. Anything that changes over time belongs in `$`. Helpers defined by component scripts are picked up automatically (mounts re-settle once their scripts have run).
92
+
91
93
  ## Control Flow
92
94
 
93
95
  ### Iteration
@@ -201,7 +203,7 @@ Multi-segment paths, conditionals (`<!-- if this.editing -->`), and iterations (
201
203
 
202
204
  Mechanism:
203
205
  1. `component({...})` claims the nearest unprocessed `<component>` (or `<div class="component">`) wrapper
204
- 2. It calls `ensureBoot()` from `@ape-egg/vibe/boot`, which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once per page, even if multiple drop-in blocks call `component()`
206
+ 2. It triggers the internal boot, which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once per page, even if multiple drop-in blocks call `component()`
205
207
  3. `@[this.X]`, `onclick="this.fn()"`, `<!-- if this.X -->`, etc. resolve against that block's bucket
206
208
 
207
209
  Multiple drop-in blocks on the same page each get their own state and run independently. They can still read each other's state via global `$['<id>']` if coordination is needed.
@@ -245,7 +247,7 @@ A standalone router maintaining `$.page = { path, route, params, src }`; a react
245
247
 
246
248
  Rules: tables are pre-sorted most-specific-first, first match wins. Clicks are claimed only for same-origin, unmodified, untargeted links whose pathname matches a real route (`'*'` never claims) — everything else navigates natively, so mixed MPA/SPA output works. Route titles swap `document.title`. `resolve(location, routes)` is pure. A custom `onNavigate` makes it a pure router without Vibe.
247
249
 
248
- State semantics under SPA: `vibe()` state is app-lifetime, `component()` state is mount-lifetime (resets per visit). `@ape-egg/vibe/defaults` is the vibe entry with defaults semantics already booted, it sets only keys that don't exist yet on `$`, so re-running page scripts never clobber live state. The compiler's SPA mode (`"spa": true` in `vibe-compiler` config, or `--spa`) compiles an MPA `pages/` tree into page fragments under `/components/vibe-spa/`, a generated route table, and a composed `/index.html` shell wired to this router — deploy with one rewrite: every route → `/index.html`.
250
+ State semantics under SPA: `vibe()` state is app-lifetime, `component()` state is mount-lifetime (resets per visit). `vibe()` itself has defaults semantics once booted it sets only keys that don't exist yet on `$`, so re-running page scripts (SPA fragment re-mounts) never clobber live state. The compiler's SPA mode (`"spa": true` in `vibe-compiler` config, or `--spa`) compiles an MPA `pages/` tree into page fragments under `/components/vibe-spa/`, a generated route table, and a composed `/index.html` shell wired to this router — deploy with one rewrite: every route → `/index.html`.
249
251
 
250
252
  ## Special Attributes
251
253
 
@@ -328,15 +330,15 @@ Imported from `@ape-egg/vibe/component`. Registers a component-local state bucke
328
330
 
329
331
  ### `$.on(event, callback)`
330
332
 
331
- Register a lifecycle listener. Events: `'ready'`, `'afterUpdate'`, `'afterDomMutation'`. Listeners registered before boot are queued and replayed once vibe is ready.
333
+ Register a lifecycle listener. Events: `'ready'`, `'afterUpdate'`, `'afterDomMutation'`, `'unmount'`. Listeners registered before boot are queued and replayed once vibe is ready. A `'ready'` listener registered AFTER boot (a fetched fragment's script) fires once its mount settles — same timing as `await $.ready`. `'unmount'` is scope-resolved: inside a component script it fires on that component's unmount (and before an HMR re-run); at page level it fires on pagehide.
332
334
 
333
335
  ### `$.ready`
334
336
 
335
337
  A Promise that resolves once vibe has finished initial parse + hydrate + component loading.
336
338
 
337
- ### `$.reconcile(el, html)` and `$.renderComponent(rawHtml, props, slot, opts)` (advanced)
339
+ ### `$.reconcile(el, html)` (advanced)
338
340
 
339
- Subtree reconciliation primitives used by `@ape-egg/vite-plugin-vibe` for surgical HMR. `$.reconcile` diffs `el`'s children against fresh `html` while preserving DOM identity, focus, and selection; vibe-managed regions (iterations, conditionals, components, slot pairs) are treated as opaque. Treat these as plumbing — shape may evolve.
341
+ Subtree reconciliation used by `@ape-egg/vite-plugin-vibe` for surgical HMR: diffs `el`'s children against fresh `html` while preserving DOM identity, focus, and selection; vibe-managed regions (iterations, conditionals, components, slot pairs) are treated as opaque. Treat as plumbing — shape may evolve. (Internal renders/registration live on underscore-prefixed `$` members and are not API.)
340
342
 
341
343
  ## Scoped Variables in Iterations
342
344
 
@@ -397,7 +399,7 @@ Vibe consists of these core runtime modules (in `runtime/`):
397
399
 
398
400
  - **No computed values primitive**: derived state can be done with `Object.defineProperty($, 'x', { get })` or with `afterUpdate` listeners; a first-class `computed` API isn't shipped
399
401
  - **Expression security**: `new Function()` evaluation — don't bind untrusted input
400
- - **HTML binding**: `@[expr]` always sets `textContent` (escape-safe). A first-class opt-in for trusted HTML is planned but not finalized
402
+ - **HTML binding**: `@[expr]` always sets `textContent` (escape-safe). For trusted HTML, `@[$.unsafe(trustedHtml)]` sets `innerHTML` when it is the element's sole content (no sanitizing — trusted input only; injected markup is inert)
401
403
 
402
404
  ## Best Practices
403
405
 
package/package.json CHANGED
@@ -1,35 +1,40 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "2.3.0",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
- "description": "Runtime-first reactivity with optional compiler",
5
+ "description": "Runtime-first reactivity for plain HTML — no build step, no virtual DOM, no new syntax to learn",
6
6
  "main": "index.js",
7
7
  "homepage": "https://vibe.korte.kim",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/kkortes/vibe.git"
11
+ },
8
12
  "exports": {
9
13
  ".": "./index.js",
10
- "./boot": "./boot.js",
11
14
  "./component": "./component.js",
12
- "./hot-module-refresh": "./hot-module-refresh.js",
13
15
  "./spa": "./spa.js",
14
- "./runtime": "./runtime/index.js",
15
- "./compiler": "./compiler/bin/vibe-compile.js"
16
- },
17
- "bin": {
18
- "vibe": "./compiler/bin/vibe-compile.js"
16
+ "./hot-module-refresh": "./hot-module-refresh.js"
19
17
  },
18
+ "files": [
19
+ "index.js",
20
+ "boot.js",
21
+ "component.js",
22
+ "spa.js",
23
+ "hot-module-refresh.js",
24
+ "runtime",
25
+ "vibe.css",
26
+ "llms.txt"
27
+ ],
20
28
  "keywords": [
21
29
  "reactive",
22
30
  "framework",
23
31
  "frontend",
24
32
  "ui",
33
+ "no-build",
25
34
  "mutation-observer",
26
35
  "proxy",
27
- "minimalistic",
28
- "compiler"
36
+ "minimalistic"
29
37
  ],
30
- "scripts": {
31
- "test": "echo \"Error: no test specified\" && exit 1"
32
- },
33
38
  "author": "kkortes",
34
39
  "license": "ISC",
35
40
  "publishConfig": {