glashjs 0.3.0 → 0.4.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
@@ -77,6 +77,19 @@ glash dev # dev server: routing + SSR + API, live route reload
77
77
  glash serve # production server over routes/ + built assets (Brotli-negotiated)
78
78
  ```
79
79
 
80
+ **`<Image>`** (better than `next/image` — no runtime image server, uses the build's AVIF/WebP):
81
+ ```jsx
82
+ import { Image } from 'glashjs/image';
83
+ <Image src="/hero.png" alt="Hero" width={1200} height={630} />
84
+ // -> <picture><source …avif><source …webp><img loading=lazy decoding=async></picture>
85
+ ```
86
+
87
+ **File-based middleware** (`_middleware.mjs`, runs root→leaf before the route):
88
+ ```js
89
+ import { redirect } from 'glashjs';
90
+ export default (ctx) => { if (!ctx.headers.authorization) return redirect('/login'); };
91
+ ```
92
+
80
93
  Every response carries the secure-by-default headers; static files are served from the build with Brotli negotiation.
81
94
 
82
95
  ### JSX components + client hydration (new in 0.2)
@@ -148,7 +161,9 @@ animatedFavicon: true, // bundled animated glash mark (d
148
161
  - [x] Nested layouts (`_layout.jsx` composing root→leaf, server + hydration)
149
162
  - [x] Streaming SSR (shell flushed before the component renders)
150
163
  - [x] Dev live-reload over SSE (auto-refresh on save)
151
- - [ ] State-preserving fast-refresh (HMR), Suspense streaming, `<Image>`/`<Video>` components
164
+ - [x] `<Image>` — zero-config `<picture>` with AVIF/WebP from the optimizer (beats next/image: no runtime image server)
165
+ - [x] File-based middleware (`_middleware.mjs`, root→leaf) — auth, redirects, headers
166
+ - [ ] State-preserving fast-refresh, Suspense streaming, `<Video>`, `glash deploy`
152
167
  - [ ] `<Image>` / `<Video>` components that emit `<picture>`/`<source>` from the manifest
153
168
  - [ ] Edge adapter for the glashdb Worker (serve `.br`/`.avif` by `Accept`)
154
169
  - [ ] `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.0",
3
+ "version": "0.4.0",
4
4
  "description": "glashjs — a web framework with file-based routing, SSR, API routes, a best-in-class asset optimizer, offline PWA layer, animated favicon, and secure-by-default headers. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,9 @@
9
9
  "exports": {
10
10
  ".": "./src/index.mjs",
11
11
  "./config": "./src/config.mjs",
12
- "./security": "./src/security/headers.mjs"
12
+ "./security": "./src/security/headers.mjs",
13
+ "./image": "./src/components/image.mjs",
14
+ "./package.json": "./package.json"
13
15
  },
