glashjs 0.11.2 → 0.12.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 +31 -0
- package/bin/glash.mjs +5 -0
- package/package.json +1 -1
- package/src/index.mjs +1 -0
- package/src/migrate.mjs +179 -0
- package/src/server/jsx.mjs +34 -0
- package/src/server/router.mjs +3 -2
- package/src/server/server.mjs +13 -4
package/README.md
CHANGED
|
@@ -127,6 +127,37 @@ Hydration is **CSP-safe**: server props ride in a non-executed `<script type="ap
|
|
|
127
127
|
|
|
128
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).
|
|
129
129
|
|
|
130
|
+
## Migrating from Next.js
|
|
131
|
+
|
|
132
|
+
glashjs is a Next.js alternative with the same conventions, so an existing Next app can be moved over with one command:
|
|
133
|
+
|
|
134
|
+
```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
|
|
137
|
+
```
|
|
138
|
+
|
|
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) |
|
|
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).
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm i glashjs preact preact-render-to-string esbuild
|
|
156
|
+
npx glashjs dev # then work the TODOs the report flagged
|
|
157
|
+
```
|
|
158
|
+
|
|
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.
|
|
160
|
+
|
|
130
161
|
## Usage
|
|
131
162
|
|
|
132
163
|
```js
|
package/bin/glash.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { optimizeAssets } from '../src/assets/optimize.mjs';
|
|
|
7
7
|
import { createGlashServer } from '../src/server/server.mjs';
|
|
8
8
|
import { deploy } from '../src/deploy.mjs';
|
|
9
9
|
import { update } from '../src/update.mjs';
|
|
10
|
+
import { migrate } from '../src/migrate.mjs';
|
|
10
11
|
|
|
11
12
|
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
12
13
|
const [, , cmd, ...rest] = process.argv;
|
|
@@ -61,6 +62,9 @@ async function main() {
|
|
|
61
62
|
case 'upgrade':
|
|
62
63
|
await update({ root: arg('--root', process.cwd()) });
|
|
63
64
|
break;
|
|
65
|
+
case 'migrate':
|
|
66
|
+
await migrate({ root: arg('--root', process.cwd()), dryRun: rest.includes('--dry-run') });
|
|
67
|
+
break;
|
|
64
68
|
case 'dev':
|
|
65
69
|
await serve(true);
|
|
66
70
|
break;
|
|
@@ -88,6 +92,7 @@ Usage: (run as "glashjs <cmd>"; "glash <cmd>" also works unless the glashdb depl
|
|
|
88
92
|
glashjs dev [--port 3000] Run the dev server (routing, SSR, API, live reload) + Network preview URL
|
|
89
93
|
glashjs serve [--port 3000] Run the production server over routes/ + built assets
|
|
90
94
|
glashjs build [--root <dir>] Optimize assets, precompile routes, generate offline SW + PWA + security
|
|
95
|
+
glashjs migrate [--dry-run] Migrate a Next.js project to glashjs (scaffold routes/ + report)
|
|
91
96
|
glashjs update Update glashjs to the latest published version
|
|
92
97
|
glashjs deploy [--dry-run] Build, then deploy to glashdb (hands off to the glashdb CLI)
|
|
93
98
|
glashjs optimize [<dir>] Just run the asset optimizer over a directory
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glashjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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": {
|
package/src/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ export { defineConfig, loadConfig, DEFAULT_CONFIG } from './config.mjs';
|
|
|
3
3
|
export { build } from './build.mjs';
|
|
4
4
|
export { deploy } from './deploy.mjs';
|
|
5
5
|
export { update } from './update.mjs';
|
|
6
|
+
export { migrate } from './migrate.mjs';
|
|
6
7
|
export { optimizeAssets } from './assets/optimize.mjs';
|
|
7
8
|
export { generateAnimatedFavicon } from './assets/animated-favicon.mjs';
|
|
8
9
|
export { generateServiceWorker } from './offline/generate-sw.mjs';
|
package/src/migrate.mjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// glashjs migrate — convert a Next.js project to glashjs conventions.
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Honest scope: this does the MECHANICAL migration (file/route mapping, import
|
|
4
|
+
// rewrites, config + scripts) and writes a MIGRATION.md report listing exactly
|
|
5
|
+
// what still needs hands-on porting (RSC/server actions/SSG/next-specific APIs).
|
|
6
|
+
// It never deletes your Next code — it scaffolds `routes/` alongside it.
|
|
7
|
+
import { promises as fs, existsSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const PAGE_EXT = /\.(tsx|jsx|ts|js)$/;
|
|
11
|
+
|
|
12
|
+
// next import -> glashjs equivalent (safe, mechanical rewrites)
|
|
13
|
+
const IMPORT_REWRITES = [
|
|
14
|
+
[/(['"])next\/link\1/g, "'glashjs/link'"],
|
|
15
|
+
[/(['"])next\/image\1/g, "'glashjs/image'"],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// patterns that need a human — surfaced in the report, not silently "fixed"
|
|
19
|
+
const MANUAL = [
|
|
20
|
+
[/from\s+['"]next\/navigation['"]/, 'next/navigation (useRouter/usePathname/redirect) → glashjs <Link> + redirect() + ctx.url'],
|
|
21
|
+
[/from\s+['"]next\/headers['"]/, 'next/headers (cookies/headers) → read ctx.headers in getServerData / API handlers'],
|
|
22
|
+
[/from\s+['"]next\/font/, 'next/font → load fonts via <link rel="preload"> or CSS @font-face'],
|
|
23
|
+
[/getServerSideProps/, 'getServerSideProps → rename to `export function getServerData(ctx)` (same role)'],
|
|
24
|
+
[/getStaticProps|getStaticPaths/, 'getStaticProps/Paths (SSG) → use getServerData (SSR) or precompute at build'],
|
|
25
|
+
[/['"]use server['"]/, 'Server Actions ("use server") → port to a glashjs API route (routes/api/*)'],
|
|
26
|
+
[/createServerClient|auth\.getUser\(\)|supabase\.auth/, 'Supabase server auth → re-wire via glashjs _middleware.mjs + API routes'],
|
|
27
|
+
[/export\s+const\s+(dynamic|revalidate|fetchCache)/, 'Route segment config (dynamic/revalidate) → handle in getServerData / cache headers'],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function srcDirs(root) {
|
|
31
|
+
// Next supports app/ or pages/, optionally under src/.
|
|
32
|
+
const bases = ['app', 'pages', 'src/app', 'src/pages'];
|
|
33
|
+
return bases.map((b) => path.join(root, b)).filter(existsSync);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function walk(dir) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const e of await fs.readdir(dir, { withFileTypes: true })) {
|
|
39
|
+
const full = path.join(dir, e.name);
|
|
40
|
+
if (e.isDirectory()) out.push(...(await walk(full)));
|
|
41
|
+
else out.push(full);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// app/ route folder -> URL path (strip page/route/layout file, route groups
|
|
47
|
+
// "(grp)", keep [param]). Handles root-level files (no leading slash) too.
|
|
48
|
+
function appPath(rel) {
|
|
49
|
+
let p = rel.replace(/\\/g, '/').replace(/(^|\/)(page|route|layout)\.(tsx|jsx|ts|js)$/, '');
|
|
50
|
+
p = p.split('/').filter((seg) => seg && !(seg.startsWith('(') && seg.endsWith(')'))).join('/');
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Map one Next file -> { to, kind } in glashjs routes/, or null to skip.
|
|
55
|
+
function mapFile(rel, router) {
|
|
56
|
+
const base = path.basename(rel);
|
|
57
|
+
if (router === 'app') {
|
|
58
|
+
if (/^page\.(tsx|jsx|ts|js)$/.test(base)) {
|
|
59
|
+
const p = appPath(rel); return { to: `routes/${p || 'index'}.tsx`, kind: 'page' };
|
|
60
|
+
}
|
|
61
|
+
if (/^layout\.(tsx|jsx|ts|js)$/.test(base)) {
|
|
62
|
+
const p = appPath(rel); return { to: `routes/${p ? p + '/' : ''}_layout.tsx`, kind: 'layout' };
|
|
63
|
+
}
|
|
64
|
+
if (/^route\.(ts|js)$/.test(base)) {
|
|
65
|
+
const p = appPath(rel); const api = p.startsWith('api/') ? p : `api/${p}`; return { to: `routes/${api}.ts`, kind: 'api' };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// pages/ router
|
|
70
|
+
if (rel.startsWith('api/')) {
|
|
71
|
+
const p = rel.replace(PAGE_EXT, ''); return { to: `routes/${p}.ts`, kind: 'api' };
|
|
72
|
+
}
|
|
73
|
+
if (base.startsWith('_app') || base.startsWith('_document')) {
|
|
74
|
+
return { to: `routes/_layout.tsx`, kind: 'layout' };
|
|
75
|
+
}
|
|
76
|
+
let p = rel.replace(PAGE_EXT, '').replace(/\/index$/, '');
|
|
77
|
+
if (p === 'index') p = '';
|
|
78
|
+
return { to: `routes/${p ? p : 'index'}.tsx`, kind: 'page' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function transform(code, kind) {
|
|
82
|
+
let out = code;
|
|
83
|
+
const notes = [];
|
|
84
|
+
// strip the "use client" directive (glashjs hydrates components by default)
|
|
85
|
+
out = out.replace(/^\s*['"]use client['"];?\s*\n/m, '');
|
|
86
|
+
for (const [re, to] of IMPORT_REWRITES) out = out.replace(re, to);
|
|
87
|
+
// getServerSideProps -> getServerData (best-effort rename of the export)
|
|
88
|
+
out = out.replace(/export\s+(async\s+)?function\s+getServerSideProps/, 'export $1function getServerData');
|
|
89
|
+
for (const [re, msg] of MANUAL) if (re.test(code)) notes.push(msg);
|
|
90
|
+
const header = `// ⤷ auto-migrated from Next.js by \`glashjs migrate\`. Review TODOs below.\n` +
|
|
91
|
+
(notes.length ? notes.map((n) => `// TODO(glashjs): ${n}`).join('\n') + '\n' : '');
|
|
92
|
+
return { code: header + out, notes };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function migrate({ root = process.cwd(), dryRun = false, log = console.log } = {}) {
|
|
96
|
+
const dirs = srcDirs(root);
|
|
97
|
+
if (!dirs.length) {
|
|
98
|
+
log('No Next.js app/ or pages/ directory found — nothing to migrate.');
|
|
99
|
+
return { migrated: 0 };
|
|
100
|
+
}
|
|
101
|
+
log(`glashjs migrate ${dryRun ? '(dry run) ' : ''}— scanning ${dirs.map((d) => path.relative(root, d)).join(', ')}\n`);
|
|
102
|
+
|
|
103
|
+
const report = [];
|
|
104
|
+
let migrated = 0;
|
|
105
|
+
const manualItems = new Set();
|
|
106
|
+
|
|
107
|
+
for (const dir of dirs) {
|
|
108
|
+
const router = path.basename(dir) === 'app' ? 'app' : 'pages';
|
|
109
|
+
for (const file of await walk(dir)) {
|
|
110
|
+
if (!PAGE_EXT.test(file)) continue;
|
|
111
|
+
const rel = path.relative(dir, file).replace(/\\/g, '/');
|
|
112
|
+
const mapped = mapFile(rel, router);
|
|
113
|
+
if (!mapped) continue;
|
|
114
|
+
const target = path.join(root, mapped.to);
|
|
115
|
+
const src = await fs.readFile(file, 'utf8');
|
|
116
|
+
const { code, notes } = transform(src, mapped.kind);
|
|
117
|
+
notes.forEach((n) => manualItems.add(n));
|
|
118
|
+
if (!dryRun) {
|
|
119
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
120
|
+
await fs.writeFile(target, code);
|
|
121
|
+
}
|
|
122
|
+
migrated += 1;
|
|
123
|
+
report.push({ from: `${router}/${rel}`, to: mapped.to, kind: mapped.kind, notes });
|
|
124
|
+
log(` ${mapped.kind.padEnd(6)} ${router}/${rel} → ${mapped.to}${notes.length ? ` (${notes.length} TODO)` : ''}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!dryRun) {
|
|
129
|
+
await ensureConfigAndScripts(root, log);
|
|
130
|
+
await writeReport(root, report, manualItems);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
log(`\n${migrated} file(s) ${dryRun ? 'would be' : ''} migrated → routes/`);
|
|
134
|
+
log(`${manualItems.size} pattern(s) need manual porting — see MIGRATION.md`);
|
|
135
|
+
log('\nNext steps:');
|
|
136
|
+
log(' npm i glashjs preact preact-render-to-string esbuild');
|
|
137
|
+
log(' glashjs dev # then work through the TODOs in the migrated files');
|
|
138
|
+
return { migrated, manual: manualItems.size };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function ensureConfigAndScripts(root, log) {
|
|
142
|
+
const cfgPath = path.join(root, 'glash.config.mjs');
|
|
143
|
+
if (!existsSync(cfgPath)) {
|
|
144
|
+
await fs.writeFile(cfgPath, `import { defineConfig } from 'glashjs/config';\n\nexport default defineConfig({\n name: 'Migrated app',\n routesDir: 'routes',\n offline: true,\n});\n`);
|
|
145
|
+
log(' + glash.config.mjs');
|
|
146
|
+
}
|
|
147
|
+
const pkgPath = path.join(root, 'package.json');
|
|
148
|
+
try {
|
|
149
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
150
|
+
pkg.scripts = pkg.scripts || {};
|
|
151
|
+
pkg.scripts['glash:dev'] = 'glashjs dev';
|
|
152
|
+
pkg.scripts['glash:build'] = 'glashjs build';
|
|
153
|
+
pkg.scripts['glash:start'] = 'glashjs serve';
|
|
154
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
155
|
+
log(' + package.json scripts (glash:dev / glash:build / glash:start)');
|
|
156
|
+
} catch { /* no package.json */ }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function writeReport(root, report, manualItems) {
|
|
160
|
+
const lines = ['# Next.js → glashjs migration report', '',
|
|
161
|
+
`Auto-migrated **${report.length}** files into \`routes/\`. Your original Next code was left untouched.`, '',
|
|
162
|
+
'## Files migrated', '', '| Next | glashjs | kind | TODOs |', '|---|---|---|---|'];
|
|
163
|
+
for (const r of report) lines.push(`| \`${r.from}\` | \`${r.to}\` | ${r.kind} | ${r.notes.length} |`);
|
|
164
|
+
lines.push('', '## Needs manual porting', '');
|
|
165
|
+
if (manualItems.size) for (const m of manualItems) lines.push(`- ${m}`);
|
|
166
|
+
else lines.push('- Nothing detected — but review the migrated components in a browser.');
|
|
167
|
+
lines.push('', '## Next.js → glashjs cheatsheet', '',
|
|
168
|
+
'| Next.js | glashjs |', '|---|---|',
|
|
169
|
+
'| `app/page.tsx` | `routes/index.tsx` |',
|
|
170
|
+
'| `app/blog/[slug]/page.tsx` | `routes/blog/[slug].tsx` |',
|
|
171
|
+
'| `app/layout.tsx` | `routes/_layout.tsx` |',
|
|
172
|
+
'| `app/api/x/route.ts` (GET/POST) | `routes/api/x.ts` (export GET/POST) |',
|
|
173
|
+
'| `middleware.ts` | `routes/_middleware.mjs` |',
|
|
174
|
+
'| `getServerSideProps` | `export function getServerData(ctx)` |',
|
|
175
|
+
'| `next/link` | `glashjs/link` |',
|
|
176
|
+
'| `next/image` | `glashjs/image` |',
|
|
177
|
+
'| `useRouter().push` | `<Link>` / `redirect()` |', '');
|
|
178
|
+
await fs.writeFile(path.join(root, 'MIGRATION.md'), lines.join('\n'));
|
|
179
|
+
}
|
package/src/server/jsx.mjs
CHANGED
|
@@ -38,10 +38,42 @@ async function preactRuntime() {
|
|
|
38
38
|
return { h: _h, renderToString: _renderToString };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Make migrated React/Next components compile + run under Preact.
|
|
42
|
+
const REACT_ALIAS = {
|
|
43
|
+
react: 'preact/compat',
|
|
44
|
+
'react-dom': 'preact/compat',
|
|
45
|
+
'react-dom/client': 'preact/compat',
|
|
46
|
+
'react/jsx-runtime': 'preact/jsx-runtime',
|
|
47
|
+
'react/jsx-dev-runtime': 'preact/jsx-runtime',
|
|
48
|
+
};
|
|
49
|
+
|
|
41
50
|
export function isComponentRoute(file) {
|
|
42
51
|
return /\.(jsx|tsx)$/.test(file);
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Import any route/middleware module. Plain `.mjs`/`.js` import directly (zero
|
|
56
|
+
* dep); `.ts`/`.tsx`/`.jsx` are esbuild-compiled first (so migrated TypeScript
|
|
57
|
+
* API routes & middleware run). Used for API routes and `_middleware`.
|
|
58
|
+
*/
|
|
59
|
+
export async function compileModule(file, root, dev) {
|
|
60
|
+
if (/\.(mjs|js)$/.test(file)) {
|
|
61
|
+
return import(pathToFileURL(file).href + (dev ? `?t=${Date.now()}` : ''));
|
|
62
|
+
}
|
|
63
|
+
const eb = await esbuild();
|
|
64
|
+
if (!eb) throw new Error('TypeScript routes need esbuild: npm i esbuild');
|
|
65
|
+
const out = path.join(root, '.glash', 'server', 'mod-' + routeId(file) + '.mjs');
|
|
66
|
+
if (!dev && existsSync(out)) return import(pathToFileURL(out).href);
|
|
67
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
68
|
+
await eb.build({
|
|
69
|
+
entryPoints: [file], bundle: true, platform: 'node', format: 'esm',
|
|
70
|
+
jsx: 'automatic', jsxImportSource: 'preact',
|
|
71
|
+
external: ['preact', 'preact/*', 'preact-render-to-string'],
|
|
72
|
+
alias: REACT_ALIAS, outfile: out, logLevel: 'silent',
|
|
73
|
+
});
|
|
74
|
+
return import(pathToFileURL(out).href + (dev ? `?t=${Date.now()}` : ''));
|
|
75
|
+
}
|
|
76
|
+
|
|
45
77
|
export function routeId(file) {
|
|
46
78
|
return createHash('sha1').update(file).digest('hex').slice(0, 10);
|
|
47
79
|
}
|
|
@@ -119,6 +151,7 @@ export async function loadComponentRoute(pageFile, layouts, root, dev, force = f
|
|
|
119
151
|
stdin: { contents: serverEntry(pageFile, layouts), resolveDir: path.dirname(pageFile), loader: 'js', sourcefile: 'glash-server-entry.js' },
|
|
120
152
|
bundle: true, platform: 'node', format: 'esm', jsx: 'automatic', jsxImportSource: 'preact',
|
|
121
153
|
external: ['preact', 'preact/*', 'preact-render-to-string'],
|
|
154
|
+
alias: REACT_ALIAS,
|
|
122
155
|
outfile: out, logLevel: 'silent',
|
|
123
156
|
});
|
|
124
157
|
const mod = await import(pathToFileURL(out).href + (dev ? `?t=${Date.now()}` : ''));
|
|
@@ -135,6 +168,7 @@ export async function clientBundle(pageFile, layouts, dev) {
|
|
|
135
168
|
const res = await eb.build({
|
|
136
169
|
stdin: { contents: clientEntry(pageFile, layouts), resolveDir: path.dirname(pageFile), loader: 'jsx', sourcefile: 'glash-client-entry.jsx' },
|
|
137
170
|
bundle: true, platform: 'browser', format: 'esm', minify: !dev, jsx: 'automatic', jsxImportSource: 'preact',
|
|
171
|
+
alias: REACT_ALIAS,
|
|
138
172
|
write: false, logLevel: 'silent',
|
|
139
173
|
});
|
|
140
174
|
const js = res.outputFiles[0].text;
|
package/src/server/router.mjs
CHANGED
|
@@ -38,7 +38,8 @@ export async function discoverRoutes(routesDir) {
|
|
|
38
38
|
const files = [];
|
|
39
39
|
await walk(root, root, files);
|
|
40
40
|
const routes = files
|
|
41
|
-
.filter((f) => /\.(mjs|js|jsx|tsx)$/.test(f.rel))
|
|
41
|
+
.filter((f) => /\.(mjs|js|jsx|tsx|ts)$/.test(f.rel))
|
|
42
|
+
.filter((f) => !/\.d\.ts$/.test(f.rel))
|
|
42
43
|
// `_`-prefixed files are private (layouts, helpers) — not routes.
|
|
43
44
|
.filter((f) => !f.rel.split('/').some((seg) => seg.startsWith('_')))
|
|
44
45
|
.map((f) => toRoute(f.rel, f.file));
|
|
@@ -58,7 +59,7 @@ async function walk(root, dir, out) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function toRoute(rel, file) {
|
|
61
|
-
const clean = rel.replace(/\.(mjs|js|jsx|tsx)$/, '');
|
|
62
|
+
const clean = rel.replace(/\.(mjs|js|jsx|tsx|ts)$/, '');
|
|
62
63
|
const isApi = clean === 'api' || clean.startsWith('api/');
|
|
63
64
|
const segs = [];
|
|
64
65
|
for (const part of clean.split('/').filter(Boolean)) {
|
package/src/server/server.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
14
14
|
import { discoverRoutes, matchRoute, findMiddleware } from './router.mjs';
|
|
15
15
|
import { renderDocument, documentParts, renderMeta, escapeHtml } from './html.mjs';
|
|
16
16
|
import { NAV_CLIENT } from './nav-client.mjs';
|
|
17
|
-
import { isComponentRoute, loadComponentRoute, clientBundle, renderComponent, composeVNode, getPipeableRenderer, routeId, findLayouts, clearJsxCaches } from './jsx.mjs';
|
|
17
|
+
import { isComponentRoute, loadComponentRoute, clientBundle, renderComponent, composeVNode, getPipeableRenderer, routeId, findLayouts, clearJsxCaches, compileModule } from './jsx.mjs';
|
|
18
18
|
import { securityHeaders } from '../security/headers.mjs';
|
|
19
19
|
import { loadConfig } from '../config.mjs';
|
|
20
20
|
|
|
@@ -83,7 +83,7 @@ export async function createGlashServer({ root = process.cwd(), dev = false } =
|
|
|
83
83
|
const ctx = makeCtx(req, res, url, match.params);
|
|
84
84
|
// Run the middleware chain (root -> leaf). Any return value short-circuits.
|
|
85
85
|
for (const mwFile of findMiddleware(routesDir, match.route.file)) {
|
|
86
|
-
const mwMod = await
|
|
86
|
+
const mwMod = await compileModule(mwFile, root, dev);
|
|
87
87
|
const mw = mwMod.default || mwMod.middleware;
|
|
88
88
|
if (typeof mw !== 'function') continue;
|
|
89
89
|
const result = await mw(ctx);
|
|
@@ -94,13 +94,13 @@ export async function createGlashServer({ root = process.cwd(), dev = false } =
|
|
|
94
94
|
return send(res, 200, match.route.isApi ? 'application/json' : 'text/html; charset=utf-8', '', secHeaders);
|
|
95
95
|
}
|
|
96
96
|
if (match.route.isApi) {
|
|
97
|
-
const mod = await
|
|
97
|
+
const mod = await compileModule(match.route.file, root, dev);
|
|
98
98
|
return await handleApi(res, mod, req, ctx, secHeaders);
|
|
99
99
|
}
|
|
100
100
|
if (isComponentRoute(match.route.file)) {
|
|
101
101
|
return await handleComponentPage(res, match.route, ctx, cfg, secHeaders, root, routesDir, dev);
|
|
102
102
|
}
|
|
103
|
-
const mod = await
|
|
103
|
+
const mod = await compileModule(match.route.file, root, dev);
|
|
104
104
|
return await handlePage(res, mod, ctx, cfg, secHeaders, dev);
|
|
105
105
|
} catch (err) {
|
|
106
106
|
if (res.headersSent) return res.end(); // error mid-stream — can't replace headers
|
|
@@ -131,6 +131,15 @@ async function handleApi(res, mod, req, ctx, secHeaders) {
|
|
|
131
131
|
}
|
|
132
132
|
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) ctx.body = await readJson(req);
|
|
133
133
|
const result = await handler(ctx);
|
|
134
|
+
// Next-style route handlers return a Web `Response` (e.g. Response.json(...)).
|
|
135
|
+
// Pass it through so migrated API routes work unchanged.
|
|
136
|
+
if (typeof Response !== 'undefined' && result instanceof Response) {
|
|
137
|
+
const headers = { ...secHeaders };
|
|
138
|
+
result.headers.forEach((v, k) => { headers[k] = v; });
|
|
139
|
+
const buf = Buffer.from(await result.arrayBuffer());
|
|
140
|
+
res.writeHead(result.status || 200, headers);
|
|
141
|
+
return res.end(buf);
|
|
142
|
+
}
|
|
134
143
|
if (result && result.__response) {
|
|
135
144
|
return send(res, result.status || 200, result.contentType || 'application/json',
|
|
136
145
|
typeof result.body === 'string' ? result.body : JSON.stringify(result.body), { ...secHeaders, ...(result.headers || {}) });
|