glashjs 0.11.1 → 0.12.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
@@ -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.11.1",
3
+ "version": "0.12.0",
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,14 +3,15 @@ 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';
9
10
  export { securityHeaders, buildCsp, sri, glashSecurity } from './security/headers.mjs';
10
11
  export { createGlashServer, json, redirect } from './server/server.mjs';
11
12
  export { discoverRoutes, matchRoute, findMiddleware } from './server/router.mjs';
12
- export { html, raw, escapeHtml, renderDocument } from './server/html.mjs';
13
- export { Image } from './components/image.mjs';
14
- export { Video } from './components/video.mjs';
15
- export { Link } from './components/link.mjs';
16
- export { renderMeta } from './server/html.mjs';
13
+ export { html, raw, escapeHtml, renderDocument, renderMeta } from './server/html.mjs';
14
+ // NOTE: <Image>/<Video>/<Link> are Preact components, so they live on subpaths
15
+ // (glashjs/image, glashjs/video, glashjs/link) and are NOT re-exported here —
16
+ // otherwise importing anything from 'glashjs' would require preact to be
17
+ // installed, breaking the zero-dependency core (html/json/createGlashServer/build).
@@ -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
+ }
@@ -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;
@@ -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)) {
@@ -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 importRoute(mwFile);
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 importRoute(match.route.file);
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 importRoute(match.route.file);
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