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.
package/README.md ADDED
@@ -0,0 +1,410 @@
1
+ # ProxyJS
2
+
3
+ <img src="https://img.shields.io/badge/Version-1.0%20(Beta)-6320d6" alt="Version - Badge">
4
+
5
+ <p align="center">
6
+ <img src="https://i.postimg.cc/2ydY08wy/proxyjs-logo-nobg.png" width="80" alt="ProxyJS logo" />
7
+ </p>
8
+
9
+ ProxyJS é uma biblioteca JavaScript reativa minimalista inspirada no Vue e no React. Ela usa `Proxy` nativo do JS para rastrear dependências automaticamente e atualizar o DOM quando o estado muda — sem compilador, sem build step obrigatório.
10
+
11
+ ---
12
+
13
+ ## Instalação
14
+
15
+ ProxyJS é usado diretamente via módulos ES. Basta importar do `index.js`:
16
+
17
+ ```js
18
+ import Proxy, {
19
+ html,
20
+ newState,
21
+ renderToRoot,
22
+ registerComponent,
23
+ } from "./src/index.js";
24
+ ```
25
+
26
+ > Certifique-se de que seu servidor serve os arquivos com MIME type correto (`text/javascript`). O Live Server do VSCode funciona bem.
27
+
28
+ ---
29
+
30
+ ## Estrutura de arquivos
31
+
32
+ ```
33
+ src/
34
+ ├── index.js
35
+ ├── core/
36
+ │ ├── reactive.js
37
+ │ ├── createRoot.js
38
+ │ ├── createStyle.js
39
+ │ ├── computed.js
40
+ │ ├── errorBoundary.js
41
+ ├── hooks/
42
+ │ └── newState.js
43
+ │ └── newNavigate.js
44
+ ├── dom/
45
+ │ ├── createElement.js
46
+ │ ├── html.js
47
+ │ └── render.js
48
+ └── utils/
49
+ └── is.js
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Conceitos principais
55
+
56
+ O sistema reativo é baseado em três peças: `reactive` envolve um objeto em um Proxy que rastreia leituras e dispara atualizações nas escritas. `effect` executa uma função e registra quais propriedades reativas ela leu — quando qualquer uma mudar, a função é re-executada automaticamente. `newState` é o açúcar em cima disso, retornando um getter e um setter prontos pra usar dentro de componentes.
57
+
58
+ O fluxo é: `effect` lê um reativo → leitura registra dependência → escrita dispara o `effect` novamente.
59
+
60
+ ---
61
+
62
+ ## Componentes
63
+
64
+ Componentes são funções que retornam vnodes. Todo estado local deve ser declarado com `newState` — que é um hook e segue as [regras de hooks](./hooks.md).
65
+
66
+ ```js
67
+ function Contador() {
68
+ const [count, setCount] = newState(0);
69
+
70
+ return html.div(
71
+ html.p(count),
72
+ html.button({ onClick: () => setCount(count() + 1) }, "+"),
73
+ html.button({ onClick: () => setCount(count() - 1) }, "-"),
74
+ );
75
+ }
76
+ ```
77
+
78
+ Dois `Contador` montados na tela terão estados completamente independentes.
79
+
80
+ ---
81
+
82
+ ## API
83
+
84
+ ### `html`
85
+
86
+ Proxy que gera vnodes para qualquer tag HTML ou componente registrado.
87
+
88
+ ```js
89
+ html.div(props?, ...children)
90
+ html.button(props?, ...children)
91
+ html.span(props?, ...children)
92
+ ```
93
+
94
+ Props são passadas como primeiro argumento quando são um objeto:
95
+
96
+ ```js
97
+ html.div({ class: "container", id: "app" }, "Olá mundo");
98
+ ```
99
+
100
+ Eventos seguem a convenção `onClick`, `onInput`, `onChange` etc:
101
+
102
+ ```js
103
+ html.button({ onClick: () => setCount(count() + 1) }, "Incrementar");
104
+ ```
105
+
106
+ Filhos reativos — passe o getter diretamente sem chamar:
107
+
108
+ ```js
109
+ html.p(count); // ✅ reativo — atualiza automaticamente
110
+ html.p(count()); // ❌ estático — captura o valor na criação
111
+ ```
112
+
113
+ Filhos podem ser strings, números, outros vnodes ou funções getter.
114
+
115
+ ---
116
+
117
+ ### `Proxy.fragment(...children)`
118
+
119
+ Agrupa múltiplos elementos sem criar um wrapper no DOM. Útil quando um componente precisa retornar mais de um elemento raiz.
120
+
121
+ ```js
122
+ import Proxy, { html } from "./src/index.js";
123
+
124
+ function App() {
125
+ return Proxy.fragment(
126
+ html.h1("Título"),
127
+ html.p("Parágrafo"),
128
+ html.span("Outro elemento"),
129
+ );
130
+ }
131
+ ```
132
+
133
+ > O `fragment` fica no objeto `Proxy` default para não colidir com o `Proxy` nativo do JavaScript.
134
+
135
+ ---
136
+
137
+ ### `renderToRoot(component, container)`
138
+
139
+ Forma simplificada de montar a aplicação.
140
+
141
+ ```js
142
+ import { renderToRoot } from "./src/index.js";
143
+
144
+ renderToRoot(App, "#app");
145
+ ```
146
+
147
+ `container` pode ser um seletor CSS string ou um elemento DOM direto.
148
+
149
+ ---
150
+
151
+ ### `createRoot(container)`
152
+
153
+ Versão explícita do `renderToRoot`, útil quando você precisa de mais controle.
154
+
155
+ ```js
156
+ createRoot("#app").render(App);
157
+ ```
158
+
159
+ ---
160
+
161
+ ### `registerComponent(name, component)`
162
+
163
+ Registra um componente para usar via `html.NomeDoComponente`.
164
+
165
+ ```js
166
+ function Card({ title, children }) {
167
+ return html.div({ class: "card" }, html.h2(title), ...children);
168
+ }
169
+
170
+ registerComponent("Card", Card);
171
+
172
+ html.Card({ title: "Meu Card" }, html.p("Conteúdo aqui"));
173
+ ```
174
+
175
+ > `children` chega como array. Use spread `...children` ao passá-los para outra tag.
176
+
177
+ ---
178
+
179
+ ### `computed(fn)`
180
+
181
+ Valor derivado de outros reativos. Só recalcula quando as dependências mudam. Retorna um getter reativo.
182
+
183
+ ```js
184
+ import { computed } from "./src/index.js";
185
+
186
+ function App() {
187
+ const [count, setCount] = newState(0);
188
+ const dobro = computed(() => count() * 2);
189
+
190
+ return html.div(
191
+ html.p(count),
192
+ html.p(dobro),
193
+ html.button({ onClick: () => setCount(count() + 1) }, "+"),
194
+ );
195
+ }
196
+ ```
197
+
198
+ Assim como getters do `newState`, passe o `computed` diretamente para filhos reativos sem chamar com `()`.
199
+
200
+ ---
201
+
202
+ ### `createStyle(css)`
203
+
204
+ Injeta CSS com escopo por componente. Gera um atributo único baseado no hash do CSS e prefixa todos os seletores com ele — os estilos não vazam para outros componentes.
205
+
206
+ ```js
207
+ import { createStyle } from "./src/index.js";
208
+
209
+ function Card({ title }) {
210
+ const scope = createStyle(`
211
+ .card { background: white; padding: 1rem; border-radius: 8px; }
212
+ .card h2 { font-size: 1.5rem; color: navy; }
213
+ `);
214
+
215
+ return html.div({ class: "card", ...scope }, html.h2(title));
216
+ }
217
+ ```
218
+
219
+ Espalhe `...scope` nas props do elemento raiz. O CSS é injetado no `<head>` uma única vez, mesmo que o componente seja montado várias vezes.
220
+
221
+ ---
222
+
223
+ ### `html.ErrorBoundary`
224
+
225
+ Captura erros em componentes filhos e renderiza um fallback. Já vem registrado automaticamente no `html`.
226
+
227
+ ```js
228
+ html.ErrorBoundary(
229
+ { fallback: (err) => html.p(`Erro: ${err.message}`) },
230
+ html.MeuComponente(),
231
+ );
232
+ ```
233
+
234
+ Sem `fallback`, renderiza uma mensagem padrão em vermelho:
235
+
236
+ ```js
237
+ html.ErrorBoundary({}, html.MeuComponente());
238
+ ```
239
+
240
+ ---
241
+
242
+ ### `createRouter(routes)`
243
+
244
+ Cria um router client-side baseado em hash routing (`/#/rota`). Recebe um array de rotas e retorna um componente.
245
+
246
+ ```js
247
+ import { createRouter, renderToRoot } from "./src/index.js";
248
+
249
+ const router = createRouter([
250
+ { path: "/", component: Home },
251
+ { path: "/sobre", component: Sobre },
252
+ { path: "/user/:id", component: User },
253
+ { path: "*", component: NotFound },
254
+ ]);
255
+
256
+ renderToRoot(router, "#app");
257
+ ```
258
+
259
+ Parâmetros de rota são acessados via `params`:
260
+
261
+ ```js
262
+ function User({ params }) {
263
+ return html.div(html.h1(`Usuário: ${params.id}`));
264
+ }
265
+ ```
266
+
267
+ > O router usa hash routing — as URLs ficam no formato `/#/rota`. Isso funciona em qualquer servidor, incluindo o Live Server do VSCode, sem nenhuma configuração extra.
268
+
269
+ ---
270
+
271
+ ### `navigate(path)`
272
+
273
+ Navega para uma rota sem recarregar a página.
274
+
275
+ ```js
276
+ import { navigate } from "./src/index.js";
277
+
278
+ html.button({ onClick: () => navigate("/sobre") }, "Ir para Sobre");
279
+ ```
280
+
281
+ ---
282
+
283
+ ### `reactive(obj)`
284
+
285
+ API de baixo nível. Envolve qualquer objeto em um Proxy reativo.
286
+
287
+ ```js
288
+ import { reactive, effect } from "./src/index.js";
289
+
290
+ const state = reactive({ nome: "Arthur", idade: 20 });
291
+
292
+ effect(() => {
293
+ console.log(`${state.nome}, ${state.idade}`);
294
+ });
295
+
296
+ state.nome = "João"; // re-executa o effect automaticamente
297
+ ```
298
+
299
+ Prefira `newState` para estados dentro de componentes. Use `reactive` para objetos compartilhados com múltiplas propriedades relacionadas.
300
+
301
+ ---
302
+
303
+ ### `effect(fn)`
304
+
305
+ Executa `fn` imediatamente e a re-executa sempre que uma dependência reativa mudar.
306
+
307
+ ```js
308
+ const state = reactive({ x: 1 });
309
+
310
+ effect(() => {
311
+ document.title = `x = ${state.x}`;
312
+ });
313
+
314
+ state.x = 42; // → document.title vira "x = 42"
315
+ ```
316
+
317
+ ---
318
+
319
+ ## Listas com key
320
+
321
+ Use a prop `key` em listas dinâmicas para que o ProxyJS reutilize e reordene os elementos certos em vez de recriar tudo.
322
+
323
+ ```js
324
+ function Lista() {
325
+ const [items, setItems] = newState(["maçã", "banana", "laranja"]);
326
+
327
+ return html.ul(...items().map((item) => html.li({ key: item }, item)));
328
+ }
329
+ ```
330
+
331
+ Sem `key`, remover ou reordenar itens pode causar comportamentos inesperados no DOM.
332
+
333
+ ---
334
+
335
+ ## Exemplo completo
336
+
337
+ ```js
338
+ import Proxy, {
339
+ html,
340
+ newState,
341
+ renderToRoot,
342
+ registerComponent,
343
+ createStyle,
344
+ computed,
345
+ createRouter,
346
+ navigate,
347
+ } from "./src/index.js";
348
+
349
+ function Card({ title, children }) {
350
+ const scope = createStyle(`
351
+ .card { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
352
+ .card h3 { margin: 0 0 0.5rem; }
353
+ `);
354
+ return html.div({ class: "card", ...scope }, html.h3(title), ...children);
355
+ }
356
+ registerComponent("Card", Card);
357
+
358
+ function Home() {
359
+ const [count, setCount] = newState(0);
360
+ const dobro = computed(() => count() * 2);
361
+
362
+ return Proxy.fragment(
363
+ html.h1("Home"),
364
+ html.ErrorBoundary(
365
+ { fallback: (err) => html.p(`Erro: ${err.message}`) },
366
+ html.Card(
367
+ { title: "Contador" },
368
+ html.p(count),
369
+ html.p(dobro),
370
+ html.button({ onClick: () => setCount(count() + 1) }, "+"),
371
+ html.button({ onClick: () => setCount(count() - 1) }, "-"),
372
+ ),
373
+ ),
374
+ html.button({ onClick: () => navigate("/sobre") }, "Ir para Sobre"),
375
+ );
376
+ }
377
+
378
+ function Sobre() {
379
+ return html.div(
380
+ html.h1("Sobre o ProxyJS"),
381
+ html.button({ onClick: () => navigate("/") }, "Voltar"),
382
+ );
383
+ }
384
+
385
+ function NotFound() {
386
+ return html.div(html.h1("404 — Página não encontrada"));
387
+ }
388
+
389
+ const router = createRouter([
390
+ { path: "/", component: Home },
391
+ { path: "/sobre", component: Sobre },
392
+ { path: "*", component: NotFound },
393
+ ]);
394
+
395
+ renderToRoot(router, "#app");
396
+ ```
397
+
398
+ ## Usar o ProxyJS via cdn:
399
+
400
+ ### Versão Fixa:
401
+
402
+ ```javascript
403
+ import Proxy, { html, newState, renderToRoot } from "https://cdn.jsdelivr.net/gh/arthurferreira-dev/ProxyJS/src/index.js";
404
+ ```
405
+
406
+ ### Versão Atual:
407
+
408
+ ```javascript
409
+ import Proxy, { html, newState, renderToRoot } from "https://cdn.jsdelivr.net/gh/arthurferreira-dev/ProxyJS@6a9e0a4/src/index.js"; /* Versão atual (v0.1.0-beta) */
410
+ ```
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "proxyjs-web",
3
+ "version": "0.1.0-beta",
4
+ "type": "module",
5
+ "description": "Biblioteca JavaScript reativa minimalista baseada em Proxy nativo do JavaScript e inspirada em frameworks/bibliotecas como React, Vue, JQuery e Preact.",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js"
10
+ },
11
+ "files": [
12
+ "src/"
13
+ ],
14
+ "keywords": [
15
+ "reactive",
16
+ "proxy",
17
+ "frontend",
18
+ "javascript",
19
+ "ProxyJS",
20
+ "proxyjs",
21
+ "Web"
22
+ ],
23
+ "bugs": {
24
+ "url": "https://github.com/arthurferreira-dev/ProxyJS/issues"
25
+ },
26
+ "homepage": "https://github.com/arthurferreira-dev/ProxyJS#readme",
27
+ "author": "arthurferreira-dev",
28
+ "license": "AGPL-3.0-only"
29
+ }
@@ -0,0 +1,11 @@
1
+ import { reactive, effect } from "./reactive.js";
2
+
3
+ export function computed(fn) {
4
+ const state = reactive({ value: undefined });
5
+
6
+ effect(() => {
7
+ state.value = fn();
8
+ });
9
+
10
+ return () => state.value;
11
+ }
@@ -0,0 +1,28 @@
1
+ import { render } from "../dom/render.js";
2
+ import { effect } from "./reactive.js";
3
+ import { isFunction } from "../utils/is.js";
4
+ import { setCurrentComponent } from "../hooks/newState.js";
5
+
6
+ export function createRoot(container) {
7
+ const el =
8
+ typeof container === "string"
9
+ ? document.querySelector(container)
10
+ : container;
11
+ if (!el) throw new Error("Root container not found!");
12
+ return {
13
+ render(component) {
14
+ const instance = { __hooks: [], __hookIndex: 0 };
15
+ effect(() => {
16
+ instance.__hookIndex = 0;
17
+ setCurrentComponent(instance);
18
+ const vnode = isFunction(component) ? component() : component;
19
+ setCurrentComponent(null);
20
+ render(vnode, el);
21
+ });
22
+ },
23
+ };
24
+ }
25
+
26
+ export function renderToRoot(component, container) {
27
+ createRoot(container).render(component);
28
+ }
@@ -0,0 +1,39 @@
1
+ const injectedStyles = new Set();
2
+
3
+ function hashString(str) {
4
+ let hash = 0;
5
+ for (let i = 0; i < str.length; i++) {
6
+ hash = (hash << 5) - hash + str.charCodeAt(i);
7
+ hash |= 0;
8
+ }
9
+ return Math.abs(hash).toString(36).slice(0, 6);
10
+ }
11
+
12
+ function scopeCSS(css, attr) {
13
+ return css.replace(/([^{}]+)\{/g, (match, selectors) => {
14
+ const scoped = selectors
15
+ .split(",")
16
+ .map((sel) => {
17
+ sel = sel.trim();
18
+ if (!sel || sel.startsWith("@")) return sel;
19
+ return `${sel}[${attr}]`;
20
+ })
21
+ .join(", ");
22
+ return `${scoped} {`;
23
+ });
24
+ }
25
+
26
+ export function createStyle(css) {
27
+ const hash = hashString(css);
28
+ const attr = `data-pjs-${hash}`;
29
+
30
+ if (!injectedStyles.has(hash)) {
31
+ const style = document.createElement("style");
32
+ style.setAttribute("data-pjs-scope", hash);
33
+ style.textContent = scopeCSS(css, attr);
34
+ document.head.appendChild(style);
35
+ injectedStyles.add(hash);
36
+ }
37
+
38
+ return { [attr]: "" };
39
+ }
@@ -0,0 +1,5 @@
1
+ import { createElement } from "../dom/createElement.js";
2
+
3
+ export function ErrorBoundary({ fallback, children }) {
4
+ return createElement("div", { __errorBoundary: true, __fallback: fallback }, ...children);
5
+ }
@@ -0,0 +1,50 @@
1
+ let activeEffect = null;
2
+ const targetMap = new WeakMap();
3
+
4
+ const effectStack = [];
5
+
6
+ export function effect(fn) {
7
+ const run = () => {
8
+ effectStack.push(run);
9
+ activeEffect = run;
10
+ try {
11
+ fn();
12
+ } finally {
13
+ effectStack.pop();
14
+ activeEffect = effectStack[effectStack.length - 1] ?? null;
15
+ }
16
+ };
17
+ run();
18
+ return run;
19
+ }
20
+
21
+ export function reactive(target) {
22
+ return new Proxy(target, {
23
+ get(obj, key) {
24
+ const current = effectStack[effectStack.length - 1];
25
+ if (current) {
26
+ let depsMap = targetMap.get(obj);
27
+ if (!depsMap) {
28
+ depsMap = new Map();
29
+ targetMap.set(obj, depsMap);
30
+ }
31
+ let dep = depsMap.get(key);
32
+ if (!dep) {
33
+ dep = new Set();
34
+ depsMap.set(key, dep);
35
+ }
36
+ dep.add(current);
37
+ }
38
+ return obj[key];
39
+ },
40
+ set(obj, key, value) {
41
+ obj[key] = value;
42
+ const depsMap = targetMap.get(obj);
43
+ if (!depsMap) return true;
44
+ const dep = depsMap.get(key);
45
+ if (!dep) return true;
46
+ [...dep].forEach((eff) => eff());
47
+ return true;
48
+ },
49
+ });
50
+ }
@@ -0,0 +1,9 @@
1
+ export const Fragment = Symbol("Fragment");
2
+
3
+ export function createElement(type, props = {}, ...children) {
4
+ return {
5
+ type,
6
+ props: props || {},
7
+ children: children.flat(Infinity).filter((c) => c != null),
8
+ };
9
+ }
@@ -0,0 +1,37 @@
1
+ import { createElement } from "./createElement.js";
2
+
3
+ const components = {};
4
+
5
+ function registerComponent(name, component) {
6
+ components[name] = component;
7
+ }
8
+
9
+ function isObject(value) {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ function base(type, ...args) {
14
+ let props = {};
15
+ let children = [];
16
+
17
+ if (isObject(args[0])) {
18
+ props = args[0];
19
+ children = args.slice(1);
20
+ } else {
21
+ children = args;
22
+ }
23
+
24
+ return createElement(type, props, ...children);
25
+ }
26
+
27
+ const html = new Proxy({}, {
28
+ get(_, tag) {
29
+ return (...args) => {
30
+ const type = components[tag] || tag;
31
+ return base(type, ...args);
32
+ };
33
+ }
34
+ });
35
+
36
+ export default html;
37
+ export { registerComponent };