@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 +0 -0
- package/LICENSE +1 -1
- package/README.md +155 -105
- package/global.d.ts +30 -0
- package/index.d.ts +30 -0
- package/index.ts +210 -327
- package/package.json +5 -2
- package/revjs.log +135 -0
- package/tsconfig.json +27 -26
package/.prettierrc
CHANGED
|
File without changes
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,147 +1,197 @@
|
|
|
1
1
|
# Rev.js
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- Components
|
|
25
|
-
-
|
|
26
|
-
- Client-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
27
|
+
## Quick Start
|
|
40
28
|
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
47
|
+
In `index.ts`:
|
|
78
48
|
|
|
79
|
-
```
|
|
49
|
+
```typescript
|
|
80
50
|
import Rev from "@ijo-elaja/rev.js";
|
|
81
51
|
|
|
82
52
|
new Rev({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
Add custom API routes using the `elysia` option:
|
|
135
162
|
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
PAGE: "/_page
|
|
51
|
+
LAYOUT: "/_layout",
|
|
52
|
+
PAGE: "/_page",
|
|
48
53
|
SLUG: "/.slug",
|
|
49
|
-
NOT_FOUND: "/404
|
|
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 (
|
|
56
|
-
await Bun.file(
|
|
57
|
-
|
|
58
|
-
const localExists = async (
|
|
59
|
-
await Bun.file(
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
return
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
187
|
-
|
|
165
|
+
if (entries.length > 0) {
|
|
166
|
+
const result = await resolvePath(restSegments, staticPath, params, layouts);
|
|
167
|
+
if (result) return result;
|
|
188
168
|
}
|
|
189
169
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
219
|
-
(
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
199
|
+
return null;
|
|
226
200
|
};
|
|
227
201
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
369
|
-
};
|
|
268
|
+
let html = render(rendered);
|
|
370
269
|
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
processedPageContent,
|
|
399
|
-
);
|
|
273
|
+
// Prettify for development only
|
|
274
|
+
if (isDebug) {
|
|
275
|
+
html = await prettier.format(html, { parser: "html" });
|
|
276
|
+
}
|
|
400
277
|
|
|
401
|
-
|
|
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
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|