glashjs 0.4.0 → 0.5.1

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
@@ -162,8 +162,10 @@ animatedFavicon: true, // bundled animated glash mark (d
162
162
  - [x] Streaming SSR (shell flushed before the component renders)
163
163
  - [x] Dev live-reload over SSE (auto-refresh on save)
164
164
  - [x] `<Image>` — zero-config `<picture>` with AVIF/WebP from the optimizer (beats next/image: no runtime image server)
165
+ - [x] `<Video>` — `<video>` with AV1/WebM + mp4 fallback + auto poster
165
166
  - [x] File-based middleware (`_middleware.mjs`, root→leaf) — auth, redirects, headers
166
- - [ ] State-preserving fast-refresh, Suspense streaming, `<Video>`, `glash deploy`
167
+ - [x] Production route precompile (`glash build` bakes server modules + minified client bundles → no runtime esbuild on `glash serve`)
168
+ - [ ] State-preserving fast-refresh, Suspense streaming, `glash deploy` → glashdb
167
169
  - [ ] `<Image>` / `<Video>` components that emit `<picture>`/`<source>` from the manifest
168
170
  - [ ] Edge adapter for the glashdb Worker (serve `.br`/`.avif` by `Accept`)
169
171
  - [ ] `glash deploy` → glashdb hosting in one command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glashjs",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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": {
@@ -11,6 +11,7 @@
11
11
  "./config": "./src/config.mjs",
12
12
  "./security": "./src/security/headers.mjs",
13
13
  "./image": "./src/components/image.mjs",
14
+ "./video": "./src/components/video.mjs",
14
15
  "./package.json": "./package.json"
15
16
  },
