@mauroandre/velojs 0.0.51 → 0.0.53

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
@@ -1320,7 +1320,7 @@ Hooks (`useParams`, `useQuery`, `usePathname`, `Loader`, `useLoader`) access thi
1320
1320
 
1321
1321
  ## Testing
1322
1322
 
1323
- VeloJS ships a backend testing toolkit at `@mauroandre/velojs/testing`. Spin up the app in memory, fire HTTP requests against the registered handlers, subscribe to event streams. No socket, no browser, no fragile mocks of framework internals.
1323
+ VeloJS ships a backend testing toolkit at `@mauroandre/velojs/testing`. Spin up the app in memory, fire HTTP requests against the registered handlers, subscribe to event streams. No browser, no fragile mocks of framework internals — and a real TCP port when an external actor has to reach the app.
1324
1324
 
1325
1325
  ```typescript
1326
1326
  import { createTestApp } from "@mauroandre/velojs/testing";
@@ -1332,6 +1332,7 @@ const app = await createTestApp({
1332
1332
  routes,
1333
1333
  bootstrap: async () => { await connect(process.env.MONGO_URI!); },
1334
1334
  getSessionCookie: async ({ user }) => ({ session: await sign(user) }),
1335
+ // port: 0, // optional — also serve this app over real TCP (see below)
1335
1336
  });
1336
1337
 
1337
1338
  // HTTP
@@ -1358,6 +1359,7 @@ await app.close();
1358
1359
  | API | Purpose |
1359
1360
  |-----|---------|
1360
1361
  | `createTestApp(options)` | Build isolated app with bootstrap + auth callback |
1362
+ | `app.port` / `app.url` | Real port and base URL of the TCP listener — `undefined` in the default in-memory mode |
1361
1363
  | `app.get/post/put/patch/delete` | HTTP requests (cookies, headers, query, JSON/FormData body) |
1362
1364
  | `app.action(fn, opts)` | Invoke `action_*` by function reference |
1363
1365
  | `app.loader(fn, opts)` | Invoke `loader` and unwrap response data |
@@ -1368,6 +1370,26 @@ await app.close();
1368
1370
  | `app.reset()` | Clear stream buffers/listeners between tests |
1369
1371
  | `app.close()` | Tear down everything (zero open handles guaranteed) |
1370
1372
 
1373
+ ### External actors over TCP
1374
+
1375
+ In-memory requests never leave the process, so an actor that does — a worker in another VM POSTing a notify callback — needs a real socket. Pass `port` and the **same app instance** is served over TCP as well:
1376
+
1377
+ ```typescript
1378
+ const app = await createTestApp({ routes, port: 0 }); // 0 → free port
1379
+
1380
+ // Hand the URL to the external client (worker, container, another test process)
1381
+ await provisioning.startWorker({ notifyUrl: `${app.url}/_action/Jobs/notify` });
1382
+
1383
+ // The worker POSTs over real HTTP; the in-memory subscription sees the event —
1384
+ // same app, same stream registry, no glue and no second instance
1385
+ const sub = await app.subscribe(stream_progress, { channel: jobId });
1386
+ const event = await sub.next({ timeoutMs: 5000 });
1387
+
1388
+ await app.close(); // closes the listener, dropping in-flight SSE/WebSocket connections
1389
+ ```
1390
+
1391
+ Pages, endpoints, actions, `?_data=1` loaders, SSE and WebSocket all answer as in production; `app.close()` also terminates connections still open on the listener. `port` and `hostname` come from the options only — never from `process.env.PORT`/`HOST` (test determinism). Omitted `hostname` follows Node's default bind (all interfaces — the listener is reachable from the local network); `app.url` reports `http://localhost:<port>` unless an explicit hostname was given.
1392
+
1371
1393
  See [Testing docs](https://github.com/mauro-andre/velojs/blob/dev/site/docs/17-testing.md) for the full guide with patterns, isolation, and FAQ.
1372
1394
 
1373
1395
  ---
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: velojs-boot-state
3
+ description: "Load localStorage/cookies BEFORE the first paint with `<Boot>` in client-root and read them with `useBoot()` — flicker-free themes, sidebars, and layout preferences. Use when UI state from the browser flashes on reload or when persisting layout preferences."
4
+ ---
5
+
6
+ # Boot state (pre-paint loading)
7
+
8
+ State stored in the browser (theme, collapsed sidebar, panel sizes) causes a flash on reload: the SSR HTML paints with defaults, then client JS corrects it — one visible frame of the wrong layout. The only code that runs between the `<head>` being parsed and the `<body>` being painted is a **synchronous inline script in the head**. `<Boot>` generates that script for you.
9
+
10
+ ## Declaring what to load
11
+
12
+ In `client-root.tsx`, declare each storage separately with the exact key names your app uses:
13
+
14
+ ```tsx
15
+ import { Boot, Scripts } from "@mauroandre/velojs";
16
+
17
+ export const Component = ({ children }) => (
18
+ <html>
19
+ <head>
20
+ <Boot localStorage={["theme", "sidebarHidden"]} />
21
+ <Boot cookies={["org"]} />
22
+ </head>
23
+ <body>
24
+ {children}
25
+ <Scripts />
26
+ </body>
27
+ </html>
28
+ );
29
+ ```
30
+
31
+ The generated script runs before the first paint and does two things:
32
+
33
+ 1. Populates `window.__VELO_BOOT__ = { theme: "dark", sidebarHidden: "true" }` — only keys that exist; absent keys stay out.
34
+ 2. Mirrors each found key as a sanitized `data-*` attribute on `<html>`: `data-theme="dark"`, `data-sidebar-hidden`... Key sanitization: anything outside `[a-zA-Z0-9-]` becomes `-` — `"bc:terminal-colapsado"` → `data-bc-terminal-colapsado`.
35
+
36
+ ## Reading
37
+
38
+ ```tsx
39
+ import { useBoot } from "@mauroandre/velojs/hooks";
40
+
41
+ const { theme, sidebarHidden } = useBoot();
42
+ ```
43
+
44
+ Values are **raw strings**, immutable by definition — the boot happened once. Seed your own signal with them for reactivity (`signal(boot.sidebarHidden === "true")`); parse numbers/JSON yourself.
45
+
46
+ On the server `useBoot()` returns `{}` — SSR always renders defaults, never touches `window`.
47
+
48
+ ## The anti-flicker contract (the part that matters)
49
+
50
+ **Visual state must be CSS reacting to the mirrored data-attribute — never conditional JSX.**
51
+
52
+ ```css
53
+ html[data-theme="dark"] { --color-surface: #11151f; }
54
+ html[data-sidebar-hidden="true"] .sidebar { display: none; }
55
+ ```
56
+
57
+ - ✅ Always render the sidebar in the vdom; hide it with CSS on the attribute. The attribute is correct **before the first paint** → single correct paint, no flash, no hydration mismatch.
58
+ - ❌ `{!sidebarHidden && <Sidebar />}` — the client's first render diverges from the SSR DOM (hydration mismatch) and only corrects **after** paint: the flash is back.
59
+
60
+ A CSS `transition` on the mirrored property does not fire on load (the attribute is set before the first style computation) — transitions only run on interactive toggles, which is what you want.
61
+
62
+ ## Writing is the app's business
63
+
64
+ `<Boot>` only loads. Persisting on change is a signal + storage write in your gestures:
65
+
66
+ ```tsx
67
+ const sidebarHidden = signal(/* seeded from useBoot */);
68
+ useEffect(() => {
69
+ document.documentElement.dataset.sidebarHidden = String(sidebarHidden.value);
70
+ localStorage.setItem("sidebarHidden", String(sidebarHidden.value));
71
+ }, [sidebarHidden.value]);
72
+ ```
73
+
74
+ ## Reference
75
+
76
+ | Piece | Behavior |
77
+ |-------|----------|
78
+ | `<Boot localStorage={[...]} />` | Inline pre-paint script; one `<Boot>` per storage |
79
+ | `<Boot cookies={[...]} />` | Same, from `document.cookie` |
80
+ | `useBoot()` | `Record<string, string>` — `{}` on the server, boot values on the client |
81
+ | Key `a:b` in storage | Attribute `data-a-b`; object key stays `"a:b"` |
82
+ | Storage throws (private mode) | Script is a no-op — app renders defaults |
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: velojs-testing
3
- description: "Writing tests with `createTestApp` from `@mauroandre/velojs/testing`: HTTP requests, actions, loaders, auth, SSE subscriptions, sockets. Use when writing or fixing a test for a VeloJS app."
3
+ description: "Writing tests with `createTestApp` from `@mauroandre/velojs/testing`: HTTP requests, actions, loaders, auth, SSE subscriptions, sockets, and serving the app on a real TCP port for external actors. Use when writing or fixing a test for a VeloJS app."
4
4
  ---
5
5
 
6
6
  # Testing
7
7
 
8
- VeloJS ships a backend testing toolkit at `@mauroandre/velojs/testing`. It spins up your app in memory, fires HTTP requests against the registered handlers, subscribes to event streams, and asserts on the result. No socket. No browser. No fragile mocks of the framework internals.
8
+ VeloJS ships a backend testing toolkit at `@mauroandre/velojs/testing`. It spins up your app in memory, fires HTTP requests against the registered handlers, subscribes to event streams, and asserts on the result. No browser. No fragile mocks of the framework internals. And when an actor outside this process has to reach the app — a worker sending a notify callback — the same app instance is served over a real TCP port.
9
9
 
10
10
  You test the whole stack — middleware, auth, routing, conventions, serialization — through the API your real users hit.
11
11
 
@@ -77,6 +77,7 @@ const app = await createTestApp({
77
77
  routes,
78
78
  bootstrap?: async () => { ... },
79
79
  getSessionCookie?: async ({ user }) => ({ ... }),
80
+ port?: 0,
80
81
  });
81
82
  ```
82
83
 
@@ -85,10 +86,12 @@ const app = await createTestApp({
85
86
  | `routes` | Required. Your app's `routes` array — typically `import routes from "./app/routes.js"` (it is the **default** export of `routes.tsx`) |
86
87
  | `bootstrap` | Runs once before the app is created. Use for DB connections, index creation, anything `app/server.tsx` does at startup. `addRoutes()` and `onServer()` calls here are **scoped to this app only** |
87
88
  | `getSessionCookie` | Maps a user object to cookies the test client will attach. Required for `app.as(user)` and `app.sessionCookies(user)` |
89
+ | `port` | Optional. Serves the same app over real TCP on this port (`0` → free port, see [External actors over TCP](#external-actors-over-tcp)) |
90
+ | `hostname` | Interface to bind when `port` is set (e.g. `"127.0.0.1"`). Omitted → Node's default, all interfaces |
88
91
 
89
92
  ### app.close()
90
93
 
91
- Tears down everything: heartbeats, retention timers, listeners, callbacks. After `close()`, Vitest must report **zero open handles**. Always call it in `afterAll`.
94
+ Tears down everything: heartbeats, retention timers, listeners, callbacks — and the TCP listener when `port` was passed, terminating the SSE/WebSocket connections still open on it. After `close()`, Vitest must report **zero open handles**. Always call it in `afterAll`.
92
95
 
93
96
  ### app.reset()
94
97
 
@@ -159,6 +162,34 @@ await app.get("/api/posts", { query: { tag: ["preact", "ssr"] } });
159
162
  // → /api/posts?tag=preact&tag=ssr
160
163
  ```
161
164
 
165
+ ## External actors over TCP
166
+
167
+ The in-memory client never leaves the process. When the actor under test *does* — a worker in another VM POSTing a notify callback to the control-plane — pass `port` and the **same instance** is served over real TCP:
168
+
169
+ ```typescript
170
+ const app = await createTestApp({ routes, port: 0 }); // 0 → free port
171
+
172
+ // `app.url` / `app.port` are the real, usable address — hand it to the worker
173
+ await provisioning.startWorker({ notifyUrl: `${app.url}/_action/Jobs/notify` });
174
+
175
+ // The worker POSTs over real HTTP. It's the same instance behind app.get and
176
+ // app.subscribe, so a stream watched in-memory sees the event — no glue and no
177
+ // "test watches an empty stream because it hit another instance":
178
+ const sub = await app.subscribe(stream_progress, { channel: jobId });
179
+ const event = await sub.next({ timeoutMs: 5000 });
180
+
181
+ await app.close(); // listener down; in-flight SSE/WS connections dropped
182
+ ```
183
+
184
+ Everything answers as in production — pages, endpoints, `action_*`, `?_data=1` loaders, SSE (`stream_*`) and WebSocket (`socket_*`). `app.close()` refuses new connections and terminates the ones still open, so a suite that keeps the app alive across tests tears down promptly even with a worker mid-SSE.
185
+
186
+ Two rules keep tests deterministic:
187
+
188
+ - **Ports come from the options only.** `port` and `hostname` are never read from `process.env.PORT`/`HOST`.
189
+ - **The bind follows the option.** An explicit `hostname` (e.g. `"127.0.0.1"`) keeps the listener loopback; omitted, Node's default applies — all interfaces, reachable from the local network. `app.url` reports `http://localhost:<port>` unless an explicit hostname was given.
190
+
191
+ Without `port`, nothing changes: the app is purely in-memory, and `app.port`/`app.url` are `undefined`.
192
+
162
193
  ## Auth — getSessionCookie and .as
163
194
 
164
195
  Most authenticated tests look like:
@@ -293,7 +324,7 @@ it("perChannelSource aborts when the last subscriber leaves", async () => {
293
324
 
294
325
  ## Sockets — `.socket(handler)`
295
326
 
296
- `app.socket(handlerOrStubOrPath, opts)` opens an **in-memory** WebSocket session against a `socket_*` handler — no real TCP, no upgrade handshake. The handler runs directly with a mock Context.
327
+ `app.socket(handlerOrStubOrPath, opts)` opens an **in-memory** WebSocket session against a `socket_*` handler — no real TCP, no upgrade handshake. The handler runs directly with a mock Context. (For a real handshake, create the app with `port` and connect a client to `ws://...` — see [External actors over TCP](#external-actors-over-tcp).)
297
328
 
298
329
  ```typescript
299
330
  import { socket_terminal } from "../app/workers/WorkerTerminal.js";
@@ -29,6 +29,45 @@ interface ScriptsProps {
29
29
  * ```
30
30
  */
31
31
  export declare function Scripts({ basePath, favicon }?: ScriptsProps): import("preact").JSX.Element;
32
+ interface BootProps {
33
+ /**
34
+ * localStorage keys to load before the first paint. The value of each
35
+ * key found is exposed on `window.__VELO_BOOT__` and mirrored as a
36
+ * sanitized `data-*` attribute on `<html>` — CSS reacting to the
37
+ * attribute is what makes the first paint flicker-free.
38
+ */
39
+ localStorage?: string[];
40
+ /**
41
+ * Cookie names to load before the first paint (same exposure/mirroring
42
+ * as localStorage keys). Declare storages separately: one Boot per
43
+ * storage keeps each script minimal.
44
+ */
45
+ cookies?: string[];
46
+ }
47
+ /**
48
+ * Generates the pre-paint boot script. Pure and isomorphic — Boot must
49
+ * render the SAME string on server and client so hydration matches.
50
+ * Exported for tests.
51
+ */
52
+ export declare function bootScript({ localStorage: lsKeys, cookies: ckKeys }?: BootProps): string;
53
+ /**
54
+ * Declares client state to load BEFORE the first paint. Place it in the
55
+ * `<head>` of client-root — a synchronous inline script is the only code
56
+ * that runs between the head being parsed and the body being painted, so
57
+ * values it mirrors as data-attributes produce a single, correct paint.
58
+ *
59
+ * Reading: `useBoot()` destructures by the declared names.
60
+ * Writing/persisting stays with the app (signals + storage in its gestures).
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * <head>
65
+ * <Boot localStorage={["theme", "sidebarHidden"]} />
66
+ * <Boot cookies={["org"]} />
67
+ * </head>
68
+ * ```
69
+ */
70
+ export declare function Boot(props: BootProps): import("preact").JSX.Element | null;
32
71
  import type { ComponentProps } from "preact";
33
72
  import type { RouteModule } from "./types.js";
34
73
  type WouterLinkProps = ComponentProps<typeof WouterLink>;
@@ -1,58 +1,69 @@
1
- import { jsx as o, jsxs as d, Fragment as m } from "preact/jsx-runtime";
2
- import { useRouter as v, Link as L } from "wouter-preact";
1
+ import { jsx as r, jsxs as p, Fragment as g } from "preact/jsx-runtime";
2
+ import { useRouter as L, Link as $ } from "wouter-preact";
3
3
  import "@preact/signals";
4
4
  import "preact/hooks";
5
5
  import { __veloUpdatePending as C } from "./loader-store.js";
6
- function j({ basePath: e, favicon: t = "/favicon.ico" } = {}) {
6
+ function w({ basePath: t, favicon: e = "/favicon.ico" } = {}) {
7
7
  const n = process.env.NODE_ENV !== "production";
8
- e = e || process.env.STATIC_BASE_URL || (process.env.VELO_STATIC ? "/client" : "");
9
- const r = t === !1 ? void 0 : t.endsWith(".svg") ? "image/svg+xml" : t.endsWith(".png") ? "image/png" : "image/x-icon", i = t !== !1 && /* @__PURE__ */ o("link", { rel: "icon", href: `${e}${t}`, type: r });
8
+ t = t || process.env.STATIC_BASE_URL || (process.env.VELO_STATIC ? "/client" : "");
9
+ const s = e === !1 ? void 0 : e.endsWith(".svg") ? "image/svg+xml" : e.endsWith(".png") ? "image/png" : "image/x-icon", i = e !== !1 && /* @__PURE__ */ r("link", { rel: "icon", href: `${t}${e}`, type: s });
10
10
  if (n)
11
- return /* @__PURE__ */ d(m, { children: [
11
+ return /* @__PURE__ */ p(g, { children: [
12
12
  i,
13
- /* @__PURE__ */ o("script", { type: "module", src: "/@vite/client" }),
14
- /* @__PURE__ */ o("script", { type: "module", src: "/__velo_client.js" })
13
+ /* @__PURE__ */ r("script", { type: "module", src: "/@vite/client" }),
14
+ /* @__PURE__ */ r("script", { type: "module", src: "/__velo_client.js" })
15
15
  ] });
16
- const l = globalThis.__veloClientJs || (typeof __VELO_CLIENT_JS__ < "u" ? __VELO_CLIENT_JS__ : "client.js"), c = globalThis.__veloClientCss || (typeof __VELO_CLIENT_CSS__ < "u" ? __VELO_CLIENT_CSS__ : "client.css");
17
- return /* @__PURE__ */ d(m, { children: [
16
+ const c = globalThis.__veloClientJs || (typeof __VELO_CLIENT_JS__ < "u" ? __VELO_CLIENT_JS__ : "client.js"), l = globalThis.__veloClientCss || (typeof __VELO_CLIENT_CSS__ < "u" ? __VELO_CLIENT_CSS__ : "client.css");
17
+ return /* @__PURE__ */ p(g, { children: [
18
18
  i,
19
- /* @__PURE__ */ o("link", { rel: "stylesheet", href: `${e}/${c}` }),
20
- /* @__PURE__ */ o("script", { type: "module", src: `${e}/${l}` })
19
+ /* @__PURE__ */ r("link", { rel: "stylesheet", href: `${t}/${l}` }),
20
+ /* @__PURE__ */ r("script", { type: "module", src: `${t}/${c}` })
21
21
  ] });
22
22
  }
23
- function h(e, t) {
24
- let n = e;
25
- for (const [r, i] of Object.entries(t))
26
- n = n.replace(`:${r}`, i);
23
+ function S({ localStorage: t, cookies: e } = {}) {
24
+ if ((!t || t.length === 0) && (!e || e.length === 0)) return "";
25
+ const n = (c) => JSON.stringify(c).replace(/</g, "\\u003c"), s = t && t.length > 0 ? `var L=${n(t)};for(var i=0;i<L.length;i++){var v=localStorage.getItem(L[i]);if(v!==null)b[L[i]]=v;}` : "", i = e && e.length > 0 ? `var C=${n(e)};var s=document.cookie?document.cookie.split("; "):[];var m={};for(var j=0;j<s.length;j++){var p=s[j].indexOf("=");if(p>0){m[s[j].slice(0,p)]=decodeURIComponent(s[j].slice(p+1));}}for(var k=0;k<C.length;k++){if(C[k]in m)b[C[k]]=m[C[k]];}` : "";
26
+ return `(function(){var b={};try{${s}}catch(e){}try{${i}}catch(e){}if(Object.keys(b).length){window.__VELO_BOOT__=Object.assign(window.__VELO_BOOT__||{},b);var d=document.documentElement;for(var q in b){d.setAttribute("data-"+String(q).replace(/[^a-zA-Z0-9-]/g,"-"),b[q]);}}})();`;
27
+ }
28
+ function y(t) {
29
+ const e = S(t);
30
+ return e ? /* @__PURE__ */ r("script", { dangerouslySetInnerHTML: { __html: e } }) : null;
31
+ }
32
+ function m(t, e) {
33
+ let n = t;
34
+ for (const [s, i] of Object.entries(e))
35
+ n = n.replace(`:${s}`, i);
27
36
  return n;
28
37
  }
29
- function I({ to: e, params: t, search: n, absolute: r, ...i }) {
30
- const l = typeof e != "string", c = v(), f = l ? e.metadata?.fullPath ?? "/" : e, u = t ? h(f, t) : f, a = n ? `?${new URLSearchParams(n).toString()}` : "", $ = `${u}${a}`;
38
+ function I({ to: t, params: e, search: n, absolute: s, ...i }) {
39
+ const c = typeof t != "string", l = L(), _ = c ? t.metadata?.fullPath ?? "/" : t, f = e ? m(_, e) : _, u = n ? `?${new URLSearchParams(n).toString()}` : "", v = `${f}${u}`;
31
40
  if (typeof window < "u" && C.value) {
32
- let _;
33
- if (l) {
34
- const s = e.metadata?.fullPath ?? f;
35
- _ = `${t ? h(s, t) : s}${a}`;
41
+ let a;
42
+ if (c) {
43
+ const o = t.metadata?.fullPath ?? _;
44
+ a = `${e ? m(o, e) : o}${u}`;
36
45
  } else {
37
- const s = u.replace(/^~/, "");
38
- _ = `${!u.startsWith("~") && c.base ? c.base : ""}${s}${a}`;
46
+ const o = f.replace(/^~/, "");
47
+ a = `${!f.startsWith("~") && l.base ? l.base : ""}${o}${u}`;
39
48
  }
40
- const { onClick: p, ...g } = i;
41
- return /* @__PURE__ */ o(
49
+ const { onClick: d, ...h } = i;
50
+ return /* @__PURE__ */ r(
42
51
  "a",
43
52
  {
44
- href: _,
45
- onClick: (s) => {
46
- p && p(s), s.defaultPrevented || (s.preventDefault(), window.location.href = _);
53
+ href: a,
54
+ onClick: (o) => {
55
+ d && d(o), o.defaultPrevented || (o.preventDefault(), window.location.href = a);
47
56
  },
48
- ...g
57
+ ...h
49
58
  }
50
59
  );
51
60
  }
52
- return /* @__PURE__ */ o(L, { to: $, ...i });
61
+ return /* @__PURE__ */ r($, { to: v, ...i });
53
62
  }
54
63
  export {
64
+ y as Boot,
55
65
  I as Link,
56
- j as Scripts,
57
- h as substituteParams
66
+ w as Scripts,
67
+ S as bootScript,
68
+ m as substituteParams
58
69
  };
package/dist/hooks.d.ts CHANGED
@@ -3,6 +3,30 @@ import type { EventStream } from "./events.js";
3
3
  import type { SocketHandler, SocketStub } from "./sockets.js";
4
4
  import { __veloUpdatePending } from "./loader-store.js";
5
5
  export { __veloUpdatePending };
6
+ declare global {
7
+ interface Window {
8
+ /** Populated by <Boot>'s pre-paint script — see components.tsx. */
9
+ __VELO_BOOT__?: Record<string, string>;
10
+ }
11
+ }
12
+ /**
13
+ * Reads the boot state loaded before the first paint by `<Boot>`.
14
+ *
15
+ * Values are raw strings, immutable by definition (the boot happened once —
16
+ * state evolves through the app's own signals, seeded with these values).
17
+ * On the server it returns an empty object: there is no window to read.
18
+ *
19
+ * The anti-flicker contract: visual state derived from these values must be
20
+ * CSS reacting to the mirrored data-attributes on `<html>`, not conditional
21
+ * JSX — the attribute is correct before the first paint, a conditional
22
+ * render only corrects after hydration.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const { theme, sidebarHidden } = useBoot();
27
+ * ```
28
+ */
29
+ export declare function useBoot(): Record<string, string>;
6
30
  /**
7
31
  * Força o signal a notificar mudanças após mutação de propriedades aninhadas
8
32
  *
package/dist/hooks.js CHANGED
@@ -1,30 +1,33 @@
1
- import { signal as S, useSignal as c } from "@preact/signals";
2
- import { useEffect as E, useRef as w } from "preact/hooks";
3
- import { useLocation as D, useParams as $, useSearch as C } from "wouter-preact";
4
- import { refetchModule as L, loaderLoading as m, loaderEntry as N } from "./loader-store.js";
1
+ import { signal as w, useSignal as u } from "@preact/signals";
2
+ import { useEffect as E, useRef as S } from "preact/hooks";
3
+ import { useLocation as b, useParams as O, useSearch as $ } from "wouter-preact";
4
+ import { refetchModule as D, loaderLoading as m, loaderEntry as N } from "./loader-store.js";
5
5
  import { __veloUpdatePending as z } from "./loader-store.js";
6
- function U(e) {
6
+ function U() {
7
+ return typeof window > "u" ? {} : window.__VELO_BOOT__ ?? {};
8
+ }
9
+ function W(e) {
7
10
  const n = e.value;
8
11
  n === null || typeof n != "object" || (e.value = Array.isArray(n) ? [...n] : { ...n });
9
12
  }
10
- function W(e) {
13
+ function j(e) {
11
14
  return typeof window > "u" ? {
12
15
  data: {
13
16
  get value() {
14
17
  return e ? globalThis.__veloServerData?.getStore?.()?.[e] ?? null : null;
15
18
  }
16
19
  },
17
- loading: S(!1),
20
+ loading: w(!1),
18
21
  refetch: () => {
19
22
  }
20
23
  } : e ? {
21
24
  data: N(e),
22
25
  loading: m(e),
23
- refetch: () => L(e)
24
- } : { data: S(null), loading: S(!1), refetch: () => {
26
+ refetch: () => D(e)
27
+ } : { data: w(null), loading: w(!1), refetch: () => {
25
28
  } };
26
29
  }
27
- function j(e, n) {
30
+ function B(e, n) {
28
31
  let t, r;
29
32
  if (Array.isArray(e) ? r = e : (t = e, r = n), typeof window > "u") {
30
33
  let g = null;
@@ -33,40 +36,40 @@ function j(e, n) {
33
36
  h?.[t] && (g = h[t]);
34
37
  }
35
38
  return {
36
- data: c(g),
37
- loading: c(!1),
39
+ data: u(g),
40
+ loading: u(!1),
38
41
  refetch: () => {
39
42
  }
40
43
  };
41
44
  }
42
45
  if (!t)
43
- return { data: c(null), loading: c(!1), refetch: () => {
46
+ return { data: u(null), loading: u(!1), refetch: () => {
44
47
  } };
45
- const o = N(t), a = m(t), u = () => L(t), d = w(!0);
48
+ const o = N(t), a = m(t), c = () => D(t), d = S(!0);
46
49
  return E(() => {
47
- d.current && (d.current = !1, o.value !== null) || u();
48
- }, r ?? []), { data: o, loading: a, refetch: u };
50
+ d.current && (d.current = !1, o.value !== null) || c();
51
+ }, r ?? []), { data: o, loading: a, refetch: c };
49
52
  }
50
53
  function J() {
51
- const [, e] = D();
54
+ const [, e] = b();
52
55
  return e;
53
56
  }
54
57
  function M() {
55
- return typeof window > "u" ? globalThis.__veloServerData?.getStore?.()?.__params ?? {} : $();
58
+ return typeof window > "u" ? globalThis.__veloServerData?.getStore?.()?.__params ?? {} : O();
56
59
  }
57
60
  function q() {
58
61
  if (typeof window > "u")
59
62
  return globalThis.__veloServerData?.getStore?.()?.__query ?? {};
60
- const e = C(), n = {};
63
+ const e = $(), n = {};
61
64
  return new URLSearchParams(e).forEach((t, r) => {
62
65
  n[r] = t;
63
66
  }), n;
64
67
  }
65
68
  function x() {
66
- return typeof window > "u" ? globalThis.__veloServerData?.getStore?.()?.__pathname ?? "/" : (D(), window.location.pathname);
69
+ return typeof window > "u" ? globalThis.__veloServerData?.getStore?.()?.__pathname ?? "/" : (b(), window.location.pathname);
67
70
  }
68
71
  function G(e, n = {}) {
69
- const t = c(null), r = c(null), o = c(!1), a = c(null), { channel: u, enabled: d = !0 } = n;
72
+ const t = u(null), r = u(null), o = u(!1), a = u(null), { channel: c, enabled: d = !0 } = n;
70
73
  return E(() => {
71
74
  if (typeof window > "u" || !d) return;
72
75
  if (!e.__path) {
@@ -74,18 +77,18 @@ function G(e, n = {}) {
74
77
  return;
75
78
  }
76
79
  t.value = null, r.value = null, o.value = !1, a.value = null;
77
- const g = u ? `${e.__path}?channel=${encodeURIComponent(u)}` : e.__path, s = new EventSource(g);
80
+ const g = c ? `${e.__path}?channel=${encodeURIComponent(c)}` : e.__path, s = new EventSource(g);
78
81
  return s.addEventListener("snapshot", (h) => {
79
82
  try {
80
83
  r.value = JSON.parse(h.data);
81
- } catch (_) {
82
- a.value = _;
84
+ } catch (y) {
85
+ a.value = y;
83
86
  }
84
87
  }), s.addEventListener("message", (h) => {
85
88
  try {
86
89
  t.value = JSON.parse(h.data);
87
- } catch (_) {
88
- a.value = _;
90
+ } catch (y) {
91
+ a.value = y;
89
92
  }
90
93
  }), s.addEventListener("heartbeat", () => {
91
94
  }), s.addEventListener("close", () => {
@@ -95,12 +98,12 @@ function G(e, n = {}) {
95
98
  }), () => {
96
99
  s.close();
97
100
  };
98
- }, [e, u, d]), { data: t, snapshot: r, closed: o, error: a };
101
+ }, [e, c, d]), { data: t, snapshot: r, closed: o, error: a };
99
102
  }
100
- function B(e, n = {}) {
101
- const t = c("connecting"), r = c(null), o = c(null), a = w(null), u = w([]), { channel: d, onMessage: g, onOpen: s, onClose: h, onError: _, enabled: b = !0 } = n;
103
+ function F(e, n = {}) {
104
+ const t = u("connecting"), r = u(null), o = u(null), a = S(null), c = S([]), { channel: d, onMessage: g, onOpen: s, onClose: h, onError: y, enabled: L = !0 } = n;
102
105
  return E(() => {
103
- if (typeof window > "u" || !b) return;
106
+ if (typeof window > "u" || !L) return;
104
107
  if (!e.__path) {
105
108
  console.warn("[velojs] useSocket: stub has no __path");
106
109
  return;
@@ -109,27 +112,27 @@ function B(e, n = {}) {
109
112
  const v = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}`, i = d ? `${v}${e.__path}?channel=${encodeURIComponent(d)}` : `${v}${e.__path}`, f = new WebSocket(i);
110
113
  return f.binaryType = "arraybuffer", a.current = f, f.addEventListener("open", () => {
111
114
  t.value = "open";
112
- const l = u.current;
113
- u.current = [];
114
- for (const y of l)
115
+ const l = c.current;
116
+ c.current = [];
117
+ for (const _ of l)
115
118
  try {
116
- f.send(y);
119
+ f.send(_);
117
120
  } catch {
118
121
  }
119
122
  s?.();
120
123
  }), f.addEventListener("message", (l) => {
121
- let y;
124
+ let _;
122
125
  if (typeof l.data == "string")
123
- y = l.data;
126
+ _ = l.data;
124
127
  else if (l.data instanceof ArrayBuffer)
125
- y = new Uint8Array(l.data);
128
+ _ = new Uint8Array(l.data);
126
129
  else
127
130
  return;
128
- r.value = y, g?.(y);
131
+ r.value = _, g?.(_);
129
132
  }), f.addEventListener("close", (l) => {
130
133
  t.value = "closed", a.current = null, h?.(l);
131
134
  }), f.addEventListener("error", (l) => {
132
- o.value = l, _?.(l);
135
+ o.value = l, y?.(l);
133
136
  }), () => {
134
137
  a.current = null;
135
138
  try {
@@ -137,7 +140,7 @@ function B(e, n = {}) {
137
140
  } catch {
138
141
  }
139
142
  };
140
- }, [e.__path, d, b]), { send: (p) => {
143
+ }, [e.__path, d, L]), { send: (p) => {
141
144
  let v;
142
145
  typeof p == "string" || p instanceof Uint8Array ? v = p : v = JSON.stringify(p);
143
146
  const i = a.current;
@@ -146,7 +149,7 @@ function B(e, n = {}) {
146
149
  i.send(v);
147
150
  } catch {
148
151
  }
149
- else i && i.readyState === WebSocket.CONNECTING && u.current.push(v);
152
+ else i && i.readyState === WebSocket.CONNECTING && c.current.push(v);
150
153
  }, close: (p, v) => {
151
154
  const i = a.current;
152
155
  if (i)
@@ -157,14 +160,15 @@ function B(e, n = {}) {
157
160
  }, status: t, lastMessage: r, error: o };
158
161
  }
159
162
  export {
160
- W as Loader,
163
+ j as Loader,
161
164
  z as __veloUpdatePending,
162
- U as touch,
165
+ W as touch,
166
+ U as useBoot,
163
167
  G as useEventStream,
164
- j as useLoader,
168
+ B as useLoader,
165
169
  J as useNavigate,
166
170
  M as useParams,
167
171
  x as usePathname,
168
172
  q as useQuery,
169
- B as useSocket
173
+ F as useSocket
170
174
  };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { AppRoutes, RouteNode, RouteModule, LoaderArgs, ActionArgs, Metadata, HTTPMethod, EndpointHandler, EndpointHandlerArgs, } from "./types.js";
2
2
  export type { VeloConfig } from "./config.js";
3
3
  export { defineConfig } from "./config.js";
4
- export { Scripts, Link } from "./components.js";
4
+ export { Scripts, Link, Boot } from "./components.js";
5
5
  export { createEventStream, poll } from "./events.js";
6
6
  export type { EventStream, EventStreamConfig, EmitFn, EmitOptions, SourceFn, PerChannelSourceFn, StreamPipeFn, StreamPipeArgs, ChannelResolver, } from "./events.js";
7
7
  export type { SocketHandler, SocketHandlerArgs, SocketStub, } from "./sockets.js";
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
- import { defineConfig as o } from "./config.js";
2
- import { Link as f, Scripts as p } from "./components.js";
3
- import { createEventStream as m, poll as n } from "./events.js";
1
+ import { defineConfig as r } from "./config.js";
2
+ import { Boot as f, Link as p, Scripts as i } from "./components.js";
3
+ import { createEventStream as n, poll as x } from "./events.js";
4
4
  export {
5
- f as Link,
6
- p as Scripts,
7
- m as createEventStream,
8
- o as defineConfig,
9
- n as poll
5
+ f as Boot,
6
+ p as Link,
7
+ i as Scripts,
8
+ n as createEventStream,
9
+ r as defineConfig,
10
+ x as poll
10
11
  };