@ijo-elaja/rev.js 0.5.1 → 0.6.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/.prettierrc CHANGED
File without changes
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Elia Perry
3
+ Copyright (c) 2026 Elia Perry
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,147 +1,197 @@
1
1
  # Rev.js
2
2
 
3
- A highly opionionated, lightweight, frontend<sup>1</sup> only solution.
3
+ A lightweight, **pure SSR** JSX/TSX framework using **Preact** and **TypeScript**.
4
4
 
5
5
  ## Table of Contents
6
6
 
7
- - [Table of Contents](#table-of-contents)
8
7
  - [Features](#features)
9
- - [Current](#current)
10
- - [Planned/Unplanned](#plannedunplanned)
11
- - [Won't Do](#wont-do)
12
- - [Usage](#usage)
13
- - [Contributing](#contributing)
8
+ - [Quick Start](#quick-start)
9
+ - [File-Based Routing](#file-based-routing)
10
+ - [Async Components](#async-components)
11
+ - [Authentication](#authentication)
12
+ - [API Integration](#api-integration)
14
13
  - [License](#license)
15
14
 
16
- <sup>1</sup> As of v0.3.0 you *can* write "backend" code by extending the `Elysia` object through the config.
17
-
18
15
  ## Features
19
16
 
20
- ### Current
21
-
22
- - Filesystem Based Router
23
- - Slugs (dynamic data based routing)
24
- - Components
25
- - Inline JavaScript
26
- - Client-Side Conditional Rendering
27
-
28
- ### Planned/Unplanned
29
-
30
- - Template
31
- - Static Site Building
32
- - Component Props (think react but less bad)
33
- - Documentation(?)
34
-
35
- ### Won't Do
36
-
37
- I will not *specifically* support non-Bun JavaScript runtimes. Bun has support for Windows, Mac, and Linux, and is faster than node/npm, yarn, or pnpm.
17
+ - **JSX/TSX Components** - Write components in TSX, render on server
18
+ - **File-Based Routing** - Pages organized by folder structure (`pages/about/_page.tsx` → `/about`)
19
+ - **Dynamic Routes** - Support for `[slug]`, `[id]`, and `[...catch-all]` patterns
20
+ - **Nested Layouts** - Automatic layout composition at each directory level
21
+ - **Async Components** - Fetch data server-side, render with complete HTML
22
+ - **httpOnly Cookie Auth** - Built-in support for secure token-based authentication
23
+ - **Zero Client JS** - All rendering happens on the server
24
+ - **Bun Runtime** - TypeScript-native, no build step needed
25
+ - **Global Types** - Type-safe props with `PageContext` and `PageProps`
38
26
 
39
- If you don't want Bun, you don't want this package.
27
+ ## Quick Start
40
28
 
41
- ## Usage
42
-
43
- First, you can install the package like so:
29
+ Install:
44
30
 
45
31
  ```bash
46
32
  bun install @ijo-elaja/rev.js
47
33
  ```
48
34
 
49
- Next, make sure you have this directory structure:
50
-
51
- ```directory
52
- | index.ts
53
- | pages/
54
- | _page.html
55
- | _layout.html
56
- | 404.html
57
- | public/
58
- | (public files such
59
- | as a favicon.ico)
60
- ```
35
+ Create your project structure:
61
36
 
62
- You can also have a components directory at the root level:
63
-
64
- ```directory
65
- | index.ts
66
- | pages/
67
- | _page.html
68
- | _layout.html
69
- | 404.html
70
- | components/
71
- | SomeComponent.html
72
- | public/
73
- | (public files such
74
- | as a favicon.ico)
37
+ ```tree
38
+ index.ts
39
+ pages/
40
+ _layout.tsx
41
+ _page.tsx
42
+ public/
43
+ (whatever public
44
+ files you might need)
75
45
  ```
76
46
 
77
- In your `index.ts` file, all you need is:
47
+ In `index.ts`:
78
48
 
79
- ```ts
49
+ ```typescript
80
50
  import Rev from "@ijo-elaja/rev.js";
81
51
 
82
52
  new Rev({
83
- port: 8080,
84
-
85
- // debug info isn't too important most of the time
86
- // but it can be useful if you did something stupid
87
- showDebug: false,
88
-
89
- // im working to fix this, but no promises
90
- // for now, it needs to be an "absolute" path
91
- rootDir: __dirname,
92
-
93
- // this is 100% optional, but can be useful
94
- // when you want more functionality than just
95
- // what rev.js provides by default
96
- // note that typescript will complain without
97
- // the `as any` if you actually make changes
98
- elysia: (app) => app as any
53
+ port: 3000,
54
+ rootDir: __dirname,
55
+ showDebug: true,
56
+ elysia: (app) => app, // Optional: add custom routes
99
57
  });
100
58
  ```
101
59
 
102
- In your `pages/_layout.html` file, is your (you guessed it) main layout. Any `pages/**/_page.html` will go in the `Outlet` built-in component.
60
+ ## File-Based Routing
61
+
62
+ Pages are TypeScript components in the `pages/` directory:
63
+
64
+ - `pages/_page.tsx` → `/`
65
+ - `pages/about/_page.tsx` → `/about`
66
+ - `pages/blog/[slug]/_page.tsx` → `/blog/getting-started`, `/blog/my-post`, etc.
67
+ - `pages/docs/[...slug]/_page.tsx` → `/docs/foo/bar/baz` (catch-all)
68
+ - `pages/404.tsx` → Custom 404 page
69
+
70
+ ## Async Components
71
+
72
+ Fetch data server-side and render complete pages:
103
73
 
104
- Here's the absolute *bare minimum* `pages/_layout.html`:
74
+ ```typescript
75
+ // you'll probably want to set under
76
+ // "compilationOptions" in your tsconfig.json:
77
+ // "jsx": "react-jsx",
78
+ // "jsxImportSource": "preact",
79
+ // instead of doing these
105
80
 
106
- ```ts
107
- {{ %Outlet% }}
81
+ /** @jsx h */
82
+ /** @jsxFrag Fragment */
83
+
84
+ import { h } from "preact";
85
+
86
+ interface Post {
87
+ id: number;
88
+ title: string;
89
+ body: string;
90
+ }
91
+
92
+ export default async function BlogPost({ context }: PageProps<{ slug: string }>) {
93
+ // This runs on the server only
94
+ const response = await fetch(`https://api.example.com/posts/${context.params.slug}`);
95
+ const post = await response.json();
96
+
97
+ // Rendered as HTML before sending to browser
98
+ return (
99
+ <>
100
+ <h1>{post.title}</h1>
101
+ <p>{post.body}</p>
102
+ </>
103
+ );
104
+ }
108
105
  ```
109
106
 
110
- Of course, this might not look very nice, but it will work.
111
-
112
- However, if you do want the basics, here's a more "complete" example:
113
-
114
- ```html
115
- <!doctype html>
116
- <html lang="en">
117
- <head>
118
- <!-- this is a component (very useful for sharing heads between pages) -->
119
- {{ %Head% }}
120
- </head>
121
- <body>
122
- <header>
123
- <h1>Header</h1>
124
- </header>
125
- <!-- this is a built-in component -->
126
- <main>{{ %Outlet% }}</main>
127
- <footer>Footer</footer>
128
- </body>
129
- </html>
107
+ Benefits:
108
+
109
+ - Data is fetched server-side = faster initial load
110
+ - Complete HTML sent to browser
111
+ - No loading states or spinners needed
112
+ - Better for SEO
113
+ - Zero client JavaScript
114
+
115
+ ## Authentication
116
+
117
+ Rev.js supports run-of-the-mill httpOnly cookie-based authentication:
118
+
119
+ ```typescript
120
+ export default async function Dashboard({ context }: PageProps) {
121
+ let user = null;
122
+
123
+ try {
124
+ // Get cookies from the browser's request
125
+ const cookies = context?.request?.headers.get("cookie") || "";
126
+
127
+ // Make authenticated request to your API
128
+ const response = await fetch("http://localhost:3000/api/user", {
129
+ headers: { cookie: cookies },
130
+ });
131
+
132
+ if (response.ok) {
133
+ user = await response.json();
134
+ }
135
+ } catch (err) {
136
+ // Handle error
137
+ }
138
+
139
+ return (
140
+ <>
141
+ {user ? (
142
+ <h1>Welcome {user.name}!</h1>
143
+ ) : (
144
+ <p>Not authenticated</p>
145
+ )}
146
+ </>
147
+ );
148
+ }
130
149
  ```
131
150
 
132
- The `Head` component is **not** built-in. But its not very hard to implement:
151
+ How it works:
152
+
153
+ 1. User logs in → server sets httpOnly cookies
154
+ 2. Browser sends cookies on every request automatically
155
+ 3. Component reads cookies from `context.request.headers`
156
+ 4. Component fetches authenticated data
157
+ 5. Page renders with data already loaded
158
+
159
+ ## API Integration
133
160
 
134
- In `components/Head.html`:
161
+ Add custom API routes using the `elysia` option:
135
162
 
136
- ```html
137
- <meta charset="UTF-8" />
138
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
139
- <title>Title</title>
163
+ ```typescript
164
+ new Rev({
165
+ port: 3000,
166
+ rootDir: __dirname,
167
+ elysia: (app) => {
168
+ return app
169
+ .post("/api/login", ({ body }) => {
170
+ // Validate credentials, set cookies
171
+ return { success: true };
172
+ })
173
+ .get("/api/user", ({ request }) => {
174
+ // Validate cookies, return user data
175
+ return { id: 1, name: "John" };
176
+ });
177
+ },
178
+ });
140
179
  ```
141
180
 
181
+ ## Why Bun?
182
+
183
+ Rev.js uses Bun for its:
184
+
185
+ - TypeScript support without build step
186
+ - Fast runtime (probably 3x faster than Node.js)
187
+ - Native file I/O and HTTP
188
+ - Cross-platform support (Windows, Mac, Linux)
189
+
190
+ If you prefer Node.js, this framework really isn't for you.
191
+
142
192
  ## Contributing
143
193
 
144
- This repository is not currently accepting contributions.
194
+ Rev.js is early stage and may change significantly. Contributions are not currently accepted.
145
195
 
146
196
  ## License
147
197
 
package/global.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ // Global type definitions for Rev.js applications
2
+ import type { VNode } from "preact";
3
+
4
+ declare global {
5
+ namespace JSX {
6
+ interface Element extends VNode {}
7
+ interface ElementClass {
8
+ render(): any;
9
+ }
10
+ interface ElementAttributesProperty {
11
+ props: any;
12
+ }
13
+ interface IntrinsicElements {
14
+ [elem: string]: any;
15
+ }
16
+ }
17
+
18
+ type PageContext = {
19
+ params: Record<string, string>;
20
+ query: Record<string, string>;
21
+ request?: Request;
22
+ };
23
+
24
+ type PageProps<T extends Record<string, string> = {}> = {
25
+ context?: PageContext & { params: T };
26
+ children?: VNode;
27
+ };
28
+ }
29
+
30
+ export {};
package/index.d.ts CHANGED
@@ -1,10 +1,21 @@
1
1
  import Elysia from "elysia";
2
+ import { VNode, Fragment } from "preact";
2
3
 
3
4
  declare const log: (...args: any[]) => void;
4
5
  declare const debug: (...args: any[]) => void;
5
6
  declare const warn: (...args: any[]) => void;
6
7
  declare const error: (...args: any[]) => void;
7
8
 
9
+ /**
10
+ * Context object passed to page and layout components
11
+ */
12
+ declare interface PageContext {
13
+ /** Dynamic route parameters (e.g., [slug] becomes params.slug) */
14
+ params: Record<string, string>;
15
+ /** Query string parameters */
16
+ query: Record<string, string>;
17
+ }
18
+
8
19
  declare interface RevConfig {
9
20
  port?: number;
10
21
  showDebug?: boolean;
@@ -12,6 +23,25 @@ declare interface RevConfig {
12
23
  elysia?: (app: Elysia) => Elysia;
13
24
  }
14
25
 
26
+ /**
27
+ * Rev - A JSX/TSX-based SSR framework
28
+ *
29
+ * File structure:
30
+ * - pages/_layout.tsx - Root layout (receives children and context)
31
+ * - pages/_page.tsx - Home page
32
+ * - pages/about/_page.tsx - About page with its own layout (optional)
33
+ * - pages/blog/[slug]/_page.tsx - Dynamic route
34
+ * - components/Header.tsx - Reusable component
35
+ *
36
+ * Component signature:
37
+ * export default function Page({ context, children }: { context?: PageContext; children?: VNode }) {
38
+ * return <div>{children}</div>;
39
+ * }
40
+ */
15
41
  declare class Rev {
16
42
  constructor(config: RevConfig);
17
43
  }
44
+
45
+ export { Rev as default, Fragment };
46
+ export type { RevConfig, PageContext };
47
+
package/index.ts CHANGED
@@ -37,380 +37,262 @@ export const error = (...args: any[]) => _errorf("revjs.log", ...args);
37
37
 
38
38
  //#endregion
39
39
 
40
- //#region custom ssr
40
+ /** @jsx h */
41
+ /** @jsxFrag Fragment */
41
42
 
43
+ //#region JSX SSR
44
+
45
+ import { h, Fragment, type VNode } from "preact";
46
+ import { render } from "preact-render-to-string";
42
47
  import prettier from "prettier";
43
48
  import path from "node:path";
44
49
 
45
50
  const FILENAMES = {
46
- LAYOUT: "/_layout.html",
47
- PAGE: "/_page.html",
51
+ LAYOUT: "/_layout",
52
+ PAGE: "/_page",
48
53
  SLUG: "/.slug",
49
- NOT_FOUND: "/404.html",
54
+ NOT_FOUND: "/404",
50
55
  };
51
56
 
52
57
  let PAGES_DIR = "/pages";
53
58
  let COMPONENTS_DIR = "/components/";
54
59
 
55
- const localFetch = async (path: string): Promise<string> =>
56
- await Bun.file(path).text();
57
-
58
- const localExists = async (path: string): Promise<boolean> =>
59
- await Bun.file(path).exists();
60
+ const localFetch = async (filePath: string): Promise<string> =>
61
+ await Bun.file(filePath).text();
62
+
63
+ const localExists = async (filePath: string): Promise<boolean> =>
64
+ await Bun.file(filePath).exists();
65
+
66
+ /**
67
+ * Dynamically imports a TSX/TS component file
68
+ * Supports both default exports and named exports
69
+ */
70
+ const loadComponent = async (
71
+ componentPath: string,
72
+ ): Promise<(props?: any) => any> => {
73
+ try {
74
+ const module = await import(componentPath);
75
+ // Support both default export and named export
76
+ const component = module.default || Object.values(module)[0];
77
+
78
+ if (typeof component !== "function") {
79
+ throw new Error(`${componentPath} does not export a valid component`);
80
+ }
60
81
 
61
- const urlToBasePath = (url: URL): string => {
62
- if (url.pathname == "/") return PAGES_DIR;
63
- switch (url.searchParams.get("type")) {
64
- default:
65
- return PAGES_DIR + url.pathname;
82
+ return component;
83
+ } catch (e) {
84
+ error(`Failed to load component at ${componentPath}: ${e}`);
85
+ throw e;
66
86
  }
67
87
  };
68
88
 
69
- const getComponent = async (name: string): Promise<string> => {
70
- const component = await localFetch(COMPONENTS_DIR + name + ".html");
71
- return component;
72
- };
73
-
74
- const removeFirst = function (str: string): string {
75
- return str.slice(1);
76
- };
77
-
78
- const removeLast = function (str: string): string {
79
- return str.slice(0, -1);
80
- };
81
-
82
- const removeFirstN = function (str: string, n: number): string {
83
- return str.slice(n);
89
+ const urlToBasePath = (url: URL): string => {
90
+ if (url.pathname == "/") return PAGES_DIR;
91
+ return PAGES_DIR + url.pathname;
84
92
  };
85
93
 
86
- const removeLastN = function (str: string, n: number): string {
87
- return str.slice(0, -n);
88
- };
94
+ /**
95
+ * Creates a context object for passing data to layout/page components
96
+ */
97
+ interface PageContext {
98
+ params: Record<string, string>;
99
+ query: Record<string, string>;
100
+ request?: Request;
101
+ }
89
102
 
90
- const removeFirstNIf = function (
91
- str: string,
92
- n: number,
93
- predicate: () => boolean,
94
- ): string {
95
- if (predicate()) return str.slice(n);
96
- else return str.toString();
103
+ /**
104
+ * Lists directory entries (cross-platform)
105
+ */
106
+ const listDirEntries = async (dirPath: string): Promise<string[]> => {
107
+ try {
108
+ // Use fs.promises for directory reading
109
+ const { readdir } = await import("node:fs/promises");
110
+ const entries = await readdir(dirPath);
111
+ return entries;
112
+ } catch {
113
+ return [];
114
+ }
97
115
  };
98
116
 
99
- const removeLastNIf = function (
100
- str: string,
101
- n: number,
102
- predicate: () => boolean,
103
- ): string {
104
- if (predicate()) return str.slice(0, -n);
105
- return str.toString();
117
+ /**
118
+ * Finds [param] and [...catch] style route segments
119
+ */
120
+ const findRouteSegments = (entries: string[]): { [key: string]: string } => {
121
+ const segments: { [key: string]: string } = {};
122
+ for (const entry of entries) {
123
+ if (entry.startsWith("[") && entry.endsWith("]")) {
124
+ const param = entry.slice(1, -1);
125
+ segments[entry] = param;
126
+ }
127
+ }
128
+ return segments;
106
129
  };
107
130
 
108
- enum EvalstMode {
109
- LAYOUT,
110
- PAGE,
111
- SCRIPT,
112
- CLIENT_CONDITIONAL,
113
- }
114
-
115
- interface ConditionalBranch {
116
- condition: string | null;
117
- content: string;
118
- }
119
-
120
- interface EvalstElement {
121
- match: string;
122
- mode: EvalstMode;
123
- content: string;
124
- branches?: ConditionalBranch[];
125
- }
126
-
127
- interface EvalsElement {
128
- selector: string;
129
- evaluation: string;
130
- content: string;
131
- isJs: boolean;
132
- }
131
+ /**
132
+ * Resolves a URL path to a page component and collects layouts
133
+ */
134
+ const resolvePath = async (
135
+ urlSegments: string[],
136
+ currentPath: string = PAGES_DIR,
137
+ params: Record<string, string> = {},
138
+ layouts: Array<{ path: string; component?: any }> = [],
139
+ ): Promise<{
140
+ pagePath: string | null;
141
+ params: Record<string, string>;
142
+ layouts: Array<{ path: string }>;
143
+ } | null> => {
144
+ // Check for layout at current level
145
+ const layoutPath = currentPath + FILENAMES.LAYOUT + ".tsx";
146
+ if (await localExists(layoutPath)) {
147
+ layouts.push({ path: layoutPath });
148
+ }
133
149
 
134
- const parseConditionalBlock = (
135
- content: string,
136
- startPos: number,
137
- ): { branches: ConditionalBranch[]; endPos: number; match: string } => {
138
- const ifMatch = content
139
- .slice(startPos)
140
- .match(/^\{\{#\s*if\s+(.+?)\s*#\}\}/);
141
- if (!ifMatch) throw new Error("Expected {{# if condition #}}");
142
-
143
- const branches: ConditionalBranch[] = [];
144
- const fullMatch = [ifMatch[0]];
145
- let condition: string | null = ifMatch[1].replaceAll("~", "__EVAL_STATE__");
146
- let currentPos = startPos + ifMatch[0].length;
147
- let branchContent = "";
148
-
149
- while (currentPos < content.length) {
150
- const remaining = content.slice(currentPos);
151
-
152
- const elseIfMatch = remaining.match(
153
- /^\{\{#\s*else\s+if\s+(.+?)\s*#\}\}/,
154
- );
155
- if (elseIfMatch) {
156
- branches.push({ condition, content: branchContent });
157
- fullMatch.push(elseIfMatch[0]);
158
- condition = elseIfMatch[1].replaceAll("~", "__EVAL_STATE__");
159
- currentPos += elseIfMatch[0].length;
160
- branchContent = "";
161
- continue;
150
+ // Base case: no more segments
151
+ if (urlSegments.length === 0) {
152
+ const pagePath = currentPath + FILENAMES.PAGE + ".tsx";
153
+ if (await localExists(pagePath)) {
154
+ return { pagePath, params, layouts };
162
155
  }
156
+ return null;
157
+ }
163
158
 
164
- const elseMatch = remaining.match(/^\{\{#\s*else\s*#\}\}/);
165
- if (elseMatch) {
166
- branches.push({ condition, content: branchContent });
167
- fullMatch.push(elseMatch[0]);
168
- condition = null;
169
- currentPos += elseMatch[0].length;
170
- branchContent = "";
171
- continue;
172
- }
159
+ const [nextSegment, ...restSegments] = urlSegments;
173
160
 
174
- const endifMatch = remaining.match(/^\{\{#\s*endif\s*#\}\}/);
175
- if (endifMatch) {
176
- branches.push({ condition, content: branchContent });
177
- fullMatch.push(endifMatch[0]);
178
- currentPos += endifMatch[0].length;
179
- return {
180
- branches,
181
- endPos: currentPos,
182
- match: fullMatch.join(""),
183
- };
184
- }
161
+ // Try static match first
162
+ const staticPath = path.join(currentPath, nextSegment);
163
+ const entries = await listDirEntries(staticPath);
185
164
 
186
- branchContent += remaining[0];
187
- currentPos++;
165
+ if (entries.length > 0) {
166
+ const result = await resolvePath(restSegments, staticPath, params, layouts);
167
+ if (result) return result;
188
168
  }
189
169
 
190
- throw new Error("Unclosed {{# if #}} block");
191
- };
192
-
193
- const generateConditionalHTML = (
194
- branches: ConditionalBranch[],
195
- containerId: string,
196
- state: Record<string, any>,
197
- ): string => {
198
- let html = `<div id="${containerId}">`;
199
-
200
- branches.forEach((branch, idx) => {
201
- html += `<div data-conditional-branch="${idx}" data-condition="${branch.condition || "else"}" style="display: none;">${branch.content}</div>`;
202
- });
203
-
204
- html += `</div>`;
205
-
206
- let conditionScript = "";
207
- for (let i = 0; i < branches.length; i++) {
208
- const branch = branches[i];
209
- if (i === 0) {
210
- conditionScript += `if (${branch.condition}) { document.querySelector('[data-conditional-branch="${i}"]').style.display = "block"; }`;
211
- } else if (branch.condition !== null) {
212
- conditionScript += ` else if (${branch.condition}) { document.querySelector('[data-conditional-branch="${i}"]').style.display = "block"; }`;
213
- } else {
214
- conditionScript += ` else { document.querySelector('[data-conditional-branch="${i}"]').style.display = "block"; }`;
170
+ // Try dynamic matches
171
+ const parentEntries = await listDirEntries(currentPath);
172
+ const dynamicSegments = findRouteSegments(parentEntries);
173
+
174
+ for (const [dirName, paramName] of Object.entries(dynamicSegments)) {
175
+ const dynamicPath = path.join(currentPath, dirName);
176
+ const newParams = { ...params, [paramName]: nextSegment };
177
+
178
+ // Check for catch-all [...]
179
+ if (paramName.startsWith("...")) {
180
+ const catchAllParam = paramName.slice(3);
181
+ newParams[catchAllParam] = [nextSegment, ...restSegments].join("/");
182
+ const pagePath = dynamicPath + FILENAMES.PAGE + ".tsx";
183
+ if (await localExists(pagePath)) {
184
+ // Check for layout at this dynamic level
185
+ const dynamicLayout = dynamicPath + FILENAMES.LAYOUT + ".tsx";
186
+ if (await localExists(dynamicLayout)) {
187
+ layouts.push({ path: dynamicLayout });
188
+ }
189
+ return { pagePath, params: newParams, layouts };
190
+ }
191
+ return null;
215
192
  }
216
- }
217
193
 
218
- html += `<script>
219
- (function() {
220
- let __EVAL_STATE__ = ${JSON.stringify(state)};
221
- ${conditionScript}
222
- })();
223
- </script>`;
194
+ // Regular dynamic segment
195
+ const result = await resolvePath(restSegments, dynamicPath, newParams, layouts);
196
+ if (result) return result;
197
+ }
224
198
 
225
- return html;
199
+ return null;
226
200
  };
227
201
 
228
- const evaluate = async (
229
- str: string,
230
- initialState?: Record<string, any>,
231
- outletContent?: string,
202
+ /**
203
+ * Load and render layout with page content
204
+ */
205
+ const loadLayout = async (
206
+ urlPath: string,
207
+ context: PageContext,
208
+ isDebug: boolean,
232
209
  ): Promise<string> => {
233
- let processedPageContent = str;
234
- const evalst: EvalstElement[] = [];
235
- let __EVAL_STATE__ = initialState || {};
236
-
237
- const regex = /{{ ?(.*?)? ?}}/gims;
238
- let m;
239
-
240
- while ((m = regex.exec(processedPageContent)) !== null) {
241
- if (m.index === regex.lastIndex) {
242
- regex.lastIndex++;
243
- }
244
-
245
- const match = m[1];
246
- let toEval = match.trim();
247
- if (toEval.startsWith("%") && toEval.endsWith("%")) {
248
- toEval = removeLast(removeFirst(toEval.trim())).trim();
249
- if (toEval == "Outlet" && outletContent) {
250
- evalst.push({
251
- match: m[0],
252
- mode: EvalstMode.LAYOUT,
253
- content: outletContent,
254
- });
255
- } else if (toEval == "Outlet" && !outletContent) {
256
- // str is an error!
257
- // we place some default 404 content here
258
- error(`Missing outlet content for ${toEval}`);
259
- evalst.push({
260
- match: m[0],
261
- mode: EvalstMode.LAYOUT,
262
- content: "404 Not Found (Missing Outlet Content)",
263
- });
210
+ try {
211
+ // Parse URL into segments
212
+ const urlSegments = urlPath
213
+ .split("/")
214
+ .filter(Boolean);
215
+
216
+ // Resolve the route
217
+ const routeInfo = await resolvePath(urlSegments, PAGES_DIR, {}, []);
218
+
219
+ let pageContent: any = h(Fragment, null);
220
+ let layouts: Array<{ path: string }> = [];
221
+
222
+ if (routeInfo) {
223
+ // Update context with resolved params
224
+ context.params = routeInfo.params;
225
+ layouts = routeInfo.layouts;
226
+
227
+ // Load the page component
228
+ const PageComponent = await loadComponent(routeInfo.pagePath!);
229
+ // Support both sync and async component functions
230
+ const componentResult = PageComponent({ context });
231
+ pageContent = componentResult instanceof Promise ? await componentResult : componentResult;
232
+ } else {
233
+ // Log 404 in debug mode
234
+ if (isDebug) debug(`404: Route not resolved for ${urlPath}`);
235
+
236
+ // Try 404 page
237
+ const notFoundPath = PAGES_DIR + FILENAMES.NOT_FOUND + ".tsx";
238
+ if (await localExists(notFoundPath)) {
239
+ const NotFoundComponent = await loadComponent(notFoundPath);
240
+ // Support both sync and async component functions
241
+ const componentResult = NotFoundComponent({ context });
242
+ pageContent = componentResult instanceof Promise ? await componentResult : componentResult;
264
243
  } else {
265
- try {
266
- evalst.push({
267
- match: m[0],
268
- mode: EvalstMode.LAYOUT,
269
- content: await evaluate(await getComponent(toEval)),
270
- });
271
- } catch (e) {
272
- error(`Failed to load component ${toEval}`);
273
- evalst.push({
274
- match: m[0],
275
- mode: EvalstMode.LAYOUT,
276
- content:
277
- '<i style="color: red !important">404 Not Found (Missing Component)</i>',
278
- });
279
- }
244
+ pageContent = h("html", null,
245
+ h("body", null,
246
+ h("h1", null, "404 - Not Found"),
247
+ h("p", null, "The requested page could not be found.")
248
+ )
249
+ );
280
250
  }
281
- } else if (toEval.startsWith("<") && toEval.endsWith(">")) {
282
- toEval = removeLast(removeFirst(toEval.trim()))
283
- .trim()
284
- .replaceAll("~", "__EVAL_STATE__");
285
- evalst.push({
286
- match: m[0],
287
- mode: EvalstMode.PAGE,
288
- content: toEval,
289
- });
290
- } else if (toEval.startsWith("/") && toEval.endsWith("/")) {
291
- // script mode!
292
- toEval = removeLast(removeFirst(toEval)).replaceAll(
293
- "~",
294
- "__EVAL_STATE__",
295
- );
296
251
 
297
- const code = `let __EVAL_STATE__=${JSON.stringify(
298
- __EVAL_STATE__,
299
- )};${toEval}`;
300
- const data = await Bun.$`bun -e ${code}`.json();
301
- // copy result into __EVAL_STATE__
302
- Object.assign(__EVAL_STATE__, data);
303
- evalst.push({
304
- match: m[0],
305
- mode: EvalstMode.SCRIPT,
306
- content: "",
307
- });
308
- } else {
309
- evalst.push({
310
- match: m[0],
311
- mode: EvalstMode.PAGE,
312
- content: toEval,
313
- });
252
+ // Add root layout if it exists
253
+ const rootLayout = PAGES_DIR + FILENAMES.LAYOUT + ".tsx";
254
+ if (await localExists(rootLayout)) {
255
+ layouts.push({ path: rootLayout });
256
+ }
314
257
  }
315
- }
316
-
317
- const evals: EvalsElement[] = [];
318
258
 
319
- evalst.forEach(async (element) => {
320
- if (element.mode == EvalstMode.PAGE) {
321
- evals.push({
322
- selector: element.match,
323
- evaluation: eval(element.content),
324
- content: element.content,
325
- isJs: true,
326
- });
327
- } else {
328
- evals.push({
329
- selector: element.match,
330
- evaluation: element.content,
331
- content: element.content,
332
- isJs: false,
333
- });
259
+ // Render with all layouts applied (from root outward)
260
+ let rendered = pageContent;
261
+ for (let i = layouts.length - 1; i >= 0; i--) {
262
+ const LayoutComponent = await loadComponent(layouts[i].path);
263
+ // Support both sync and async layout functions
264
+ const layoutResult = LayoutComponent({ context, children: rendered });
265
+ rendered = layoutResult instanceof Promise ? await layoutResult : layoutResult;
334
266
  }
335
- });
336
-
337
- evals.forEach((element) => {
338
- processedPageContent = processedPageContent.replace(
339
- element.selector,
340
- element.evaluation,
341
- );
342
- });
343
-
344
- let conditionalIndex = 0;
345
- let pos = 0;
346
- while (pos < processedPageContent.length) {
347
- const ifMatch = processedPageContent.slice(pos).match(/\{\{#\s*if\s+/);
348
- if (!ifMatch || ifMatch.index === undefined) break;
349
-
350
- const ifPos = pos + ifMatch.index;
351
- const parsed = parseConditionalBlock(processedPageContent, ifPos);
352
-
353
- const conditionalHTML = generateConditionalHTML(
354
- parsed.branches,
355
- `cond-${conditionalIndex}`,
356
- __EVAL_STATE__,
357
- );
358
-
359
- processedPageContent = processedPageContent.replace(
360
- parsed.match,
361
- conditionalHTML,
362
- );
363
-
364
- pos = ifPos + conditionalHTML.length;
365
- conditionalIndex++;
366
- }
367
267
 
368
- return processedPageContent;
369
- };
268
+ let html = render(rendered);
370
269
 
371
- const loadLayout = async (basePath: string): Promise<string> => {
372
- const layout = (await localExists(basePath + FILENAMES.LAYOUT))
373
- ? await localFetch(basePath + FILENAMES.LAYOUT)
374
- : await localFetch(PAGES_DIR + FILENAMES.LAYOUT);
375
-
376
- let pageContent: string;
377
- let initialState: Record<string, any> = {};
378
-
379
- if (await localExists(basePath + FILENAMES.PAGE)) {
380
- pageContent = await localFetch(basePath + FILENAMES.PAGE);
381
- } else if (await localExists(path.join(basePath, "../") + FILENAMES.SLUG)) {
382
- const slug = await localFetch(
383
- path.join(basePath, "../", FILENAMES.SLUG),
384
- );
385
- let slugName = removeLast(removeFirst(slug));
386
- initialState[slugName] = path.basename(basePath);
387
- pageContent = await localFetch(
388
- path.join(basePath, "../", slug, FILENAMES.PAGE),
389
- );
390
- } else {
391
- pageContent = await localFetch(PAGES_DIR + FILENAMES.NOT_FOUND);
392
- }
270
+ // Add DOCTYPE
271
+ html = "<!DOCTYPE html>\n" + html;
393
272
 
394
- const processedPageContent = await evaluate(pageContent, initialState);
395
- const processedLayout = await evaluate(
396
- layout,
397
- initialState,
398
- processedPageContent,
399
- );
273
+ // Prettify for development only
274
+ if (isDebug) {
275
+ html = await prettier.format(html, { parser: "html" });
276
+ }
400
277
 
401
- return processedLayout;
278
+ return html;
279
+ } catch (err) {
280
+ error(`Error loading layout: ${err}`);
281
+ throw err;
282
+ }
402
283
  };
403
284
 
404
- const loadPage = async (request: Request): Promise<string> => {
285
+ const loadPage = async (request: Request, isDebug: boolean): Promise<string> => {
405
286
  const url = new URL(request.url);
406
287
  log(`Loading ${url.pathname}`);
407
288
 
408
- const basePath = urlToBasePath(url);
409
- let page = await loadLayout(basePath);
410
-
411
- // prettify for devtools
412
- page = await prettier.format(page, { parser: "html" });
289
+ const context: PageContext = {
290
+ params: {},
291
+ query: Object.fromEntries(url.searchParams),
292
+ request,
293
+ };
413
294
 
295
+ const page = await loadLayout(url.pathname, context, isDebug);
414
296
  return page;
415
297
  };
416
298
 
@@ -448,7 +330,7 @@ class Rev {
448
330
  .get("*", async ({ request }) => {
449
331
  try {
450
332
  // custom ssr anyone?
451
- return new Response(await loadPage(request), {
333
+ return new Response(await loadPage(request, config.showDebug || false), {
452
334
  status: 200,
453
335
  headers: {
454
336
  "Content-Type": "text/html",
@@ -458,8 +340,8 @@ class Rev {
458
340
  error(
459
341
  `Error on GET ${new URL(request.url).pathname}: ${err}`,
460
342
  );
461
- if (config.showDebug) debug();
462
- return "Internal Server Error";
343
+ if (config.showDebug) debug(`${err}`);
344
+ return new Response("Internal Server Error", { status: 500 });
463
345
  }
464
346
  })
465
347
  .get("/public/*", async ({ request }) =>
@@ -493,6 +375,7 @@ class Rev {
493
375
  }
494
376
 
495
377
  export default Rev;
496
- export type { RevConfig };
378
+ export type { RevConfig, PageContext };
379
+ export { Fragment, h } from "preact";
497
380
 
498
381
  //#endregion
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@ijo-elaja/rev.js",
3
3
  "module": "index.ts",
4
- "version": "0.5.1",
4
+ "version": "0.6.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "type": "module",
9
+ "license": "MIT",
9
10
  "scripts": {
10
11
  "live": "bun run test/live/live.test.ts",
11
12
  "livedev": "bun run --watch test/live/live.test.ts"
@@ -19,6 +20,8 @@
19
20
  "dependencies": {
20
21
  "@ijo-elaja/log4js": "^1.2.0",
21
22
  "elysia": "^1.2.10",
22
- "prettier": "^3.4.2"
23
+ "preact": "^10.28.2",
24
+ "preact-render-to-string": "^6.6.5",
25
+ "prettier": "^3.8.1"
23
26
  }
24
27
  }
package/revjs.log ADDED
@@ -0,0 +1,135 @@
1
+ [main/INFO] Thu Jan 22 2026 22:46:38 Starting web app at http://localhost:8080
2
+ [main/DEBUG] Thu Jan 22 2026 22:46:38 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
3
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
4
+ [main/INFO] Thu Jan 22 2026 22:46:40 Loading /
5
+ [main/ERROR] Thu Jan 22 2026 22:46:40 Error on GET /: ShellError: Failed with exit code 1
6
+ [main/DEBUG] Thu Jan 22 2026 22:46:40
7
+ [main/INFO] Thu Jan 22 2026 22:56:22 Starting web app at http://localhost:8080
8
+ [main/DEBUG] Thu Jan 22 2026 22:56:22 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
9
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
10
+ [main/INFO] Thu Jan 22 2026 22:56:23 Loading /
11
+ [main/INFO] Thu Jan 22 2026 22:56:26 Loading /blog/getting-started
12
+ [main/ERROR] Thu Jan 22 2026 22:56:26 Error loading layout: ShellError: Failed with exit code 1
13
+ [main/ERROR] Thu Jan 22 2026 22:56:26 Error on GET /blog/getting-started: ShellError: Failed with exit code 1
14
+ [main/DEBUG] Thu Jan 22 2026 22:56:26
15
+ [main/INFO] Thu Jan 22 2026 22:56:31 Loading /about
16
+ [main/INFO] Thu Jan 22 2026 22:56:34 Loading /
17
+ [main/INFO] Thu Jan 22 2026 22:58:09 Starting web app at http://localhost:8080
18
+ [main/DEBUG] Thu Jan 22 2026 22:58:09 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
19
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
20
+ [main/INFO] Thu Jan 22 2026 22:58:11 Loading /
21
+ [main/INFO] Thu Jan 22 2026 22:58:11 Loading /about
22
+ [main/INFO] Thu Jan 22 2026 22:58:12 Loading /blog/getting-started
23
+ [main/INFO] Thu Jan 22 2026 22:58:12 Loading /
24
+ [main/INFO] Thu Jan 22 2026 22:58:13 Loading /about
25
+ [main/INFO] Thu Jan 22 2026 22:58:13 Loading /blog/getting-started
26
+ [main/INFO] Thu Jan 22 2026 22:58:48 Starting web app at http://localhost:8080
27
+ [main/DEBUG] Thu Jan 22 2026 22:58:48 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
28
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
29
+ [main/INFO] Thu Jan 22 2026 22:58:49 Loading /blog/getting-started
30
+ [main/INFO] Thu Jan 22 2026 22:58:51 Loading /blog/getting-started
31
+ [main/INFO] Thu Jan 22 2026 22:58:51 Loading /blog/file-based-routing
32
+ [main/INFO] Thu Jan 22 2026 22:58:52 Loading /blog/components
33
+ [main/INFO] Thu Jan 22 2026 22:58:52 Loading /blog/getting-started
34
+ [main/INFO] Thu Jan 22 2026 23:00:13 Starting web app at http://localhost:8080
35
+ [main/DEBUG] Thu Jan 22 2026 23:00:13 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
36
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
37
+ [main/INFO] Thu Jan 22 2026 23:01:00 Starting web app at http://localhost:8080
38
+ [main/DEBUG] Thu Jan 22 2026 23:01:00 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
39
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
40
+ [main/INFO] Thu Jan 22 2026 23:01:01 Loading /blog/getting-started
41
+ [main/INFO] Thu Jan 22 2026 23:01:02 Loading /blog/getting-started
42
+ [main/INFO] Thu Jan 22 2026 23:01:08 Loading /
43
+ [main/INFO] Thu Jan 22 2026 23:07:04 Starting web app at http://localhost:8080
44
+ [main/DEBUG] Thu Jan 22 2026 23:07:04 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
45
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
46
+ [main/INFO] Thu Jan 22 2026 23:07:05 Loading /
47
+ [main/INFO] Thu Jan 22 2026 23:07:06 Loading /
48
+ [main/INFO] Thu Jan 22 2026 23:07:07 Loading /blog/getting-started
49
+ [main/INFO] Thu Jan 22 2026 23:07:07 Loading /about
50
+ [main/INFO] Thu Jan 22 2026 23:09:33 Starting web app at http://localhost:8080
51
+ [main/DEBUG] Thu Jan 22 2026 23:09:33 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
52
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
53
+ [main/INFO] Thu Jan 22 2026 23:09:34 Loading /about
54
+ [main/INFO] Thu Jan 22 2026 23:09:37 Loading /
55
+ [main/INFO] Thu Jan 22 2026 23:09:38 Loading /blog/getting-started
56
+ [main/INFO] Thu Jan 22 2026 23:09:39 Loading /blog/file-based-routing
57
+ [main/INFO] Thu Jan 22 2026 23:11:11 Starting web app at http://localhost:8080
58
+ [main/DEBUG] Thu Jan 22 2026 23:11:11 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
59
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
60
+ [main/INFO] Thu Jan 22 2026 23:11:11 Loading /blog/file-based-routing
61
+ [main/INFO] Thu Jan 22 2026 23:13:40 Starting web app at http://localhost:8080
62
+ [main/DEBUG] Thu Jan 22 2026 23:13:40 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
63
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
64
+ [main/INFO] Thu Jan 22 2026 23:13:40 Loading /blog/file-based-routing
65
+ [main/DEBUG] Thu Jan 22 2026 23:13:40 404: Route not resolved for /blog/file-based-routing
66
+ [main/INFO] Thu Jan 22 2026 23:14:11 Starting web app at http://localhost:8080
67
+ [main/DEBUG] Thu Jan 22 2026 23:14:11 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
68
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
69
+ [main/INFO] Thu Jan 22 2026 23:14:12 Loading /blog/file-based-routing
70
+ [main/DEBUG] Thu Jan 22 2026 23:14:12 resolvePath: segments=[blog/file-based-routing] currentPath=/home/elia/dev/rev.js/test/live/pages
71
+ [main/DEBUG] Thu Jan 22 2026 23:14:12 ✗ No matching route found
72
+ [main/DEBUG] Thu Jan 22 2026 23:14:12 404: Route not resolved for /blog/file-based-routing
73
+ [main/INFO] Thu Jan 22 2026 23:14:39 Starting web app at http://localhost:8080
74
+ [main/DEBUG] Thu Jan 22 2026 23:14:39 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
75
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
76
+ [main/INFO] Thu Jan 22 2026 23:14:41 Loading /blog/file-based-routing
77
+ [main/DEBUG] Thu Jan 22 2026 23:14:41 resolvePath: segments=[blog/file-based-routing] currentPath=/home/elia/dev/rev.js/test/live/pages
78
+ [main/DEBUG] Thu Jan 22 2026 23:14:41 Checking static path: /home/elia/dev/rev.js/test/live/pages/blog - found 0 entries
79
+ [main/DEBUG] Thu Jan 22 2026 23:14:41 Parent entries at /home/elia/dev/rev.js/test/live/pages: _layout.tsx, _page.tsx, 404.tsx
80
+ [main/DEBUG] Thu Jan 22 2026 23:14:41 ✗ No matching route found
81
+ [main/DEBUG] Thu Jan 22 2026 23:14:41 404: Route not resolved for /blog/file-based-routing
82
+ [main/INFO] Thu Jan 22 2026 23:14:57 Starting web app at http://localhost:8080
83
+ [main/DEBUG] Thu Jan 22 2026 23:14:57 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
84
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
85
+ [main/INFO] Thu Jan 22 2026 23:14:59 Loading /blog/file-based-routing
86
+ [main/DEBUG] Thu Jan 22 2026 23:14:59 resolvePath: segments=[blog/file-based-routing] currentPath=/home/elia/dev/rev.js/test/live/pages
87
+ [main/DEBUG] Thu Jan 22 2026 23:14:59 Checking static path: /home/elia/dev/rev.js/test/live/pages/blog - found 0 entries
88
+ [main/DEBUG] Thu Jan 22 2026 23:14:59 Parent entries at /home/elia/dev/rev.js/test/live/pages:
89
+ [main/DEBUG] Thu Jan 22 2026 23:14:59 ✗ No matching route found
90
+ [main/DEBUG] Thu Jan 22 2026 23:14:59 404: Route not resolved for /blog/file-based-routing
91
+ [main/INFO] Thu Jan 22 2026 23:15:10 Starting web app at http://localhost:8080
92
+ [main/DEBUG] Thu Jan 22 2026 23:15:10 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
93
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
94
+ [main/INFO] Thu Jan 22 2026 23:15:11 Loading /blog/file-based-routing
95
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 resolvePath: segments=[blog/file-based-routing] currentPath=/home/elia/dev/rev.js/test/live/pages
96
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 Checking static path: /home/elia/dev/rev.js/test/live/pages/blog - found 1 entries
97
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 → Trying static path: /home/elia/dev/rev.js/test/live/pages/blog
98
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 resolvePath: segments=[file-based-routing] currentPath=/home/elia/dev/rev.js/test/live/pages/blog
99
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 Checking static path: /home/elia/dev/rev.js/test/live/pages/blog/file-based-routing - found 0 entries
100
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 Parent entries at /home/elia/dev/rev.js/test/live/pages/blog: [slug]
101
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 Found dynamic segments: [slug]→slug
102
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 → Trying dynamic path: /home/elia/dev/rev.js/test/live/pages/blog/[slug] (slug=file-based-routing)
103
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 resolvePath: segments=[] currentPath=/home/elia/dev/rev.js/test/live/pages/blog/[slug]
104
+ [main/DEBUG] Thu Jan 22 2026 23:15:11 ✓ Found page at /home/elia/dev/rev.js/test/live/pages/blog/[slug]/_page.tsx
105
+ [main/INFO] Thu Jan 22 2026 23:15:36 Starting web app at http://localhost:8080
106
+ [main/DEBUG] Thu Jan 22 2026 23:15:36 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
107
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
108
+ [main/INFO] Thu Jan 22 2026 23:15:37 Loading /blog/file-based-routing
109
+ [main/INFO] Thu Jan 22 2026 23:16:00 Starting web app at http://localhost:8080
110
+ [main/DEBUG] Thu Jan 22 2026 23:16:00 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
111
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
112
+ [main/INFO] Thu Jan 22 2026 23:16:00 Loading /blog/file-based-routing
113
+ [main/INFO] Thu Jan 22 2026 23:19:15 Starting web app at http://localhost:8080
114
+ [main/DEBUG] Thu Jan 22 2026 23:19:15 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
115
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
116
+ [main/INFO] Thu Jan 22 2026 23:19:17 Loading /blog/file-based-routing
117
+ [main/INFO] Thu Jan 22 2026 23:19:19 Loading /
118
+ [main/INFO] Thu Jan 22 2026 23:23:35 Starting web app at http://localhost:8080
119
+ [main/DEBUG] Thu Jan 22 2026 23:23:35 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
120
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
121
+ [main/INFO] Thu Jan 22 2026 23:23:36 Loading /
122
+ [main/INFO] Thu Jan 22 2026 23:23:58 Starting web app at http://localhost:8080
123
+ [main/DEBUG] Thu Jan 22 2026 23:23:58 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
124
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
125
+ [main/INFO] Thu Jan 22 2026 23:23:58 Loading /
126
+ [main/INFO] Thu Jan 22 2026 23:24:02 Loading /about
127
+ [main/INFO] Thu Jan 22 2026 23:25:55 Starting web app at http://localhost:8080
128
+ [main/DEBUG] Thu Jan 22 2026 23:25:55 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
129
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
130
+ [main/INFO] Thu Jan 22 2026 23:35:09 Starting web app at http://localhost:8080
131
+ [main/DEBUG] Thu Jan 22 2026 23:35:09 PAGES_DIR = /home/elia/dev/rev.js/test/live/pages
132
+ COMPONENTS_DIR = /home/elia/dev/rev.js/test/live/components/
133
+ [main/INFO] Thu Jan 22 2026 23:35:12 Loading /
134
+ [main/INFO] Thu Jan 22 2026 23:35:24 Loading /how-it-works
135
+ [main/INFO] Thu Jan 22 2026 23:36:03 Loading /dashboard
package/tsconfig.json CHANGED
@@ -1,27 +1,28 @@
1
1
  {
2
- "compilerOptions": {
3
- // Enable latest features
4
- "lib": ["ESNext", "DOM"],
5
- "target": "ESNext",
6
- "module": "ESNext",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
-
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
16
-
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
21
-
22
- // Some stricter flags (disabled by default)
23
- "noUnusedLocals": false,
24
- "noUnusedParameters": false,
25
- "noPropertyAccessFromIndexSignature": false
26
- }
27
- }
2
+ "compilerOptions": {
3
+ // Enable latest features
4
+ "lib": [
5
+ "ESNext",
6
+ "DOM"
7
+ ],
8
+ "target": "ESNext",
9
+ "module": "ESNext",
10
+ "moduleDetection": "force",
11
+ "jsx": "react-jsx",
12
+ "jsxImportSource": "preact",
13
+ "allowJs": true,
14
+ // Bundler mode
15
+ "moduleResolution": "bundler",
16
+ "allowImportingTsExtensions": true,
17
+ "verbatimModuleSyntax": true,
18
+ "noEmit": true,
19
+ // Best practices
20
+ "strict": true,
21
+ "skipLibCheck": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ // Some stricter flags (disabled by default)
24
+ "noUnusedLocals": false,
25
+ "noUnusedParameters": false,
26
+ "noPropertyAccessFromIndexSignature": false
27
+ },
28
+ }