nukejs 0.0.16 → 0.0.17

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/dist/ssr.js DELETED
@@ -1,262 +0,0 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import { createElement } from "react";
4
- import { pathToFileURL } from "url";
5
- import { tsImport } from "tsx/esm/api";
6
- import { log, getDebugLevel } from "./logger.js";
7
- import { matchRoute, findLayoutsForRoute } from "./router.js";
8
- import { findClientComponentsInTree } from "./component-analyzer.js";
9
- import { renderElementToHtml } from "./renderer.js";
10
- import { runWithRequestStore, normaliseHeaders, sanitiseHeaders } from "./request-store.js";
11
- import {
12
- runWithHtmlStore,
13
- resolveTitle
14
- } from "./html-store.js";
15
- async function wrapWithLayouts(pageElement, layoutPaths) {
16
- let element = pageElement;
17
- for (let i = layoutPaths.length - 1; i >= 0; i--) {
18
- const { default: LayoutComponent } = await tsImport(
19
- pathToFileURL(layoutPaths[i]).href,
20
- { parentURL: import.meta.url }
21
- );
22
- element = createElement(LayoutComponent, { children: element });
23
- }
24
- return element;
25
- }
26
- function toClientDebugLevel(level) {
27
- if (level === true) return "verbose";
28
- if (level === "info") return "info";
29
- if (level === "error") return "error";
30
- return "silent";
31
- }
32
- function escapeAttr(str) {
33
- return str.replace(/&/g, "&").replace(/"/g, """);
34
- }
35
- function renderAttrs(attrs) {
36
- return Object.entries(attrs).filter(([, v]) => v !== void 0 && v !== false).map(([k, v]) => v === true ? k : `${k}="${escapeAttr(String(v))}"`).join(" ");
37
- }
38
- function openTag(tag, attrs) {
39
- const str = renderAttrs(attrs);
40
- return str ? `<${tag} ${str}>` : `<${tag}>`;
41
- }
42
- function metaKey(k) {
43
- return k === "httpEquiv" ? "http-equiv" : k;
44
- }
45
- function linkKey(k) {
46
- if (k === "hrefLang") return "hreflang";
47
- if (k === "crossOrigin") return "crossorigin";
48
- return k;
49
- }
50
- function renderMetaTag(tag) {
51
- const attrs = {};
52
- for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[metaKey(k)] = v;
53
- return ` <meta ${renderAttrs(attrs)} />`;
54
- }
55
- function renderLinkTag(tag) {
56
- const attrs = {};
57
- for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[linkKey(k)] = v;
58
- return ` <link ${renderAttrs(attrs)} />`;
59
- }
60
- function renderScriptTag(tag) {
61
- const attrs = {
62
- src: tag.src,
63
- type: tag.type,
64
- crossorigin: tag.crossOrigin,
65
- integrity: tag.integrity,
66
- defer: tag.defer,
67
- async: tag.async,
68
- nomodule: tag.noModule
69
- };
70
- const attrStr = renderAttrs(attrs);
71
- const open = attrStr ? `<script ${attrStr}>` : "<script>";
72
- return ` ${open}${tag.src ? "" : tag.content ?? ""}</script>`;
73
- }
74
- function renderStyleTag(tag) {
75
- const media = tag.media ? ` media="${escapeAttr(tag.media)}"` : "";
76
- return ` <style${media}>${tag.content ?? ""}</style>`;
77
- }
78
- function renderManagedHeadTags(store) {
79
- const headScripts = store.script.filter((s) => (s.position ?? "head") === "head");
80
- const tags = [
81
- ...store.meta.map(renderMetaTag),
82
- ...store.link.map(renderLinkTag),
83
- ...store.style.map(renderStyleTag),
84
- ...headScripts.map(renderScriptTag)
85
- ];
86
- if (tags.length === 0) return [];
87
- return [" <!--n-head-->", ...tags, " <!--/n-head-->"];
88
- }
89
- function renderManagedBodyScripts(store) {
90
- const bodyScripts = store.script.filter((s) => s.position === "body");
91
- if (bodyScripts.length === 0) return [];
92
- return [" <!--n-body-scripts-->", ...bodyScripts.map(renderScriptTag), " <!--/n-body-scripts-->"];
93
- }
94
- async function renderFile(filePath, params, url, pagesDir, isDev, res, req, statusCode, skipClientSSR) {
95
- const cleanUrl = url.split("?")[0];
96
- const searchParams = new URL(url, "http://localhost").searchParams;
97
- const queryParams = {};
98
- searchParams.forEach((_, k) => {
99
- if (!(k in params)) {
100
- const all = searchParams.getAll(k);
101
- queryParams[k] = all.length > 1 ? all : all[0];
102
- }
103
- });
104
- const mergedParams = { ...queryParams, ...params };
105
- const rawHeaders = req?.headers ?? {};
106
- const normHeaders = normaliseHeaders(rawHeaders);
107
- const safeHeaders = sanitiseHeaders(rawHeaders);
108
- const layoutPaths = findLayoutsForRoute(filePath, pagesDir);
109
- const { default: PageComponent } = await tsImport(
110
- pathToFileURL(filePath).href,
111
- { parentURL: import.meta.url }
112
- );
113
- const wrappedElement = await wrapWithLayouts(
114
- createElement(PageComponent, mergedParams),
115
- layoutPaths
116
- );
117
- const registry = /* @__PURE__ */ new Map();
118
- for (const [id, p] of findClientComponentsInTree(filePath, pagesDir))
119
- registry.set(id, p);
120
- for (const layoutPath of layoutPaths)
121
- for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
122
- registry.set(id, p);
123
- const ctx = { registry, hydrated: /* @__PURE__ */ new Set(), skipClientSSR };
124
- let appHtml = "";
125
- const store = await runWithRequestStore(
126
- {
127
- url,
128
- pathname: cleanUrl,
129
- params,
130
- query: queryParams,
131
- headers: normHeaders
132
- },
133
- () => runWithHtmlStore(async () => {
134
- appHtml = await renderElementToHtml(wrappedElement, ctx);
135
- })
136
- );
137
- const pageTitle = resolveTitle(store.titleOps, "NukeJS");
138
- const headLines = [
139
- ' <meta charset="utf-8" />',
140
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
141
- ` <title>${escapeAttr(pageTitle)}</title>`,
142
- ...renderManagedHeadTags(store)
143
- ];
144
- const runtimeData = JSON.stringify({
145
- hydrateIds: [...ctx.hydrated],
146
- allIds: [...registry.keys()],
147
- url,
148
- params,
149
- query: queryParams,
150
- headers: safeHeaders,
151
- debug: toClientDebugLevel(getDebugLevel())
152
- }).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
153
- const bodyScriptLines = renderManagedBodyScripts(store);
154
- const bodyScriptsHtml = bodyScriptLines.length > 0 ? "\n" + bodyScriptLines.join("\n") + "\n" : "";
155
- const html = `<!DOCTYPE html>
156
- ${openTag("html", store.htmlAttrs)}
157
- <head>
158
- ${headLines.join("\n")}
159
- </head>
160
- ${openTag("body", store.bodyAttrs)}
161
- <div id="app">${appHtml}</div>
162
-
163
- <script id="__n_data" type="application/json">${runtimeData}</script>
164
-
165
- <script type="importmap">
166
- {
167
- "imports": {
168
- "react": "/__react.js",
169
- "react-dom/client": "/__react.js",
170
- "react/jsx-runtime": "/__react.js",
171
- "nukejs": "/__n.js"
172
- }
173
- }
174
- </script>
175
-
176
- <script type="module">
177
- await import('react');
178
- const { initRuntime } = await import('nukejs');
179
- const data = JSON.parse(document.getElementById('__n_data').textContent);
180
- initRuntime(data);
181
- </script>
182
-
183
- ${isDev ? '<script type="module" src="/__hmr.js"></script>' : ""}
184
- ${bodyScriptsHtml}</body>
185
- </html>`;
186
- res.statusCode = statusCode;
187
- res.setHeader("Content-Type", "text/html");
188
- res.end(html);
189
- }
190
- function serializeError(err) {
191
- if (err instanceof Error) {
192
- const props = {
193
- errorMessage: err.message
194
- };
195
- if (process.env.NODE_ENV !== "production" && err.stack)
196
- props.errorStack = err.stack;
197
- const status = err.status ?? err.statusCode;
198
- if (status != null)
199
- props.errorStatus = String(status);
200
- return props;
201
- }
202
- return { errorMessage: String(err) };
203
- }
204
- async function tryRenderErrorPage(statusCode, pagesDir, res, isDev, req, error) {
205
- const errorFile = path.join(pagesDir, `_${statusCode}.tsx`);
206
- if (!fs.existsSync(errorFile)) return false;
207
- const errorProps = error != null ? serializeError(error) : {};
208
- try {
209
- await renderFile(errorFile, errorProps, "/", pagesDir, isDev, res, req, statusCode, false);
210
- return true;
211
- } catch (err) {
212
- log.error(`Error rendering _${statusCode}.tsx:`, err);
213
- return false;
214
- }
215
- }
216
- async function serverSideRender(url, res, pagesDir, isDev = false, req) {
217
- const skipClientSSR = url.includes("__hmr=1");
218
- const cleanUrl = url.split("?")[0];
219
- const searchParams = new URL(url, "http://localhost").searchParams;
220
- if (searchParams.has("__clientError")) {
221
- const errorProps = {
222
- errorMessage: searchParams.get("__clientError") ?? "Client error"
223
- };
224
- const stack = searchParams.get("__clientStack");
225
- if (stack) errorProps.errorStack = stack;
226
- const errorFile = path.join(pagesDir, "_500.tsx");
227
- if (fs.existsSync(errorFile)) {
228
- try {
229
- await renderFile(errorFile, errorProps, url, pagesDir, isDev, res, req, 500, false);
230
- } catch (err) {
231
- log.error("Error rendering _500.tsx for client error:", err);
232
- res.statusCode = 500;
233
- res.end("Internal Server Error");
234
- }
235
- } else {
236
- res.statusCode = 500;
237
- res.end("Internal Server Error");
238
- }
239
- return;
240
- }
241
- const routeMatch = matchRoute(cleanUrl, pagesDir);
242
- if (!routeMatch) {
243
- log.verbose(`No route found for: ${url}`);
244
- if (await tryRenderErrorPage(404, pagesDir, res, isDev, req)) return;
245
- res.statusCode = 404;
246
- res.end("Page not found");
247
- return;
248
- }
249
- const { filePath, params, routePattern } = routeMatch;
250
- log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);
251
- try {
252
- await renderFile(filePath, params, url, pagesDir, isDev, res, req, 200, skipClientSSR);
253
- } catch (err) {
254
- log.error("SSR render error:", err);
255
- if (await tryRenderErrorPage(500, pagesDir, res, isDev, req, err)) return;
256
- res.statusCode = 500;
257
- res.end("Internal Server Error");
258
- }
259
- }
260
- export {
261
- serverSideRender
262
- };
package/dist/store.d.ts DELETED
@@ -1,104 +0,0 @@
1
- /**
2
- * store.ts — NukeJS Client State Management
3
- *
4
- * Provides a lightweight, cross-boundary state system for "use client"
5
- * components. The core problem it solves: NukeJS hydrates every client
6
- * component into its own independent React root (via hydrateRoot / createRoot),
7
- * so React Context cannot carry state across component boundaries. Each
8
- * component's esbuild bundle is also a separate module instance, so a plain
9
- * module-level variable would not be shared between bundles.
10
- *
11
- * Solution: all store state lives in `window.__nukeStores`, a Map that persists
12
- * for the lifetime of the page regardless of how many times the store module
13
- * is evaluated. Every bundle that imports `createStore('counter', …)` gets
14
- * a thin proxy object that reads from and writes to the same backing entry —
15
- * state is automatically shared across roots and bundles.
16
- *
17
- * API:
18
- *
19
- * const cartStore = createStore('cart', { items: [], total: 0 });
20
- *
21
- * // Inside any "use client" component on the same page:
22
- * const items = useStore(cartStore, s => s.items);
23
- * cartStore.setState(s => ({ ...s, items: [...s.items, newItem] }));
24
- *
25
- * The store is safe to import in server components — it detects the absence of
26
- * `window` and returns a lightweight no-op stub so SSR never throws.
27
- */
28
- type Listener = () => void;
29
- type Unsubscribe = () => void;
30
- type Updater<T> = T | ((prev: T) => T);
31
- /**
32
- * A NukeJS store handle. Create it once at module scope; pass it into
33
- * `useStore()` inside any client component to subscribe.
34
- */
35
- export interface Store<T extends object> {
36
- /** Returns the current state snapshot. */
37
- getState(): T;
38
- /**
39
- * Updates the state and notifies every subscriber.
40
- *
41
- * Accepts a full replacement value or an updater function:
42
- * store.setState({ count: 0 })
43
- * store.setState(s => ({ ...s, count: s.count + 1 }))
44
- */
45
- setState(updater: Updater<T>): void;
46
- /**
47
- * Registers a change listener. Returns an unsubscribe function.
48
- * Compatible with `useSyncExternalStore`.
49
- */
50
- subscribe(listener: Listener): Unsubscribe;
51
- /** The name this store is registered under in the global registry. */
52
- readonly name: string;
53
- /**
54
- * The value passed to `createStore` as its second argument.
55
- * Used internally as the server snapshot so `useSyncExternalStore` always
56
- * reconciles against a value that matches the server-rendered HTML —
57
- * the server never has mutations, so initial state is always what it renders.
58
- */
59
- readonly initialState: T;
60
- }
61
- interface StoreEntry<T> {
62
- state: T;
63
- listeners: Set<Listener>;
64
- }
65
- declare global {
66
- interface Window {
67
- __nukeStores?: Map<string, StoreEntry<any>>;
68
- }
69
- }
70
- /**
71
- * Creates (or retrieves) a named store backed by the page-global registry.
72
- *
73
- * If a store with the given `name` already exists in the registry (e.g.
74
- * because another bundle called `createStore` first), the existing entry is
75
- * reused and `initialState` is ignored. This means the first bundle to run
76
- * wins the initial value — define your stores in a single shared file, or
77
- * treat `initialState` as a consistent default across all bundles.
78
- *
79
- * @param name A unique string key for this store.
80
- * @param initialState Default state used when the store is first created.
81
- */
82
- export declare function createStore<T extends object>(name: string, initialState: T): Store<T>;
83
- /**
84
- * React hook that subscribes a component to a store.
85
- *
86
- * An optional `selector` lets you derive a slice of state. The component
87
- * only re-renders when the selected value changes (by reference equality),
88
- * not on every store mutation.
89
- *
90
- * Works across independent React roots — any component on the page that calls
91
- * `useStore` with the same store will re-render when that store changes,
92
- * regardless of which component boundary each lives in.
93
- *
94
- * @example
95
- * // Full state
96
- * const state = useStore(cartStore);
97
- *
98
- * @example
99
- * // Selected slice — re-renders only when `items` changes
100
- * const items = useStore(cartStore, s => s.items);
101
- */
102
- export declare function useStore<T extends object>(store: Store<T>): T;
103
- export declare function useStore<T extends object, U>(store: Store<T>, selector: (state: T) => U): U;
104
- export {};
package/dist/store.js DELETED
@@ -1,45 +0,0 @@
1
- import { useSyncExternalStore } from "react";
2
- function getRegistry() {
3
- if (typeof window === "undefined") {
4
- return /* @__PURE__ */ new Map();
5
- }
6
- if (!window.__nukeStores) {
7
- window.__nukeStores = /* @__PURE__ */ new Map();
8
- }
9
- return window.__nukeStores;
10
- }
11
- function createStore(name, initialState) {
12
- const registry = getRegistry();
13
- if (!registry.has(name)) {
14
- registry.set(name, {
15
- state: initialState,
16
- listeners: /* @__PURE__ */ new Set()
17
- });
18
- }
19
- const entry = registry.get(name);
20
- const subscribe = (listener) => {
21
- entry.listeners.add(listener);
22
- return () => {
23
- entry.listeners.delete(listener);
24
- };
25
- };
26
- const getState = () => entry.state;
27
- const setState = (updater) => {
28
- entry.state = typeof updater === "function" ? updater(entry.state) : updater;
29
- for (const l of Array.from(entry.listeners)) l();
30
- };
31
- return { name, initialState, getState, setState, subscribe };
32
- }
33
- function useStore(store, selector) {
34
- const getSnapshot = selector ? () => selector(store.getState()) : () => store.getState();
35
- const getServerSnapshot = selector ? () => selector(store.initialState) : () => store.initialState;
36
- return useSyncExternalStore(
37
- store.subscribe,
38
- getSnapshot,
39
- getServerSnapshot
40
- );
41
- }
42
- export {
43
- createStore,
44
- useStore
45
- };
@@ -1,64 +0,0 @@
1
- /**
2
- * use-html.ts — useHtml() Hook
3
- *
4
- * A universal hook that lets React components control the HTML document's
5
- * <head>, <html> attributes, and <body> attributes from within JSX — on both
6
- * the server (SSR) and the client (hydration / SPA navigation).
7
- *
8
- * Server behaviour:
9
- * Writes directly into the per-request html-store. The store is flushed
10
- * into the HTML document after the component tree is fully rendered.
11
- * useHtml() is called synchronously during rendering so no actual React
12
- * hook is used — it's just a function that pokes the globalThis store.
13
- *
14
- * Client behaviour:
15
- * Uses useEffect() to apply changes to the live document and clean them up
16
- * when the component unmounts (navigation, unmount). Each effect is keyed
17
- * to its options object via JSON.stringify so React re-runs it when the
18
- * options change.
19
- *
20
- * Layout title templates:
21
- * Layouts typically set title as a function so they can append a site suffix:
22
- *
23
- * ```tsx
24
- * // Root layout
25
- * useHtml({ title: (prev) => `${prev} | Acme` });
26
- *
27
- * // A page
28
- * useHtml({ title: 'About' });
29
- * // → 'About | Acme'
30
- * ```
31
- *
32
- * Example usage:
33
- * ```tsx
34
- * useHtml({
35
- * title: 'Blog Post',
36
- * meta: [{ name: 'description', content: 'A great post' }],
37
- * link: [{ rel: 'canonical', href: 'https://example.com/post' }],
38
- * });
39
- * ```
40
- */
41
- import type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag } from './html-store';
42
- export type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag };
43
- export interface HtmlOptions {
44
- /**
45
- * Page title.
46
- * string → sets the title directly (page wins over layout).
47
- * function → receives the inner title; use in layouts to append a suffix:
48
- * `(prev) => \`${prev} | MySite\``
49
- */
50
- title?: TitleValue;
51
- /** Attributes merged onto <html>. Per-attribute last-write-wins. */
52
- htmlAttrs?: HtmlAttrs;
53
- /** Attributes merged onto <body>. Per-attribute last-write-wins. */
54
- bodyAttrs?: BodyAttrs;
55
- meta?: MetaTag[];
56
- link?: LinkTag[];
57
- script?: ScriptTag[];
58
- style?: StyleTag[];
59
- }
60
- /**
61
- * Applies HTML document customisations from a React component.
62
- * Automatically detects whether it is running on the server or the client.
63
- */
64
- export declare function useHtml(options: HtmlOptions): void;
package/dist/use-html.js DELETED
@@ -1,128 +0,0 @@
1
- import { useEffect } from "react";
2
- import { getHtmlStore } from "./html-store.js";
3
- function useHtml(options) {
4
- if (typeof document === "undefined") {
5
- serverUseHtml(options);
6
- } else {
7
- clientUseHtml(options);
8
- }
9
- }
10
- function serverUseHtml(options) {
11
- const store = getHtmlStore();
12
- if (!store) return;
13
- if (options.title !== void 0) store.titleOps.push(options.title);
14
- if (options.htmlAttrs) Object.assign(store.htmlAttrs, options.htmlAttrs);
15
- if (options.bodyAttrs) Object.assign(store.bodyAttrs, options.bodyAttrs);
16
- if (options.meta?.length) store.meta.push(...options.meta);
17
- if (options.link?.length) store.link.push(...options.link);
18
- if (options.script?.length) store.script.push(...options.script);
19
- if (options.style?.length) store.style.push(...options.style);
20
- }
21
- let _uid = 0;
22
- const uid = () => `uh${++_uid}`;
23
- function clientUseHtml(options) {
24
- useEffect(() => {
25
- if (options.title === void 0) return;
26
- const prev = document.title;
27
- document.title = typeof options.title === "function" ? options.title(prev) : options.title;
28
- return () => {
29
- document.title = prev;
30
- };
31
- }, [typeof options.title === "function" ? options.title.toString() : options.title]);
32
- useEffect(() => {
33
- if (!options.htmlAttrs) return;
34
- return applyAttrs(document.documentElement, options.htmlAttrs);
35
- }, [JSON.stringify(options.htmlAttrs)]);
36
- useEffect(() => {
37
- if (!options.bodyAttrs) return;
38
- return applyAttrs(document.body, options.bodyAttrs);
39
- }, [JSON.stringify(options.bodyAttrs)]);
40
- useEffect(() => {
41
- if (!options.meta?.length) return;
42
- const id = uid();
43
- const nodes = options.meta.map((tag) => {
44
- const el = document.createElement("meta");
45
- for (const [k, v] of Object.entries(tag)) {
46
- if (v !== void 0) el.setAttribute(domAttr(k), v);
47
- }
48
- el.dataset.usehtml = id;
49
- document.head.appendChild(el);
50
- return el;
51
- });
52
- return () => nodes.forEach((n) => n.remove());
53
- }, [JSON.stringify(options.meta)]);
54
- useEffect(() => {
55
- if (!options.link?.length) return;
56
- const id = uid();
57
- const nodes = options.link.map((tag) => {
58
- const el = document.createElement("link");
59
- for (const [k, v] of Object.entries(tag)) {
60
- if (v !== void 0) el.setAttribute(domAttr(k), v);
61
- }
62
- el.dataset.usehtml = id;
63
- document.head.appendChild(el);
64
- return el;
65
- });
66
- return () => nodes.forEach((n) => n.remove());
67
- }, [JSON.stringify(options.link)]);
68
- useEffect(() => {
69
- if (!options.script?.length) return;
70
- const id = uid();
71
- const nodes = options.script.map((tag) => {
72
- const el = document.createElement("script");
73
- if (tag.src) el.src = tag.src;
74
- if (tag.type) el.type = tag.type;
75
- if (tag.defer) el.defer = true;
76
- if (tag.async) el.async = true;
77
- if (tag.noModule) el.setAttribute("nomodule", "");
78
- if (tag.crossOrigin) el.crossOrigin = tag.crossOrigin;
79
- if (tag.integrity) el.integrity = tag.integrity;
80
- if (tag.content) el.textContent = tag.content;
81
- el.dataset.usehtml = id;
82
- if (tag.position === "body") {
83
- document.body.appendChild(el);
84
- } else {
85
- document.head.appendChild(el);
86
- }
87
- return el;
88
- });
89
- return () => nodes.forEach((n) => n.remove());
90
- }, [JSON.stringify(options.script)]);
91
- useEffect(() => {
92
- if (!options.style?.length) return;
93
- const id = uid();
94
- const nodes = options.style.map((tag) => {
95
- const el = document.createElement("style");
96
- if (tag.media) el.media = tag.media;
97
- if (tag.content) el.textContent = tag.content;
98
- el.dataset.usehtml = id;
99
- document.head.appendChild(el);
100
- return el;
101
- });
102
- return () => nodes.forEach((n) => n.remove());
103
- }, [JSON.stringify(options.style)]);
104
- }
105
- function applyAttrs(el, attrs) {
106
- const prev = {};
107
- for (const [k, v] of Object.entries(attrs)) {
108
- if (v === void 0) continue;
109
- const attr = domAttr(k);
110
- prev[attr] = el.getAttribute(attr);
111
- el.setAttribute(attr, v);
112
- }
113
- return () => {
114
- for (const [attr, was] of Object.entries(prev)) {
115
- if (was === null) el.removeAttribute(attr);
116
- else el.setAttribute(attr, was);
117
- }
118
- };
119
- }
120
- function domAttr(key) {
121
- if (key === "httpEquiv") return "http-equiv";
122
- if (key === "hrefLang") return "hreflang";
123
- if (key === "crossOrigin") return "crossorigin";
124
- return key;
125
- }
126
- export {
127
- useHtml
128
- };
@@ -1,74 +0,0 @@
1
- /**
2
- * use-request.ts — useRequest() Hook
3
- *
4
- * Universal hook that exposes the current request's URL parameters, query
5
- * string, and headers to any React component — server or client, dev or prod.
6
- *
7
- * ┌───────────────────────────────────────────────────────────────────────────┐
8
- * │ Environment │ Data source │
9
- * ├─────────────────┼─────────────────────────────────────────────────────────┤
10
- * │ SSR (server) │ request-store, populated by ssr.ts before rendering │
11
- * │ Client │ __n_data JSON blob + window.location (reactive) │
12
- * └───────────────────────────────────────────────────────────────────────────┘
13
- *
14
- * The hook stays reactive on the client: it listens to 'locationchange' events
15
- * fired by NukeJS's SPA router so values update on soft navigation without a
16
- * full page reload.
17
- *
18
- * --- Usage ---
19
- *
20
- * Basic:
21
- * ```tsx
22
- * // Works in server components (SSR) and client components ("use client")
23
- * const { params, query, headers, pathname } = useRequest();
24
- * const slug = params.slug as string;
25
- * const lang = query.lang as string;
26
- * const locale = headers['accept-language'];
27
- * ```
28
- *
29
- * Building useI18n on top:
30
- * ```tsx
31
- * // hooks/useI18n.ts
32
- * import { useRequest } from 'nukejs';
33
- *
34
- * const translations = {
35
- * en: { welcome: 'Welcome' },
36
- * fr: { welcome: 'Bienvenue' },
37
- * } as const;
38
- * type Locale = keyof typeof translations;
39
- *
40
- * function parseLocale(header = ''): Locale {
41
- * const tag = header.split(',')[0]?.split('-')[0]?.trim().toLowerCase();
42
- * return (tag in translations ? tag : 'en') as Locale;
43
- * }
44
- *
45
- * export function useI18n() {
46
- * const { query, headers } = useRequest();
47
- * // ?lang=fr wins over Accept-Language header
48
- * const locale = ((query.lang as string) ?? parseLocale(headers['accept-language'])) as Locale;
49
- * return { t: translations[locale] ?? translations.en, locale };
50
- * }
51
- *
52
- * // Page.tsx
53
- * const { t } = useI18n();
54
- * return <h1>{t.welcome}</h1>;
55
- * ```
56
- *
57
- * --- Notes ---
58
- * - `headers` on the client never contains `cookie`, `authorization`, or
59
- * `proxy-authorization` — these are stripped by the SSR pipeline before
60
- * embedding in __n_data. See request-store.ts for the full exclusion list.
61
- * - In a "use client" component, `params` always reflects the __n_data blob
62
- * written at the time of the most recent SSR/navigation. For the freshest
63
- * pathname use `useRouter().path` instead.
64
- */
65
- import type { RequestContext } from './request-store';
66
- export type { RequestContext };
67
- /**
68
- * Returns the current request context: URL params, query string, and headers.
69
- *
70
- * Automatically detects SSR vs browser and returns the correct data for
71
- * each environment. On the client it is reactive — values update on SPA
72
- * navigation without a page reload.
73
- */
74
- export declare function useRequest(): RequestContext;