14
16
  "files": [
15
17
  "bin",
@@ -0,0 +1,42 @@
1
+ // glashjs <Image> — a zero-config image component that's better than next/image
2
+ // out of the box: it emits a <picture> that prefers the AVIF/WebP variants the
3
+ // glashjs asset optimizer already produced at build time, with no runtime image
4
+ // server, no signed URLs, and no config. Width/height are required-by-habit to
5
+ // prevent layout shift (CLS), and images lazy-load + async-decode by default.
6
+ //
7
+ // import { Image } from 'glashjs/image';
8
+ // <Image src="/hero.png" alt="Hero" width={1200} height={630} />
9
+ //
10
+ // After `glash build`, /hero.avif and /hero.webp exist next to /hero.png, so
11
+ // the browser downloads the smallest format it supports. Deterministic output
12
+ // (same on server + during hydration), so it never causes a hydration mismatch.
13
+ import { h } from 'preact';
14
+
15
+ const RASTER = /\.(png|jpe?g|webp|avif)$/i;
16
+
17
+ export function Image({ src, alt = '', width, height, sizes, loading = 'lazy', fetchpriority, class: className, style, ...rest }) {
18
+ if (!src || !RASTER.test(src)) {
19
+ // SVG/unknown: render a plain <img>, nothing to transcode.
20
+ return h('img', { src, alt, width, height, loading, decoding: 'async', class: className, style, ...rest });
21
+ }
22
+ const base = src.replace(RASTER, '');
23
+ return h(
24
+ 'picture',
25
+ { class: className, style },
26
+ h('source', { srcset: `${base}.avif`, type: 'image/avif', sizes }),
27
+ h('source', { srcset: `${base}.webp`, type: 'image/webp', sizes }),
28
+ h('img', {
29
+ src,
30
+ alt,
31
+ width,
32
+ height,
33
+ sizes,
34
+ loading,
35
+ fetchpriority,
36
+ decoding: 'async',
37
+ ...rest,
38
+ }),
39
+ );
40
+ }
41
+
42
+ export default Image;
package/src/index.mjs CHANGED
@@ -5,6 +5,7 @@ export { optimizeAssets } from './assets/optimize.mjs';
5
5
  export { generateAnimatedFavicon } from './assets/animated-favicon.mjs';
6
6
  export { generateServiceWorker } from './offline/generate-sw.mjs';
7
7
  export { securityHeaders, buildCsp, sri, glashSecurity } from './security/headers.mjs';
8
- export { createGlashServer, json } from './server/server.mjs';
9
- export { discoverRoutes, matchRoute } from './server/router.mjs';
8
+ export { createGlashServer, json, redirect } from './server/server.mjs';
9
+ export { discoverRoutes, matchRoute, findMiddleware } from './server/router.mjs';
10
10
  export { html, raw, escapeHtml, renderDocument } from './server/html.mjs';
11
+ export { Image } from './components/image.mjs';
@@ -7,9 +7,32 @@
7
7
  // routes/docs/[...path].mjs -> /docs/*path (catch-all)
8
8
  // routes/api/hello.mjs -> /api/hello (API route — exports GET/POST/…)
9
9
  // Anything under routes/api/ is an API route; everything else is a page (SSR).
10
- import { promises as fs } from 'node:fs';
10
+ import { promises as fs, existsSync } from 'node:fs';
11
11
  import path from 'node:path';
12
12
 
13
+ /**
14
+ * File-based middleware: `_middleware.{mjs,js}` in any routes dir runs before
15
+ * the route, root→leaf. Each default-exports `(ctx) => Response | void` — return
16
+ * a value to short-circuit (redirect/auth/json), or nothing to continue. The
17
+ * root middleware runs for every request; `dash/_middleware` only for /dash/*.
18
+ */
19
+ export function findMiddleware(routesDir, routeFile) {
20
+ const root = path.resolve(routesDir);
21
+ const dir = path.dirname(path.resolve(routeFile));
22
+ const rel = path.relative(root, dir);
23
+ const dirs = [root];
24
+ let acc = root;
25
+ for (const part of rel ? rel.split(path.sep) : []) { acc = path.join(acc, part); dirs.push(acc); }
26
+ const files = [];
27
+ for (const d of dirs) {
28
+ for (const name of ['_middleware.mjs', '_middleware.js']) {
29
+ const f = path.join(d, name);
30
+ if (existsSync(f)) { files.push(f); break; }
31
+ }
32
+ }
33
+ return files;
34
+ }
35
+
13
36
  export async function discoverRoutes(routesDir) {
14
37
  const root = path.resolve(routesDir);
15
38
  const files = [];
@@ -10,7 +10,7 @@ import { promises as fs, existsSync, statSync, watch } from 'node:fs';
10
10
  import { randomBytes } from 'node:crypto';
11
11
  import path from 'node:path';
12
12
  import { pathToFileURL } from 'node:url';
13
- import { discoverRoutes, matchRoute } from './router.mjs';
13
+ import { discoverRoutes, matchRoute, findMiddleware } from './router.mjs';
14
14
  import { renderDocument, documentParts } from './html.mjs';
15
15
  import { isComponentRoute, loadComponentRoute, clientBundle, renderComponent, routeId, findLayouts, clearJsxCaches } from './jsx.mjs';
16
16
  import { securityHeaders } from '../security/headers.mjs';
@@ -72,7 +72,15 @@ export async function createGlashServer({ root = process.cwd(), dev = false } =
72
72
  if (await serveStatic(res, outDir, pathname, req, secHeaders)) return;
73
73
  const match = matchRoute(routes, pathname);
74
74
  if (!match) return send(res, 404, 'text/plain; charset=utf-8', 'Not found', secHeaders);
75
- const ctx = makeCtx(req, url, match.params);
75
+ const ctx = makeCtx(req, res, url, match.params);
76
+ // Run the middleware chain (root -> leaf). Any return value short-circuits.
77
+ for (const mwFile of findMiddleware(routesDir, match.route.file)) {
78
+ const mwMod = await importRoute(mwFile);
79
+ const mw = mwMod.default || mwMod.middleware;
80
+ if (typeof mw !== 'function') continue;
81
+ const result = await mw(ctx);
82
+ if (result) return sendMiddlewareResult(res, result, secHeaders);
83
+ }
76
84
  if (match.route.isApi) {
77
85
  const mod = await importRoute(match.route.file);
78
86
  return await handleApi(res, mod, req, ctx, secHeaders);
@@ -178,9 +186,10 @@ async function serveStatic(res, outDir, pathname, req, secHeaders) {
178
186
  return false;
179
187
  }
180
188
 
181
- function makeCtx(req, url, params) {
189
+ function makeCtx(req, res, url, params) {
182
190
  return {
183
191
  req,
192
+ res,
184
193
  method: req.method,
185
194
  url,
186
195
  path: url.pathname,
@@ -213,3 +222,22 @@ function safeDecode(p) {
213
222
  export function json(body, { status = 200, headers } = {}) {
214
223
  return { __response: true, status, contentType: 'application/json', body, headers };
215
224
  }
225
+
226
+ /** Middleware/handler helper: redirect to another path. */
227
+ export function redirect(location, { status = 302 } = {}) {
228
+ return { __redirect: location, status };
229
+ }
230
+
231
+ function sendMiddlewareResult(res, result, secHeaders) {
232
+ if (result.__redirect) {
233
+ res.writeHead(result.status || 302, { ...secHeaders, location: result.__redirect });
234
+ return res.end();
235
+ }
236
+ if (result.__response) {
237
+ return send(res, result.status || 200, result.contentType || 'application/json',
238
+ typeof result.body === 'string' ? result.body : JSON.stringify(result.body), { ...secHeaders, ...(result.headers || {}) });
239
+ }
240
+ // A bare object/string from middleware is treated as a JSON/text body.
241
+ if (typeof result === 'string') return send(res, 200, 'text/plain; charset=utf-8', result, secHeaders);
242
+ return send(res, 200, 'application/json', JSON.stringify(result), secHeaders);
243
+ }