glashjs 0.12.0 → 0.13.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
@@ -1,228 +1,225 @@
1
1
  # glashjs
2
2
 
3
- The glashdb-native web framework **a Next.js alternative** with the file-based routing, SSR, API routes, layouts, and JSX component model you know from Next, made **fast, offline-capable, and secure by default**. It ships real features instead of promises.
3
+ **The Postgres-native full-stack framework for builders who want to ship without DevOps.**
4
4
 
5
- > **Status:** `0.6.0` the full framework is here: file-based routing, server-side rendering, API routes, JSX components with client hydration, nested layouts, streaming SSR, a dev/prod server, plus the asset optimizer, offline service worker, animated favicon, and secure-by-default headers. Core installs with zero mandatory dependencies.
5
+ glashjs is the GlashDB-native framework layer: **Framework + Hosting + Database + Auth + Deploy**. It gives you file-based routing, typed routes, server functions, API routes, Postgres helpers, glashAuth helpers, SQL runner support, AI deployment prompts, zero-config hosting, automatic env management, and one-command deployment.
6
6
 
7
- ## What it does today
7
+ ## Create a Glash project
8
8
 
9
9
  ```bash
10
- glash build # optimize assets, emit offline SW + PWA + security manifests
11
- glash optimize public # just run the asset optimizer over a directory
10
+ npx glashjs create my-app
11
+ cd my-app
12
+ npm run dev
12
13
  ```
13
14
 
14
- Run against the real glashdb SVG logos (zero optional deps installed):
15
+ The create CLI asks for:
15
16
 
16
- ```
17
- assets 6
18
- original 33.2 KB
19
- optimized 10.1 KB
20
- saved 69.7% ← real Brotli, browser-transparent
21
- offline 10 files precached (works offline after first visit)
22
- security strict CSP + 11 headers
23
- ```
17
+ - Project name and folder.
18
+ - CSS setup: Tailwind, plain CSS, or none.
19
+ - Postgres by default.
20
+ - glashAuth route helpers.
21
+ - SQL runner support.
22
+ - AI deployment prompts.
23
+ - Package manager and dependency install.
24
+
25
+ For non-interactive use:
24
26
 
25
- ## The three pillars
27
+ ```bash
28
+ npx glashjs create my-app --yes --css tailwind --pm npm
29
+ npx glashjs create my-app --yes --css plain --no-install
30
+ ```
26
31
 
27
- ### 1. Asset optimizer — the smallest payload a browser can decode
28
- At build time glashjs re-encodes every asset to the leanest format the client supports, then serves the best variant per request — no config, no runtime image server, originals never touched. How each asset type is handled:
29
- - **Text / SVG / JS / CSS / HTML** → **Brotli + Gzip** (`zlib`, built in). Real 4–8× on text/SVG. The browser decompresses transparently via `Content-Encoding` — compress on build, decompress in the browser.
30
- - **jpg / png / webp** → **AVIF + WebP** variants (needs optional `sharp`). Typically 3–10× vs unoptimized originals.
31
- - **mp4 / mov / webm** → **AV1** + poster frame (needs optional `ffmpeg`).
32
- - Emits `glash-assets.manifest.json` so the glashdb edge (or any server) serves the best variant per client. Originals are never mutated.
32
+ The generated app includes:
33
33
 
34
- > **glashjs installs with zero dependencies.** Brotli/Gzip works out of the box.
35
- > To enable image/video transcoding, add the optional enhancers yourself:
36
- > `npm i sharp` (AVIF/WebP) and install `ffmpeg` on your PATH (AV1). They're
37
- > declared as **optional peers**, so a plain `npm i glashjs` pulls nothing else.
34
+ ```text
35
+ routes/
36
+ index.jsx
37
+ _layout.jsx
38
+ api/health.mjs
39
+ api/auth/signin.mjs
40
+ api/auth/signup.mjs
41
+ api/auth/me.mjs
42
+ api/functions/contact.mjs
43
+ api/sql/run.mjs
44
+ db/schema.sql
45
+ .glash/prompts/deploy.md
46
+ glash.config.mjs
47
+ .env.example
48
+ .env.local
49
+ ```
50
+
51
+ ## First-class features
52
+
53
+ - **File-based routing**: every file in `routes/` becomes a page or API route.
54
+ - **Typed routes**: `glashjs typegen` writes `glash-routes.d.ts` with literal page/API route unions.
55
+ - **Server functions**: wrap trusted server code with `serverFunction()` and call it from the browser.
56
+ - **API routes**: method exports such as `GET`, `POST`, `PUT`, and `DELETE`.
57
+ - **Postgres by default**: `glashjs/postgres` reads `DATABASE_URL` and exposes `sql`, `query`, and transactions.
58
+ - **glashAuth built in**: `glashjs/auth` calls the real GlashDB auth API under `/api/auth/v1/:projectId`.
59
+ - **SQL runner support**: `glashjs sql db/schema.sql` runs checked-in SQL against `DATABASE_URL`.
60
+ - **AI deployment prompts**: starters include `.glash/prompts/deploy.md` for agents and deployment review.
61
+ - **Zero-config hosting**: `glashjs deploy` builds and hands off to the GlashDB deploy CLI.
62
+ - **Automatic env management**: `.env`, `.env.local`, and mode-specific env files load automatically; hosted deploys use GlashDB project env vars.
63
+ - **One-command deployment**: `npm run deploy`.
64
+
65
+ ## Commands
38
66
 