16
17
  "files": [
package/src/build.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // Ties the pieces together: optimize assets -> derive a content version ->
3
3
  // generate the offline Service Worker + PWA manifest -> emit security headers
4
4
  // + a deploy manifest the glashdb edge (or any server) can consume.
5
- import { promises as fs } from 'node:fs';
5
+ import { promises as fs, existsSync } from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { createHash } from 'node:crypto';
@@ -10,8 +10,32 @@ import { optimizeAssets } from './assets/optimize.mjs';
10
10
  import { generateAnimatedFavicon } from './assets/animated-favicon.mjs';
11
11
  import { generateServiceWorker } from './offline/generate-sw.mjs';
12
12
  import { securityHeaders } from './security/headers.mjs';
13
+ import { discoverRoutes } from './server/router.mjs';
14
+ import { isComponentRoute, findLayouts, loadComponentRoute, clientBundle, routeId } from './server/jsx.mjs';
13
15
  import { loadConfig } from './config.mjs';
14
16
 
17
+ // Precompile JSX routes: server modules (-> .glash/server) + minified client
18
+ // hydration bundles (-> outDir/_glash/<id>.js). Production `glash serve` then
19
+ // serves bundles statically and imports prebuilt server modules — no esbuild
20
+ // on the serving host's hot path.
21
+ async function buildRoutes(root, cfg, outDir, log) {
22
+ const routesDir = path.resolve(root, cfg.routesDir || 'routes');
23
+ if (!existsSync(routesDir)) return { compiled: 0 };
24
+ const routes = await discoverRoutes(routesDir);
25
+ const comp = routes.filter((r) => isComponentRoute(r.file));
26
+ if (!comp.length) return { compiled: 0 };
27
+ const bundleDir = path.join(outDir, '_glash');
28
+ await fs.mkdir(bundleDir, { recursive: true });
29
+ for (const r of comp) {
30
+ const layouts = findLayouts(routesDir, r.file);
31
+ await loadComponentRoute(r.file, layouts, root, false, true);
32
+ const js = await clientBundle(r.file, layouts, false);
33
+ await fs.writeFile(path.join(bundleDir, routeId(r.file) + '.js'), js);
34
+ log(` route ${r.pattern} -> _glash/${routeId(r.file)}.js`);
35
+ }
36
+ return { compiled: comp.length };
37
+ }
38
+
15
39
  function deriveVersion(manifest) {
16
40
  const h = createHash('sha256');
17
41
  for (const [rel, entry] of Object.entries(manifest.assets)) h.update(rel + ':' + entry.hash);
@@ -39,14 +63,31 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
39
63
  log(`\nglashjs build — "${cfg.name}"`);
40
64
  log(` public: ${path.relative(root, publicDir)} -> out: ${path.relative(root, outDir)}\n`);
41
65
 
42
- log('Optimizing assets:');
43
- const manifest = await optimizeAssets(publicDir, { log });
66
+ // A routes-only app may have no public/ dir — that's fine, just skip assets.
67
+ let manifest;
68
+ if (existsSync(publicDir)) {
69
+ log('Optimizing assets:');
70
+ manifest = await optimizeAssets(publicDir, { log });
71
+ } else {
72
+ log(`(no ${cfg.publicDir}/ dir — skipping asset optimization)`);
73
+ manifest = { totals: { assets: 0, originalBytes: 0, optimizedBytes: 0, savedPercent: 0 }, assets: {} };
74
+ }
44
75
  const version = deriveVersion(manifest);
45
76
  manifest.version = version;
46
77
  manifest.generatedAt = new Date().toISOString();
47
78
 
48
79
  await fs.mkdir(outDir, { recursive: true });
49
80
 
81
+ // Precompile JSX routes (server modules + client bundles) for production.
82
+ let routesBuilt = { compiled: 0 };
83
+ try {
84
+ log('\nCompiling routes:');
85
+ routesBuilt = await buildRoutes(root, cfg, outDir, log);
86
+ if (!routesBuilt.compiled) log(' (no JSX routes)');
87
+ } catch (error) {
88
+ log(` ! route compile skipped: ${(error instanceof Error ? error.message : error)}`);
89
+ }
90
+
50
91
  // Offline Service Worker + registration helper.
51
92
  const offline = cfg.offline
52
93
  ? await generateServiceWorker(outDir, manifest, { dataPrefixes: cfg.dataPrefixes })
@@ -67,6 +108,7 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
67
108
  log(` original ${kb(t.originalBytes)}`);
68
109
  log(` optimized ${kb(t.optimizedBytes)}`);
69
110
  log(` saved ${t.savedPercent}% (${kb(t.originalBytes - t.optimizedBytes)})`);
111
+ log(` routes ${routesBuilt.compiled} JSX route(s) precompiled (no runtime esbuild in prod)`);
70
112
  log(` offline ${cfg.offline ? `${offline.precached} files precached (works offline after first visit)` : 'disabled'}`);
71
113
  log(` favicon static glashdb logo${animated.enabled ? ' + animated (glash-favicon.mjs)' : ''}`);
72
114
  log(` version ${version}`);
@@ -0,0 +1,27 @@
1
+ // glashjs <Video> — zero-config video that prefers the AV1/WebM the glashjs
2
+ // optimizer produced at build time, with the original as fallback and an
3
+ // auto-derived poster frame. Deterministic output (SSR + hydration match).
4
+ //
5
+ // import { Video } from 'glashjs/video';
6
+ // <Video src="/clip.mp4" width={1280} height={720} />
7
+ //
8
+ // After `glash build`, /clip.glash.webm (AV1) and /clip.poster.jpg exist, so
9
+ // browsers stream the far-smaller AV1 and fall back to the mp4 otherwise.
10
+ import { h } from 'preact';
11
+
12
+ const VID = /\.(mp4|mov|webm|m4v)$/i;
13
+
14
+ export function Video({ src, poster, width, height, controls = true, autoplay, loop, muted, playsinline, preload = 'metadata', class: className, style, ...rest }) {
15
+ if (!src || !VID.test(src)) {
16
+ return h('video', { src, poster, width, height, controls, preload, class: className, style, ...rest });
17
+ }
18
+ const base = src.replace(VID, '');
19
+ return h(
20
+ 'video',
21
+ { width, height, controls, autoplay, loop, muted, playsinline, preload, poster: poster || `${base}.poster.jpg`, class: className, style, ...rest },
22
+ h('source', { src: `${base}.glash.webm`, type: 'video/webm' }),
23
+ h('source', { src, type: 'video/mp4' }),
24
+ );
25
+ }
26
+
27
+ export default Video;
package/src/index.mjs CHANGED
@@ -9,3 +9,4 @@ export { createGlashServer, json, redirect } from './server/server.mjs';
9
9
  export { discoverRoutes, matchRoute, findMiddleware } from './server/router.mjs';
10
10
  export { html, raw, escapeHtml, renderDocument } from './server/html.mjs';
11
11
  export { Image } from './components/image.mjs';
12
+ export { Video } from './components/video.mjs';
@@ -101,12 +101,19 @@ if (el) hydrate(tree, el);`;
101
101
  }
102
102
 
103
103
  /** Compile page + its layout chain into a server module ({ Page, layouts, getServerData, title }). */
104
- export async function loadComponentRoute(pageFile, layouts, root, dev) {
105
- const eb = await esbuild();
106
- if (!eb) throw new Error(MISSING);
104
+ export async function loadComponentRoute(pageFile, layouts, root, dev, force = false) {
107
105
  const id = compId(pageFile, layouts);
108
106
  if (!dev && serverCache.has(id)) return serverCache.get(id);
109
107
  const out = path.join(root, '.glash', 'server', id + '.mjs');
108
+ // Production: if `glash build` already precompiled this module, import it
109
+ // directly — no esbuild needed on the serving host.
110
+ if (!dev && !force && existsSync(out)) {
111
+ const mod = await import(pathToFileURL(out).href);
112
+ serverCache.set(id, mod);
113
+ return mod;
114
+ }
115
+ const eb = await esbuild();
116
+ if (!eb) throw new Error(MISSING);
110
117
  await fs.mkdir(path.dirname(out), { recursive: true });
111
118
  await eb.build({
112
119
  stdin: { contents: serverEntry(pageFile, layouts), resolveDir: path.dirname(pageFile), loader: 'js', sourcefile: 'glash-server-entry.js' },
@@ -61,7 +61,10 @@ export async function createGlashServer({ root = process.cwd(), dev = false } =
61
61
  req.on('close', () => hmrClients.delete(res));
62
62
  return;
63
63
  }
64
- // Client hydration bundles: /_glash/<routeId>.js
64
+ // Static first: in production this serves prebuilt /_glash/<id>.js bundles
65
+ // (written by `glash build`) — no runtime esbuild needed.
66
+ if (await serveStatic(res, outDir, pathname, req, secHeaders)) return;
67
+ // Dynamic hydration bundles (dev, or when not prebuilt): /_glash/<routeId>.js
65
68
  if (pathname.startsWith('/_glash/')) {
66
69
  const id = pathname.slice('/_glash/'.length).replace(/\.js$/, '');
67
70
  const comp = routes.find((r) => isComponentRoute(r.file) && routeId(r.file) === id);
@@ -69,7 +72,6 @@ export async function createGlashServer({ root = process.cwd(), dev = false } =
69
72
  const js = await clientBundle(comp.file, findLayouts(routesDir, comp.file), dev);
70
73
  return send(res, 200, 'text/javascript; charset=utf-8', js, { ...secHeaders, 'cache-control': dev ? 'no-store' : 'public, max-age=31536000, immutable' });
71
74
  }
72
- if (await serveStatic(res, outDir, pathname, req, secHeaders)) return;
73
75
  const match = matchRoute(routes, pathname);
74
76
  if (!match) return send(res, 404, 'text/plain; charset=utf-8', 'Not found', secHeaders);
75
77
  const ctx = makeCtx(req, res, url, match.params);