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 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 **dev live-reload** (`glash dev` auto-refreshes on save over SSE) are all in. **Honest scope:** uses Preact (React-compatible via `preact/compat`), not React; live-reload is auto-refresh, not yet state-preserving fast-refresh; streaming is shell-flush, not Suspense-chunked.
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] Dev live-reload over SSE (auto-refresh on save)
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
- - [ ] State-preserving fast-refresh, Suspense streaming, edge adapter
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.8.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": {
@@ -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 live-reload: a nonce'd inline script (CSP-safe) that reloads on change.
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
- // Intercepts clicks on <a data-glash-link>, fetches the route as a partial
3
- // (X-Glash-Nav: 1 -> JSON { title, html, props, bundle }), swaps #glash-root,
4
- // updates the props block + <title>, pushes history, and re-hydrates by
5
- // importing the new route's bundle. Falls back to a full navigation on any error.
6
- export const NAV_CLIENT = `// glashjs client navigation
7
- async function navigate(href, push) {
8
- let data;
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
- const res = await fetch(href, { headers: { 'X-Glash-Nav': '1' }, credentials: 'same-origin' });
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
- const root = document.getElementById('glash-root');
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
- let pe = document.getElementById('glash-props');
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
- document.addEventListener('click', (e) => {
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
- const a = e.target.closest && e.target.closest('a[data-glash-link]');
35
+ var a = e.target.closest && e.target.closest('a[data-glash-link]');
28
36
  if (!a) return;
29
- const href = a.getAttribute('href');
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', () => navigate(location.pathname + location.search, false));
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
  `;