39
- ### 2. Offline layer — usable with no internet after first visit
40
- Generates a **Service Worker** (`glash-sw.js`) + PWA manifest that precache the app shell and hashed assets into small cached files. Strategies:
41
- - **assets** cache-first (immutable, hash-busted on deploy)
42
- - **HTML** stale-while-revalidate (instant, self-healing)
43
- - **`/api` `/rest` `/auth` `/live` `/stream`** → **network-first**, so offline mode degrades *exactly* at live/updated data and streaming — the site keeps working, just without fresh data. (Configurable via `dataPrefixes`.)
67
+ ```bash
68
+ glashjs create [name] # scaffold a new Glash project
69
+ glashjs dev [--port 3000] # dev server with live reload and LAN URL
70
+ glashjs build # optimize assets, precompile routes, emit PWA/security manifests
71
+ glashjs serve # serve the production build
72
+ glashjs typegen # generate typed route declarations
73
+ glashjs sql <file.sql> # run SQL against DATABASE_URL
74
+ glashjs deploy # build, then deploy to GlashDB
75
+ glashjs optimize public # optimize public assets only
76
+ glashjs migrate # scaffold routes from an existing Next app
77
+ glashjs update # update glashjs
78
+ ```
44
79
 
45
- ### 3. Security secure by default
46
- glashjs ships strong, opinionated defaults so you're secure unless you loosen them:
47
- - **Strict CSP** with no `'unsafe-inline'` scripts (XSS-via-injection blocked by default)
48
- - HSTS, `X-Content-Type-Options`, `X-Frame-Options: DENY`, COOP/COEP/CORP isolation, tight `Permissions-Policy` & `Referrer-Policy`
49
- - **Subresource Integrity** helper (`sri()`) for build assets
50
- - Emitted to `glash-headers.json` for the edge, plus a `glashSecurity()` Express middleware
80
+ `glashjs` installs two bins: `glashjs` and `glash`. In projects that also install the GlashDB deploy CLI, prefer `glashjs <cmd>` so the package names stay clear.
51
81
 
52
- ## Routing, SSR & API (new in 0.1)
53
- glashjs now has a real server runtime — **file-based routing**, **server-side rendering**, and **API routes** — on Node built-ins, zero deps.
82
+ ## Routing
54
83
 
55
- ```
84
+ ```text
56
85
  routes/
