proxyjs-web 0.1.0-beta

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.
@@ -0,0 +1,354 @@
1
+ import { isFunction, isPrimitive } from "../utils/is.js";
2
+ import { effect } from "../core/reactive.js";
3
+ import { setCurrentComponent } from "../hooks/newState.js";
4
+ import { Fragment } from "./createElement.js";
5
+
6
+ export function render(vnode, container) {
7
+ const oldVnode = container.__vnode;
8
+ if (oldVnode == null) {
9
+ const el = mount(vnode, container);
10
+ container.__vnode = vnode;
11
+ container.appendChild(el);
12
+ } else {
13
+ container.innerHTML = "";
14
+ container.__vnode = null;
15
+ const el = mount(vnode, container);
16
+ container.__vnode = vnode;
17
+ container.appendChild(el);
18
+ }
19
+ }
20
+
21
+ function mount(vnode, parentEl, errorHandler) {
22
+ if (vnode == null) return document.createTextNode("");
23
+
24
+ if (isPrimitive(vnode)) {
25
+ return document.createTextNode(String(vnode));
26
+ }
27
+
28
+ if (isFunction(vnode)) {
29
+ const textNode = document.createTextNode("");
30
+ effect(() => {
31
+ textNode.textContent = vnode();
32
+ });
33
+ return textNode;
34
+ }
35
+
36
+ if (vnode.type === Fragment) {
37
+ return mountFragment(vnode, parentEl, errorHandler);
38
+ }
39
+
40
+ if (isFunction(vnode.type)) {
41
+ return mountComponent(vnode, parentEl, errorHandler);
42
+ }
43
+
44
+ return mountElement(vnode, errorHandler);
45
+ }
46
+
47
+ function mountElement(vnode, errorHandler) {
48
+ const isErrorBoundary = vnode.props?.__errorBoundary;
49
+ const fallback = vnode.props?.__fallback;
50
+
51
+ const el = document.createElement(vnode.type);
52
+
53
+ const cleanProps = { ...vnode.props };
54
+ delete cleanProps.__errorBoundary;
55
+ delete cleanProps.__fallback;
56
+ patchProps(el, {}, cleanProps);
57
+
58
+ const children = normalizeChildren(vnode.children);
59
+
60
+ const boundaryHandler = isErrorBoundary
61
+ ? (err) => {
62
+ console.error("[ProxyJS] Erro capturado pelo ErrorBoundary:", err);
63
+ el.innerHTML = "";
64
+ const fallbackVnode = fallback
65
+ ? fallback(err)
66
+ : {
67
+ type: "div",
68
+ props: {
69
+ style:
70
+ "padding:1rem;background:#fee2e2;color:#991b1b;border-radius:6px;font-family:monospace",
71
+ },
72
+ children: [
73
+ { type: "strong", props: {}, children: ["⚠ Algo deu errado"] },
74
+ {
75
+ type: "p",
76
+ props: {},
77
+ children: [err.message || String(err)],
78
+ },
79
+ ],
80
+ };
81
+ el.appendChild(mount(fallbackVnode, el, null));
82
+ }
83
+ : errorHandler;
84
+
85
+ children.forEach((child) => {
86
+ if (child == null) return;
87
+ try {
88
+ el.appendChild(mount(child, el, boundaryHandler));
89
+ } catch (err) {
90
+ if (boundaryHandler) {
91
+ boundaryHandler(err);
92
+ } else {
93
+ throw err;
94
+ }
95
+ }
96
+ });
97
+
98
+ el.__vnode = vnode;
99
+ return el;
100
+ }
101
+
102
+ function mountFragment(vnode, parentEl, errorHandler) {
103
+ const anchor = document.createComment("fragment");
104
+ parentEl.appendChild(anchor);
105
+
106
+ const children = normalizeChildren(vnode.children);
107
+ children.forEach((child) => {
108
+ if (child == null) return;
109
+ parentEl.insertBefore(mount(child, parentEl, errorHandler), anchor);
110
+ });
111
+
112
+ anchor.__vnode = vnode;
113
+ return anchor;
114
+ }
115
+
116
+ function mountComponent(vnode, parentEl, errorHandler) {
117
+ const instance = {
118
+ __hooks: [],
119
+ __hookIndex: 0,
120
+ vnode,
121
+ el: null,
122
+ };
123
+
124
+ const el = runComponent(instance, vnode, parentEl, errorHandler);
125
+ instance.el = el;
126
+ el.__componentInstance = instance;
127
+ return el;
128
+ }
129
+
130
+ function runComponent(instance, vnode, parentEl, errorHandler) {
131
+ instance.__hookIndex = 0;
132
+ setCurrentComponent(instance);
133
+ try {
134
+ const childVnode = vnode.type({ ...vnode.props, children: vnode.children });
135
+ setCurrentComponent(null);
136
+ return mount(childVnode, parentEl, errorHandler);
137
+ } catch (err) {
138
+ setCurrentComponent(null);
139
+ if (errorHandler) {
140
+ errorHandler(err);
141
+ return document.createTextNode("");
142
+ }
143
+ console.error(
144
+ `[ProxyJS] Erro no componente "${vnode.type.name || "Anônimo"}":`,
145
+ err,
146
+ );
147
+ throw err;
148
+ }
149
+ }
150
+
151
+ function patch(parentEl, oldVnode, newVnode) {
152
+ if (sameVnode(oldVnode, newVnode)) {
153
+ if (isPrimitive(newVnode) || isFunction(newVnode)) {
154
+ const el = findDomNode(parentEl, oldVnode);
155
+ if (el && el.nodeType === Node.TEXT_NODE && isPrimitive(newVnode)) {
156
+ if (el.textContent !== String(newVnode)) {
157
+ el.textContent = String(newVnode);
158
+ }
159
+ }
160
+ return el;
161
+ }
162
+ if (isFunction(newVnode.type)) {
163
+ return patchComponent(parentEl, oldVnode, newVnode);
164
+ }
165
+ return patchElement(parentEl, oldVnode, newVnode);
166
+ }
167
+
168
+ const oldEl = findDomNode(parentEl, oldVnode);
169
+ const newEl = mount(newVnode, parentEl);
170
+
171
+ if (
172
+ oldEl &&
173
+ oldEl.parentNode === parentEl &&
174
+ oldEl.nodeType !== Node.COMMENT_NODE
175
+ ) {
176
+ parentEl.replaceChild(newEl, oldEl);
177
+ } else if (oldEl && oldEl.nodeType === Node.COMMENT_NODE) {
178
+ oldEl.parentNode.insertBefore(newEl, oldEl);
179
+ oldEl.parentNode.removeChild(oldEl);
180
+ } else {
181
+ parentEl.appendChild(newEl);
182
+ }
183
+ return newEl;
184
+ }
185
+
186
+ function patchElement(parentEl, oldVnode, newVnode) {
187
+ const el = parentEl.querySelector
188
+ ? Array.from(parentEl.childNodes).find((n) => n.__vnode === oldVnode) ||
189
+ parentEl.firstChild
190
+ : parentEl;
191
+
192
+ if (!el) return mount(newVnode, parentEl);
193
+
194
+ patchProps(el, oldVnode.props || {}, newVnode.props || {});
195
+ patchChildren(el, oldVnode.children || [], newVnode.children || []);
196
+
197
+ el.__vnode = newVnode;
198
+ return el;
199
+ }
200
+
201
+ function patchComponent(parentEl, oldVnode, newVnode) {
202
+ const oldEl = findDomNode(parentEl, oldVnode);
203
+ if (!oldEl) return mount(newVnode, parentEl);
204
+
205
+ const instance = oldEl.__componentInstance;
206
+ if (!instance) {
207
+ const newEl = mount(newVnode, parentEl);
208
+ parentEl.replaceChild(newEl, oldEl);
209
+ return newEl;
210
+ }
211
+
212
+ instance.__hookIndex = 0;
213
+ setCurrentComponent(instance);
214
+ const childVnode = newVnode.type({
215
+ ...newVnode.props,
216
+ children: newVnode.children,
217
+ });
218
+ setCurrentComponent(null);
219
+
220
+ const newEl = mount(childVnode, parentEl);
221
+ parentEl.replaceChild(newEl, oldEl);
222
+ newEl.__componentInstance = instance;
223
+ instance.el = newEl;
224
+ return newEl;
225
+ }
226
+
227
+ function patchChildren(parentEl, oldChildren, newChildren) {
228
+ oldChildren = normalizeChildren(oldChildren);
229
+ newChildren = normalizeChildren(newChildren);
230
+
231
+ const hasKeys = newChildren.some((c) => c && c.props && c.props.key != null);
232
+
233
+ if (hasKeys) {
234
+ patchKeyedChildren(parentEl, oldChildren, newChildren);
235
+ } else {
236
+ patchUnkeyedChildren(parentEl, oldChildren, newChildren);
237
+ }
238
+ }
239
+
240
+ function patchKeyedChildren(parentEl, oldChildren, newChildren) {
241
+ const oldKeyMap = new Map();
242
+ const oldDomNodes = Array.from(parentEl.childNodes);
243
+
244
+ oldChildren.forEach((child, i) => {
245
+ if (child && child.props && child.props.key != null) {
246
+ oldKeyMap.set(child.props.key, { vnode: child, el: oldDomNodes[i] });
247
+ }
248
+ });
249
+
250
+ const newDomNodes = [];
251
+
252
+ newChildren.forEach((newChild) => {
253
+ if (newChild == null) return;
254
+ const key = newChild.props?.key;
255
+
256
+ if (key != null && oldKeyMap.has(key)) {
257
+ const { vnode: oldChild, el: oldEl } = oldKeyMap.get(key);
258
+ if (sameVnode(oldChild, newChild)) {
259
+ patchElement(parentEl, oldChild, newChild);
260
+ newDomNodes.push(oldEl);
261
+ } else {
262
+ const newEl = mount(newChild, parentEl);
263
+ parentEl.replaceChild(newEl, oldEl);
264
+ newDomNodes.push(newEl);
265
+ }
266
+ oldKeyMap.delete(key);
267
+ } else {
268
+ const newEl = mount(newChild, parentEl);
269
+ newDomNodes.push(newEl);
270
+ }
271
+ });
272
+
273
+ oldKeyMap.forEach(({ el }) => {
274
+ if (el && el.parentNode === parentEl) {
275
+ parentEl.removeChild(el);
276
+ }
277
+ });
278
+
279
+ newDomNodes.forEach((el) => {
280
+ parentEl.appendChild(el);
281
+ });
282
+ }
283
+
284
+ function patchUnkeyedChildren(parentEl, oldChildren, newChildren) {
285
+ const domNodes = Array.from(parentEl.childNodes);
286
+ const max = Math.max(oldChildren.length, newChildren.length);
287
+
288
+ for (let i = 0; i < max; i++) {
289
+ const oldChild = oldChildren[i];
290
+ const newChild = newChildren[i];
291
+
292
+ if (newChild == null) {
293
+ if (domNodes[i] && domNodes[i].parentNode === parentEl) {
294
+ parentEl.removeChild(domNodes[i]);
295
+ }
296
+ } else if (oldChild == null) {
297
+ parentEl.appendChild(mount(newChild, parentEl));
298
+ } else {
299
+ patch(parentEl, oldChild, newChild);
300
+ }
301
+ }
302
+ }
303
+
304
+ function patchProps(el, oldProps, newProps) {
305
+ for (const key in oldProps) {
306
+ if (key === "key") continue;
307
+ if (!(key in newProps)) {
308
+ if (key.startsWith("on") && isFunction(oldProps[key])) {
309
+ el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
310
+ } else {
311
+ el.removeAttribute(key);
312
+ }
313
+ }
314
+ }
315
+
316
+ for (const key in newProps) {
317
+ if (key === "key") continue;
318
+ const newVal = newProps[key];
319
+ const oldVal = oldProps[key];
320
+
321
+ if (newVal === oldVal) continue;
322
+
323
+ if (key.startsWith("on") && isFunction(newVal)) {
324
+ if (oldVal) el.removeEventListener(key.slice(2).toLowerCase(), oldVal);
325
+ el.addEventListener(key.slice(2).toLowerCase(), newVal);
326
+ } else if (newVal == null) {
327
+ el.removeAttribute(key);
328
+ } else {
329
+ el.setAttribute(key, newVal);
330
+ }
331
+ }
332
+ }
333
+
334
+ function sameVnode(a, b) {
335
+ if (isPrimitive(a) && isPrimitive(b)) return true;
336
+ if (isFunction(a) && isFunction(b)) return true;
337
+ if (a == null || b == null) return false;
338
+ if (typeof a !== "object" || typeof b !== "object") return false;
339
+ return a.type === b.type && a.props?.key === b.props?.key;
340
+ }
341
+
342
+ function normalizeChildren(children) {
343
+ if (!children) return [];
344
+ return Array.isArray(children) ? children.flat(Infinity) : [children];
345
+ }
346
+
347
+ function findDomNode(parentEl, vnode) {
348
+ const nodes = Array.from(parentEl.childNodes);
349
+ return nodes.find((n) => n.__vnode === vnode) || nodes[0] || null;
350
+ }
351
+
352
+ export function renderTemplate(template, container) {
353
+ container.innerHTML = template;
354
+ }
@@ -0,0 +1,3 @@
1
+ export function renderTemplate(template, container) {
2
+ container.innerHTML = template;
3
+ }
@@ -0,0 +1,173 @@
1
+ # Hooks — ProxyJS
2
+
3
+ Hooks são funções especiais que permitem usar estado e outros recursos do ProxyJS dentro de componentes funcionais. No ProxyJS, todos os hooks seguem uma convenção própria: **sempre começam com `new`**.
4
+
5
+ ---
6
+
7
+ ## Regras dos hooks
8
+
9
+ **1. Sempre começam com `new`**
10
+
11
+ Todo hook do ProxyJS começa com o prefixo `new`. Isso diferencia os hooks do ProxyJS de funções comuns e de hooks de outras bibliotecas.
12
+
13
+ ```js
14
+ newState(0) // ✅
15
+ newNavigate() // ✅
16
+ useState(0) // ❌ não é ProxyJS
17
+ useNavigate() // ❌ não é ProxyJS
18
+ ```
19
+
20
+ **2. Só podem ser chamados dentro de componentes funcionais**
21
+
22
+ Hooks dependem do contexto do componente atual. Chamá-los fora de um componente vai lançar um erro.
23
+
24
+ ```js
25
+ // ✅ correto
26
+ function App() {
27
+ const [count, setCount] = newState(0);
28
+ return html.p(count);
29
+ }
30
+
31
+ // ❌ errado — fora de um componente
32
+ const [count, setCount] = newState(0);
33
+ ```
34
+
35
+ **3. Sempre chamados no topo do componente**
36
+
37
+ Nunca chame hooks dentro de condicionais, loops ou funções aninhadas. A ordem de chamada precisa ser a mesma em todo render.
38
+
39
+ ```js
40
+ // ✅ correto
41
+ function App() {
42
+ const [count, setCount] = newState(0);
43
+ const [nome, setNome] = newState("Arthur");
44
+ return html.div(html.p(count), html.p(nome));
45
+ }
46
+
47
+ // ❌ errado — hook dentro de condicional
48
+ function App() {
49
+ if (algumaCoisa) {
50
+ const [count, setCount] = newState(0); // nunca faça isso
51
+ }
52
+ }
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Hooks disponíveis
58
+
59
+ ### `newState(initialValue)`
60
+
61
+ O hook principal do ProxyJS. Cria um estado local por instância de componente. Retorna `[getter, setter]`.
62
+
63
+ ```js
64
+ function Contador() {
65
+ const [count, setCount] = newState(0);
66
+
67
+ return html.div(
68
+ html.p(count),
69
+ html.button({ onClick: () => setCount(count() + 1) }, "+"),
70
+ html.button({ onClick: () => setCount(count() - 1) }, "-")
71
+ );
72
+ }
73
+ ```
74
+
75
+ O getter é uma função — sempre chame com `()` para ler o valor. Para passar o valor como filho reativo de um elemento, passe o getter sem chamar:
76
+
77
+ ```js
78
+ html.p(count) // ✅ reativo
79
+ html.p(count()) // ❌ estático
80
+ ```
81
+
82
+ Multiplos estados no mesmo componente:
83
+
84
+ ```js
85
+ function Formulario() {
86
+ const [nome, setNome] = newState("");
87
+ const [email, setEmail] = newState("");
88
+ const [idade, setIdade] = newState(0);
89
+
90
+ return html.div(
91
+ html.input({ onInput: (e) => setNome(e.target.value) }),
92
+ html.input({ onInput: (e) => setEmail(e.target.value) }),
93
+ html.input({ type: "number", onInput: (e) => setIdade(Number(e.target.value)) })
94
+ );
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ### `newNavigate()`
101
+
102
+ Retorna a função `navigate` do router. Permite navegar entre rotas de dentro de um componente.
103
+
104
+ ```js
105
+ import { newNavigate, html } from "./src/index.js";
106
+
107
+ function Home() {
108
+ const navigate = newNavigate();
109
+
110
+ return html.div(
111
+ html.h1("Home"),
112
+ html.button({ onClick: () => navigate("/sobre") }, "Ir para Sobre"),
113
+ html.button({ onClick: () => navigate("/user/42") }, "Ver usuário 42")
114
+ );
115
+ }
116
+ ```
117
+
118
+ > `newNavigate` só faz sentido quando a aplicação usa `createRouter`. Fora desse contexto, importe o `navigate` diretamente.
119
+
120
+ ---
121
+
122
+ ## Criando seus próprios hooks
123
+
124
+ Você pode compor hooks existentes para criar hooks reutilizáveis. A única regra é que o nome deve começar com `new` e o hook só pode ser chamado dentro de componentes.
125
+
126
+ ```js
127
+ function newContador(inicial = 0) {
128
+ const [count, setCount] = newState(inicial);
129
+ const incrementar = () => setCount(count() + 1);
130
+ const decrementar = () => setCount(count() - 1);
131
+ const resetar = () => setCount(inicial);
132
+ return { count, incrementar, decrementar, resetar };
133
+ }
134
+
135
+ function App() {
136
+ const { count, incrementar, decrementar, resetar } = newContador(10);
137
+
138
+ return html.div(
139
+ html.p(count),
140
+ html.button({ onClick: incrementar }, "+"),
141
+ html.button({ onClick: decrementar }, "-"),
142
+ html.button({ onClick: resetar }, "Reset")
143
+ );
144
+ }
145
+ ```
146
+
147
+ Outro exemplo — hook de toggle:
148
+
149
+ ```js
150
+ function newToggle(inicial = false) {
151
+ const [value, setValue] = newState(inicial);
152
+ const toggle = () => setValue(!value());
153
+ return [value, toggle];
154
+ }
155
+
156
+ function App() {
157
+ const [aberto, toggleAberto] = newToggle();
158
+
159
+ return html.div(
160
+ html.button({ onClick: toggleAberto }, "Alternar"),
161
+ html.p(() => aberto() ? "Aberto" : "Fechado")
162
+ );
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Resumo
169
+
170
+ | Hook | Retorna | Onde usar |
171
+ |------|---------|-----------|
172
+ | `newState(initial)` | `[getter, setter]` | Dentro de componentes |
173
+ | `newNavigate()` | `navigate(path)` | Dentro de componentes com router |
@@ -0,0 +1,65 @@
1
+ import { reactive } from "../core/reactive.js";
2
+
3
+ function normalizePath() {
4
+ const hash = window.location.hash;
5
+ if (hash.startsWith("#/")) return hash.slice(1);
6
+ if (hash.startsWith("#")) return "/" + hash.slice(1);
7
+ return "/";
8
+ }
9
+
10
+ const state = reactive({
11
+ path: normalizePath(),
12
+ });
13
+
14
+ window.addEventListener("hashchange", () => {
15
+ state.path = normalizePath();
16
+ });
17
+
18
+ function navigate(path) {
19
+ window.location.hash = "#" + path;
20
+ state.path = path;
21
+ }
22
+
23
+ function matchRoute(pattern, path) {
24
+ const paramNames = [];
25
+ const regexStr = pattern
26
+ .replace(/:([^/]+)/g, (_, name) => {
27
+ paramNames.push(name);
28
+ return "([^/]+)";
29
+ })
30
+ .replace(/\*/g, ".*");
31
+
32
+ const regex = new RegExp(`^${regexStr}$`);
33
+ const match = path.match(regex);
34
+ if (!match) return null;
35
+
36
+ const params = {};
37
+ paramNames.forEach((name, i) => {
38
+ params[name] = match[i + 1];
39
+ });
40
+ return params;
41
+ }
42
+
43
+ export function createRouter(routes) {
44
+ return function Router() {
45
+ const path = state.path;
46
+ console.log("Router executou, path:", path);
47
+ for (const route of routes) {
48
+ const params = matchRoute(route.path, path);
49
+ if (params !== null) {
50
+ return route.component({ params });
51
+ }
52
+ }
53
+ return null;
54
+ };
55
+ }
56
+
57
+ export { navigate };
58
+
59
+ export function newNavigate() {
60
+ return navigate;
61
+ }
62
+
63
+ export function currentPath() {
64
+ return state.path;
65
+ }
@@ -0,0 +1,28 @@
1
+ import { reactive } from "../core/reactive.js";
2
+
3
+ let currentComponent = null;
4
+
5
+ export function setCurrentComponent(component) {
6
+ currentComponent = component;
7
+ }
8
+
9
+ export function getCurrentComponent() {
10
+ return currentComponent;
11
+ }
12
+ export function newState(initial) {
13
+ const component = currentComponent;
14
+ if (!component) {
15
+ throw new Error("newState deve ser chamado dentro de um componente funcional.");
16
+ }
17
+
18
+ const index = component.__hookIndex++;
19
+ if (component.__hooks[index] === undefined) {
20
+ const state = reactive({ value: initial });
21
+ component.__hooks[index] = state;
22
+ }
23
+
24
+ const state = component.__hooks[index];
25
+ const get = () => state.value;
26
+ const set = (newValue) => { state.value = newValue; };
27
+ return [get, set];
28
+ }
package/src/index.js ADDED
@@ -0,0 +1,26 @@
1
+ export { reactive, effect } from "./core/reactive.js";
2
+ export { createStyle } from "./core/createStyle.js";
3
+ export { computed } from "./core/computed.js";
4
+ export { ErrorBoundary } from "./core/errorBoundary.js";
5
+ export { createRoot, renderToRoot } from "./core/createRoot.js";
6
+ export { createElement, Fragment } from "./dom/createElement.js";
7
+ export { default as html, registerComponent } from "./dom/html.js";
8
+ export {
9
+ createRouter,
10
+ navigate,
11
+ newNavigate,
12
+ currentPath,
13
+ } from "./hooks/newNavigate.js";
14
+ export { newState } from "./hooks/newState.js";
15
+
16
+ import { createElement, Fragment } from "./dom/createElement.js";
17
+ import { ErrorBoundary } from "./core/errorBoundary.js";
18
+ import { registerComponent } from "./dom/html.js";
19
+
20
+ registerComponent("ErrorBoundary", ErrorBoundary);
21
+
22
+ const ProxyJS = {
23
+ fragment: (...children) => createElement(Fragment, {}, ...children),
24
+ };
25
+
26
+ export default ProxyJS;
@@ -0,0 +1,11 @@
1
+ export function isFunction(value) {
2
+ return typeof value === "function";
3
+ }
4
+
5
+ export function isObject(value) {
6
+ return value !== null && typeof value === "object";
7
+ }
8
+
9
+ export function isPrimitive(value) {
10
+ return typeof value === "string" || typeof value === "number";
11
+ }