@nuraly/lumenjs 0.1.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.
Files changed (76) hide show
  1. package/README.md +297 -0
  2. package/dist/build/build.d.ts +5 -0
  3. package/dist/build/build.js +172 -0
  4. package/dist/build/error-page.d.ts +1 -0
  5. package/dist/build/error-page.js +74 -0
  6. package/dist/build/scan.d.ts +21 -0
  7. package/dist/build/scan.js +93 -0
  8. package/dist/build/serve-api.d.ts +3 -0
  9. package/dist/build/serve-api.js +56 -0
  10. package/dist/build/serve-loaders.d.ts +4 -0
  11. package/dist/build/serve-loaders.js +115 -0
  12. package/dist/build/serve-ssr.d.ts +7 -0
  13. package/dist/build/serve-ssr.js +121 -0
  14. package/dist/build/serve-static.d.ts +6 -0
  15. package/dist/build/serve-static.js +80 -0
  16. package/dist/build/serve.d.ts +5 -0
  17. package/dist/build/serve.js +79 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +65 -0
  20. package/dist/dev-server/config.d.ts +25 -0
  21. package/dist/dev-server/config.js +55 -0
  22. package/dist/dev-server/index-html.d.ts +16 -0
  23. package/dist/dev-server/index-html.js +46 -0
  24. package/dist/dev-server/nuralyui-aliases.d.ts +16 -0
  25. package/dist/dev-server/nuralyui-aliases.js +164 -0
  26. package/dist/dev-server/plugins/vite-plugin-api-routes.d.ts +23 -0
  27. package/dist/dev-server/plugins/vite-plugin-api-routes.js +250 -0
  28. package/dist/dev-server/plugins/vite-plugin-auto-import.d.ts +5 -0
  29. package/dist/dev-server/plugins/vite-plugin-auto-import.js +47 -0
  30. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +5 -0
  31. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +62 -0
  32. package/dist/dev-server/plugins/vite-plugin-lit-hmr.d.ts +5 -0
  33. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +46 -0
  34. package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +38 -0
  35. package/dist/dev-server/plugins/vite-plugin-loaders.js +320 -0
  36. package/dist/dev-server/plugins/vite-plugin-routes.d.ts +21 -0
  37. package/dist/dev-server/plugins/vite-plugin-routes.js +157 -0
  38. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +5 -0
  39. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +39 -0
  40. package/dist/dev-server/plugins/vite-plugin-virtual-modules.d.ts +5 -0
  41. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +38 -0
  42. package/dist/dev-server/server.d.ts +23 -0
  43. package/dist/dev-server/server.js +155 -0
  44. package/dist/dev-server/ssr-render.d.ts +20 -0
  45. package/dist/dev-server/ssr-render.js +170 -0
  46. package/dist/editor/click-select.d.ts +1 -0
  47. package/dist/editor/click-select.js +46 -0
  48. package/dist/editor/editor-bridge.d.ts +17 -0
  49. package/dist/editor/editor-bridge.js +101 -0
  50. package/dist/editor/element-annotator.d.ts +33 -0
  51. package/dist/editor/element-annotator.js +83 -0
  52. package/dist/editor/hover-detect.d.ts +1 -0
  53. package/dist/editor/hover-detect.js +36 -0
  54. package/dist/editor/inline-text-edit.d.ts +1 -0
  55. package/dist/editor/inline-text-edit.js +114 -0
  56. package/dist/integrations/add.d.ts +1 -0
  57. package/dist/integrations/add.js +89 -0
  58. package/dist/runtime/app-shell.d.ts +1 -0
  59. package/dist/runtime/app-shell.js +22 -0
  60. package/dist/runtime/response.d.ts +15 -0
  61. package/dist/runtime/response.js +13 -0
  62. package/dist/runtime/router-data.d.ts +3 -0
  63. package/dist/runtime/router-data.js +40 -0
  64. package/dist/runtime/router-hydration.d.ts +10 -0
  65. package/dist/runtime/router-hydration.js +68 -0
  66. package/dist/runtime/router.d.ts +35 -0
  67. package/dist/runtime/router.js +202 -0
  68. package/dist/shared/dom-shims.d.ts +5 -0
  69. package/dist/shared/dom-shims.js +63 -0
  70. package/dist/shared/route-matching.d.ts +6 -0
  71. package/dist/shared/route-matching.js +44 -0
  72. package/dist/shared/types.d.ts +16 -0
  73. package/dist/shared/types.js +1 -0
  74. package/dist/shared/utils.d.ts +42 -0
  75. package/dist/shared/utils.js +109 -0
  76. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,297 @@