57
- index.mjs -> / (SSR page)
58
- about.mjs -> /about
59
- blog/[slug].mjs -> /blog/:slug (dynamic segment -> ctx.params.slug)
60
- docs/[...path].mjs -> /docs/* (catch-all)
61
- api/hello.mjs -> /api/hello (API: export GET/POST/…)
62
- 404.jsx -> custom not-found page (any unmatched path -> 404)
63
- 500.jsx -> custom error page (rendered in production on a throw)
86
+ index.mjs -> /
87
+ about.jsx -> /about
88
+ blog/[slug].jsx -> /blog/:slug
89
+ docs/[...path].mjs -> /docs/*
90
+ api/hello.mjs -> /api/hello
91
+ 404.jsx -> custom not-found page
92
+ 500.jsx -> custom error page
64
93
  ```
65
94
 
66
95
  ```js
67
- // routes/index.mjs — a server-rendered page (XSS-safe `html` template)
68
- import { html } from 'glashjs';
69
- export default (ctx) => ({ title: 'Home', body: html`<h1>Hello ${ctx.query.name}</h1>` });
70
-
71
- // routes/api/hello.mjs — an API route
96
+ // routes/api/hello.mjs
72
97
  import { json } from 'glashjs';
73
- export const GET = (ctx) => ({ ok: true, name: ctx.query.name });
74
- export const POST = (ctx) => json({ created: ctx.body }, { status: 201 });
75
- ```
76
98
 
77
- ```bash
78
- glashjs dev # dev server (live reload) — prints a Local + Network (LAN IP) URL
79
- # ➜ Local: http://localhost:3000
80
- # ➜ Network: http://192.168.1.57:3000 (open from your phone/other devices)
81
- glashjs serve # production server over routes/ + built assets (Brotli-negotiated)
82
- glashjs update # update glashjs to the latest published version
83
- glashjs build # optimize assets + precompile routes
84
- glashjs deploy # build, then deploy to glashdb
99
+ export const GET = (ctx) => json({
100
+ ok: true,
101
+ name: ctx.query.name ?? 'builder',
102
+ });
85
103
  ```
86
104
 
87
- > glashjs installs **two** bins — `glashjs` and `glash` — both run the same CLI.
88
- > Use **`glashjs <cmd>`** in projects that also have the `@glash/cli`/`glashdb`
89
- > deploy CLI installed, since that package owns the `glash` name there.
90
-
91
- **`<Image>`** (better than `next/image` — no runtime image server, uses the build's AVIF/WebP):
92
105
  ```jsx
93
- import { Image } from 'glashjs/image';
94
- <Image src="/hero.png" alt="Hero" width={1200} height={630} />
95
- // -> <picture><source …avif><source …webp><img loading=lazy decoding=async></picture>
96
- ```
106
+ // routes/index.jsx
107
+ export const metadata = { title: 'Home' };
97
108
 
98
- **File-based middleware** (`_middleware.mjs`, runs root→leaf before the route):
99
- ```js
100
- import { redirect } from 'glashjs';
101
- export default (ctx) => { if (!ctx.headers.authorization) return redirect('/login'); };
109
+ export default function Home() {
110
+ return <main>Ship without DevOps.</main>;
111
+ }
102
112
  ```
103
113
 
104
- **SEO metadata** + **client-side navigation**:
105
- ```jsx
106
- import { Link } from 'glashjs/link';
107
- export const metadata = { title: 'About', description: '…', openGraph: { image: '/og.png' } };
108
- export default () => <Link href="/">Home</Link>; // SPA swap, no full reload; crawlable <a>
109
- ```
114
+ ## Postgres
110
115
 
111
- Every response carries the secure-by-default headers; static files are served from the build with Brotli negotiation.
116
+ ```js
117
+ import { sql, transaction } from 'glashjs/postgres';
112
118
 
113
- ### JSX components + client hydration (new in 0.2)
114
- Author pages as **JSX components**, server-render them, and **hydrate** in the browser so hooks work — real interactivity, the Next-style model. Built on **Preact** (React-compatible) + **esbuild**, added as *optional peers* (`npm i preact preact-render-to-string esbuild`); glashjs core stays zero-dep.
119
+ const users = await sql`
120
+ select id, email
121
+ from users
122
+ order by created_at desc
123
+ limit ${20}
124
+ `;
115
125
 
116
- ```jsx
117
- // routes/counter.jsx — SSR + hydrated, the button is interactive
118
- import { useState } from 'preact/hooks';
119
- export function getServerData(ctx) { return { start: Number(ctx.query.start || 0) }; } // server props
120
- export default function Counter({ start = 0 }) {
121
- const [n, setN] = useState(start);
122
- return <button onClick={() => setN(n + 1)}>count is {n}</button>;
123
- }
126
+ await transaction(async (db) => {
127
+ await db.query('insert into events(name) values ($1)', ['signup']);
128
+ });
124
129
  ```
125
130
 
126
- 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'`).
131
+ ## glashAuth
127
132
 
128
- **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. **Suspense streaming** is in too — wrap a data-dependent subtree in `<Suspense fallback={…}>` (from `preact/compat`) and the shell + fallback flush immediately, then each boundary streams in as its data resolves (`renderToPipeableStream`), with preact's inline swap scripts nonce-injected so the strict CSP holds. **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).
133
+ ```js
134
+ import { glashAuth } from 'glashjs/auth';
129
135
 
130
- ## Migrating from Next.js
136
+ const auth = glashAuth();
137
+ const session = await auth.signin({ email, password });
138
+ const user = await auth.me(session.accessToken);
139
+ ```
131
140
 
132
- glashjs is a Next.js alternative with the same conventions, so an existing Next app can be moved over with one command:
141
+ Required env:
133
142
 
134
143
  ```bash
135
- npx glashjs migrate # scaffold routes/ from your app/ or pages/ + a report
136
- npx glashjs migrate --dry-run # preview the mapping first, write nothing
144
+ GLASHDB_API_URL="https://api.glashdb.com/api"
145
+ GLASHDB_PROJECT_ID="..."
146
+ GLASHDB_ANON_KEY="..."
147
+ DATABASE_URL="postgresql://..."
137
148
  ```
138
149
 
139
- `glashjs migrate` does the **mechanical** migration and writes a `MIGRATION.md` punch-list — it never deletes your Next code:
140
-
141
- | Next.js | → | glashjs |
142
- |---|---|---|
143
- | `app/page.tsx` | → | `routes/index.tsx` |
144
- | `app/blog/[slug]/page.tsx` | → | `routes/blog/[slug].tsx` |
145
- | `app/layout.tsx` | → | `routes/_layout.tsx` |
146
- | `app/api/x/route.ts` (`GET`/`POST`) | → | `routes/api/x.ts` (same `GET`/`POST` exports) |
147
- | `pages/index.tsx`, `pages/api/x.ts` | → | `routes/index.tsx`, `routes/api/x.ts` |
148
- | `middleware.ts` | → | `routes/_middleware.mjs` |
149
- | `getServerSideProps` | → | `getServerData(ctx)` (auto-renamed) |
150
- | `next/link`, `next/image` | → | `glashjs/link`, `glashjs/image` (auto-rewritten) |
150
+ ## Server Functions
151
151
 
152
- Your **React/Next components run as-is**: glashjs aliases `react`/`react-dom` → `preact/compat` in its build, and runs **TypeScript** routes/API/middleware directly (esbuild). `'use client'` is stripped (glashjs hydrates by default).
152
+ ```js
153
+ // routes/api/functions/contact.mjs
154
+ import { serverFunction } from 'glashjs/server-functions';
155
+ import { sql } from 'glashjs/postgres';
153
156
 
154
- ```bash
155
- npm i glashjs preact preact-render-to-string esbuild
156
- npx glashjs dev # then work the TODOs the report flagged
157
+ export const POST = serverFunction(async ({ email }) => {
158
+ await sql`insert into contacts(email) values (${email}) on conflict do nothing`;
159
+ return { saved: true };
160
+ });
157
161
  ```
158
162
 
159
- **Honest scope:** the mechanical 80% is automatic. **React Server Components**, **Server Actions**, `getStaticProps` (SSG), and `next/navigation`/`next/headers`/Supabase-server-auth are **flagged in `MIGRATION.md` for hands-on porting** — they don't map 1:1 and glashjs won't pretend they do.
163
+ ```js
164
+ import { callServerFunction } from 'glashjs/server-functions';
165
+
166
+ await callServerFunction('/api/functions/contact', { email: 'you@example.com' });
167
+ ```
160
168
 
161
- ## Usage
169
+ ## Configuration
162
170
 
163
171
  ```js
164
172
  // glash.config.mjs
165
173
  import { defineConfig } from 'glashjs/config';
166
174
 
167
175
  export default defineConfig({
168
- name: 'My Site',
176
+ name: 'My Glash App',
177
+ routesDir: 'routes',
169
178
  publicDir: 'public',
170
179
  outDir: '.glash/out',
180
+ stylesheets: ['/app.css'],
171
181
  offline: true,
172
- dataPrefixes: ['/api/', '/rest/', '/live'], // network-first (no stale live data offline)
173
- // favicon defaults to the official glashdb logo bundled with glashjs
182
+ animatedFavicon: true,
183
+ dataPrefixes: ['/api/', '/auth/', '/rest/', '/live', '/stream'],
174
184
  });
175
185
  ```
176
186
 
177
- ```js
178
- import { build, optimizeAssets, securityHeaders, sri } from 'glashjs';
187
+ ## Deploy
188
+
189
+ ```bash
190
+ npm run build
191
+ npm run deploy
179
192
  ```
180
193
 
181
- The preview favicon defaults to the **official glashdb logo** (`templates/favicon.svg`).
194
+ `glashjs deploy`:
182
195
 
183
- ### Animated favicon (on by default)
184
- Every build also emits an **animated favicon** — the bundled glash mark, your own
185
- animated SVG/GIF, or a set of frames that cycle in the tab. The build writes a tiny
186
- runtime; call it once and it animates the tab icon, pausing while the tab is hidden:
196
+ 1. Loads env files.
197
+ 2. Builds the app.
198
+ 3. Optimizes public assets.
199
+ 4. Precompiles JSX routes and client bundles.
200
+ 5. Emits PWA, offline, security, and asset manifests.
201
+ 6. Hands off to the GlashDB CLI for upload, env sync, build logs, live URL, and hosting.
187
202
 
188
- ```js
189
- import { startGlashFavicon } from '/glash-favicon.mjs';
190
- startGlashFavicon(); // config is baked in at build time
191
- ```
203
+ ## Asset Optimization
192
204
 
193
- ```js
194
- // glash.config.mjs
195
- animatedFavicon: true, // bundled animated glash mark (default)
196
- // animatedFavicon: '/brand/logo-animated.svg' // your own animated SVG/GIF
197
- // animatedFavicon: { frames: ['/f0.svg','/f1.svg'], fps: 10 }
205
+ glashjs optimizes assets at build time:
206
+
207
+ - Text, SVG, JS, CSS, and HTML -> Brotli + Gzip.
208
+ - Images -> AVIF/WebP when optional `sharp` is installed.
209
+ - Video -> AV1/WebM with ffmpeg available.
210
+ - Emits `glash-assets.manifest.json` for edge/runtime negotiation.
211
+
212
+ Core remains light. Optional image/video enhancers are installed only when you choose them.
213
+
214
+ ## Migrating an Existing App
215
+
216
+ ```bash
217
+ npx glashjs migrate
218
+ npx glashjs migrate --dry-run
198
219
  ```
199
220
 
200
- ## What's built (a Next.js alternative)
201
- - [x] Asset optimizer (Brotli/Gzip real; AVIF/WebP/AV1 via optional sharp/ffmpeg)
202
- - [x] Offline Service Worker + PWA manifest
203
- - [x] Secure-by-default headers + CSP + SRI
204
- - [x] File-based routing (pages + `api/`, dynamic `[param]` & catch-all `[...path]`)
205
- - [x] Server-side rendering (XSS-safe `html` templates) + full-document runtime
206
- - [x] API routes (per-method handlers, JSON body parsing, typed `json()` responses)
207
- - [x] Dev/prod server with live route reload + Brotli-negotiated static serving
208
- - [x] JSX components + client-side hydration (Preact + esbuild) — CSP-safe with nonces
209
- - [x] Client-JS bundling (esbuild) per route
210
- - [x] Nested layouts (`_layout.jsx` composing root→leaf, server + hydration)
211
- - [x] Streaming SSR (shell flushed before the component renders)
212
- - [x] Instant HMR — in-place soft re-render on save (no full reload, no flash; preserves scroll, focus, and form input)
213
- - [x] `<Image>` — zero-config `<picture>` with AVIF/WebP from the optimizer (beats next/image: no runtime image server)
214
- - [x] `<Video>` — `<video>` with AV1/WebM + mp4 fallback + auto poster
215
- - [x] File-based middleware (`_middleware.mjs`, root→leaf) — auth, redirects, headers
216
- - [x] Production route precompile (`glash build` bakes server modules + minified client bundles → no runtime esbuild on `glash serve`)
217
- - [x] SEO metadata API (`export const metadata` → title, description, Open Graph, Twitter cards)
218
- - [x] `<Link>` client-side navigation (SPA swap of `#glash-root` + re-hydrate; progressive-enhancement `<a>`)
219
- - [x] `glash deploy` → glashdb (builds, then hands off to the `glashdb` CLI)
220
- - [x] Production-grade runtime — custom `404`/`500` routes, dev error overlay, HEAD support, Range requests + streamed static (video seeking), graceful mid-stream error handling
221
- - [x] Suspense streaming (`renderToPipeableStream` — fallback in the shell, each boundary streams in as its data resolves; CSP-safe via per-request nonce injection)
222
- - [ ] React-Fast-Refresh (`useState` preservation via `@prefresh`; browser-verified), edge adapter
223
- - [ ] `<Image>` / `<Video>` components that emit `<picture>`/`<source>` from the manifest
224
- - [ ] Edge adapter for the glashdb Worker (serve `.br`/`.avif` by `Accept`)
225
- - [ ] `glash deploy` → glashdb hosting in one command
226
-
227
- ## Design stance
228
- glashjs is **a Next.js alternative** — it keeps the conventions you know from Next (file-based routing, SSR, layouts, the component model) and composes proven primitives rather than reinventing them. The value is in the **defaults**: every glashjs site is optimized, offline-capable, and secure out of the box.
221
+ The migration command scaffolds `routes/` beside your existing app and writes a report. It handles mechanical route mapping and flags framework-specific code that needs hands-on porting.
222
+
223
+ ## Design Stance
224
+
225
+ glashjs is built for builders who want the product surface and the production platform to work together. Your app code, Postgres database, auth system, environment variables, deploy logs, domains, and live URL all belong to one GlashDB project.
package/bin/glash.mjs CHANGED
@@ -8,6 +8,8 @@ import { createGlashServer } from '../src/server/server.mjs';
8
8
  import { deploy } from '../src/deploy.mjs';
9
9
  import { update } from '../src/update.mjs';
10
10
  import { migrate } from '../src/migrate.mjs';
11
+ import { createProject } from '../src/create.mjs';
12
+ import { generateTypedRoutes } from '../src/typed-routes.mjs';
11
13
 
12
14
  const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
13
15
  const [, , cmd, ...rest] = process.argv;
@@ -17,6 +19,26 @@ function arg(name, fallback) {
17
19
  return i >= 0 && rest[i + 1] ? rest[i + 1] : fallback;
18
20
  }
19
21
 
22
+ function flag(name) {
23
+ return rest.includes(name);
24
+ }
25
+
26
+ function optionBool(disableFlag, fallback = true) {
27
+ return rest.includes(disableFlag) ? false : fallback;
28
+ }
29
+
30
+ function firstPositional(args = rest) {
31
+ for (let i = 0; i < args.length; i += 1) {
32
+ const item = args[i];
33
+ if (item.startsWith('--')) {
34
+ if (!item.includes('=') && args[i + 1] && !args[i + 1].startsWith('--')) i += 1;
35
+ continue;
36
+ }
37
+ return item;
38
+ }
39
+ return undefined;
40
+ }
41
+
20
42
  // LAN IPv4 addresses, so the dev server prints a Network URL you can open from
21
43
  // your phone or another device on the same network.
22
44
  function lanAddresses() {
@@ -49,10 +71,43 @@ async function serve(dev) {
49
71
 
50
72
  async function main() {
51
73
  switch (cmd) {
74
+ case 'create':
75
+ case 'init':
76
+ case 'new': {
77
+ await createProject({
78
+ name: firstPositional(),
79
+ css: arg('--css', undefined),
80
+ packageManager: arg('--pm', arg('--package-manager', undefined)),
81
+ yes: flag('--yes') || flag('-y'),
82
+ force: flag('--force'),
83
+ install: optionBool('--no-install', undefined),
84
+ git: optionBool('--no-git', undefined),
85
+ postgres: optionBool('--no-postgres', undefined),
86
+ auth: optionBool('--no-auth', undefined),
87
+ sqlRunner: optionBool('--no-sql-runner', undefined),
88
+ aiPrompts: optionBool('--no-ai-prompts', undefined),
89
+ });
90
+ break;
91
+ }
52
92
  case 'build': {
53
93
  await build({ root: arg('--root', process.cwd()) });
54
94
  break;
55
95
  }
96
+ case 'typegen':
97
+ case 'routes': {
98
+ await generateTypedRoutes({ root: arg('--root', process.cwd()), outFile: arg('--out', 'glash-routes.d.ts') });
99
+ break;
100
+ }
101
+ case 'sql': {
102
+ const files = rest.filter((item, i) => !item.startsWith('--') && rest[i - 1] !== '--root');
103
+ if (!files.length) throw new Error('usage: glashjs sql <file.sql> [...more.sql]');
104
+ const { runSqlFiles } = await import('../src/sql-runner.mjs');
105
+ const results = await runSqlFiles(files, { root: arg('--root', process.cwd()) });
106
+ for (const result of results) {
107
+ console.log(`sql ${result.file} -> ${result.rowCount} row(s), ${result.ms}ms`);
108
+ }
109
+ break;
110
+ }
56
111
  case 'deploy': {
57
112
  const passthrough = rest.filter((a, i) => a !== '--dry-run' && a !== '--root' && rest[i - 1] !== '--root');
58
113
  await deploy({ root: arg('--root', process.cwd()), dryRun: rest.includes('--dry-run'), args: passthrough });
@@ -89,9 +144,12 @@ async function main() {
89
144
  console.log(`glashjs — fast, offline-capable, hard-to-hack sites
90
145
 
91
146
  Usage: (run as "glashjs <cmd>"; "glash <cmd>" also works unless the glashdb deploy CLI owns that name)
147
+ glashjs create [name] Create a new Glash project (interactive)
92
148
  glashjs dev [--port 3000] Run the dev server (routing, SSR, API, live reload) + Network preview URL
93
149
  glashjs serve [--port 3000] Run the production server over routes/ + built assets
94
150
  glashjs build [--root <dir>] Optimize assets, precompile routes, generate offline SW + PWA + security
151
+ glashjs typegen Generate typed route declarations from routes/
152
+ glashjs sql <file.sql> Run a SQL file against DATABASE_URL
95
153
  glashjs migrate [--dry-run] Migrate a Next.js project to glashjs (scaffold routes/ + report)
96
154
  glashjs update Update glashjs to the latest published version
97
155
  glashjs deploy [--dry-run] Build, then deploy to glashdb (hands off to the glashdb CLI)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "glashjs",
3
- "version": "0.12.0",
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.",
3
+ "version": "0.13.0",
4
+ "description": "glashjs — The Postgres-native full-stack framework for builders who want to ship without DevOps. Framework, hosting, database, auth, and deploy in one GlashDB-native runtime.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "glashjs": "bin/glash.mjs",
@@ -11,6 +11,13 @@
11
11
  ".": "./src/index.mjs",
12
12
  "./config": "./src/config.mjs",
13
13
  "./security": "./src/security/headers.mjs",
14
+ "./env": "./src/env.mjs",
15
+ "./postgres": "./src/postgres.mjs",
16
+ "./auth": "./src/auth.mjs",
17
+ "./server-functions": "./src/server-functions.mjs",
18
+ "./sql": "./src/sql-runner.mjs",
19
+ "./typed-routes": "./src/typed-routes.mjs",
20
+ "./routes": "./src/routes.mjs",
14
21
  "./image": "./src/components/image.mjs",
15
22
  "./video": "./src/components/video.mjs",
16
23
  "./link": "./src/components/link.mjs",
@@ -28,9 +35,13 @@
28
35
  "esbuild": ">=0.20.0",
29
36
  "preact": "^10.25.0",
30
37
  "preact-render-to-string": "^6.5.0",
38
+ "pg": "^8.16.0",
31
39
  "sharp": "^0.34.5"
32
40
  },
33
41
  "peerDependenciesMeta": {
42
+ "pg": {
43
+ "optional": true
44
+ },
34
45
  "sharp": {
35
46
  "optional": true
36
47
  },
package/src/auth.mjs ADDED
@@ -0,0 +1,96 @@
1
+ // glashAuth helpers for glashjs
2
+ // ---------------------------------------------------------------------------
3
+ // Thin client/server wrapper for the GlashDB auth API:
4
+ // /api/auth/v1/:projectId/signup
5
+ // /api/auth/v1/:projectId/signin
6
+ // /api/auth/v1/:projectId/me
7
+
8
+ export function glashAuth(options = {}) {
9
+ const apiUrl = trimSlash(options.apiUrl ?? process.env.GLASHDB_API_URL ?? 'https://api.glashdb.com/api');
10
+ const projectId = options.projectId
11
+ ?? process.env.GLASHDB_PROJECT_ID
12
+ ?? process.env.GLASHAUTH_PROJECT_ID
13
+ ?? process.env.NEXT_PUBLIC_GLASHDB_PROJECT_ID;
14
+ const anonKey = options.anonKey
15
+ ?? process.env.GLASHDB_ANON_KEY
16
+ ?? process.env.GLASHAUTH_ANON_KEY
17
+ ?? process.env.NEXT_PUBLIC_GLASHAUTH_ANON_KEY;
18
+
19
+ if (!projectId) throw new Error('GLASHDB_PROJECT_ID is required for glashAuth');
20
+ if (!anonKey) throw new Error('GLASHDB_ANON_KEY is required for glashAuth');
21
+
22
+ async function request(path, { body, accessToken, method = 'POST', headers = {} } = {}) {
23
+ const res = await fetch(`${apiUrl}/auth/v1/${encodeURIComponent(projectId)}/${path.replace(/^\/+/, '')}`, {
24
+ method,
25
+ headers: {
26
+ apikey: anonKey,
27
+ 'content-type': 'application/json',
28
+ ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
29
+ ...headers,
30
+ },
31
+ body: body === undefined ? undefined : JSON.stringify(body),
32
+ });
33
+ const data = await readJson(res);
34
+ if (!res.ok) {
35
+ const message = data?.message || data?.error || `glashAuth ${path} failed with HTTP ${res.status}`;
36
+ throw new Error(message);
37
+ }
38
+ return data;
39
+ }
40
+
41
+ return {
42
+ projectId,
43
+ apiUrl,
44
+ signup: ({ email, password, fullName }) => request('signup', { body: { email, password, fullName } }),
45
+ signin: ({ email, password }) => request('signin', { body: { email, password } }),
46
+ signout: (refreshToken) => request('signout', { body: { refreshToken } }),
47
+ refresh: (refreshToken) => request('refresh', { body: { refreshToken } }),
48
+ magicLink: ({ email, redirectTo }) => request('magic-link/request', { body: { email, redirectTo } }),
49
+ me: (accessToken) => request('me', { method: 'GET', accessToken }),
50
+ googleAuthorizeUrl: (redirectTo) => {
51
+ const url = new URL(`${apiUrl}/auth/v1/${encodeURIComponent(projectId)}/google/authorize`);
52
+ if (redirectTo) url.searchParams.set('redirect_to', redirectTo);
53
+ return url.toString();
54
+ },
55
+ };
56
+ }
57
+
58
+ export function readSessionCookie(ctx, name = 'glash_session') {
59
+ const cookie = ctx?.headers?.cookie || ctx?.req?.headers?.cookie || '';
60
+ const found = cookie.split(';').map((part) => part.trim()).find((part) => part.startsWith(`${name}=`));
61
+ if (!found) return null;
62
+ try {
63
+ return JSON.parse(Buffer.from(decodeURIComponent(found.slice(name.length + 1)), 'base64url').toString('utf8'));
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ export function sessionCookie(session, options = {}) {
70
+ const name = options.name ?? 'glash_session';
71
+ const maxAge = options.maxAge ?? 60 * 60 * 24 * 30;
72
+ const encoded = Buffer.from(JSON.stringify(session), 'utf8').toString('base64url');
73
+ const parts = [
74
+ `${name}=${encodeURIComponent(encoded)}`,
75
+ 'Path=/',
76
+ `Max-Age=${maxAge}`,
77
+ 'HttpOnly',
78
+ 'SameSite=Lax',
79
+ ];
80
+ if (options.secure ?? process.env.NODE_ENV === 'production') parts.push('Secure');
81
+ return parts.join('; ');
82
+ }
83
+
84
+ export function clearSessionCookie(name = 'glash_session') {
85
+ return `${name}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`;
86
+ }
87
+
88
+ async function readJson(res) {
89
+ const text = await res.text();
90
+ if (!text) return null;
91
+ try { return JSON.parse(text); } catch { return { message: text }; }
92
+ }
93
+
94
+ function trimSlash(value) {
95
+ return String(value || '').replace(/\/+$/, '');
96
+ }
package/src/build.mjs CHANGED
@@ -13,6 +13,7 @@ import { securityHeaders } from './security/headers.mjs';
13
13
  import { discoverRoutes } from './server/router.mjs';
14
14
  import { isComponentRoute, findLayouts, loadComponentRoute, clientBundle, routeId } from './server/jsx.mjs';
15
15
  import { loadConfig } from './config.mjs';
16
+ import { loadEnvFiles } from './env.mjs';
16
17
 
17
18
  // Precompile JSX routes: server modules (-> .glash/server) + minified client
18
19
  // hydration bundles (-> outDir/_glash/<id>.js). Production `glash serve` then
@@ -56,6 +57,7 @@ function pwaManifest(cfg, version) {
56
57
  }
57
58
 
58
59
  export async function build({ root = process.cwd(), log = console.log } = {}) {
60
+ loadEnvFiles(root, { mode: process.env.NODE_ENV || 'production' });
59
61
  const cfg = await loadConfig(root);
60
62
  const publicDir = path.resolve(root, cfg.publicDir);
61
63
  const outDir = path.resolve(root, cfg.outDir);
@@ -77,6 +79,9 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
77
79
  manifest.generatedAt = new Date().toISOString();
78
80
 
79
81
  await fs.mkdir(outDir, { recursive: true });
82
+ if (existsSync(publicDir)) {
83
+ await fs.cp(publicDir, outDir, { recursive: true, force: true });
84
+ }
80
85
 
81
86
  // Precompile JSX routes (server modules + client bundles) for production.
82
87
  let routesBuilt = { compiled: 0 };