nukejs 0.0.12 → 0.0.14
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/build-common.js +5 -2
- package/dist/builder.js +2 -0
- package/dist/bundle.js +11 -2
- package/dist/hmr-bundle.js +33 -1
- package/dist/hmr.js +3 -0
- package/dist/html-store.d.ts +128 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/renderer.js +2 -1
- package/dist/request-store.d.ts +3 -7
- package/package.json +1 -1
package/dist/build-common.js
CHANGED
|
@@ -149,7 +149,7 @@ function collectServerPages(pagesDir) {
|
|
|
149
149
|
return walkFiles(pagesDir).filter((relPath) => {
|
|
150
150
|
const stem = path.basename(relPath, path.extname(relPath));
|
|
151
151
|
if (stem === "layout" || stem === "_404" || stem === "_500") return false;
|
|
152
|
-
return
|
|
152
|
+
return true;
|
|
153
153
|
}).map((relPath) => ({
|
|
154
154
|
...analyzeFile(relPath, "page"),
|
|
155
155
|
absPath: path.join(pagesDir, relPath)
|
|
@@ -426,7 +426,8 @@ function buildWrapperAttrString(attrs: Record<string, any>): string {
|
|
|
426
426
|
.map(([key, value]) => {
|
|
427
427
|
if (key === 'className') key = 'class';
|
|
428
428
|
if (key === 'style' && typeof value === 'object') {
|
|
429
|
-
|
|
429
|
+
// Always prepend display:contents so the wrapper span is invisible to layout.
|
|
430
|
+
const css = 'display:contents;' + Object.entries(value as Record<string, any>)
|
|
430
431
|
.map(([p, val]) => \`\${p.replace(/[A-Z]/g, m => \`-\${m.toLowerCase()}\`)}:\${escapeHtml(String(val))}\`)
|
|
431
432
|
.join(';');
|
|
432
433
|
return \`style="\${css}"\`;
|
|
@@ -436,6 +437,8 @@ function buildWrapperAttrString(attrs: Record<string, any>): string {
|
|
|
436
437
|
return \`\${key}="\${escapeHtml(String(value))}"\`;
|
|
437
438
|
})
|
|
438
439
|
.filter(Boolean);
|
|
440
|
+
// When no style prop was passed, still emit display:contents.
|
|
441
|
+
if (!('style' in attrs)) parts.push('style="display:contents"');
|
|
439
442
|
return parts.length ? ' ' + parts.join(' ') : '';
|
|
440
443
|
}
|
|
441
444
|
|
package/dist/builder.js
CHANGED
package/dist/bundle.js
CHANGED
|
@@ -10,7 +10,7 @@ function setupLocationChangeMonitor() {
|
|
|
10
10
|
originalReplaceState(...args);
|
|
11
11
|
dispatch(args[2]);
|
|
12
12
|
};
|
|
13
|
-
window.addEventListener("popstate", () => dispatch(window.location.pathname));
|
|
13
|
+
window.addEventListener("popstate", () => dispatch(window.location.pathname + window.location.search));
|
|
14
14
|
}
|
|
15
15
|
function makeLogger(level) {
|
|
16
16
|
return {
|
|
@@ -248,7 +248,15 @@ function syncAttrs(live, next) {
|
|
|
248
248
|
if (!next.hasAttribute(name)) live.removeAttribute(name);
|
|
249
249
|
}
|
|
250
250
|
function setupNavigation(log) {
|
|
251
|
+
let hmrNavPending = false;
|
|
251
252
|
window.addEventListener("locationchange", async ({ detail: { href, hmr } }) => {
|
|
253
|
+
if (hmr) {
|
|
254
|
+
if (hmrNavPending) {
|
|
255
|
+
log.info("[HMR] Navigation already in flight \u2014 skipping duplicate for", href);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
hmrNavPending = true;
|
|
259
|
+
}
|
|
252
260
|
try {
|
|
253
261
|
const fetchUrl = hmr ? href + (href.includes("?") ? "&" : "?") + "__hmr=1" : href;
|
|
254
262
|
const response = await fetch(fetchUrl, { headers: { Accept: "text/html" } });
|
|
@@ -279,7 +287,7 @@ function setupNavigation(log) {
|
|
|
279
287
|
activeRoots.splice(0).forEach((r) => r.unmount());
|
|
280
288
|
const navData = JSON.parse(currDataEl?.textContent ?? "{}");
|
|
281
289
|
log.info("\u{1F504} Route \u2192", href, "\u2014 mounting", navData.hydrateIds?.length ?? 0, "component(s)");
|
|
282
|
-
const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));
|
|
290
|
+
const mods = await loadModules(navData.allIds ?? [], log, hmr ? String(Date.now()) : "");
|
|
283
291
|
await mountNodes(mods, log);
|
|
284
292
|
window.scrollTo(0, 0);
|
|
285
293
|
log.info("\u{1F389} Navigation complete:", href);
|
|
@@ -287,6 +295,7 @@ function setupNavigation(log) {
|
|
|
287
295
|
log.error("Navigation error, falling back to full reload:", err);
|
|
288
296
|
window.location.href = href;
|
|
289
297
|
} finally {
|
|
298
|
+
if (hmr) hmrNavPending = false;
|
|
290
299
|
clientErrorPending = false;
|
|
291
300
|
}
|
|
292
301
|
});
|
package/dist/hmr-bundle.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { log } from "./logger.js";
|
|
2
2
|
function hmr() {
|
|
3
3
|
const es = new EventSource("/__hmr");
|
|
4
|
+
let reconnecting = false;
|
|
4
5
|
es.onopen = () => {
|
|
6
|
+
reconnecting = false;
|
|
5
7
|
log.info("[HMR] Connected");
|
|
6
8
|
};
|
|
7
9
|
es.onerror = () => {
|
|
10
|
+
if (reconnecting) return;
|
|
11
|
+
reconnecting = true;
|
|
8
12
|
es.close();
|
|
9
13
|
waitForReconnect();
|
|
10
14
|
};
|
|
15
|
+
document.addEventListener("visibilitychange", () => {
|
|
16
|
+
if (document.visibilityState !== "visible") return;
|
|
17
|
+
if (es.readyState === EventSource.OPEN) return;
|
|
18
|
+
if (reconnecting) return;
|
|
19
|
+
reconnecting = true;
|
|
20
|
+
es.close();
|
|
21
|
+
waitForReconnect(500, 20);
|
|
22
|
+
});
|
|
11
23
|
es.onmessage = async (event) => {
|
|
12
24
|
try {
|
|
13
25
|
const msg = JSON.parse(event.data);
|
|
@@ -28,6 +40,16 @@ function hmr() {
|
|
|
28
40
|
}
|
|
29
41
|
return;
|
|
30
42
|
}
|
|
43
|
+
if (msg.type === "layout-reload") {
|
|
44
|
+
const base = msg.base === "/" ? "" : msg.base;
|
|
45
|
+
const pathname = window.location.pathname;
|
|
46
|
+
const isUnder = pathname === (base || "/") || pathname.startsWith(base + "/");
|
|
47
|
+
if (isUnder) {
|
|
48
|
+
log.info("[HMR] Layout changed:", msg.base);
|
|
49
|
+
navigate(pathname + window.location.search);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
31
53
|
if (msg.type === "replace") {
|
|
32
54
|
log.info("[HMR] Component changed:", msg.component);
|
|
33
55
|
navigate(window.location.pathname + window.location.search);
|
|
@@ -38,8 +60,18 @@ function hmr() {
|
|
|
38
60
|
}
|
|
39
61
|
};
|
|
40
62
|
}
|
|
63
|
+
let _navTimer = null;
|
|
64
|
+
let _navHref = null;
|
|
41
65
|
function navigate(href) {
|
|
42
|
-
|
|
66
|
+
_navHref = href;
|
|
67
|
+
if (_navTimer) clearTimeout(_navTimer);
|
|
68
|
+
_navTimer = setTimeout(() => {
|
|
69
|
+
_navTimer = null;
|
|
70
|
+
if (_navHref !== null) {
|
|
71
|
+
window.dispatchEvent(new CustomEvent("locationchange", { detail: { href: _navHref, hmr: true } }));
|
|
72
|
+
_navHref = null;
|
|
73
|
+
}
|
|
74
|
+
}, 50);
|
|
43
75
|
}
|
|
44
76
|
function patternMatchesPathname(pattern, pathname) {
|
|
45
77
|
const normPattern = pattern.length > 1 ? pattern.replace(/\/+$/, "") : pattern;
|
package/dist/hmr.js
CHANGED
|
@@ -29,6 +29,9 @@ function buildPayload(filename) {
|
|
|
29
29
|
return { type: "replace", component: stem };
|
|
30
30
|
}
|
|
31
31
|
const url = pageFileToUrl(normalized);
|
|
32
|
+
if (stem === "layout") {
|
|
33
|
+
return { type: "layout-reload", base: url };
|
|
34
|
+
}
|
|
32
35
|
return { type: "reload", url };
|
|
33
36
|
}
|
|
34
37
|
const ext = path.extname(filename).toLowerCase();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* html-store.ts — Per-Request HTML Head Store
|
|
3
|
+
*
|
|
4
|
+
* Provides a request-scoped store that server components can write to via
|
|
5
|
+
* `useHtml()` during SSR. The accumulated values are flushed into the
|
|
6
|
+
* rendered HTML document after the component tree is fully rendered.
|
|
7
|
+
*
|
|
8
|
+
* Why globalThis?
|
|
9
|
+
* Node's module system may import this file multiple times if the page
|
|
10
|
+
* module and the nukejs package resolve to different copies (e.g. when
|
|
11
|
+
* running from source in dev with tsx). Using a well-known Symbol on
|
|
12
|
+
* globalThis guarantees all copies share the same store instance.
|
|
13
|
+
*
|
|
14
|
+
* Request isolation:
|
|
15
|
+
* runWithHtmlStore() creates a fresh store before rendering and clears it
|
|
16
|
+
* in the `finally` block, so concurrent requests cannot bleed into each other.
|
|
17
|
+
*
|
|
18
|
+
* Title resolution:
|
|
19
|
+
* Layouts and pages can both call useHtml({ title: … }). Layouts typically
|
|
20
|
+
* pass a template function:
|
|
21
|
+
*
|
|
22
|
+
* useHtml({ title: (prev) => `${prev} | Acme` })
|
|
23
|
+
*
|
|
24
|
+
* Operations are collected in render order (outermost layout first, page
|
|
25
|
+
* last) then resolved *in reverse* so the page's string value is the base
|
|
26
|
+
* and layout template functions wrap outward.
|
|
27
|
+
*/
|
|
28
|
+
/** A page sets a literal string; a layout wraps with a template function. */
|
|
29
|
+
export type TitleValue = string | ((prev: string) => string);
|
|
30
|
+
export interface HtmlAttrs {
|
|
31
|
+
lang?: string;
|
|
32
|
+
class?: string;
|
|
33
|
+
style?: string;
|
|
34
|
+
dir?: string;
|
|
35
|
+
[attr: string]: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface BodyAttrs {
|
|
38
|
+
class?: string;
|
|
39
|
+
style?: string;
|
|
40
|
+
[attr: string]: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
export interface MetaTag {
|
|
43
|
+
name?: string;
|
|
44
|
+
property?: string;
|
|
45
|
+
httpEquiv?: string;
|
|
46
|
+
charset?: string;
|
|
47
|
+
content?: string;
|
|
48
|
+
[attr: string]: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
export interface LinkTag {
|
|
51
|
+
rel?: string;
|
|
52
|
+
href?: string;
|
|
53
|
+
type?: string;
|
|
54
|
+
media?: string;
|
|
55
|
+
as?: string;
|
|
56
|
+
crossOrigin?: string;
|
|
57
|
+
integrity?: string;
|
|
58
|
+
hrefLang?: string;
|
|
59
|
+
sizes?: string;
|
|
60
|
+
[attr: string]: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
export interface ScriptTag {
|
|
63
|
+
src?: string;
|
|
64
|
+
content?: string;
|
|
65
|
+
type?: string;
|
|
66
|
+
defer?: boolean;
|
|
67
|
+
async?: boolean;
|
|
68
|
+
crossOrigin?: string;
|
|
69
|
+
integrity?: string;
|
|
70
|
+
noModule?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Where to inject the script in the document.
|
|
73
|
+
* 'head' (default) — placed inside <head>, inside the <!--n-head--> block.
|
|
74
|
+
* 'body' — placed at the very end of <body>, inside the
|
|
75
|
+
* <!--n-body-scripts--> block, just before </body>.
|
|
76
|
+
*/
|
|
77
|
+
position?: 'head' | 'body';
|
|
78
|
+
}
|
|
79
|
+
export interface StyleTag {
|
|
80
|
+
content?: string;
|
|
81
|
+
media?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface HtmlStore {
|
|
84
|
+
/** Collected in render order; resolved in reverse so the page title wins. */
|
|
85
|
+
titleOps: TitleValue[];
|
|
86
|
+
/** Attributes merged onto <html>; last write wins per attribute. */
|
|
87
|
+
htmlAttrs: HtmlAttrs;
|
|
88
|
+
/** Attributes merged onto <body>; last write wins per attribute. */
|
|
89
|
+
bodyAttrs: BodyAttrs;
|
|
90
|
+
/** Accumulated in render order: layouts first, page last. */
|
|
91
|
+
meta: MetaTag[];
|
|
92
|
+
link: LinkTag[];
|
|
93
|
+
script: ScriptTag[];
|
|
94
|
+
style: StyleTag[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Runs `fn` inside a fresh HTML store and returns the collected values.
|
|
98
|
+
*
|
|
99
|
+
* Usage in SSR:
|
|
100
|
+
* ```ts
|
|
101
|
+
* const store = await runWithHtmlStore(async () => {
|
|
102
|
+
* appHtml = await renderElementToHtml(element, ctx);
|
|
103
|
+
* });
|
|
104
|
+
* // store.titleOps, store.meta, etc. are now populated
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore>;
|
|
108
|
+
/**
|
|
109
|
+
* Returns the current request's store, or `undefined` if called outside of
|
|
110
|
+
* a `runWithHtmlStore` context (e.g. in the browser or in a test).
|
|
111
|
+
*/
|
|
112
|
+
export declare function getHtmlStore(): HtmlStore | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Resolves the final page title from a list of title operations.
|
|
115
|
+
*
|
|
116
|
+
* Operations are walked in *reverse* so the page's value is the starting
|
|
117
|
+
* point and layout template functions wrap it outward:
|
|
118
|
+
*
|
|
119
|
+
* ```
|
|
120
|
+
* ops = [ (p) => `${p} | Acme`, 'About' ] ← layout pushed first, page last
|
|
121
|
+
* Walk in reverse:
|
|
122
|
+
* i=1: op = 'About' → title = 'About'
|
|
123
|
+
* i=0: op = (p) => … → title = 'About | Acme'
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @param fallback Used when ops is empty (e.g. a page that didn't call useHtml).
|
|
127
|
+
*/
|
|
128
|
+
export declare function resolveTitle(ops: TitleValue[], fallback?: string): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { useHtml } from './use-html';
|
|
2
|
-
export type { HtmlOptions
|
|
2
|
+
export type { HtmlOptions } from './use-html';
|
|
3
|
+
export type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag, } from './html-store';
|
|
3
4
|
export { default as useRouter } from './use-router';
|
|
4
5
|
export { useRequest } from './use-request';
|
|
5
6
|
export type { RequestContext } from './use-request';
|
|
6
|
-
export { normaliseHeaders, sanitiseHeaders } from './request-store';
|
|
7
|
+
export { normaliseHeaders, sanitiseHeaders, getRequestStore } from './request-store';
|
|
7
8
|
export { default as Link } from './Link';
|
|
8
9
|
export { setupLocationChangeMonitor, initRuntime } from './bundle';
|
|
9
10
|
export type { RuntimeData } from './bundle';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useHtml } from "./use-html.js";
|
|
2
2
|
import { default as default2 } from "./use-router.js";
|
|
3
3
|
import { useRequest } from "./use-request.js";
|
|
4
|
-
import { normaliseHeaders, sanitiseHeaders } from "./request-store.js";
|
|
4
|
+
import { normaliseHeaders, sanitiseHeaders, getRequestStore } from "./request-store.js";
|
|
5
5
|
import { default as default3 } from "./Link.js";
|
|
6
6
|
import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
|
|
7
7
|
import { escapeHtml } from "./utils.js";
|
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
c,
|
|
13
13
|
escapeHtml,
|
|
14
14
|
getDebugLevel,
|
|
15
|
+
getRequestStore,
|
|
15
16
|
initRuntime,
|
|
16
17
|
log,
|
|
17
18
|
normaliseHeaders,
|
package/dist/renderer.js
CHANGED
|
@@ -20,7 +20,7 @@ function buildWrapperAttrString(attrs) {
|
|
|
20
20
|
const parts = Object.entries(attrs).map(([key, value]) => {
|
|
21
21
|
if (key === "className") key = "class";
|
|
22
22
|
if (key === "style" && typeof value === "object") {
|
|
23
|
-
const css = Object.entries(value).map(([k, v]) => {
|
|
23
|
+
const css = "display:contents;" + Object.entries(value).map(([k, v]) => {
|
|
24
24
|
const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
25
25
|
const safeVal = String(v).replace(/[<>"'`\\]/g, "");
|
|
26
26
|
return `${prop}:${safeVal}`;
|
|
@@ -31,6 +31,7 @@ function buildWrapperAttrString(attrs) {
|
|
|
31
31
|
if (value == null) return "";
|
|
32
32
|
return `${key}="${escapeHtml(String(value))}"`;
|
|
33
33
|
}).filter(Boolean);
|
|
34
|
+
if (!("style" in attrs)) parts.push('style="display:contents"');
|
|
34
35
|
return parts.length ? " " + parts.join(" ") : "";
|
|
35
36
|
}
|
|
36
37
|
async function renderElementToHtml(element, ctx) {
|
package/dist/request-store.d.ts
CHANGED
|
@@ -67,13 +67,9 @@ export declare function normaliseHeaders(raw: Record<string, string | string[] |
|
|
|
67
67
|
export declare function sanitiseHeaders(raw: Record<string, string | string[] | undefined>): Record<string, string>;
|
|
68
68
|
/**
|
|
69
69
|
* Runs `fn` inside the context of the given request, then clears the store.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* const store = await runWithRequestStore(ctx, async () => {
|
|
74
|
-
* appHtml = await renderElementToHtml(element, renderCtx);
|
|
75
|
-
* });
|
|
76
|
-
* ```
|
|
70
|
+
* The store is set synchronously before `fn` is called, so any code that
|
|
71
|
+
* reads getRequestStore() during the synchronous phase of a server component
|
|
72
|
+
* (before its first `await`) will always see the correct context.
|
|
77
73
|
*/
|
|
78
74
|
export declare function runWithRequestStore<T>(ctx: RequestContext, fn: () => Promise<T>): Promise<T>;
|
|
79
75
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|