1
+ # LumenJS
2
+
3
+ A full-stack web framework for [Lit](https://lit.dev/) web components. File-based routing, server loaders, SSR with hydration, nested layouts, API routes, and a Vite-powered dev server.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx lumenjs dev --project ./my-app
9
+ ```
10
+
11
+ ## Project Structure
12
+
13
+ ```
14
+ my-app/
15
+ ├── lumenjs.config.ts # Project config
16
+ ├── package.json
17
+ ├── pages/ # File-based routes
18
+ │ ├── _layout.ts # Root layout
19
+ │ ├── index.ts # → /
20
+ │ ├── about.ts # → /about
21
+ │ └── blog/
22
+ │ ├── _layout.ts # Nested layout (wraps blog/*)
23
+ │ ├── index.ts # → /blog
24
+ │ └── [slug].ts # → /blog/:slug
25
+ ├── api/ # API routes
26
+ │ └── hello.ts # → /api/hello
27
+ └── public/ # Static assets
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ ```typescript
33
+ // lumenjs.config.ts
34
+ export default {
35
+ title: 'My App',
36
+ integrations: ['tailwind'],
37
+ };
38
+ ```
39
+
40
+ | Option | Type | Description |
41
+ |---|---|---|
42
+ | `title` | `string` | HTML page title |
43
+ | `integrations` | `string[]` | Optional integrations: `'tailwind'`, `'nuralyui'` |
44
+
45
+ ## Pages
46
+
47
+ Pages are Lit components in the `pages/` directory. The file path determines the URL.
48
+
49
+ ```typescript
50
+ // pages/index.ts
51
+ import { LitElement, html, css } from 'lit';
52
+ import { customElement } from 'lit/decorators.js';
53
+
54
+ @customElement('page-index')
55
+ export class PageIndex extends LitElement {
56
+ static styles = css`:host { display: block; }`;
57
+
58
+ render() {
59
+ return html`<h1>Hello, LumenJS!</h1>`;
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### Routing
65
+
66
+ | File | URL | Tag |
67
+ |---|---|---|
68
+ | `pages/index.ts` | `/` | `<page-index>` |
69
+ | `pages/about.ts` | `/about` | `<page-about>` |
70
+ | `pages/blog/index.ts` | `/blog` | `<page-blog-index>` |
71
+ | `pages/blog/[slug].ts` | `/blog/:slug` | `<page-blog-slug>` |
72
+ | `pages/[...slug].ts` | `/*` (catch-all) | `<page-slug>` |
73
+
74
+ Static routes take priority over dynamic ones. Dynamic `[param]` routes take priority over catch-all `[...param]` routes.
75
+
76
+ ## Loaders
77
+
78
+ Export a `loader()` function from any page or layout to fetch data on the server.
79
+
80
+ ```typescript
81
+ // pages/blog/[slug].ts
82
+ export async function loader({ params, headers, query, url }) {
83
+ const post = await db.posts.findOne({ slug: params.slug });
84
+ if (!post) return { __nk_redirect: true, location: '/404', status: 302 };
85
+ return { post };
86
+ }
87
+
88
+ @customElement('page-blog-slug')
89
+ export class BlogPost extends LitElement {
90
+ @property({ type: Object }) loaderData: any = {};
91
+
92
+ render() {
93
+ return html`<h1>${this.loaderData.post?.title}</h1>`;
94
+ }
95
+ }
96
+ ```
97
+
98
+ Loaders run server-side on initial load (SSR) and are fetched via `/__nk_loader/<path>` during client-side navigation. The `loader()` export is automatically stripped from client bundles.
99
+
100
+ ### Loader Context
101
+
102
+ | Property | Type | Description |
103
+ |---|---|---|
104
+ | `params` | `Record<string, string>` | Dynamic route parameters |
105
+ | `query` | `Record<string, string>` | Query string parameters |
106
+ | `url` | `string` | Request pathname |
107
+ | `headers` | `Record<string, any>` | Request headers |
108
+
109
+ ### Redirects
110
+
111
+ ```typescript
112
+ export async function loader({ headers }) {
113
+ const user = await getUser(headers.authorization);
114
+ if (!user) return { __nk_redirect: true, location: '/login', status: 302 };
115
+ return { user };
116
+ }
117
+ ```
118
+
119
+ ## Nested Layouts
120
+
121
+ Create `_layout.ts` in any directory to wrap all pages in that directory and its subdirectories.
122
+
123
+ ```typescript
124
+ // pages/_layout.ts
125
+ @customElement('layout-root')
126
+ export class RootLayout extends LitElement {
127
+ render() {
128
+ return html`
129
+ <header>My App</header>
130
+ <main><slot></slot></main>
131
+ <footer>Footer</footer>
132
+ `;
133
+ }
134
+ }
135
+ ```
136
+
137
+ Layouts persist across navigation — when navigating between pages that share the same layout, only the page component is swapped.
138
+
139
+ Layouts can have their own `loader()` function for shared data like auth or navigation:
140
+
141
+ ```typescript
142
+ // pages/dashboard/_layout.ts
143
+ export async function loader({ headers }) {
144
+ const user = await getUser(headers.authorization);
145
+ if (!user) return { __nk_redirect: true, location: '/login', status: 302 };
146
+ return { user };
147
+ }
148
+
149
+ @customElement('layout-dashboard')
150
+ export class DashboardLayout extends LitElement {
151
+ @property({ type: Object }) loaderData: any = {};
152
+
153
+ render() {
154
+ return html`
155
+ <nav>Welcome, ${this.loaderData.user?.name}</nav>
156
+ <slot></slot>
157
+ `;
158
+ }
159
+ }
160
+ ```
161
+
162
+ ## API Routes
163
+
164
+ Create files in `api/` and export named functions for each HTTP method.
165
+
166
+ ```typescript
167
+ // api/users/[id].ts
168
+ export async function GET(req) {
169
+ return { user: { id: req.params.id, name: 'Alice' } };
170
+ }
171
+
172
+ export async function POST(req) {
173
+ const { name } = req.body;
174
+ return { created: true, name };
175
+ }
176
+ ```
177
+
178
+ ### Request Object
179
+
180
+ | Property | Type | Description |
181
+ |---|---|---|
182
+ | `method` | `string` | HTTP method |
183
+ | `url` | `string` | Request pathname |
184
+ | `query` | `Record<string, string>` | Query string parameters |
185
+ | `params` | `Record<string, string>` | Dynamic route parameters |
186
+ | `body` | `any` | Parsed JSON body (non-GET) |
187
+ | `files` | `NkUploadedFile[]` | Uploaded files (multipart) |
188
+ | `headers` | `Record<string, any>` | Request headers |
189
+
190
+ ### Error Responses
191
+
192
+ ```typescript
193
+ export async function GET(req) {
194
+ const item = await db.find(req.params.id);
195
+ if (!item) throw { status: 404, message: 'Not found' };
196
+ return item;
197
+ }
198
+ ```
199
+
200
+ ### File Uploads
201
+
202
+ Multipart form data is parsed automatically:
203
+
204
+ ```typescript
205
+ export async function POST(req) {
206
+ for (const file of req.files) {
207
+ console.log(file.fileName, file.size, file.contentType);
208
+ // file.data is a Buffer
209
+ }
210
+ return { uploaded: req.files.length };
211
+ }
212
+ ```
213
+
214
+ ## SSR & Hydration
215
+
216
+ Pages with loaders are automatically server-rendered using `@lit-labs/ssr`:
217
+
218
+ 1. Loader runs on the server
219
+ 2. Lit component renders to HTML
220
+ 3. Loader data is embedded as JSON in the response
221
+ 4. Browser receives pre-rendered HTML (fast first paint)
222
+ 5. Client hydrates the existing DOM without re-rendering
223
+
224
+ Pages without loaders render client-side only (SPA mode). If SSR fails, LumenJS falls back gracefully to client-side rendering.
225
+
226
+ ## Integrations
227
+
228
+ ### Tailwind CSS
229
+
230
+ ```bash
231
+ npx lumenjs add tailwind
232
+ ```
233
+
234
+ This installs `tailwindcss` and `@tailwindcss/vite`, creates `styles/tailwind.css`, and updates your config. For pages using Tailwind classes in light DOM:
235
+
236
+ ```typescript
237
+ createRenderRoot() { return this; }
238
+ ```
239
+
240
+ ### NuralyUI
241
+
242
+ Add `'nuralyui'` to integrations to enable auto-import of `<nr-*>` components:
243
+
244
+ ```typescript
245
+ // lumenjs.config.ts
246
+ export default {
247
+ title: 'My App',
248
+ integrations: ['nuralyui'],
249
+ };
250
+ ```
251
+
252
+ NuralyUI components are detected in `html\`\`` templates and imported automatically, including implicit dependencies (e.g., `nr-button` auto-imports `nr-icon`).
253
+
254
+ ## CLI
255
+
256
+ ```
257
+ lumenjs dev [--project <dir>] [--port <port>] [--base <path>] [--editor-mode]
258
+ lumenjs build [--project <dir>] [--out <dir>]
259
+ lumenjs serve [--project <dir>] [--port <port>]
260
+ lumenjs add <integration>
261
+ ```
262
+
263
+ | Command | Description |
264
+ |---|---|
265
+ | `dev` | Start Vite dev server with HMR, SSR, and API routes |
266
+ | `build` | Bundle client assets and server modules for production |
267
+ | `serve` | Serve the production build with SSR and gzip compression |
268
+ | `add` | Add an integration (e.g., `tailwind`) |
269
+
270
+ ### Default Ports
271
+
272
+ | Mode | Default |
273
+ |---|---|
274
+ | `dev` | 3000 |
275
+ | `serve` | 3000 |
276
+
277
+ ## Production Build
278
+
279
+ ```bash
280
+ npx lumenjs build --project ./my-app
281
+ npx lumenjs serve --project ./my-app --port 8080
282
+ ```
283
+
284
+ The build outputs to `.lumenjs/`:
285
+
286
+ ```
287
+ .lumenjs/
288
+ ├── client/ # Static assets (HTML, JS, CSS)
289
+ ├── server/ # Server modules (loaders, API routes, SSR runtime)
290
+ └── manifest.json # Route manifest
291
+ ```
292
+
293
+ The production server includes gzip compression and serves pre-built assets while executing loaders and API routes on demand.
294
+
295
+ ## License
296
+
297
+ MIT
@@ -0,0 +1,5 @@
1
+ export interface BuildOptions {
2
+ projectDir: string;
3
+ outDir?: string;
4
+ }
5
+ export declare function buildProject(options: BuildOptions): Promise<void>;
@@ -0,0 +1,172 @@
1
+ import { build as viteBuild } from 'vite';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { getSharedViteConfig } from '../dev-server/server.js';
5
+ import { readProjectConfig } from '../dev-server/config.js';
6
+ import { generateIndexHtml } from '../dev-server/index-html.js';
7
+ import { scanPages, scanLayouts, scanApiRoutes, getLayoutDirsForPage } from './scan.js';
8
+ export async function buildProject(options) {
9
+ const { projectDir } = options;
10
+ const outDir = options.outDir || path.join(projectDir, '.lumenjs');
11
+ const clientDir = path.join(outDir, 'client');
12
+ const serverDir = path.join(outDir, 'server');
13
+ const pagesDir = path.join(projectDir, 'pages');
14
+ const apiDir = path.join(projectDir, 'api');
15
+ const publicDir = path.join(projectDir, 'public');
16
+ // Clean output directory
17
+ if (fs.existsSync(outDir)) {
18
+ fs.rmSync(outDir, { recursive: true });
19
+ }
20
+ fs.mkdirSync(outDir, { recursive: true });
21
+ const { title, integrations } = readProjectConfig(projectDir);
22
+ const shared = getSharedViteConfig(projectDir, { mode: 'production', integrations });
23
+ // Scan pages, layouts, and API routes for the manifest
24
+ const pageEntries = scanPages(pagesDir);
25
+ const layoutEntries = scanLayouts(pagesDir);
26
+ const apiEntries = scanApiRoutes(apiDir);
27
+ // --- Client build ---
28
+ console.log('[LumenJS] Building client bundle...');
29
+ // Generate index.html as build entry
30
+ const indexHtml = generateIndexHtml({ title, editorMode: false, integrations });
31
+ const tempIndexPath = path.join(projectDir, '__nk_build_index.html');
32
+ fs.writeFileSync(tempIndexPath, indexHtml);
33
+ try {
34
+ await viteBuild({
35
+ root: projectDir,
36
+ publicDir: fs.existsSync(publicDir) ? publicDir : undefined,
37
+ resolve: shared.resolve,
38
+ plugins: shared.plugins,
39
+ esbuild: shared.esbuild,
40
+ build: {
41
+ outDir: clientDir,
42
+ emptyOutDir: true,
43
+ rollupOptions: {
44
+ input: tempIndexPath,
45
+ },
46
+ },
47
+ logLevel: 'warn',
48
+ });
49
+ }
50
+ finally {
51
+ // Clean up temp file
52
+ if (fs.existsSync(tempIndexPath)) {
53
+ fs.unlinkSync(tempIndexPath);
54
+ }
55
+ }
56
+ // Rename the built HTML file from __nk_build_index.html to index.html
57
+ const builtHtmlPath = path.join(clientDir, '__nk_build_index.html');
58
+ const finalHtmlPath = path.join(clientDir, 'index.html');
59
+ if (fs.existsSync(builtHtmlPath)) {
60
+ fs.renameSync(builtHtmlPath, finalHtmlPath);
61
+ }
62
+ // --- Server build ---
63
+ console.log('[LumenJS] Building server bundle...');
64
+ // Collect server entry points (pages with loaders + layouts with loaders + API routes)
65
+ const serverEntries = {};
66
+ for (const entry of pageEntries) {
67
+ if (entry.hasLoader) {
68
+ serverEntries[`pages/${entry.name}`] = entry.filePath;
69
+ }
70
+ }
71
+ for (const entry of layoutEntries) {
72
+ if (entry.hasLoader) {
73
+ const entryName = entry.dir ? `layouts/${entry.dir}/_layout` : 'layouts/_layout';
74
+ serverEntries[entryName] = entry.filePath;
75
+ }
76
+ }
77
+ for (const entry of apiEntries) {
78
+ serverEntries[`api/${entry.name}`] = entry.filePath;
79
+ }
80
+ // Create SSR runtime entry — bundles @lit-labs/ssr alongside Lit so all
81
+ // server modules share one Lit instance (avoids _$EM mismatches).
82
+ const ssrEntryPath = path.join(projectDir, '__nk_ssr_entry.js');
83
+ const hasPageLoaders = pageEntries.some(e => e.hasLoader);
84
+ const hasLayoutLoaders = layoutEntries.some(e => e.hasLoader);
85
+ if (hasPageLoaders || hasLayoutLoaders) {
86
+ fs.writeFileSync(ssrEntryPath, [
87
+ "import '@lit-labs/ssr/lib/install-global-dom-shim.js';",
88
+ "export { render } from '@lit-labs/ssr';",
89
+ "export { html, unsafeStatic } from 'lit/static-html.js';",
90
+ ].join('\n'));
91
+ serverEntries['ssr-runtime'] = ssrEntryPath;
92
+ }
93
+ try {
94
+ if (Object.keys(serverEntries).length > 0) {
95
+ await viteBuild({
96
+ root: projectDir,
97
+ resolve: shared.resolve,
98
+ plugins: shared.plugins,
99
+ esbuild: shared.esbuild,
100
+ build: {
101
+ outDir: serverDir,
102
+ emptyOutDir: true,
103
+ ssr: true,
104
+ rollupOptions: {
105
+ input: serverEntries,
106
+ output: {
107
+ format: 'esm',
108
+ entryFileNames: '[name].js',
109
+ chunkFileNames: 'assets/[name]-[hash].js',
110
+ manualChunks(id) {
111
+ // Force all Lit packages into a single shared chunk so SSR runtime
112
+ // and page modules use the exact same Lit class instances.
113
+ if (id.includes('/node_modules/lit/') ||
114
+ id.includes('/node_modules/lit-html/') ||
115
+ id.includes('/node_modules/lit-element/') ||
116
+ id.includes('/node_modules/@lit/reactive-element/')) {
117
+ return 'lit-shared';
118
+ }
119
+ },
120
+ },
121
+ external: [
122
+ /^node:/,
123
+ 'os', 'fs', 'path', 'url', 'util', 'crypto', 'http', 'https', 'net',
124
+ 'stream', 'zlib', 'events', 'buffer', 'querystring', 'child_process',
125
+ 'worker_threads', 'cluster', 'dns', 'tls', 'assert', 'constants',
126
+ ],
127
+ },
128
+ },
129
+ logLevel: 'warn',
130
+ ssr: {
131
+ noExternal: true,
132
+ },
133
+ });
134
+ }
135
+ else {
136
+ fs.mkdirSync(serverDir, { recursive: true });
137
+ }
138
+ }
139
+ finally {
140
+ if (fs.existsSync(ssrEntryPath)) {
141
+ fs.unlinkSync(ssrEntryPath);
142
+ }
143
+ }
144
+ // --- Write manifest ---
145
+ const manifest = {
146
+ routes: pageEntries.map(e => {
147
+ const routeLayouts = getLayoutDirsForPage(e.filePath, pagesDir, layoutEntries);
148
+ return {
149
+ path: e.routePath,
150
+ module: e.hasLoader ? `pages/${e.name}.js` : '',
151
+ hasLoader: e.hasLoader,
152
+ ...(routeLayouts.length > 0 ? { layouts: routeLayouts } : {}),
153
+ };
154
+ }),
155
+ apiRoutes: apiEntries.map(e => ({
156
+ path: `/api/${e.routePath}`,
157
+ module: `api/${e.name}.js`,
158
+ hasLoader: false,
159
+ })),
160
+ layouts: layoutEntries.map(e => ({
161
+ dir: e.dir,
162
+ module: e.hasLoader ? (e.dir ? `layouts/${e.dir}/_layout.js` : 'layouts/_layout.js') : '',
163
+ hasLoader: e.hasLoader,
164
+ })),
165
+ };
166
+ fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
167
+ console.log('[LumenJS] Build complete.');
168
+ console.log(` Output: ${outDir}`);
169
+ console.log(` Client assets: ${clientDir}`);
170
+ console.log(` Server modules: ${serverDir}`);
171
+ console.log(` Routes: ${pageEntries.length} pages, ${apiEntries.length} API routes, ${layoutEntries.length} layouts`);
172
+ }
@@ -0,0 +1 @@
1
+ export declare function renderErrorPage(status: number, title: string, message: string, detail?: string): string;
@@ -0,0 +1,74 @@
1
+ import { escapeHtml } from '../shared/utils.js';
2
+ export function renderErrorPage(status, title, message, detail) {
3
+ const gradients = {
4
+ 404: 'linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7)',
5
+ 500: 'linear-gradient(135deg, #ef4444, #f97316, #f59e0b)',
6
+ 502: 'linear-gradient(135deg, #f97316, #ef4444)',
7
+ 503: 'linear-gradient(135deg, #64748b, #475569)',
8
+ };
9
+ const gradient = gradients[status] || gradients[500];
10
+ const detailBlock = detail
11
+ ? `<div style="margin-top:1.5rem;padding:.75rem 1rem;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;text-align:left">
12
+ <div style="font-size:.6875rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.375rem">Details</div>
13
+ <pre style="margin:0;font-size:.75rem;color:#64748b;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">${escapeHtml(detail)}</pre>
14
+ </div>`
15
+ : '';
16
+ return `<!DOCTYPE html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="UTF-8">
20
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
21
+ <title>${status} — ${escapeHtml(title)}</title>
22
+ <style>
23
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
24
+ body {
25
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
26
+ min-height: 100vh;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ background: #fafbfc;
31
+ padding: 2rem;
32
+ }
33
+ .container { text-align: center; max-width: 440px; }
34
+ .status {
35
+ font-size: 5rem;
36
+ font-weight: 200;
37
+ letter-spacing: -2px;
38
+ line-height: 1;
39
+ color: #cbd5e1;
40
+ user-select: none;
41
+ }
42
+ h1 { font-size: 1rem; font-weight: 500; color: #334155; margin: 1.25rem 0 .5rem; }
43
+ .message { color: #94a3b8; font-size: .8125rem; line-height: 1.5; margin-bottom: 2rem; }
44
+ .btn {
45
+ display: inline-flex; align-items: center; gap: .375rem;
46
+ padding: .4375rem 1rem;
47
+ background: #f8fafc; color: #475569;
48
+ border: 1px solid #e2e8f0;
49
+ border-radius: 6px; font-size: .8125rem; font-weight: 400;
50
+ text-decoration: none; transition: all .15s;
51
+ cursor: pointer;
52
+ }
53
+ .btn:hover { background: #f1f5f9; border-color: #cbd5e1; }
54
+ .btn svg { flex-shrink: 0; }
55
+ .divider { width: 32px; height: 2px; background: #e2e8f0; border-radius: 1px; margin: 1.25rem auto; }
56
+ .footer { margin-top: 3rem; font-size: .6875rem; color: #e2e8f0; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="container">
61
+ <div class="status">${status}</div>
62
+ <div class="divider"></div>
63
+ <h1>${escapeHtml(title)}</h1>
64
+ <p class="message">${escapeHtml(message)}</p>
65
+ <a href="/" class="btn">
66
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
67
+ Back to home
68
+ </a>
69
+ ${detailBlock}
70
+ <div class="footer">LumenJS</div>
71
+ </div>
72
+ </body>
73
+ </html>`;
74
+ }
@@ -0,0 +1,21 @@
1
+ export interface PageEntry {
2
+ name: string;
3
+ filePath: string;
4
+ routePath: string;
5
+ hasLoader: boolean;
6
+ }
7
+ export interface LayoutEntry {
8
+ dir: string;
9
+ filePath: string;
10
+ hasLoader: boolean;
11
+ }
12
+ export interface ApiEntry {
13
+ name: string;
14
+ filePath: string;
15
+ routePath: string;
16
+ }
17
+ export declare function scanPages(pagesDir: string): PageEntry[];
18
+ export declare function scanLayouts(pagesDir: string): LayoutEntry[];
19
+ export declare function scanApiRoutes(apiDir: string): ApiEntry[];
20
+ /** Get the layout directory chain for a given page file */
21
+ export declare function getLayoutDirsForPage(pageFilePath: string, pagesDir: string, layouts: LayoutEntry[]): string[];
@@ -0,0 +1,93 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileHasLoader, filePathToRoute } from '../shared/utils.js';
4
+ export function scanPages(pagesDir) {
5
+ if (!fs.existsSync(pagesDir))
6
+ return [];
7
+ const entries = [];
8
+ walkDir(pagesDir, '', entries, pagesDir);
9
+ return entries;
10
+ }
11
+ export function scanLayouts(pagesDir) {
12
+ if (!fs.existsSync(pagesDir))
13
+ return [];
14
+ const entries = [];
15
+ walkForLayouts(pagesDir, '', entries);
16
+ return entries;
17
+ }
18
+ export function scanApiRoutes(apiDir) {
19
+ if (!fs.existsSync(apiDir))
20
+ return [];
21
+ const entries = [];
22
+ walkApiDir(apiDir, '', entries, apiDir);
23
+ return entries;
24
+ }
25
+ /** Get the layout directory chain for a given page file */
26
+ export function getLayoutDirsForPage(pageFilePath, pagesDir, layouts) {
27
+ const relativeToPages = path.relative(pagesDir, pageFilePath).replace(/\\/g, '/');
28
+ const dirParts = path.dirname(relativeToPages).split('/').filter(p => p && p !== '.');
29
+ const chain = [];
30
+ // Check root layout
31
+ if (layouts.some(l => l.dir === '')) {
32
+ chain.push('');
33
+ }
34
+ // Check each directory level
35
+ let currentDir = '';
36
+ for (const part of dirParts) {
37
+ currentDir = currentDir ? `${currentDir}/${part}` : part;
38
+ if (layouts.some(l => l.dir === currentDir)) {
39
+ chain.push(currentDir);
40
+ }
41
+ }
42
+ return chain;
43
+ }
44
+ function walkDir(baseDir, relativePath, entries, pagesDir) {
45
+ const fullDir = path.join(baseDir, relativePath);
46
+ const dirEntries = fs.readdirSync(fullDir, { withFileTypes: true });
47
+ for (const entry of dirEntries) {
48
+ const entryRelative = path.join(relativePath, entry.name);
49
+ if (entry.isDirectory()) {
50
+ walkDir(baseDir, entryRelative, entries, pagesDir);
51
+ }
52
+ else if (entry.isFile() && /\.(ts|js)$/.test(entry.name) && !entry.name.startsWith('_')) {
53
+ const filePath = path.join(pagesDir, entryRelative);
54
+ const name = entryRelative.replace(/\.(ts|js)$/, '').replace(/\\/g, '/');
55
+ const routePath = filePathToRoute(entryRelative);
56
+ const hasLoader = fileHasLoader(filePath);
57
+ entries.push({ name, filePath, routePath, hasLoader });
58
+ }
59
+ }
60
+ }
61
+ function walkForLayouts(baseDir, relativePath, entries) {
62
+ const fullDir = path.join(baseDir, relativePath);
63
+ const dirEntries = fs.readdirSync(fullDir, { withFileTypes: true });
64
+ for (const entry of dirEntries) {
65
+ if (entry.isFile() && /^_layout\.(ts|js)$/.test(entry.name)) {
66
+ const filePath = path.join(fullDir, entry.name);
67
+ const dir = relativePath.replace(/\\/g, '/');
68
+ entries.push({ dir, filePath, hasLoader: fileHasLoader(filePath) });
69
+ }
70
+ if (entry.isDirectory()) {
71
+ walkForLayouts(baseDir, path.join(relativePath, entry.name), entries);
72
+ }
73
+ }
74
+ }
75
+ function walkApiDir(baseDir, relativePath, entries, apiDir) {
76
+ const fullDir = path.join(baseDir, relativePath);
77
+ const dirEntries = fs.readdirSync(fullDir, { withFileTypes: true });
78
+ for (const entry of dirEntries) {
79
+ const entryRelative = path.join(relativePath, entry.name);
80
+ if (entry.isDirectory()) {
81
+ walkApiDir(baseDir, entryRelative, entries, apiDir);
82
+ }
83
+ else if (entry.isFile() && /\.(ts|js)$/.test(entry.name) && !entry.name.startsWith('_')) {
84
+ const filePath = path.join(apiDir, entryRelative);
85
+ const name = entryRelative.replace(/\.(ts|js)$/, '').replace(/\\/g, '/');
86
+ const routePath = entryRelative
87
+ .replace(/\.(ts|js)$/, '')
88
+ .replace(/\\/g, '/')
89
+ .replace(/\[([^\]]+)\]/g, ':$1');
90
+ entries.push({ name, filePath, routePath });
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,3 @@
1
+ import http from 'http';
2
+ import type { BuildManifest } from '../shared/types.js';
3
+ export declare function handleApiRoute(manifest: BuildManifest, serverDir: string, pathname: string, queryString: string | undefined, method: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;