voodoojs 0.6.0 → 0.6.2

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
@@ -4,6 +4,13 @@
4
4
 
5
5
  No mandatory build step · No runtime dependencies · No Virtual DOM · No configuration required
6
6
 
7
+ [Website](https://kwy404.github.io/Voodoo.js/) ·
8
+ [Playground](https://kwy404.github.io/Voodoo.js/playground.html) ·
9
+ [Components](https://kwy404.github.io/Voodoo.js/components.html) ·
10
+ [Examples](https://kwy404.github.io/Voodoo.js/examples/) ·
11
+ [Documentation](https://kwy404.github.io/Voodoo.js/docs/) ·
12
+ [GitHub](https://github.com/kwy404/Voodoo.js)
13
+
7
14
  ---
8
15
 
9
16
  ## Install
@@ -15,14 +22,16 @@ npm install voodoojs
15
22
  Or drop it into a page, with nothing else:
16
23
 
17
24
  ```html
18
- <script src="https://cdn.jsdelivr.net/npm/voodoojs/dist/voodoo.min.js" defer></script>
25
+ <script src="https://cdn.jsdelivr.net/npm/voodoojs@0.6/dist/voodoo.min.js" defer></script>
19
26
 
20
27
  <div v-data="{ count: 0 }">
21
28
  <button @click="count++">Clicked { count } times</button>
22
29
  </div>
23
30
  ```
24
31
 
25
- That page is a complete application. There is no build step, no bundler and no configuration.
32
+ That page is a complete application. There is no build step, no bundler and no
33
+ configuration. The tag is pinned to the `0.6` line, so patch releases arrive
34
+ without an edit; pin the exact version if you would rather approve each one.
26
35
 
27
36
  ## Two ways to write it
28
37
 
@@ -45,30 +54,73 @@ effect(() => console.log(state.count));
45
54
  state.count++;
46
55
  ```
47
56
 
57
+ ## What you get
58
+
59
+ Reactivity, components, a router, HTTP, forms with validation and masks, input
60
+ masks, i18n, charts, motion, sound, a devtools inspector, and 29 ready-made
61
+ components that need no registration — writing the tag is the whole usage.
62
+
63
+ ```html
64
+ <v-button variant="primary">Save</v-button>
65
+ <v-input label="Email" type="email"></v-input>
66
+ <v-table :columns="cols" :rows="people"></v-table>
67
+ ```
68
+
69
+ ## No eval, anywhere
70
+
71
+ Expressions run through a lexer, a Pratt parser and an interpreter, never `eval`
72
+ or `new Function`. That is why the library works under a strict Content Security
73
+ Policy with no `unsafe-eval`, which a browser test proves by loading it under
74
+ `script-src 'self'` and asserting no policy violation is raised.
75
+
76
+ ## It cleans up after itself
77
+
78
+ Once a directive is installed, its attribute is read into memory and removed from
79
+ the document. What ships to the page is ordinary HTML, with no framework residue
80
+ in the inspector. Turn it off with `V.config.cleanAttributes = false` if you need
81
+ the attributes for something else.
82
+
48
83
  ## Builds
49
84
 
50
- | File | What it carries |
51
- | --- | --- |
52
- | `dist/voodoo.core.min.js` | Reactivity, expressions, DOM engine, components, core directives |
53
- | `dist/voodoo.min.js` | The above plus forms, validation, masks, UI and HTTP. Served by default on the CDN |
54
- | `dist/voodoo.full.min.js` | Everything: charts, motion, router, i18n, sound and the devtools inspector |
85
+ | File | What it carries | Gzip |
86
+ | --- | --- | --- |
87
+ | `dist/voodoo.core.min.js` | Reactivity, expressions, DOM engine, components, core directives | 46 KB |
88
+ | `dist/voodoo.min.js` | The above plus forms, validation, masks, UI and HTTP | 83 KB |
89
+ | `dist/voodoo.full.min.js` | Everything: charts, motion, router, i18n, sound, devtools | 129 KB |
55
90
 
56
- Module entry points are also published for bundlers:
91
+ Module entry points are published for bundlers, in ESM and CJS, with types:
57
92
 
58
93
  ```js
59
94
  import { reactive } from 'voodoojs/reactivity';
60
95
  import { http } from 'voodoojs/http';
61
96
  ```
62
97
 
63
- TypeScript definitions ship with the package.
98
+ ## Performance
64
99
 
65
- ## Documentation
100
+ A 1,000-row keyed list, median of 30 samples, against the same document, every
101
+ framework bundled production and minified. Lower is better.
102
+
103
+ | | create 1k | update 1 in 10 | clear 1k |
104
+ | --- | ---: | ---: | ---: |
105
+ | vanilla JS | 48.74 | 7.52 | 21.06 |
106
+ | Preact 10.29.8 | 91.62 | 2.59 | 29.48 |
107
+ | **Voodoo.js** | **97.70** | **5.42** | **30.44** |
108
+ | React 19.2.8 | 100.23 | 4.63 | 33.59 |
109
+ | Vue 3.5.42 | 110.56 | 19.21 | 31.65 |
110
+ | Solid 1.9.15 | 111.63 | 0.91 | 19.99 |
111
+ | Alpine 3.17.1 | 179.47 | 104.51 | 31.39 |
66
112
 
67
- Full documentation, guides, examples and the API reference live in the repository:
113
+ Read honestly: hand-written vanilla still builds a list twice as fast, and Voodoo
114
+ is by far the largest bundle in that table. If size is your main constraint,
115
+ Alpine and Preact are the honest recommendation. Method and per-framework
116
+ adapters are in the repository.
68
117
 
69
- **https://github.com/kwy404/Voodoo.js**
118
+ ## Documentation
70
119
 
71
- The documentation is written in Portuguese under `docs/`, with an English set under `docs/en/`.
120
+ Everything lives at **[kwy404.github.io/Voodoo.js](https://kwy404.github.io/Voodoo.js/)**:
121
+ a guide, an API reference, 13 working example applications, a playground with 26
122
+ runnable examples, and the component gallery. The site is built with Voodoo.js
123
+ itself and has no build step of its own.
72
124
 
73
125
  ## License
74
126
 
package/dist/index.cjs CHANGED
@@ -7732,6 +7732,33 @@ function buildUrl(location2) {
7732
7732
  const base = settings2.base === "/" ? "" : settings2.base.replace(/\/$/, "");
7733
7733
  return `${base}${suffix}` || "/";
7734
7734
  }
7735
+ var historyRefused = false;
7736
+ var writingHash = false;
7737
+ function writeUrl(state2, url2, replace) {
7738
+ if (!historyRefused) {
7739
+ try {
7740
+ if (replace) window.history.replaceState(state2, "", url2);
7741
+ else window.history.pushState(state2, "", url2);
7742
+ return;
7743
+ } catch (error) {
7744
+ if (!(error instanceof Error) || error.name !== "SecurityError") throw error;
7745
+ historyRefused = true;
7746
+ }
7747
+ }
7748
+ if (settings2.mode !== "hash") return;
7749
+ const marker = url2.indexOf("#");
7750
+ if (marker < 0) return;
7751
+ const hash = url2.slice(marker);
7752
+ if (window.location.hash === hash) return;
7753
+ writingHash = true;
7754
+ try {
7755
+ window.location.hash = hash;
7756
+ } finally {
7757
+ setTimeout(() => {
7758
+ writingHash = false;
7759
+ }, 0);
7760
+ }
7761
+ }
7735
7762
  function compileRoute(pattern, record) {
7736
7763
  const clean = pattern === "*" ? "*" : normalizePath(pattern);
7737
7764
  const raw = clean === "*" ? ["*"] : clean.split("/").filter(Boolean);
@@ -7918,8 +7945,7 @@ async function navigate(target2, options = {}) {
7918
7945
  const key = uid("rota");
7919
7946
  const historyState = { ...options.state ?? {}, [HISTORY_KEY]: key };
7920
7947
  const url2 = buildUrl(destination);
7921
- if (options.replace) window.history.replaceState(historyState, "", url2);
7922
- else window.history.pushState(historyState, "", url2);
7948
+ writeUrl(historyState, url2, options.replace === true);
7923
7949
  currentKey = key;
7924
7950
  applyLocation(destination);
7925
7951
  if (options.scroll !== false) scheduleScroll(destination, from, null);
@@ -7932,17 +7958,14 @@ async function navigate(target2, options = {}) {
7932
7958
  return true;
7933
7959
  }
7934
7960
  async function onHistoryChange(event) {
7961
+ if (writingHash) return;
7935
7962
  const { path, query: query2, hash } = readLocation();
7936
7963
  const destination = locationFor(path, query2, hash);
7937
7964
  const from = snapshot();
7938
7965
  if (destination.fullPath === from.fullPath) return;
7939
7966
  const verdict = await runGuards(destination, from);
7940
7967
  if (verdict === false) {
7941
- window.history.replaceState(
7942
- { [HISTORY_KEY]: currentKey },
7943
- "",
7944
- buildUrl(from)
7945
- );
7968
+ writeUrl({ [HISTORY_KEY]: currentKey }, buildUrl(from), true);
7946
7969
  devtoolsBus.emit("navigation", {
7947
7970
  from: from.fullPath,
7948
7971
  to: destination.fullPath,
@@ -7985,6 +8008,8 @@ function stopRouter() {
7985
8008
  window.removeEventListener("popstate", historyListener);
7986
8009
  window.removeEventListener("hashchange", historyListener);
7987
8010
  window.removeEventListener("beforeunload", saveScroll);
8011
+ historyRefused = false;
8012
+ writingHash = false;
7988
8013
  }
7989
8014
  async function enterInitialRoute() {
7990
8015
  if (typeof window === "undefined") return;
@@ -8005,7 +8030,7 @@ async function enterInitialRoute() {
8005
8030
  break;
8006
8031
  }
8007
8032
  currentKey = uid("rota");
8008
- window.history.replaceState({ [HISTORY_KEY]: currentKey }, "", buildUrl(destination));
8033
+ writeUrl({ [HISTORY_KEY]: currentKey }, buildUrl(destination), true);
8009
8034
  applyLocation(destination);
8010
8035
  if (destination.hash) scheduleScroll(destination, from, null);
8011
8036
  settings2.afterEach?.(snapshot(), from);
package/dist/index.js CHANGED
@@ -128,6 +128,33 @@ function buildUrl(location) {
128
128
  const base = settings.base === "/" ? "" : settings.base.replace(/\/$/, "");
129
129
  return `${base}${suffix}` || "/";
130
130
  }
131
+ var historyRefused = false;
132
+ var writingHash = false;
133
+ function writeUrl(state2, url2, replace) {
134
+ if (!historyRefused) {
135
+ try {
136
+ if (replace) window.history.replaceState(state2, "", url2);
137
+ else window.history.pushState(state2, "", url2);
138
+ return;
139
+ } catch (error) {
140
+ if (!(error instanceof Error) || error.name !== "SecurityError") throw error;
141
+ historyRefused = true;
142
+ }
143
+ }
144
+ if (settings.mode !== "hash") return;
145
+ const marker = url2.indexOf("#");
146
+ if (marker < 0) return;
147
+ const hash = url2.slice(marker);
148
+ if (window.location.hash === hash) return;
149
+ writingHash = true;
150
+ try {
151
+ window.location.hash = hash;
152
+ } finally {
153
+ setTimeout(() => {
154
+ writingHash = false;
155
+ }, 0);
156
+ }
157
+ }
131
158
  function compileRoute(pattern, record) {
132
159
  const clean = pattern === "*" ? "*" : normalizePath(pattern);
133
160
  const raw = clean === "*" ? ["*"] : clean.split("/").filter(Boolean);
@@ -314,8 +341,7 @@ async function navigate(target, options = {}) {
314
341
  const key = uid("rota");
315
342
  const historyState = { ...options.state ?? {}, [HISTORY_KEY]: key };
316
343
  const url2 = buildUrl(destination);
317
- if (options.replace) window.history.replaceState(historyState, "", url2);
318
- else window.history.pushState(historyState, "", url2);
344
+ writeUrl(historyState, url2, options.replace === true);
319
345
  currentKey = key;
320
346
  applyLocation(destination);
321
347
  if (options.scroll !== false) scheduleScroll(destination, from, null);
@@ -328,17 +354,14 @@ async function navigate(target, options = {}) {
328
354
  return true;
329
355
  }
330
356
  async function onHistoryChange(event) {
357
+ if (writingHash) return;
331
358
  const { path, query: query2, hash } = readLocation();
332
359
  const destination = locationFor(path, query2, hash);
333
360
  const from = snapshot();
334
361
  if (destination.fullPath === from.fullPath) return;
335
362
  const verdict = await runGuards(destination, from);
336
363
  if (verdict === false) {
337
- window.history.replaceState(
338
- { [HISTORY_KEY]: currentKey },
339
- "",
340
- buildUrl(from)
341
- );
364
+ writeUrl({ [HISTORY_KEY]: currentKey }, buildUrl(from), true);
342
365
  devtoolsBus.emit("navigation", {
343
366
  from: from.fullPath,
344
367
  to: destination.fullPath,
@@ -381,6 +404,8 @@ function stopRouter() {
381
404
  window.removeEventListener("popstate", historyListener);
382
405
  window.removeEventListener("hashchange", historyListener);
383
406
  window.removeEventListener("beforeunload", saveScroll);
407
+ historyRefused = false;
408
+ writingHash = false;
384
409
  }
385
410
  async function enterInitialRoute() {
386
411
  if (typeof window === "undefined") return;
@@ -401,7 +426,7 @@ async function enterInitialRoute() {
401
426
  break;
402
427
  }
403
428
  currentKey = uid("rota");
404
- window.history.replaceState({ [HISTORY_KEY]: currentKey }, "", buildUrl(destination));
429
+ writeUrl({ [HISTORY_KEY]: currentKey }, buildUrl(destination), true);
405
430
  applyLocation(destination);
406
431
  if (destination.hash) scheduleScroll(destination, from, null);
407
432
  settings.afterEach?.(snapshot(), from);
@@ -7855,6 +7855,33 @@ Suggestion: attribute expressions accept a single value. If the logic spans more
7855
7855
  const base = settings2.base === "/" ? "" : settings2.base.replace(/\/$/, "");
7856
7856
  return `${base}${suffix}` || "/";
7857
7857
  }
7858
+ var historyRefused = false;
7859
+ var writingHash = false;
7860
+ function writeUrl(state2, url2, replace) {
7861
+ if (!historyRefused) {
7862
+ try {
7863
+ if (replace) window.history.replaceState(state2, "", url2);
7864
+ else window.history.pushState(state2, "", url2);
7865
+ return;
7866
+ } catch (error) {
7867
+ if (!(error instanceof Error) || error.name !== "SecurityError") throw error;
7868
+ historyRefused = true;
7869
+ }
7870
+ }
7871
+ if (settings2.mode !== "hash") return;
7872
+ const marker = url2.indexOf("#");
7873
+ if (marker < 0) return;
7874
+ const hash = url2.slice(marker);
7875
+ if (window.location.hash === hash) return;
7876
+ writingHash = true;
7877
+ try {
7878
+ window.location.hash = hash;
7879
+ } finally {
7880
+ setTimeout(() => {
7881
+ writingHash = false;
7882
+ }, 0);
7883
+ }
7884
+ }
7858
7885
  function compileRoute(pattern, record) {
7859
7886
  const clean = pattern === "*" ? "*" : normalizePath(pattern);
7860
7887
  const raw = clean === "*" ? ["*"] : clean.split("/").filter(Boolean);
@@ -8045,8 +8072,7 @@ Suggestion: attribute expressions accept a single value. If the logic spans more
8045
8072
  const key = uid("rota");
8046
8073
  const historyState = { ...(_a2 = options.state) != null ? _a2 : {}, [HISTORY_KEY]: key };
8047
8074
  const url2 = buildUrl(destination);
8048
- if (options.replace) window.history.replaceState(historyState, "", url2);
8049
- else window.history.pushState(historyState, "", url2);
8075
+ writeUrl(historyState, url2, options.replace === true);
8050
8076
  currentKey = key;
8051
8077
  applyLocation(destination);
8052
8078
  if (options.scroll !== false) scheduleScroll(destination, from, null);
@@ -8060,17 +8086,14 @@ Suggestion: attribute expressions accept a single value. If the logic spans more
8060
8086
  }
8061
8087
  async function onHistoryChange(event) {
8062
8088
  var _a2, _b;
8089
+ if (writingHash) return;
8063
8090
  const { path, query: query2, hash } = readLocation();
8064
8091
  const destination = locationFor(path, query2, hash);
8065
8092
  const from = snapshot();
8066
8093
  if (destination.fullPath === from.fullPath) return;
8067
8094
  const verdict = await runGuards(destination, from);
8068
8095
  if (verdict === false) {
8069
- window.history.replaceState(
8070
- { [HISTORY_KEY]: currentKey },
8071
- "",
8072
- buildUrl(from)
8073
- );
8096
+ writeUrl({ [HISTORY_KEY]: currentKey }, buildUrl(from), true);
8074
8097
  devtoolsBus.emit("navigation", {
8075
8098
  from: from.fullPath,
8076
8099
  to: destination.fullPath,
@@ -8113,6 +8136,8 @@ Suggestion: attribute expressions accept a single value. If the logic spans more
8113
8136
  window.removeEventListener("popstate", historyListener);
8114
8137
  window.removeEventListener("hashchange", historyListener);
8115
8138
  window.removeEventListener("beforeunload", saveScroll);
8139
+ historyRefused = false;
8140
+ writingHash = false;
8116
8141
  }
8117
8142
  async function enterInitialRoute() {
8118
8143
  var _a2;
@@ -8134,7 +8159,7 @@ Suggestion: attribute expressions accept a single value. If the logic spans more
8134
8159
  break;
8135
8160
  }
8136
8161
  currentKey = uid("rota");
8137
- window.history.replaceState({ [HISTORY_KEY]: currentKey }, "", buildUrl(destination));
8162
+ writeUrl({ [HISTORY_KEY]: currentKey }, buildUrl(destination), true);
8138
8163
  applyLocation(destination);
8139
8164
  if (destination.hash) scheduleScroll(destination, from, null);
8140
8165
  (_a2 = settings2.afterEach) == null ? void 0 : _a2.call(settings2, snapshot(), from);