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 +161 -164
- package/bin/glash.mjs +58 -0
- package/package.json +13 -2
- package/src/auth.mjs +96 -0
- package/src/build.mjs +5 -0
- package/src/config.mjs +2 -0
- package/src/create.mjs +593 -0
- package/src/env.mjs +88 -0
- package/src/index.mjs +2 -0
- package/src/postgres.mjs +77 -0
- package/src/routes.mjs +7 -0
- package/src/server/jsx.mjs +3 -3
- package/src/server/server.mjs +24 -5
- package/src/server-functions.mjs +47 -0
- package/src/sql-runner.mjs +30 -0
- package/src/typed-routes.mjs +65 -0
package/README.md
CHANGED
|
@@ -1,228 +1,225 @@
|
|
|
1
1
|
# glashjs
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
**The Postgres-native full-stack framework for builders who want to ship without DevOps.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## Create a Glash project
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
npx glashjs create my-app
|
|
11
|
+
cd my-app
|
|
12
|
+
npm run dev
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
The create CLI asks for:
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
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 ->
|
|
58
|
-
about.
|
|
59
|
-
blog/[slug].
|
|
60
|
-
docs/[...path].mjs ->
|
|
61
|
-
api/hello.mjs ->
|
|
62
|
-
404.jsx ->
|
|
63
|
-
500.jsx ->
|
|
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/
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
+
```js
|
|
117
|
+
import { sql, transaction } from 'glashjs/postgres';
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
131
|
+
## glashAuth
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
```js
|
|
134
|
+
import { glashAuth } from 'glashjs/auth';
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+
const auth = glashAuth();
|
|
137
|
+
const session = await auth.signin({ email, password });
|
|
138
|
+
const user = await auth.me(session.accessToken);
|
|
139
|
+
```
|
|
131
140
|
|
|
132
|
-
|
|
141
|
+
Required env:
|
|
133
142
|
|
|
134
143
|
```bash
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
+
```js
|
|
153
|
+
// routes/api/functions/contact.mjs
|
|
154
|
+
import { serverFunction } from 'glashjs/server-functions';
|
|
155
|
+
import { sql } from 'glashjs/postgres';
|
|
153
156
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
163
|
+
```js
|
|
164
|
+
import { callServerFunction } from 'glashjs/server-functions';
|
|
165
|
+
|
|
166
|
+
await callServerFunction('/api/functions/contact', { email: 'you@example.com' });
|
|
167
|
+
```
|
|
160
168
|
|
|
161
|
-
##
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
182
|
+
animatedFavicon: true,
|
|
183
|
+
dataPrefixes: ['/api/', '/auth/', '/rest/', '/live', '/stream'],
|
|
174
184
|
});
|
|
175
185
|
```
|
|
176
186
|
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
## Deploy
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npm run build
|
|
191
|
+
npm run deploy
|
|
179
192
|
```
|
|
180
193
|
|
|
181
|
-
|
|
194
|
+
`glashjs deploy`:
|
|
182
195
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
import { startGlashFavicon } from '/glash-favicon.mjs';
|
|
190
|
-
startGlashFavicon(); // config is baked in at build time
|
|
191
|
-
```
|
|
203
|
+
## Asset Optimization
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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.
|
|
4
|
-
"description": "glashjs —
|
|
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 };
|