glashjs 0.8.0 → 0.9.0
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 +3 -3
- package/package.json +1 -1
- package/src/server/html.mjs +4 -2
- package/src/server/nav-client.mjs +55 -18
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ export default function Counter({ start = 0 }) {
|
|
|
116
116
|
|
|
117
117
|
Hydration is **CSP-safe**: server props ride in a non-executed `<script type="application/json">` and the hydration bundle is an external `'self'` module with a per-request **nonce** — so the strict CSP stays intact (no `'unsafe-inline'`).
|
|
118
118
|
|
|
119
|
-
**Nested layouts** (`_layout.jsx` in any routes dir wrap pages root→leaf, server + hydration), **streaming SSR** (the shell flushes before the component renders — `Transfer-Encoding: chunked`), and **
|
|
119
|
+
**Nested layouts** (`_layout.jsx` in any routes dir wrap pages root→leaf, server + hydration), **streaming SSR** (the shell flushes before the component renders — `Transfer-Encoding: chunked`), and **instant HMR** (`glash dev` does an in-place soft re-render on save over SSE — no full reload, no flash, and scroll/focus/form-input are preserved across the swap) are all in. **Honest scope:** uses Preact (React-compatible via `preact/compat`), not React; HMR preserves DOM/scroll/input state but **not** component `useState` (that's React-Fast-Refresh via `@prefresh`, still ahead); streaming is shell-flush, not Suspense-chunked.
|
|
120
120
|
|
|
121
121
|
## Usage
|
|
122
122
|
|
|
@@ -169,7 +169,7 @@ animatedFavicon: true, // bundled animated glash mark (d
|
|
|
169
169
|
- [x] Client-JS bundling (esbuild) per route
|
|
170
170
|
- [x] Nested layouts (`_layout.jsx` composing root→leaf, server + hydration)
|
|
171
171
|
- [x] Streaming SSR (shell flushed before the component renders)
|
|
172
|
-
- [x]
|
|
172
|
+
- [x] Instant HMR — in-place soft re-render on save (no full reload, no flash; preserves scroll, focus, and form input)
|
|
173
173
|
- [x] `<Image>` — zero-config `<picture>` with AVIF/WebP from the optimizer (beats next/image: no runtime image server)
|
|
174
174
|
- [x] `<Video>` — `<video>` with AV1/WebM + mp4 fallback + auto poster
|
|
175
175
|
- [x] File-based middleware (`_middleware.mjs`, root→leaf) — auth, redirects, headers
|
|
@@ -178,7 +178,7 @@ animatedFavicon: true, // bundled animated glash mark (d
|
|
|
178
178
|
- [x] `<Link>` client-side navigation (SPA swap of `#glash-root` + re-hydrate; progressive-enhancement `<a>`)
|
|
179
179
|
- [x] `glash deploy` → glashdb (builds, then hands off to the `glashdb` CLI)
|
|
180
180
|
- [x] Production-grade runtime — custom `404`/`500` routes, dev error overlay, HEAD support, Range requests + streamed static (video seeking), graceful mid-stream error handling
|
|
181
|
-
- [ ]
|
|
181
|
+
- [ ] React-Fast-Refresh (`useState` preservation via `@prefresh`), Suspense streaming, edge adapter
|
|
182
182
|
- [ ] `<Image>` / `<Video>` components that emit `<picture>`/`<source>` from the manifest
|
|
183
183
|
- [ ] Edge adapter for the glashdb Worker (serve `.br`/`.avif` by `Accept`)
|
|
184
184
|
- [ ] `glash deploy` → glashdb hosting in one command
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glashjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "glashjs — a web framework built on top of Next.js: file-based routing, SSR, API routes, JSX components with client hydration, nested layouts, streaming SSR, a best-in-class build-time asset optimizer, offline PWA layer, animated favicon, and secure-by-default headers. Zero mandatory dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server/html.mjs
CHANGED
|
@@ -57,9 +57,11 @@ function shellTail({ offline = true, animatedFavicon = true, dev = false, nonce
|
|
|
57
57
|
const off = offline
|
|
58
58
|
? `<script type="module"${n}>try{const m=await import("/glash-offline.mjs");m.registerGlashOffline&&m.registerGlashOffline();}catch{}</script>`
|
|
59
59
|
: '';
|
|
60
|
-
// Dev
|
|
60
|
+
// Dev HMR: a nonce'd inline script (CSP-safe). On a change event it does an
|
|
61
|
+
// in-place soft refresh (no full reload, keeps scroll/focus/inputs), falling
|
|
62
|
+
// back to a full reload if the nav runtime hasn't loaded yet.
|
|
61
63
|
const hmr = dev
|
|
62
|
-
? `<script${n}>try{new EventSource("/_glash/hmr").onmessage=function(e){if(e.data==="reload")location.reload();};}catch(e){}</script>`
|
|
64
|
+
? `<script${n}>try{new EventSource("/_glash/hmr").onmessage=function(e){if(e.data==="reload"){if(window.__glashSoftRefresh){window.__glashSoftRefresh();}else{location.reload();}}};}catch(e){}</script>`
|
|
63
65
|
: '';
|
|
64
66
|
// Client-side navigation runtime (external 'self' module, CSP-safe).
|
|
65
67
|
const nav = '<script type="module" src="/_glash/nav.js"></script>';
|
|
@@ -1,35 +1,72 @@
|
|
|
1
|
-
// Source for /_glash/nav.js — the client-side navigation runtime.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// Source for /_glash/nav.js — the client-side navigation + dev HMR runtime.
|
|
2
|
+
//
|
|
3
|
+
// Navigation: intercepts <a data-glash-link> clicks, fetches the route as a
|
|
4
|
+
// partial (X-Glash-Nav: 1 -> JSON { title, html, props, bundle }), swaps
|
|
5
|
+
// #glash-root, updates <title> + history, and re-hydrates by importing the
|
|
6
|
+
// route's bundle. Falls back to a full navigation on any error.
|
|
7
|
+
//
|
|
8
|
+
// Dev HMR (window.__glashSoftRefresh): on a file change the dev server pushes
|
|
9
|
+
// a reload over SSE; instead of a full page reload, we re-render the current
|
|
10
|
+
// route in place — no white flash — and restore scroll, focus, and form input
|
|
11
|
+
// across the swap. (Note: component useState resets on re-render; preserving
|
|
12
|
+
// that is React-Fast-Refresh territory, which needs @prefresh.)
|
|
13
|
+
export const NAV_CLIENT = `// glashjs client navigation + dev HMR
|
|
14
|
+
async function navigate(href, push, keepScroll) {
|
|
15
|
+
var data;
|
|
9
16
|
try {
|
|
10
|
-
|
|
17
|
+
var res = await fetch(href, { headers: { 'X-Glash-Nav': '1' }, credentials: 'same-origin' });
|
|
11
18
|
if (!res.ok) throw 0;
|
|
12
19
|
data = await res.json();
|
|
13
|
-
} catch { location.href = href; return; }
|
|
14
|
-
|
|
20
|
+
} catch (e) { location.href = href; return; }
|
|
21
|
+
var root = document.getElementById('glash-root');
|
|
15
22
|
if (!root || !data || typeof data.html !== 'string') { location.href = href; return; }
|
|
16
23
|
if (data.title) document.title = data.title;
|
|
17
|
-
|
|
24
|
+
var pe = document.getElementById('glash-props');
|
|
18
25
|
if (!pe) { pe = document.createElement('script'); pe.type = 'application/json'; pe.id = 'glash-props'; document.head.appendChild(pe); }
|
|
19
26
|
pe.textContent = JSON.stringify(data.props || {});
|
|
20
27
|
root.innerHTML = data.html;
|
|
21
28
|
if (push) history.pushState({ glash: 1 }, '', href);
|
|
22
|
-
window.scrollTo(0, 0);
|
|
23
|
-
if (data.bundle) { try { await import(data.bundle); } catch {} }
|
|
29
|
+
if (!keepScroll) window.scrollTo(0, 0);
|
|
30
|
+
if (data.bundle) { try { await import(data.bundle + '?v=' + Date.now()); } catch (e) {} }
|
|
24
31
|
}
|
|
25
|
-
|
|
32
|
+
|
|
33
|
+
document.addEventListener('click', function (e) {
|
|
26
34
|
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
27
|
-
|
|
35
|
+
var a = e.target.closest && e.target.closest('a[data-glash-link]');
|
|
28
36
|
if (!a) return;
|
|
29
|
-
|
|
37
|
+
var href = a.getAttribute('href');
|
|
30
38
|
if (!href || a.target || /^([a-z]+:)?\\/\\//i.test(href) || href.startsWith('#') || href.startsWith('mailto:')) return;
|
|
31
39
|
e.preventDefault();
|
|
32
40
|
if (href !== location.pathname + location.search) navigate(href, true);
|
|
33
41
|
});
|
|
34
|
-
window.addEventListener('popstate', ()
|
|
42
|
+
window.addEventListener('popstate', function () { navigate(location.pathname + location.search, false); });
|
|
43
|
+
|
|
44
|
+
window.__glashNavigate = navigate;
|
|
45
|
+
|
|
46
|
+
// Dev HMR: re-render the current route in place, preserving scroll/focus/inputs.
|
|
47
|
+
window.__glashSoftRefresh = async function () {
|
|
48
|
+
var root = document.getElementById('glash-root');
|
|
49
|
+
if (!root) { location.reload(); return; }
|
|
50
|
+
var sx = window.scrollX, sy = window.scrollY;
|
|
51
|
+
var active = document.activeElement;
|
|
52
|
+
var focusName = active && active.getAttribute && active.getAttribute('name');
|
|
53
|
+
var selStart = active && active.selectionStart;
|
|
54
|
+
var selEnd = active && active.selectionEnd;
|
|
55
|
+
var values = {};
|
|
56
|
+
root.querySelectorAll('input[name],textarea[name],select[name]').forEach(function (el) {
|
|
57
|
+
values[el.name] = (el.type === 'checkbox' || el.type === 'radio') ? el.checked : el.value;
|
|
58
|
+
});
|
|
59
|
+
await navigate(location.pathname + location.search, false, true);
|
|
60
|
+
root.querySelectorAll('input[name],textarea[name],select[name]').forEach(function (el) {
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(values, el.name)) {
|
|
62
|
+
if (el.type === 'checkbox' || el.type === 'radio') el.checked = values[el.name];
|
|
63
|
+
else el.value = values[el.name];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
window.scrollTo(sx, sy);
|
|
67
|
+
if (focusName) {
|
|
68
|
+
var el = root.querySelector('[name="' + focusName + '"]');
|
|
69
|
+
if (el) { el.focus(); try { el.selectionStart = selStart; el.selectionEnd = selEnd; } catch (e) {} }
|
|
70
|
+
}
|
|
71
|
+
};
|
|
35
72
|
`;
|