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/package.json +1 -1
- package/dist/Link.d.ts +0 -12
- package/dist/Link.js +0 -15
- package/dist/app.js +0 -112
- package/dist/build-common.js +0 -880
- package/dist/build-node.js +0 -217
- package/dist/build-vercel.js +0 -359
- package/dist/builder.js +0 -95
- package/dist/bundle.d.ts +0 -85
- package/dist/bundle.js +0 -322
- package/dist/bundler.js +0 -102
- package/dist/component-analyzer.js +0 -125
- package/dist/config.js +0 -29
- package/dist/hmr-bundle.js +0 -120
- package/dist/hmr.js +0 -68
- package/dist/html-store.d.ts +0 -128
- package/dist/html-store.js +0 -41
- package/dist/http-server.js +0 -172
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -28
- package/dist/logger.d.ts +0 -59
- package/dist/logger.js +0 -53
- package/dist/metadata.js +0 -42
- package/dist/middleware-loader.js +0 -53
- package/dist/middleware.example.js +0 -57
- package/dist/middleware.js +0 -71
- package/dist/renderer.js +0 -151
- package/dist/request-store.d.ts +0 -80
- package/dist/request-store.js +0 -46
- package/dist/router.js +0 -118
- package/dist/ssr.js +0 -262
- package/dist/store.d.ts +0 -104
- package/dist/store.js +0 -45
- package/dist/use-html.d.ts +0 -64
- package/dist/use-html.js +0 -128
- package/dist/use-request.d.ts +0 -74
- package/dist/use-request.js +0 -48
- package/dist/use-router.d.ts +0 -7
- package/dist/use-router.js +0 -27
- package/dist/utils.d.ts +0 -26
- package/dist/utils.js +0 -61
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
|
-
};
|
package/dist/use-html.d.ts
DELETED
|
@@ -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
|
-
};
|
package/dist/use-request.d.ts
DELETED
|
@@ -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;
|