vaderjs-native 1.0.27 β 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -56
- package/cli/web/server.ts +384 -0
- package/index.ts +703 -365
- package/main.ts +262 -136
- package/package.json +2 -2
- package/router/index.tsx +433 -0
- package/cli/web/server.js +0 -109
package/README.md
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<a href="[https://vader-js.pages.dev](https://vader-js.pages.dev)">
|
|
4
|
-
<picture>
|
|
5
|
-
<source media="(prefers-color-scheme: dark)" srcset="/icon.jpeg">
|
|
6
|
-
<img src="[https://github.com/Postr-Inc/Vader.js/blob/main/logo.png](https://github.com/Postr-Inc/Vader.js/blob/main/logo.png)" height="128">
|
|
7
|
-
</picture>
|
|
8
|
-
<h1 align="center">VaderNative</h1>
|
|
9
|
-
</a>
|
|
10
|
-
</p>
|
|
1
|
+
# VaderNative
|
|
11
2
|
|
|
12
3
|
**VaderNative** is a high-performance, reactive framework for building truly native cross-platform applications. It combines a familiar React-like developer experience with a "Native-First" philosophyβstreaming logs to your terminal, bundling single-file executables, and maintaining a zero-Virtual-DOM overhead.
|
|
13
4
|
|
|
14
|
-
---
|
|
15
|
-
|
|
16
5
|
## π Developer Environment Setup
|
|
17
6
|
|
|
18
7
|
Before you start coding, ensure your machine is equipped for native compilation.
|
|
@@ -33,11 +22,8 @@ To build and run on Android, you need the **Android SDK**:
|
|
|
33
22
|
# Add to your .bashrc, .zshrc, or Windows ENV
|
|
34
23
|
ANDROID_HOME=$HOME/Android/Sdk
|
|
35
24
|
PATH=$PATH:$ANDROID_HOME/platform-tools
|
|
36
|
-
|
|
37
25
|
```
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
41
27
|
### 3. Windows Setup (Desktop)
|
|
42
28
|
|
|
43
29
|
To build **WinUI 3** native desktop apps:
|
|
@@ -46,47 +32,81 @@ To build **WinUI 3** native desktop apps:
|
|
|
46
32
|
* **.NET 8 SDK:** [Download here](https://dotnet.microsoft.com/download/dotnet/8.0).
|
|
47
33
|
* **Windows App SDK:** Managed automatically by the VaderNative build script.
|
|
48
34
|
|
|
49
|
-
---
|
|
50
|
-
|
|
51
35
|
## π Getting Started
|
|
52
36
|
|
|
53
37
|
### 1. Installation
|
|
54
38
|
|
|
55
39
|
```bash
|
|
56
40
|
bun install vaderjs-native@latest
|
|
57
|
-
|
|
58
41
|
```
|
|
59
42
|
|
|
60
43
|
### 2. Create your first page
|
|
61
44
|
|
|
62
|
-
VaderNative uses **File-Based Routing**. Create a file at `app/index.jsx`:
|
|
63
|
-
|
|
64
45
|
```tsx
|
|
65
|
-
|
|
46
|
+
// App.tsx in root folder
|
|
47
|
+
import * as Vader from "vaderjs-native";
|
|
48
|
+
import { useRoute } from "vaderjs-native/router";
|
|
49
|
+
import { router } from "./src/router";
|
|
66
50
|
|
|
67
|
-
|
|
68
|
-
const
|
|
51
|
+
function App() {
|
|
52
|
+
const route = useRoute();
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
54
|
+
if (!route) {
|
|
55
|
+
const Fallback = router.getFallback();
|
|
56
|
+
return <Fallback />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const Component = route.route.component;
|
|
60
|
+
return <Component params={route.params} />;
|
|
76
61
|
}
|
|
77
62
|
|
|
63
|
+
Vader.render(<App />, document.getElementById("app")!);
|
|
78
64
|
```
|
|
79
65
|
|
|
80
|
-
|
|
66
|
+
```tsx
|
|
67
|
+
// src/router.tsx
|
|
68
|
+
import * as Vader from "vaderjs-native";
|
|
69
|
+
import { createRouter } from "vaderjs-native/router";
|
|
70
|
+
import Home from "../app/index";
|
|
71
|
+
import Login from "../app/login";
|
|
72
|
+
export const router = createRouter({
|
|
73
|
+
mode: "history",
|
|
74
|
+
routes: [
|
|
75
|
+
{ path: "/", component: Home },
|
|
76
|
+
{ path: "/login", component: Login },
|
|
77
|
+
{ path: "/login/", component: Login },
|
|
78
|
+
],
|
|
79
|
+
fallback: function NotFound() {
|
|
80
|
+
return <div>404 - Page Not Found</div>;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
```tsx
|
|
86
|
+
// app/index.tsx
|
|
87
|
+
import * as Vader from "vaderjs-native";
|
|
88
|
+
import { router } from "../src/router";
|
|
89
|
+
|
|
90
|
+
export default function Home() {
|
|
91
|
+
Vader.useEffect(() => {
|
|
92
|
+
(async () => {
|
|
93
|
+
const token = await Vader.secureStore.get("auth_token");
|
|
94
|
+
if (!token) {
|
|
95
|
+
router.navigate("/login");
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
return <div>Hello World</div>;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
83
103
|
|
|
84
|
-
|
|
104
|
+
## βοΈ Configuration (`vaderjs.config.js`)
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
import defineConfig from "vaderjs-native/config";
|
|
106
|
+
Control your app's DNA from a single config file:
|
|
88
107
|
|
|
89
|
-
|
|
108
|
+
```javascript
|
|
109
|
+
export default {
|
|
90
110
|
app: {
|
|
91
111
|
name: "MoviesPlus",
|
|
92
112
|
id: "com.moviesplus.app",
|
|
@@ -104,12 +124,9 @@ export default defineConfig({
|
|
|
104
124
|
icon: "./public/windows/icon.ico",
|
|
105
125
|
}
|
|
106
126
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
127
|
+
};
|
|
109
128
|
```
|
|
110
129
|
|
|
111
|
-
---
|
|
112
|
-
|
|
113
130
|
## π» CLI Commands & Workflow
|
|
114
131
|
|
|
115
132
|
VaderNative is designed for a **Terminal-First** workflow. No need to keep native IDEs (Android Studio/Visual Studio) open for debugging.
|
|
@@ -119,12 +136,14 @@ VaderNative is designed for a **Terminal-First** workflow. No need to keep nativ
|
|
|
119
136
|
Automatically syncs assets, starts the dev server, and streams native logs to your console.
|
|
120
137
|
|
|
121
138
|
```bash
|
|
122
|
-
#
|
|
139
|
+
# Web Development (SPA)
|
|
140
|
+
bun run vaderjs dev
|
|
141
|
+
|
|
142
|
+
# Windows Dev (Streams app.log to terminal)
|
|
123
143
|
bun run vaderjs windows:dev
|
|
124
144
|
|
|
125
|
-
#
|
|
145
|
+
# Android Dev
|
|
126
146
|
bun run vaderjs android:dev
|
|
127
|
-
|
|
128
147
|
```
|
|
129
148
|
|
|
130
149
|
### Production Building
|
|
@@ -132,15 +151,34 @@ bun run vaderjs android:dev
|
|
|
132
151
|
Compile your app into a distributable format.
|
|
133
152
|
|
|
134
153
|
```bash
|
|
154
|
+
# Build for web
|
|
155
|
+
bun run vaderjs build
|
|
156
|
+
|
|
135
157
|
# Create a Single-File Windows EXE (/release/App.exe)
|
|
136
158
|
bun run vaderjs windows:build
|
|
137
159
|
|
|
138
160
|
# Build Android APK/Bundle
|
|
139
|
-
bun
|
|
161
|
+
bun run vaderjs android:build
|
|
140
162
|
|
|
163
|
+
# Serve production build
|
|
164
|
+
bun run vaderjs serve
|
|
141
165
|
```
|
|
142
166
|
|
|
143
|
-
|
|
167
|
+
### Project Management
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Create a new Vader project
|
|
171
|
+
bun run vaderjs init [project-name]
|
|
172
|
+
|
|
173
|
+
# Add a Vader plugin
|
|
174
|
+
bun run vaderjs add <plugin-name>
|
|
175
|
+
|
|
176
|
+
# Remove a Vader plugin
|
|
177
|
+
bun run vaderjs remove <plugin-name>
|
|
178
|
+
|
|
179
|
+
# List installed plugins
|
|
180
|
+
bun run vaderjs list_plugins
|
|
181
|
+
```
|
|
144
182
|
|
|
145
183
|
## πͺ΅ Native Logging Strategy
|
|
146
184
|
|
|
@@ -148,22 +186,26 @@ VaderNative implements **Native Pipe & Log Tailing**.
|
|
|
148
186
|
* **Windows:** The CLI tails `app.log` using a shared-access stream, ensuring you see crashes even if the app UI freezes.
|
|
149
187
|
* **Android:** The CLI automatically filters `logcat` to show only your app's specific tags.
|
|
150
188
|
|
|
151
|
-
---
|
|
152
|
-
|
|
153
189
|
## π Project Structure
|
|
154
190
|
|
|
155
191
|
| Directory | Description |
|
|
156
192
|
| --- | --- |
|
|
157
|
-
| `
|
|
158
|
-
| `src/` | **Logic:** Shared components, hooks, and business logic
|
|
159
|
-
| `public/` | **Assets:** Images, fonts, and static data
|
|
160
|
-
| `
|
|
161
|
-
|
|
162
|
-
---
|
|
193
|
+
| `App.tsx` | **Root App Component:** Main entry point for your SPA/MPA |
|
|
194
|
+
| `src/` | **Logic:** Shared components, hooks, and business logic |
|
|
195
|
+
| `public/` | **Assets:** Images, fonts, and static data |
|
|
196
|
+
| `dist/` | **Build Output:** Generated web assets |
|
|
197
|
+
| `build/` | **Native Projects:** Generated native source code (WinUI/Android project files) |
|
|
163
198
|
|
|
164
199
|
## β¨ Why VaderNative?
|
|
165
200
|
|
|
166
|
-
* **Native Speed:** No heavy Virtual DOM; updates are sent directly to native views
|
|
167
|
-
* **Single-File Windows Apps:** No complex installers; just one `.exe
|
|
168
|
-
* **Bun-First:** Leverages the fastest JS runtime for building and bundling
|
|
169
|
-
* **Modern Tooling:** Tail logs, auto-patch `.csproj`, and hot-reload from one terminal
|
|
201
|
+
* **Native Speed:** No heavy Virtual DOM; updates are sent directly to native views
|
|
202
|
+
* **Single-File Windows Apps:** No complex installers; just one `.exe`
|
|
203
|
+
* **Bun-First:** Leverages the fastest JS runtime for building and bundling
|
|
204
|
+
* **Modern Tooling:** Tail logs, auto-patch `.csproj`, and hot-reload from one terminal
|
|
205
|
+
* **File-Based Routing:** Automatic route generation from `src/pages/` or `src/routes/`
|
|
206
|
+
* **SPA/MPA Support:** Choose between Single Page or Multi Page Application architecture
|
|
207
|
+
* **Plugin System:** Extend functionality with community plugins
|
|
208
|
+
* **Hot Module Replacement:** Fast development with real-time updates
|
|
209
|
+
* **Cross-Platform:** Build for web, Android, and Windows from the same codebase
|
|
210
|
+
|
|
211
|
+
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fsSync from "fs";
|
|
3
|
+
import { serve, FileSystemRouter, glob } from "bun";
|
|
4
|
+
import { logger } from "vaderjs-native/cli/logger.js";
|
|
5
|
+
import { buildAll } from "vaderjs-native/main.js";
|
|
6
|
+
|
|
7
|
+
const PROJECT_ROOT = path.join(process.cwd());
|
|
8
|
+
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
|
|
9
|
+
|
|
10
|
+
// Helper to check if a path should be ignored
|
|
11
|
+
function shouldIgnorePath(filePath: string): boolean {
|
|
12
|
+
const normalized = path.normalize(filePath);
|
|
13
|
+
|
|
14
|
+
// Ignore common system and build directories
|
|
15
|
+
const ignorePatterns = [
|
|
16
|
+
path.normalize('node_modules'),
|
|
17
|
+
path.normalize('dist'),
|
|
18
|
+
path.normalize('.git'),
|
|
19
|
+
path.normalize('.vscode'),
|
|
20
|
+
path.normalize('.idea'),
|
|
21
|
+
path.normalize('build'),
|
|
22
|
+
path.normalize('.next'),
|
|
23
|
+
path.normalize('.nuxt'),
|
|
24
|
+
path.normalize('.cache'),
|
|
25
|
+
path.normalize('temp'),
|
|
26
|
+
path.normalize('tmp'),
|
|
27
|
+
path.normalize('coverage'),
|
|
28
|
+
path.normalize('.npm'),
|
|
29
|
+
path.normalize('yarn.lock'),
|
|
30
|
+
path.normalize('package-lock.json'),
|
|
31
|
+
path.normalize('.env'),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Also ignore common temporary and system files
|
|
35
|
+
const ignoreFilePatterns = [
|
|
36
|
+
/\.log$/,
|
|
37
|
+
/\.tmp$/,
|
|
38
|
+
/\.temp$/,
|
|
39
|
+
/\.swp$/,
|
|
40
|
+
/~$/,
|
|
41
|
+
/\.DS_Store$/,
|
|
42
|
+
/Thumbs\.db$/,
|
|
43
|
+
/desktop\.ini$/,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Check if path contains any ignore patterns
|
|
47
|
+
for (const pattern of ignorePatterns) {
|
|
48
|
+
if (normalized.includes(pattern)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if filename matches ignore patterns
|
|
54
|
+
const filename = path.basename(normalized);
|
|
55
|
+
for (const pattern of ignoreFilePatterns) {
|
|
56
|
+
if (pattern.test(filename)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Track watchers for cleanup
|
|
65
|
+
const watchers = new Map<string, any>();
|
|
66
|
+
|
|
67
|
+
function safeWatch(dir: string, cb: (event: string, filename: string | null) => void) {
|
|
68
|
+
// Don't watch system directories or paths outside project
|
|
69
|
+
if (shouldIgnorePath(dir) || !dir.startsWith(PROJECT_ROOT)) {
|
|
70
|
+
logger.debug(`Skipping watch on directory: ${dir}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if directory exists
|
|
75
|
+
if (!fsSync.existsSync(dir)) {
|
|
76
|
+
logger.warn(`Directory does not exist: ${dir}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Close existing watcher if any
|
|
82
|
+
if (watchers.has(dir)) {
|
|
83
|
+
try {
|
|
84
|
+
watchers.get(dir).close();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Ignore close errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const watcher = fsSync.watch(dir, { recursive: true }, (event: string, filename: string | null) => {
|
|
91
|
+
if (!filename) return;
|
|
92
|
+
|
|
93
|
+
const changedFile = path.join(dir, filename);
|
|
94
|
+
|
|
95
|
+
// Skip ignored files
|
|
96
|
+
if (shouldIgnorePath(changedFile)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Only process files we care about
|
|
101
|
+
const ext = path.extname(changedFile).toLowerCase();
|
|
102
|
+
const relevantExtensions = ['.ts', '.tsx', '.js', '.jsx', '.css', '.html', '.json', '.config.js', '.config.ts'];
|
|
103
|
+
|
|
104
|
+
if (relevantExtensions.includes(ext) || ext === '') {
|
|
105
|
+
logger.info(`π File changed: ${path.relative(PROJECT_ROOT, changedFile)}`);
|
|
106
|
+
cb(event, filename);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
watcher.on("error", (err: Error) => logger.warn(`Watcher error on ${dir}:`, err.message));
|
|
111
|
+
watchers.set(dir, watcher);
|
|
112
|
+
|
|
113
|
+
logger.info(`π Watching directory: ${path.relative(PROJECT_ROOT, dir)}`);
|
|
114
|
+
return watcher;
|
|
115
|
+
} catch (err: any) {
|
|
116
|
+
logger.warn(`Failed to watch ${dir}:`, err.message);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function loadConfig() {
|
|
122
|
+
try {
|
|
123
|
+
const configModule = await import(path.join(PROJECT_ROOT, "vaderjs.config.js"));
|
|
124
|
+
return configModule.default || configModule;
|
|
125
|
+
} catch {
|
|
126
|
+
logger.warn("No 'vaderjs.config.js' found, using defaults.");
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function debounce(fn: Function, delay: number) {
|
|
132
|
+
let timeoutId: Timer;
|
|
133
|
+
return (...args: any[]) => {
|
|
134
|
+
clearTimeout(timeoutId);
|
|
135
|
+
timeoutId = setTimeout(() => fn(...args), delay);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Find all relevant source directories in the project
|
|
140
|
+
function findSourceDirectories(): string[] {
|
|
141
|
+
const dirs = new Set<string>();
|
|
142
|
+
|
|
143
|
+
// Always include project root
|
|
144
|
+
dirs.add(PROJECT_ROOT);
|
|
145
|
+
|
|
146
|
+
// Add common source directories if they exist
|
|
147
|
+
const commonDirs = ['src', 'app', 'pages', 'components', 'public', 'styles'];
|
|
148
|
+
for (const dir of commonDirs) {
|
|
149
|
+
const fullPath = path.join(PROJECT_ROOT, dir);
|
|
150
|
+
if (fsSync.existsSync(fullPath) && fsSync.statSync(fullPath).isDirectory()) {
|
|
151
|
+
dirs.add(fullPath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Array.from(dirs);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function handleRequestSimple(req: Request) {
|
|
159
|
+
const url = new URL(req.url);
|
|
160
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
161
|
+
|
|
162
|
+
// Add trailing slash for directories without extensions
|
|
163
|
+
if (!pathname.endsWith('/') && !path.extname(pathname)) {
|
|
164
|
+
return Response.redirect(`${pathname}/`, 308);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 1οΈβ£ STATIC FILES (with extension)
|
|
168
|
+
if (path.extname(pathname)) {
|
|
169
|
+
// Try exact path first: /styles.css β /dist/styles.css
|
|
170
|
+
let staticPath = path.join(DIST_DIR, pathname.slice(1));
|
|
171
|
+
let file = Bun.file(staticPath);
|
|
172
|
+
|
|
173
|
+
if (await file.exists()) {
|
|
174
|
+
return new Response(file, {
|
|
175
|
+
headers: { "Content-Type": getContentType(path.extname(staticPath)) }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Try route-relative: /login/foo.png β /dist/login/foo.png
|
|
180
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
181
|
+
if (segments.length > 1) {
|
|
182
|
+
staticPath = path.join(
|
|
183
|
+
DIST_DIR,
|
|
184
|
+
segments[0], // login
|
|
185
|
+
segments.slice(1).join('/')
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
file = Bun.file(staticPath);
|
|
189
|
+
if (await file.exists()) {
|
|
190
|
+
return new Response(file, {
|
|
191
|
+
headers: { "Content-Type": getContentType(path.extname(staticPath)) }
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return new Response("Not Found", { status: 404 });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 2οΈβ£ ROUTE HTML: /login β /dist/login/index.html
|
|
200
|
+
const routeHtml = path.join(
|
|
201
|
+
DIST_DIR,
|
|
202
|
+
pathname === '/' ? '' : pathname.slice(1),
|
|
203
|
+
'index.html'
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (await Bun.file(routeHtml).exists()) {
|
|
207
|
+
return new Response(Bun.file(routeHtml), {
|
|
208
|
+
headers: { "Content-Type": "text/html" }
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 3οΈβ£ SPA FALLBACK
|
|
213
|
+
const rootHtml = path.join(DIST_DIR, 'index.html');
|
|
214
|
+
return new Response(Bun.file(rootHtml), {
|
|
215
|
+
headers: { "Content-Type": "text/html" }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Helper function to get content type from file extension
|
|
220
|
+
function getContentType(ext: string): string {
|
|
221
|
+
const contentTypes: Record<string, string> = {
|
|
222
|
+
'.html': 'text/html',
|
|
223
|
+
'.htm': 'text/html',
|
|
224
|
+
'.js': 'text/javascript',
|
|
225
|
+
'.mjs': 'text/javascript',
|
|
226
|
+
'.cjs': 'text/javascript',
|
|
227
|
+
'.json': 'application/json',
|
|
228
|
+
'.css': 'text/css',
|
|
229
|
+
'.png': 'image/png',
|
|
230
|
+
'.jpg': 'image/jpeg',
|
|
231
|
+
'.jpeg': 'image/jpeg',
|
|
232
|
+
'.gif': 'image/gif',
|
|
233
|
+
'.svg': 'image/svg+xml',
|
|
234
|
+
'.ico': 'image/x-icon',
|
|
235
|
+
'.webp': 'image/webp',
|
|
236
|
+
'.woff': 'font/woff',
|
|
237
|
+
'.woff2': 'font/woff2',
|
|
238
|
+
'.ttf': 'font/ttf',
|
|
239
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
240
|
+
'.otf': 'font/otf',
|
|
241
|
+
'.mp4': 'video/mp4',
|
|
242
|
+
'.webm': 'video/webm',
|
|
243
|
+
'.mp3': 'audio/mpeg',
|
|
244
|
+
'.wav': 'audio/wav',
|
|
245
|
+
'.ogg': 'audio/ogg',
|
|
246
|
+
'.pdf': 'application/pdf',
|
|
247
|
+
'.txt': 'text/plain',
|
|
248
|
+
'.csv': 'text/csv',
|
|
249
|
+
'.xml': 'application/xml',
|
|
250
|
+
'.zip': 'application/zip',
|
|
251
|
+
'.gz': 'application/gzip',
|
|
252
|
+
'.tar': 'application/x-tar',
|
|
253
|
+
'.rar': 'application/vnd.rar',
|
|
254
|
+
'.7z': 'application/x-7z-compressed'
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return contentTypes[ext] || 'application/octet-stream';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Clean up all watchers
|
|
261
|
+
function cleanupWatchers() {
|
|
262
|
+
for (const [dir, watcher] of watchers) {
|
|
263
|
+
try {
|
|
264
|
+
watcher.close();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
// Ignore
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
watchers.clear();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// DEV SERVER
|
|
273
|
+
export default async function runDevServer() {
|
|
274
|
+
const config = await loadConfig();
|
|
275
|
+
|
|
276
|
+
// Initial build
|
|
277
|
+
console.log('π¨ Building project...');
|
|
278
|
+
await buildAll(true);
|
|
279
|
+
|
|
280
|
+
// Debug build output
|
|
281
|
+
console.log('\nπ¦ Build output:');
|
|
282
|
+
if (fsSync.existsSync(DIST_DIR)) {
|
|
283
|
+
const items = fsSync.readdirSync(DIST_DIR);
|
|
284
|
+
console.log(`Dist root contains: ${items.join(', ')}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const clients = new Set<WebSocket>();
|
|
288
|
+
const port = config.port || 3000;
|
|
289
|
+
|
|
290
|
+
logger.info(`π Starting dev server at http://localhost:${port}`);
|
|
291
|
+
logger.info(`π Project root: ${PROJECT_ROOT}`);
|
|
292
|
+
logger.info(`π¦ Dist directory: ${DIST_DIR}`);
|
|
293
|
+
logger.info(`π Build type: ${config.build_type || 'spa'}`);
|
|
294
|
+
|
|
295
|
+
const server = serve({
|
|
296
|
+
port,
|
|
297
|
+
fetch: async (req) => {
|
|
298
|
+
// handle /__hmr
|
|
299
|
+
const url = new URL(req.url);
|
|
300
|
+
if (url.pathname === '/__hmr') {
|
|
301
|
+
server.upgrade(req);
|
|
302
|
+
return new Response(null, { status: 101 });
|
|
303
|
+
}
|
|
304
|
+
return handleRequestSimple(req);
|
|
305
|
+
},
|
|
306
|
+
websocket: {
|
|
307
|
+
open: (ws) => {
|
|
308
|
+
clients.add(ws);
|
|
309
|
+
console.log(`π WebSocket connected. Total clients: ${clients.size}`);
|
|
310
|
+
},
|
|
311
|
+
close: (ws) => {
|
|
312
|
+
clients.delete(ws);
|
|
313
|
+
console.log(`π WebSocket disconnected. Total clients: ${clients.size}`);
|
|
314
|
+
},
|
|
315
|
+
message: (ws, message) => {
|
|
316
|
+
console.log(`π¨ WebSocket message: ${message}`);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const debouncedBuild = debounce(async () => {
|
|
322
|
+
try {
|
|
323
|
+
console.log('\nπ Changes detected, rebuilding...');
|
|
324
|
+
await buildAll(true);
|
|
325
|
+
|
|
326
|
+
// Notify all connected WebSocket clients to reload
|
|
327
|
+
console.log(`π’ Notifying ${clients.size} clients to reload`);
|
|
328
|
+
for (const client of clients) {
|
|
329
|
+
if (client.readyState === 1) { // WebSocket.OPEN
|
|
330
|
+
client.send("reload");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
logger.info("β
Rebuilt and reloaded");
|
|
334
|
+
} catch (e) {
|
|
335
|
+
logger.error("β Rebuild failed:", e);
|
|
336
|
+
}
|
|
337
|
+
}, 500); // Increased debounce time to 500ms
|
|
338
|
+
|
|
339
|
+
// Find and watch source directories
|
|
340
|
+
const watchDirs = findSourceDirectories();
|
|
341
|
+
console.log(`π Watching directories: ${watchDirs.map(d => path.relative(PROJECT_ROOT, d)).join(', ')}`);
|
|
342
|
+
|
|
343
|
+
for (const dir of watchDirs) {
|
|
344
|
+
safeWatch(dir, debouncedBuild);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Handle graceful shutdown
|
|
348
|
+
process.on('SIGINT', () => {
|
|
349
|
+
console.log('\nπ Shutting down...');
|
|
350
|
+
cleanupWatchers();
|
|
351
|
+
server.stop();
|
|
352
|
+
process.exit(0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
process.on('SIGTERM', () => {
|
|
356
|
+
console.log('\nπ Shutting down...');
|
|
357
|
+
cleanupWatchers();
|
|
358
|
+
server.stop();
|
|
359
|
+
process.exit(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Also handle uncaught errors
|
|
363
|
+
process.on('uncaughtException', (err) => {
|
|
364
|
+
logger.error('Uncaught exception:', err);
|
|
365
|
+
cleanupWatchers();
|
|
366
|
+
server.stop();
|
|
367
|
+
process.exit(1);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// PROD SERVER
|
|
372
|
+
export async function runProdServer() {
|
|
373
|
+
const config = await loadConfig();
|
|
374
|
+
const port = config.port || 3000;
|
|
375
|
+
|
|
376
|
+
logger.info(`π Serving production build from /dist on http://localhost:${port}`);
|
|
377
|
+
|
|
378
|
+
serve({
|
|
379
|
+
port,
|
|
380
|
+
fetch: async (req) => {
|
|
381
|
+
return handleRequestSimple(req);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|