elit 3.5.6 ā 3.5.7
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/Cargo.toml +1 -1
- package/README.md +1 -1
- package/desktop/build.rs +83 -0
- package/desktop/icon.rs +106 -0
- package/desktop/lib.rs +2 -0
- package/desktop/main.rs +235 -0
- package/desktop/native_main.rs +128 -0
- package/desktop/native_renderer/action_widgets.rs +184 -0
- package/desktop/native_renderer/app_models.rs +171 -0
- package/desktop/native_renderer/app_runtime.rs +140 -0
- package/desktop/native_renderer/container_rendering.rs +610 -0
- package/desktop/native_renderer/content_widgets.rs +634 -0
- package/desktop/native_renderer/css_models.rs +371 -0
- package/desktop/native_renderer/embedded_surfaces.rs +414 -0
- package/desktop/native_renderer/form_controls.rs +516 -0
- package/desktop/native_renderer/interaction_dispatch.rs +89 -0
- package/desktop/native_renderer/runtime_support.rs +135 -0
- package/desktop/native_renderer/utilities.rs +495 -0
- package/desktop/native_renderer/vector_drawing.rs +491 -0
- package/desktop/native_renderer.rs +4122 -0
- package/desktop/runtime/external.rs +422 -0
- package/desktop/runtime/mod.rs +67 -0
- package/desktop/runtime/quickjs.rs +106 -0
- package/desktop/window.rs +383 -0
- package/package.json +6 -3
- package/dist/build.d.mts +0 -20
- package/dist/chokidar.d.mts +0 -134
- package/dist/cli.d.mts +0 -81
- package/dist/config.d.mts +0 -254
- package/dist/coverage.d.mts +0 -85
- package/dist/database.d.mts +0 -52
- package/dist/desktop.d.mts +0 -68
- package/dist/dom.d.mts +0 -87
- package/dist/el.d.mts +0 -208
- package/dist/fs.d.mts +0 -255
- package/dist/hmr.d.mts +0 -38
- package/dist/http.d.mts +0 -169
- package/dist/https.d.mts +0 -108
- package/dist/index.d.mts +0 -13
- package/dist/mime-types.d.mts +0 -48
- package/dist/native.d.mts +0 -136
- package/dist/path.d.mts +0 -163
- package/dist/router.d.mts +0 -49
- package/dist/runtime.d.mts +0 -97
- package/dist/server-D0Dp4R5z.d.mts +0 -449
- package/dist/server.d.mts +0 -7
- package/dist/state.d.mts +0 -117
- package/dist/style.d.mts +0 -232
- package/dist/test-reporter.d.mts +0 -77
- package/dist/test-runtime.d.mts +0 -122
- package/dist/test.d.mts +0 -39
- package/dist/types.d.mts +0 -586
- package/dist/universal.d.mts +0 -21
- package/dist/ws.d.mts +0 -200
- package/dist/wss.d.mts +0 -108
- package/src/build.ts +0 -362
- package/src/chokidar.ts +0 -427
- package/src/cli.ts +0 -1162
- package/src/config.ts +0 -509
- package/src/coverage.ts +0 -1479
- package/src/database.ts +0 -1410
- package/src/desktop-auto-render.ts +0 -317
- package/src/desktop-cli.ts +0 -1533
- package/src/desktop.ts +0 -99
- package/src/dev-build.ts +0 -340
- package/src/dom.ts +0 -901
- package/src/el.ts +0 -183
- package/src/fs.ts +0 -609
- package/src/hmr.ts +0 -149
- package/src/http.ts +0 -856
- package/src/https.ts +0 -411
- package/src/index.ts +0 -16
- package/src/mime-types.ts +0 -222
- package/src/mobile-cli.ts +0 -2313
- package/src/native-background.ts +0 -444
- package/src/native-border.ts +0 -343
- package/src/native-canvas.ts +0 -260
- package/src/native-cli.ts +0 -414
- package/src/native-color.ts +0 -904
- package/src/native-estimation.ts +0 -194
- package/src/native-grid.ts +0 -590
- package/src/native-interaction.ts +0 -1289
- package/src/native-layout.ts +0 -568
- package/src/native-link.ts +0 -76
- package/src/native-render-support.ts +0 -361
- package/src/native-spacing.ts +0 -231
- package/src/native-state.ts +0 -318
- package/src/native-strings.ts +0 -46
- package/src/native-transform.ts +0 -120
- package/src/native-types.ts +0 -439
- package/src/native-typography.ts +0 -254
- package/src/native-units.ts +0 -441
- package/src/native-vector.ts +0 -910
- package/src/native.ts +0 -5606
- package/src/path.ts +0 -493
- package/src/pm-cli.ts +0 -2498
- package/src/preview-build.ts +0 -294
- package/src/render-context.ts +0 -138
- package/src/router.ts +0 -260
- package/src/runtime.ts +0 -97
- package/src/server.ts +0 -2294
- package/src/state.ts +0 -556
- package/src/style.ts +0 -1790
- package/src/test-globals.d.ts +0 -184
- package/src/test-reporter.ts +0 -609
- package/src/test-runtime.ts +0 -1359
- package/src/test.ts +0 -368
- package/src/types.ts +0 -381
- package/src/universal.ts +0 -81
- package/src/wapk-cli.ts +0 -3213
- package/src/workspace-package.ts +0 -102
- package/src/ws.ts +0 -648
- package/src/wss.ts +0 -241
package/src/server.ts
DELETED
|
@@ -1,2294 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Development server with HMR support
|
|
3
|
-
* Cross-runtime transpilation support
|
|
4
|
-
* - Node.js: uses stripTypeScriptTypes with esbuild fallback
|
|
5
|
-
* - Bun: uses Bun.Transpiler
|
|
6
|
-
* - Deno: uses Deno.emit
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as nodeModule from 'node:module';
|
|
10
|
-
|
|
11
|
-
import { createServer, IncomingMessage, ServerResponse, request as httpRequest } from './http';
|
|
12
|
-
import { request as httpsRequest } from './https';
|
|
13
|
-
import { WebSocketServer, WebSocket, ReadyState, CLOSE_CODES } from './ws';
|
|
14
|
-
import { watch } from './chokidar';
|
|
15
|
-
import { existsSync, readFile, stat, realpath } from './fs';
|
|
16
|
-
import { join, extname, relative, resolve, normalize, sep } from './path';
|
|
17
|
-
import { lookup } from './mime-types';
|
|
18
|
-
import { isBun, isDeno } from './runtime';
|
|
19
|
-
import type { DevServerOptions, DevServer, HMRMessage, Child, VNode, ProxyConfig, WebSocketEndpointConfig } from './types';
|
|
20
|
-
import { dom } from './dom';
|
|
21
|
-
|
|
22
|
-
type StripTypeScriptTypes = (
|
|
23
|
-
code: string,
|
|
24
|
-
options?: {
|
|
25
|
-
mode?: 'strip' | 'transform';
|
|
26
|
-
sourceMap?: boolean;
|
|
27
|
-
sourceUrl?: string;
|
|
28
|
-
},
|
|
29
|
-
) => string;
|
|
30
|
-
|
|
31
|
-
type NodeTransformLoader = 'ts' | 'tsx';
|
|
32
|
-
|
|
33
|
-
const stripTypeScriptTypes = typeof (nodeModule as { stripTypeScriptTypes?: unknown }).stripTypeScriptTypes === 'function'
|
|
34
|
-
? ((nodeModule as { stripTypeScriptTypes: StripTypeScriptTypes }).stripTypeScriptTypes)
|
|
35
|
-
: undefined;
|
|
36
|
-
|
|
37
|
-
let cachedNodeEsbuildTransformSync:
|
|
38
|
-
| ((code: string, options: { loader: NodeTransformLoader; format: 'esm'; target: 'es2020'; sourcemap: false | 'inline' }) => { code: string })
|
|
39
|
-
| null
|
|
40
|
-
| undefined;
|
|
41
|
-
|
|
42
|
-
function stripBrowserTypeScriptSource(source: string, filename: string): string {
|
|
43
|
-
if (!stripTypeScriptTypes) {
|
|
44
|
-
throw new Error(`TypeScript dev server transpilation requires Node.js 22+ or the esbuild package (${filename}).`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const originalEmitWarning = process.emitWarning;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
process.emitWarning = (((warning: string | Error, ...args: any[]) => {
|
|
51
|
-
if (typeof warning === 'string' && warning.includes('stripTypeScriptTypes')) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (originalEmitWarning as any).call(process, warning, ...args);
|
|
56
|
-
}) as typeof process.emitWarning);
|
|
57
|
-
|
|
58
|
-
return stripTypeScriptTypes(source, {
|
|
59
|
-
mode: 'transform',
|
|
60
|
-
sourceUrl: filename,
|
|
61
|
-
});
|
|
62
|
-
} finally {
|
|
63
|
-
process.emitWarning = originalEmitWarning;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function getNodeEsbuildTransformSync() {
|
|
68
|
-
if (cachedNodeEsbuildTransformSync !== undefined) {
|
|
69
|
-
return cachedNodeEsbuildTransformSync;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const esbuildModule = await import('esbuild') as {
|
|
74
|
-
transformSync?: (code: string, options: { loader: NodeTransformLoader; format: 'esm'; target: 'es2020'; sourcemap: false | 'inline' }) => { code: string };
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
cachedNodeEsbuildTransformSync = typeof esbuildModule.transformSync === 'function'
|
|
78
|
-
? esbuildModule.transformSync.bind(esbuildModule)
|
|
79
|
-
: null;
|
|
80
|
-
} catch {
|
|
81
|
-
cachedNodeEsbuildTransformSync = null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return cachedNodeEsbuildTransformSync;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function transpileNodeBrowserModule(source: string, options: { filename: string; loader: NodeTransformLoader; mode: 'dev' | 'preview' }): Promise<string> {
|
|
88
|
-
const compileWithEsbuild = async () => {
|
|
89
|
-
const esbuildTransformSync = await getNodeEsbuildTransformSync();
|
|
90
|
-
|
|
91
|
-
if (!esbuildTransformSync) {
|
|
92
|
-
const runtimeLabel = options.loader === 'tsx' ? 'TSX' : 'TypeScript';
|
|
93
|
-
throw new Error(`${runtimeLabel} dev server transpilation requires the esbuild package (${options.filename}).`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (options.mode === 'preview') {
|
|
97
|
-
const { default: JavaScriptObfuscator } = await import('javascript-obfuscator');
|
|
98
|
-
const tsResult = esbuildTransformSync(source, {
|
|
99
|
-
loader: options.loader,
|
|
100
|
-
format: 'esm',
|
|
101
|
-
target: 'es2020',
|
|
102
|
-
sourcemap: false,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return JavaScriptObfuscator.obfuscate(tsResult.code, {
|
|
106
|
-
compact: true,
|
|
107
|
-
renameGlobals: false,
|
|
108
|
-
}).getObfuscatedCode();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return esbuildTransformSync(source, {
|
|
112
|
-
loader: options.loader,
|
|
113
|
-
format: 'esm',
|
|
114
|
-
target: 'es2020',
|
|
115
|
-
sourcemap: 'inline',
|
|
116
|
-
}).code;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
if (options.loader === 'ts') {
|
|
120
|
-
try {
|
|
121
|
-
const stripped = stripBrowserTypeScriptSource(source, options.filename);
|
|
122
|
-
|
|
123
|
-
if (options.mode === 'preview') {
|
|
124
|
-
const { default: JavaScriptObfuscator } = await import('javascript-obfuscator');
|
|
125
|
-
return JavaScriptObfuscator.obfuscate(stripped, {
|
|
126
|
-
compact: true,
|
|
127
|
-
renameGlobals: false,
|
|
128
|
-
}).getObfuscatedCode();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return stripped;
|
|
132
|
-
} catch {
|
|
133
|
-
return compileWithEsbuild();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return compileWithEsbuild();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ===== Router =====
|
|
141
|
-
|
|
142
|
-
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD' | 'ALL';
|
|
143
|
-
|
|
144
|
-
export interface ElitRequest extends IncomingMessage {
|
|
145
|
-
body?: any;
|
|
146
|
-
query?: Record<string, string>;
|
|
147
|
-
params?: Record<string, string>;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export interface ElitResponse extends ServerResponse {
|
|
151
|
-
json(data: any, statusCode?: number): this;
|
|
152
|
-
send(data: any): this;
|
|
153
|
-
status(code: number): this;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface ServerRouteContext {
|
|
157
|
-
req: ElitRequest;
|
|
158
|
-
res: ElitResponse;
|
|
159
|
-
params: Record<string, string>;
|
|
160
|
-
query: Record<string, string>;
|
|
161
|
-
body: any;
|
|
162
|
-
headers: Record<string, string | string[] | undefined>;
|
|
163
|
-
user?: any;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export type ServerRouteHandler = (ctx: ServerRouteContext, next?: () => Promise<void>) => void | Promise<void>;
|
|
167
|
-
export type Middleware = (ctx: ServerRouteContext, next: () => Promise<void>) => void | Promise<void>;
|
|
168
|
-
|
|
169
|
-
interface ServerRoute {
|
|
170
|
-
method: HttpMethod;
|
|
171
|
-
pattern: RegExp;
|
|
172
|
-
paramNames: string[];
|
|
173
|
-
handler: ServerRouteHandler;
|
|
174
|
-
middlewares: Middleware[];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
export class ServerRouter {
|
|
179
|
-
private routes: ServerRoute[] = [];
|
|
180
|
-
private middlewares: Middleware[] = [];
|
|
181
|
-
|
|
182
|
-
// Accept both internal Middleware and Express-style `(req, res, next?)` functions
|
|
183
|
-
// Also support path-based middleware like Express: use(path, middleware)
|
|
184
|
-
use(...args: Array<any>): this {
|
|
185
|
-
if (typeof args[0] === 'string') {
|
|
186
|
-
// Path-based middleware: use(path, ...middlewares)
|
|
187
|
-
const path = args[0];
|
|
188
|
-
const middlewares = args.slice(1);
|
|
189
|
-
return this.addRoute('ALL', path, middlewares);
|
|
190
|
-
}
|
|
191
|
-
// Global middleware
|
|
192
|
-
const mw = args[0];
|
|
193
|
-
this.middlewares.push(this.toMiddleware(mw));
|
|
194
|
-
return this;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Express-like .all() method - matches all HTTP methods
|
|
198
|
-
all = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('ALL', path, handlers as any);
|
|
199
|
-
|
|
200
|
-
// Support per-route middleware: accept middleware(s) before the final handler
|
|
201
|
-
get = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('GET', path, handlers as any);
|
|
202
|
-
post = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('POST', path, handlers as any);
|
|
203
|
-
put = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('PUT', path, handlers as any);
|
|
204
|
-
delete = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('DELETE', path, handlers as any);
|
|
205
|
-
patch = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('PATCH', path, handlers as any);
|
|
206
|
-
options = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('OPTIONS', path, handlers as any);
|
|
207
|
-
head = (path: string, ...handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this => this.addRoute('HEAD', path, handlers as any);
|
|
208
|
-
|
|
209
|
-
// Convert Express-like handler/middleware to internal Middleware
|
|
210
|
-
private toMiddleware(fn: Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)): Middleware {
|
|
211
|
-
// If it's already our Middleware, return as-is
|
|
212
|
-
if ((fn as Middleware).length === 2 && (fn as any).name !== 'bound ') {
|
|
213
|
-
// Cannot reliably detect, so always wrap to normalize behavior
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return async (ctx: ServerRouteContext, next: () => Promise<void>) => {
|
|
217
|
-
const f: any = fn;
|
|
218
|
-
|
|
219
|
-
// Express-style with (req, res, next)
|
|
220
|
-
if (f.length >= 3) {
|
|
221
|
-
// Provide a next that triggers our next
|
|
222
|
-
const expressNext = () => {
|
|
223
|
-
// call our next but don't await here
|
|
224
|
-
void next();
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const res = f(ctx.req, ctx.res, expressNext);
|
|
228
|
-
if (res && typeof res.then === 'function') await res;
|
|
229
|
-
// If express middleware didn't call next(), we simply return and stop the chain
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Express-style with (req, res) - treat as middleware that continues after completion
|
|
234
|
-
if (f.length === 2) {
|
|
235
|
-
const res = f(ctx.req, ctx.res);
|
|
236
|
-
if (res && typeof res.then === 'function') await res;
|
|
237
|
-
await next();
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Our internal handler style (ctx) => ... - call it and continue
|
|
242
|
-
const out = (fn as ServerRouteHandler)(ctx);
|
|
243
|
-
if (out && typeof out.then === 'function') await out;
|
|
244
|
-
await next();
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private addRoute(method: HttpMethod, path: string, handlers: Array<Middleware | ServerRouteHandler | ((req: ElitRequest, res: ServerResponse, next?: () => void) => any)>): this {
|
|
249
|
-
const { pattern, paramNames } = this.pathToRegex(path);
|
|
250
|
-
// Last item is the actual route handler, preceding items are middlewares
|
|
251
|
-
if (!handlers || handlers.length === 0) throw new Error('Route must include a handler');
|
|
252
|
-
const rawMiddlewares = handlers.slice(0, handlers.length - 1);
|
|
253
|
-
const rawLast = handlers[handlers.length - 1];
|
|
254
|
-
|
|
255
|
-
const middlewares = rawMiddlewares.map(h => this.toMiddleware(h as any));
|
|
256
|
-
|
|
257
|
-
// Normalize last handler: if it's express-like, wrap into ServerRouteHandler
|
|
258
|
-
const last = ((): ServerRouteHandler => {
|
|
259
|
-
const f: any = rawLast;
|
|
260
|
-
if (typeof f !== 'function') throw new Error('Route handler must be a function');
|
|
261
|
-
|
|
262
|
-
if (f.length >= 2) {
|
|
263
|
-
// Express-style final handler
|
|
264
|
-
return async (ctx: ServerRouteContext) => {
|
|
265
|
-
if (f.length >= 3) {
|
|
266
|
-
// expects next
|
|
267
|
-
await new Promise<void>((resolve) => {
|
|
268
|
-
try {
|
|
269
|
-
f(ctx.req, ctx.res, () => resolve());
|
|
270
|
-
} catch (e) { resolve(); }
|
|
271
|
-
});
|
|
272
|
-
} else {
|
|
273
|
-
const res = f(ctx.req, ctx.res);
|
|
274
|
-
if (res && typeof res.then === 'function') await res;
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Already a ServerRouteHandler (ctx)
|
|
280
|
-
return f as ServerRouteHandler;
|
|
281
|
-
})();
|
|
282
|
-
|
|
283
|
-
this.routes.push({ method, pattern, paramNames, handler: last, middlewares });
|
|
284
|
-
return this;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
288
|
-
const paramNames: string[] = [];
|
|
289
|
-
const pattern = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\//g, '\\/').replace(/:(\w+)/g, (_, name) => (paramNames.push(name), '([^\\/]+)'));
|
|
290
|
-
return { pattern: new RegExp(`^${pattern}$`), paramNames };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private parseQuery(url: string): Record<string, string> {
|
|
294
|
-
const query: Record<string, string> = {};
|
|
295
|
-
const queryString = url.split('?')[1];
|
|
296
|
-
if (!queryString) return query;
|
|
297
|
-
|
|
298
|
-
queryString.split('&').forEach(p => {
|
|
299
|
-
const [k, v] = p.split('=');
|
|
300
|
-
if (k) {
|
|
301
|
-
query[k] = v !== undefined ? v : '';
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
return query;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* List all registered routes for debugging
|
|
309
|
-
*/
|
|
310
|
-
listRoutes(): Array<{ method: string; pattern: string; paramNames: string[]; handler: string }> {
|
|
311
|
-
return this.routes.map(route => ({
|
|
312
|
-
method: route.method,
|
|
313
|
-
pattern: route.pattern.source,
|
|
314
|
-
paramNames: route.paramNames,
|
|
315
|
-
handler: route.handler.name || '(anonymous)'
|
|
316
|
-
}));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
private async parseBody(req: IncomingMessage): Promise<any> {
|
|
320
|
-
// Bun compatibility: Check if req has text() method (Bun Request)
|
|
321
|
-
if (typeof (req as any).text === 'function') {
|
|
322
|
-
try {
|
|
323
|
-
const text = await (req as any).text();
|
|
324
|
-
if (!text) return {};
|
|
325
|
-
|
|
326
|
-
const contentType = req.headers['content-type'];
|
|
327
|
-
const ct = (Array.isArray(contentType) ? contentType[0] : (contentType || '')).toLowerCase();
|
|
328
|
-
|
|
329
|
-
// Parse JSON (either by content-type or if it looks like JSON)
|
|
330
|
-
if (ct.includes('application/json') || ct.includes('json') || text.trim().startsWith('{') || text.trim().startsWith('[')) {
|
|
331
|
-
try {
|
|
332
|
-
return JSON.parse(text);
|
|
333
|
-
} catch {
|
|
334
|
-
return text;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Parse URL-encoded
|
|
339
|
-
if (ct.includes('application/x-www-form-urlencoded') || ct.includes('urlencoded')) {
|
|
340
|
-
return Object.fromEntries(new URLSearchParams(text));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Return raw text
|
|
344
|
-
return text;
|
|
345
|
-
} catch (e) {
|
|
346
|
-
console.log('[ServerRouter] Bun body parse error:', e);
|
|
347
|
-
return {};
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Node.js stream-based parsing
|
|
352
|
-
return new Promise((resolve, reject) => {
|
|
353
|
-
const contentLengthHeader = req.headers['content-length'];
|
|
354
|
-
const contentLength = parseInt(Array.isArray(contentLengthHeader) ? contentLengthHeader[0] : (contentLengthHeader || '0'), 10);
|
|
355
|
-
|
|
356
|
-
if (contentLength === 0) {
|
|
357
|
-
resolve({});
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const chunks: Buffer[] = [];
|
|
362
|
-
|
|
363
|
-
req.on('data', chunk => {
|
|
364
|
-
chunks.push(Buffer.from(chunk));
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
req.on('end', () => {
|
|
368
|
-
const body = Buffer.concat(chunks).toString();
|
|
369
|
-
try {
|
|
370
|
-
const ct = req.headers['content-type'] || '';
|
|
371
|
-
resolve(ct.includes('json') ? (body ? JSON.parse(body) : {}) : ct.includes('urlencoded') ? Object.fromEntries(new URLSearchParams(body)) : body);
|
|
372
|
-
} catch (e) {
|
|
373
|
-
reject(e);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
req.on('error', reject);
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async handle(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
382
|
-
const method = req.method as HttpMethod, url = req.url || '/', path = url.split('?')[0];
|
|
383
|
-
|
|
384
|
-
for (const route of this.routes) {
|
|
385
|
-
if (route.method !== 'ALL' && route.method !== method) continue;
|
|
386
|
-
if (!route.pattern.test(path)) continue;
|
|
387
|
-
const match = path.match(route.pattern)!;
|
|
388
|
-
const params = Object.fromEntries(route.paramNames.map((name, i) => [name, match[i + 1]]));
|
|
389
|
-
|
|
390
|
-
let body: any = {};
|
|
391
|
-
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
392
|
-
try {
|
|
393
|
-
body = await this.parseBody(req);
|
|
394
|
-
// Attach body to req for Express-like compatibility
|
|
395
|
-
(req as ElitRequest).body = body;
|
|
396
|
-
}
|
|
397
|
-
catch (e) {
|
|
398
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
399
|
-
res.end('{"error":"Invalid request body"}');
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Parse query string and attach to req for Express-like compatibility
|
|
405
|
-
const query = this.parseQuery(url);
|
|
406
|
-
(req as ElitRequest).query = query;
|
|
407
|
-
|
|
408
|
-
// Attach params to req for Express-like compatibility
|
|
409
|
-
(req as ElitRequest).params = params;
|
|
410
|
-
|
|
411
|
-
// Add Express-like response helpers to res object
|
|
412
|
-
let _statusCode = 200;
|
|
413
|
-
const elitRes = res as ElitResponse;
|
|
414
|
-
|
|
415
|
-
// Implement status() method
|
|
416
|
-
elitRes.status = function(code: number): ElitResponse {
|
|
417
|
-
_statusCode = code;
|
|
418
|
-
return this;
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// Implement json() method
|
|
422
|
-
elitRes.json = function(data: any, statusCode?: number): ElitResponse {
|
|
423
|
-
const code = statusCode !== undefined ? statusCode : _statusCode;
|
|
424
|
-
this.writeHead(code, { 'Content-Type': 'application/json' });
|
|
425
|
-
this.end(JSON.stringify(data));
|
|
426
|
-
return this;
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
// Implement send() method
|
|
430
|
-
elitRes.send = function(data: any): ElitResponse {
|
|
431
|
-
if (typeof data === 'string') {
|
|
432
|
-
this.writeHead(_statusCode, { 'Content-Type': 'text/html' });
|
|
433
|
-
this.end(data);
|
|
434
|
-
} else {
|
|
435
|
-
this.writeHead(_statusCode, { 'Content-Type': 'application/json' });
|
|
436
|
-
this.end(JSON.stringify(data));
|
|
437
|
-
}
|
|
438
|
-
return this;
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
// Add Express-like response helpers to context
|
|
442
|
-
const ctx: ServerRouteContext = {
|
|
443
|
-
req: req as ElitRequest,
|
|
444
|
-
res: elitRes,
|
|
445
|
-
params,
|
|
446
|
-
query,
|
|
447
|
-
body,
|
|
448
|
-
headers: req.headers as any
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
// Build middleware chain: global middlewares -> route middlewares -> final handler
|
|
452
|
-
// Pass `next` to the final handler so it can optionally call await next()
|
|
453
|
-
const routeMiddlewares = route.middlewares || [];
|
|
454
|
-
const chain: Middleware[] = [
|
|
455
|
-
...this.middlewares,
|
|
456
|
-
...routeMiddlewares,
|
|
457
|
-
async (c, n) => { await route.handler(c, n); }
|
|
458
|
-
];
|
|
459
|
-
|
|
460
|
-
let i = 0;
|
|
461
|
-
const next = async () => {
|
|
462
|
-
if (i >= chain.length) return;
|
|
463
|
-
const mw = chain[i++];
|
|
464
|
-
await mw(ctx, next);
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
await next();
|
|
469
|
-
}
|
|
470
|
-
catch (e) {
|
|
471
|
-
console.error('[ServerRouter] Route error:', e);
|
|
472
|
-
!res.headersSent && (res.writeHead(500, { 'Content-Type': 'application/json' }), res.end(JSON.stringify({ error: 'Internal Server Error', message: e instanceof Error ? e.message : 'Unknown' })));
|
|
473
|
-
}
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// No route matched
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export const json = (res: ServerResponse, data: any, status = 200) => (res.writeHead(status, { 'Content-Type': 'application/json' }), res.end(JSON.stringify(data)));
|
|
483
|
-
export const text = (res: ServerResponse, data: string, status = 200) => (res.writeHead(status, { 'Content-Type': 'text/plain' }), res.end(data));
|
|
484
|
-
export const html = (res: ServerResponse, data: string, status = 200) => (res.writeHead(status, { 'Content-Type': 'text/html' }), res.end(data));
|
|
485
|
-
export const status = (res: ServerResponse, code: number, message = '') => (res.writeHead(code, { 'Content-Type': 'application/json' }), res.end(JSON.stringify({ status: code, message })));
|
|
486
|
-
|
|
487
|
-
// Helper functions for common responses
|
|
488
|
-
const sendError = (res: ServerResponse, code: number, msg: string): void => { res.writeHead(code, { 'Content-Type': 'text/plain' }); res.end(msg); };
|
|
489
|
-
const send404 = (res: ServerResponse, msg = 'Not Found'): void => sendError(res, 404, msg);
|
|
490
|
-
const send403 = (res: ServerResponse, msg = 'Forbidden'): void => sendError(res, 403, msg);
|
|
491
|
-
const send500 = (res: ServerResponse, msg = 'Internal Server Error'): void => sendError(res, 500, msg);
|
|
492
|
-
|
|
493
|
-
export async function resolveWorkspaceElitImportBasePath(rootDir: string, basePath: string, mode: 'dev' | 'preview'): Promise<string | undefined> {
|
|
494
|
-
const resolvedRootDir = await realpath(resolve(rootDir));
|
|
495
|
-
|
|
496
|
-
try {
|
|
497
|
-
const packageJsonBuffer = await readFile(join(resolvedRootDir, 'package.json'));
|
|
498
|
-
const packageJson = JSON.parse(packageJsonBuffer.toString()) as { name?: string };
|
|
499
|
-
|
|
500
|
-
if (packageJson.name === 'elit') {
|
|
501
|
-
const workspaceDir = mode === 'dev' ? 'src' : 'dist';
|
|
502
|
-
return basePath ? `${basePath}/${workspaceDir}` : `/${workspaceDir}`;
|
|
503
|
-
}
|
|
504
|
-
} catch {
|
|
505
|
-
// Fall back to generated package exports when the root is not the Elit package workspace.
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return undefined;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Import map for all Elit client-side modules (reused in serveFile and serveSSR)
|
|
512
|
-
export const createElitImportMap = async (rootDir: string, basePath: string = '', mode: 'dev' | 'preview' = 'dev'): Promise<string> => {
|
|
513
|
-
const workspaceImportBasePath = await resolveWorkspaceElitImportBasePath(rootDir, basePath, mode);
|
|
514
|
-
const fileExt = mode === 'dev' ? '.ts' : '.js';
|
|
515
|
-
|
|
516
|
-
const elitImports: ImportMapEntry = workspaceImportBasePath
|
|
517
|
-
? {
|
|
518
|
-
"elit": `${workspaceImportBasePath}/index${fileExt}`,
|
|
519
|
-
"elit/": `${workspaceImportBasePath}/`,
|
|
520
|
-
"elit/dom": `${workspaceImportBasePath}/dom${fileExt}`,
|
|
521
|
-
"elit/state": `${workspaceImportBasePath}/state${fileExt}`,
|
|
522
|
-
"elit/style": `${workspaceImportBasePath}/style${fileExt}`,
|
|
523
|
-
"elit/el": `${workspaceImportBasePath}/el${fileExt}`,
|
|
524
|
-
"elit/universal": `${workspaceImportBasePath}/universal${fileExt}`,
|
|
525
|
-
"elit/router": `${workspaceImportBasePath}/router${fileExt}`,
|
|
526
|
-
"elit/hmr": `${workspaceImportBasePath}/hmr${fileExt}`,
|
|
527
|
-
"elit/types": `${workspaceImportBasePath}/types${fileExt}`,
|
|
528
|
-
"elit/native": `${workspaceImportBasePath}/native${fileExt}`
|
|
529
|
-
}
|
|
530
|
-
: {};
|
|
531
|
-
|
|
532
|
-
// Generate external library imports
|
|
533
|
-
const externalImports = await generateExternalImportMaps(rootDir, basePath);
|
|
534
|
-
|
|
535
|
-
// Merge imports (Elit imports take precedence)
|
|
536
|
-
const allImports = { ...externalImports, ...elitImports };
|
|
537
|
-
|
|
538
|
-
return `<script type="importmap">${JSON.stringify({ imports: allImports }, null, 2)}</script>`;
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
const ELIT_INTERNAL_WS_PATH = '/__elit_ws';
|
|
542
|
-
|
|
543
|
-
// Helper function to generate HMR script (reused in serveFile and serveSSR)
|
|
544
|
-
const createHMRScript = (port: number): string =>
|
|
545
|
-
`<script>(function(){let ws;let retries=0;let maxRetries=5;const protocol=window.location.protocol==='https:'?'wss://':'ws://';function connect(){ws=new WebSocket(protocol+window.location.hostname+':${port}${ELIT_INTERNAL_WS_PATH}');ws.onopen=()=>{console.log('[Elit HMR] Connected');retries=0};ws.onmessage=(e)=>{const d=JSON.parse(e.data);if(d.type==='update'){console.log('[Elit HMR] File updated:',d.path);window.location.reload()}else if(d.type==='reload'){console.log('[Elit HMR] Reloading...');window.location.reload()}else if(d.type==='error')console.error('[Elit HMR] Error:',d.error)};ws.onclose=()=>{if(retries<maxRetries){retries++;setTimeout(connect,1000*retries)}else if(retries===maxRetries){console.log('[Elit HMR] Connection closed. Start dev server to reconnect.')}};ws.onerror=()=>{ws.close()}}connect()})();</script>`;
|
|
546
|
-
|
|
547
|
-
// Helper function to rewrite relative paths with basePath (reused in serveFile and serveSSR)
|
|
548
|
-
const rewriteRelativePaths = (html: string, basePath: string): string => {
|
|
549
|
-
if (!basePath) return html;
|
|
550
|
-
// Rewrite paths starting with ./ or just relative paths (not starting with /, http://, https://)
|
|
551
|
-
html = html.replace(/(<script[^>]+src=["'])(?!https?:\/\/|\/)(\.\/)?([^"']+)(["'])/g, `$1${basePath}/$3$4`);
|
|
552
|
-
html = html.replace(/(<link[^>]+href=["'])(?!https?:\/\/|\/)(\.\/)?([^"']+)(["'])/g, `$1${basePath}/$3$4`);
|
|
553
|
-
return html;
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
// Helper function to normalize basePath (reused in serveFile and serveSSR)
|
|
557
|
-
const normalizeBasePath = (basePath?: string): string => basePath && basePath !== '/' ? basePath : '';
|
|
558
|
-
|
|
559
|
-
const normalizeWebSocketPath = (path: string): string => {
|
|
560
|
-
let normalizedPath = path.trim();
|
|
561
|
-
|
|
562
|
-
if (!normalizedPath) {
|
|
563
|
-
return '/';
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if (!normalizedPath.startsWith('/')) {
|
|
567
|
-
normalizedPath = `/${normalizedPath}`;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
|
|
571
|
-
normalizedPath = normalizedPath.slice(0, -1);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return normalizedPath;
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
const getRequestPath = (url: string): string => {
|
|
578
|
-
const [pathname = '/'] = url.split('?');
|
|
579
|
-
return pathname || '/';
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
const parseRequestQuery = (url: string): Record<string, string> => {
|
|
583
|
-
const query: Record<string, string> = {};
|
|
584
|
-
const queryString = url.split('?')[1];
|
|
585
|
-
|
|
586
|
-
if (!queryString) {
|
|
587
|
-
return query;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
for (const entry of queryString.split('&')) {
|
|
591
|
-
if (!entry) {
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const [rawKey, rawValue = ''] = entry.split('=');
|
|
596
|
-
|
|
597
|
-
if (!rawKey) {
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
query[decodeURIComponent(rawKey)] = decodeURIComponent(rawValue);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
return query;
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
interface NormalizedWebSocketEndpoint {
|
|
608
|
-
path: string;
|
|
609
|
-
handler: WebSocketEndpointConfig['handler'];
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const normalizeWebSocketEndpoints = (endpoints: WebSocketEndpointConfig[] | undefined, basePath: string = ''): NormalizedWebSocketEndpoint[] => {
|
|
613
|
-
const normalizedBasePath = normalizeBasePath(basePath);
|
|
614
|
-
|
|
615
|
-
return (endpoints || []).map(endpoint => {
|
|
616
|
-
const normalizedPath = normalizeWebSocketPath(endpoint.path);
|
|
617
|
-
const fullPath = !normalizedBasePath
|
|
618
|
-
? normalizedPath
|
|
619
|
-
: normalizedPath === normalizedBasePath || normalizedPath.startsWith(`${normalizedBasePath}/`)
|
|
620
|
-
? normalizedPath
|
|
621
|
-
: normalizedPath === '/'
|
|
622
|
-
? normalizedBasePath
|
|
623
|
-
: `${normalizedBasePath}${normalizedPath}`;
|
|
624
|
-
|
|
625
|
-
return {
|
|
626
|
-
path: fullPath,
|
|
627
|
-
handler: endpoint.handler
|
|
628
|
-
};
|
|
629
|
-
});
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
const requestAcceptsGzip = (acceptEncoding: string | string[] | undefined): boolean => {
|
|
633
|
-
if (Array.isArray(acceptEncoding)) {
|
|
634
|
-
return acceptEncoding.some(value => /\bgzip\b/i.test(value));
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return typeof acceptEncoding === 'string' && /\bgzip\b/i.test(acceptEncoding);
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
// Helper function to find dist or node_modules directory by walking up the directory tree
|
|
641
|
-
async function findSpecialDir(startDir: string, targetDir: string): Promise<string | null> {
|
|
642
|
-
let currentDir = startDir;
|
|
643
|
-
const maxLevels = 5; // Prevent infinite loop
|
|
644
|
-
|
|
645
|
-
for (let i = 0; i < maxLevels; i++) {
|
|
646
|
-
const targetPath = resolve(currentDir, targetDir);
|
|
647
|
-
try {
|
|
648
|
-
const stats = await stat(targetPath);
|
|
649
|
-
if (stats.isDirectory()) {
|
|
650
|
-
return currentDir; // Return the parent directory containing the target
|
|
651
|
-
}
|
|
652
|
-
} catch {
|
|
653
|
-
// Directory doesn't exist, try parent
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const parentDir = resolve(currentDir, '..');
|
|
657
|
-
if (parentDir === currentDir) break; // Reached filesystem root
|
|
658
|
-
currentDir = parentDir;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// ===== External Library Import Maps =====
|
|
665
|
-
|
|
666
|
-
interface PackageExports {
|
|
667
|
-
[key: string]: string | PackageExports;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
interface PackageJson {
|
|
671
|
-
name?: string;
|
|
672
|
-
main?: string;
|
|
673
|
-
module?: string;
|
|
674
|
-
browser?: string | Record<string, string | false>;
|
|
675
|
-
exports?: string | PackageExports | { [key: string]: any };
|
|
676
|
-
type?: 'module' | 'commonjs';
|
|
677
|
-
sideEffects?: boolean | string[];
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
interface ImportMapEntry {
|
|
681
|
-
[importName: string]: string;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
interface TransformCacheEntry {
|
|
685
|
-
content: Buffer;
|
|
686
|
-
mimeType: string;
|
|
687
|
-
mtimeMs: number;
|
|
688
|
-
size: number;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Cache for generated import maps to avoid re-scanning
|
|
692
|
-
const importMapCache = new Map<string, ImportMapEntry>();
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Clear import map cache (useful when packages are added/removed)
|
|
696
|
-
*/
|
|
697
|
-
export function clearImportMapCache(): void {
|
|
698
|
-
importMapCache.clear();
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function toBuffer(content: string | Buffer): Buffer {
|
|
702
|
-
return typeof content === 'string' ? Buffer.from(content) : content;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function createTransformCacheKey(filePath: string, mode: 'dev' | 'preview', query: string): string {
|
|
706
|
-
return `${mode}:${query}:${filePath}`;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function getValidTransformCacheEntry(
|
|
710
|
-
transformCache: Map<string, TransformCacheEntry>,
|
|
711
|
-
cacheKey: string,
|
|
712
|
-
stats: { mtimeMs: number; size: number },
|
|
713
|
-
): TransformCacheEntry | undefined {
|
|
714
|
-
const entry = transformCache.get(cacheKey);
|
|
715
|
-
if (!entry) {
|
|
716
|
-
return undefined;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
if (entry.mtimeMs === stats.mtimeMs && entry.size === stats.size) {
|
|
720
|
-
return entry;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
transformCache.delete(cacheKey);
|
|
724
|
-
return undefined;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Scan node_modules and generate import maps for external libraries
|
|
729
|
-
*/
|
|
730
|
-
async function generateExternalImportMaps(rootDir: string, basePath: string = ''): Promise<ImportMapEntry> {
|
|
731
|
-
const cacheKey = `${rootDir}:${basePath}`;
|
|
732
|
-
if (importMapCache.has(cacheKey)) {
|
|
733
|
-
return importMapCache.get(cacheKey)!;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
const importMap: ImportMapEntry = {};
|
|
737
|
-
const nodeModulesPath = await findNodeModules(rootDir);
|
|
738
|
-
|
|
739
|
-
if (!nodeModulesPath) {
|
|
740
|
-
importMapCache.set(cacheKey, importMap);
|
|
741
|
-
return importMap;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
try {
|
|
745
|
-
const { readdir } = await import('./fs');
|
|
746
|
-
const packages = await readdir(nodeModulesPath);
|
|
747
|
-
|
|
748
|
-
for (const pkgEntry of packages) {
|
|
749
|
-
// Convert Dirent to string
|
|
750
|
-
const pkg = typeof pkgEntry === 'string' ? pkgEntry : pkgEntry.name;
|
|
751
|
-
|
|
752
|
-
// Skip special directories
|
|
753
|
-
if (pkg.startsWith('.')) continue;
|
|
754
|
-
|
|
755
|
-
// Handle scoped packages (@org/package)
|
|
756
|
-
if (pkg.startsWith('@')) {
|
|
757
|
-
try {
|
|
758
|
-
const scopedPackages = await readdir(join(nodeModulesPath, pkg));
|
|
759
|
-
for (const scopedEntry of scopedPackages) {
|
|
760
|
-
const scopedPkg = typeof scopedEntry === 'string' ? scopedEntry : scopedEntry.name;
|
|
761
|
-
const fullPkgName = `${pkg}/${scopedPkg}`;
|
|
762
|
-
await processPackage(nodeModulesPath, fullPkgName, importMap, basePath);
|
|
763
|
-
}
|
|
764
|
-
} catch {
|
|
765
|
-
// Skip if can't read scoped directory
|
|
766
|
-
}
|
|
767
|
-
} else {
|
|
768
|
-
await processPackage(nodeModulesPath, pkg, importMap, basePath);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
} catch (error) {
|
|
772
|
-
console.error('[Import Maps] Error scanning node_modules:', error);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
importMapCache.set(cacheKey, importMap);
|
|
776
|
-
return importMap;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Find node_modules directory by walking up the directory tree
|
|
781
|
-
*/
|
|
782
|
-
async function findNodeModules(startDir: string): Promise<string | null> {
|
|
783
|
-
const foundDir = await findSpecialDir(startDir, 'node_modules');
|
|
784
|
-
return foundDir ? join(foundDir, 'node_modules') : null;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* Check if a package is browser-compatible
|
|
789
|
-
*/
|
|
790
|
-
function isBrowserCompatible(pkgName: string, pkgJson: PackageJson): boolean {
|
|
791
|
-
// Skip build tools, compilers, and Node.js-only packages
|
|
792
|
-
const buildTools = [
|
|
793
|
-
'typescript', 'esbuild', '@esbuild/',
|
|
794
|
-
'tsx', 'tsup', 'rollup', 'vite', 'webpack', 'parcel',
|
|
795
|
-
'terser', 'uglify', 'babel', '@babel/',
|
|
796
|
-
'postcss', 'autoprefixer', 'cssnano',
|
|
797
|
-
'sass', 'less', 'stylus'
|
|
798
|
-
];
|
|
799
|
-
|
|
800
|
-
const nodeOnly = [
|
|
801
|
-
'node-', '@node-', 'fsevents', 'chokidar',
|
|
802
|
-
'express', 'koa', 'fastify', 'nest',
|
|
803
|
-
'commander', 'yargs', 'inquirer', 'chalk', 'ora',
|
|
804
|
-
'nodemon', 'pm2', 'dotenv'
|
|
805
|
-
];
|
|
806
|
-
|
|
807
|
-
const testingTools = [
|
|
808
|
-
'jest', 'vitest', 'mocha', 'chai', 'jasmine',
|
|
809
|
-
'@jest/', '@testing-library/', '@vitest/',
|
|
810
|
-
'playwright', 'puppeteer', 'cypress'
|
|
811
|
-
];
|
|
812
|
-
|
|
813
|
-
const linters = [
|
|
814
|
-
'eslint', '@eslint/', 'prettier', 'tslint',
|
|
815
|
-
'stylelint', 'commitlint'
|
|
816
|
-
];
|
|
817
|
-
|
|
818
|
-
const typeDefinitions = [
|
|
819
|
-
'@types/', '@typescript-eslint/'
|
|
820
|
-
];
|
|
821
|
-
|
|
822
|
-
const utilities = [
|
|
823
|
-
'get-tsconfig', 'resolve-pkg-maps', 'pkg-types',
|
|
824
|
-
'fast-glob', 'globby', 'micromatch',
|
|
825
|
-
'execa', 'cross-spawn', 'shelljs'
|
|
826
|
-
];
|
|
827
|
-
|
|
828
|
-
// Combine all skip lists
|
|
829
|
-
const skipPatterns = [
|
|
830
|
-
...buildTools,
|
|
831
|
-
...nodeOnly,
|
|
832
|
-
...testingTools,
|
|
833
|
-
...linters,
|
|
834
|
-
...typeDefinitions,
|
|
835
|
-
...utilities
|
|
836
|
-
];
|
|
837
|
-
|
|
838
|
-
// Check if package name matches skip patterns
|
|
839
|
-
if (skipPatterns.some(pattern => pkgName.startsWith(pattern))) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Skip CommonJS-only lodash (prefer lodash-es)
|
|
844
|
-
if (pkgName === 'lodash') {
|
|
845
|
-
return false;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// Prefer packages with explicit browser field or module field (ESM)
|
|
849
|
-
if (pkgJson.browser || pkgJson.module) {
|
|
850
|
-
return true;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Prefer packages with exports field that includes "import" or "browser"
|
|
854
|
-
if (pkgJson.exports) {
|
|
855
|
-
const exportsStr = JSON.stringify(pkgJson.exports);
|
|
856
|
-
if (exportsStr.includes('"import"') || exportsStr.includes('"browser"')) {
|
|
857
|
-
return true;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// Skip packages that are explicitly marked as type: "commonjs" without module/browser fields
|
|
862
|
-
if (pkgJson.type === 'commonjs' && !pkgJson.module && !pkgJson.browser) {
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Default: allow if it has exports or is type: "module"
|
|
867
|
-
return !!(pkgJson.exports || pkgJson.type === 'module' || pkgJson.module);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Process a single package and add its exports to the import map
|
|
872
|
-
*/
|
|
873
|
-
async function processPackage(
|
|
874
|
-
nodeModulesPath: string,
|
|
875
|
-
pkgName: string,
|
|
876
|
-
importMap: ImportMapEntry,
|
|
877
|
-
basePath: string
|
|
878
|
-
): Promise<void> {
|
|
879
|
-
const pkgPath = join(nodeModulesPath, pkgName);
|
|
880
|
-
const pkgJsonPath = join(pkgPath, 'package.json');
|
|
881
|
-
|
|
882
|
-
try {
|
|
883
|
-
const pkgJsonContent = await readFile(pkgJsonPath);
|
|
884
|
-
const pkgJson: PackageJson = JSON.parse(pkgJsonContent.toString());
|
|
885
|
-
|
|
886
|
-
// Check if package is browser-compatible
|
|
887
|
-
if (!isBrowserCompatible(pkgName, pkgJson)) {
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const baseUrl = basePath ? `${basePath}/node_modules/${pkgName}` : `/node_modules/${pkgName}`;
|
|
892
|
-
|
|
893
|
-
// Handle exports field (modern)
|
|
894
|
-
if (pkgJson.exports) {
|
|
895
|
-
processExportsField(pkgName, pkgJson.exports, baseUrl, importMap);
|
|
896
|
-
}
|
|
897
|
-
// Fallback to main/module/browser fields (legacy)
|
|
898
|
-
else {
|
|
899
|
-
const entryPoint = pkgJson.browser || pkgJson.module || pkgJson.main || 'index.js';
|
|
900
|
-
importMap[pkgName] = `${baseUrl}/${entryPoint}`;
|
|
901
|
-
|
|
902
|
-
// Add trailing slash for subpath imports
|
|
903
|
-
importMap[`${pkgName}/`] = `${baseUrl}/`;
|
|
904
|
-
}
|
|
905
|
-
} catch {
|
|
906
|
-
// Skip packages without package.json or invalid JSON
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* Process package.json exports field and add to import map
|
|
912
|
-
*/
|
|
913
|
-
function processExportsField(
|
|
914
|
-
pkgName: string,
|
|
915
|
-
exports: string | PackageExports | { [key: string]: any },
|
|
916
|
-
baseUrl: string,
|
|
917
|
-
importMap: ImportMapEntry
|
|
918
|
-
): void {
|
|
919
|
-
// Simple string export
|
|
920
|
-
if (typeof exports === 'string') {
|
|
921
|
-
importMap[pkgName] = `${baseUrl}/${exports}`;
|
|
922
|
-
importMap[`${pkgName}/`] = `${baseUrl}/`;
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// Object exports
|
|
927
|
-
if (typeof exports === 'object' && exports !== null) {
|
|
928
|
-
// Handle "." export (main entry)
|
|
929
|
-
if ('.' in exports) {
|
|
930
|
-
const dotExport = exports['.'];
|
|
931
|
-
const resolved = resolveExport(dotExport);
|
|
932
|
-
if (resolved) {
|
|
933
|
-
importMap[pkgName] = `${baseUrl}/${resolved}`;
|
|
934
|
-
}
|
|
935
|
-
} else if ('import' in exports) {
|
|
936
|
-
// Root-level import/require
|
|
937
|
-
const resolved = resolveExport(exports);
|
|
938
|
-
if (resolved) {
|
|
939
|
-
importMap[pkgName] = `${baseUrl}/${resolved}`;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// Handle subpath exports
|
|
944
|
-
for (const [key, value] of Object.entries(exports)) {
|
|
945
|
-
if (key === '.' || key === 'import' || key === 'require' || key === 'types' || key === 'default') {
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const resolved = resolveExport(value);
|
|
950
|
-
if (resolved) {
|
|
951
|
-
// Remove leading ./ from key
|
|
952
|
-
const cleanKey = key.startsWith('./') ? key.slice(2) : key;
|
|
953
|
-
const importName = cleanKey ? `${pkgName}/${cleanKey}` : pkgName;
|
|
954
|
-
importMap[importName] = `${baseUrl}/${resolved}`;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Always add trailing slash for subpath imports
|
|
959
|
-
importMap[`${pkgName}/`] = `${baseUrl}/`;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
/**
|
|
964
|
-
* Resolve export value to actual file path
|
|
965
|
-
* Handles conditional exports (import/require/default)
|
|
966
|
-
*/
|
|
967
|
-
function resolveExport(exportValue: any): string | null {
|
|
968
|
-
if (typeof exportValue === 'string') {
|
|
969
|
-
// Remove leading ./
|
|
970
|
-
return exportValue.startsWith('./') ? exportValue.slice(2) : exportValue;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (typeof exportValue === 'object' && exportValue !== null) {
|
|
974
|
-
// Prefer import over require over default
|
|
975
|
-
const resolved = exportValue.import || exportValue.browser || exportValue.default || exportValue.require;
|
|
976
|
-
|
|
977
|
-
// Handle nested objects recursively (e.g., TypeScript's complex exports)
|
|
978
|
-
if (typeof resolved === 'object' && resolved !== null) {
|
|
979
|
-
return resolveExport(resolved);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (typeof resolved === 'string') {
|
|
983
|
-
return resolved.startsWith('./') ? resolved.slice(2) : resolved;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
return null;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
// ===== Middleware =====
|
|
991
|
-
|
|
992
|
-
export function cors(options: {
|
|
993
|
-
origin?: string | string[];
|
|
994
|
-
methods?: string[];
|
|
995
|
-
credentials?: boolean;
|
|
996
|
-
maxAge?: number;
|
|
997
|
-
} = {}): Middleware {
|
|
998
|
-
const { origin = '*', methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], credentials = true, maxAge = 86400 } = options;
|
|
999
|
-
|
|
1000
|
-
return async (ctx, next) => {
|
|
1001
|
-
const requestOriginHeader = ctx.req.headers.origin;
|
|
1002
|
-
const requestOrigin = Array.isArray(requestOriginHeader) ? requestOriginHeader[0] : (requestOriginHeader || '');
|
|
1003
|
-
const allowOrigin = Array.isArray(origin) && origin.includes(requestOrigin) ? requestOrigin : (Array.isArray(origin) ? '' : origin);
|
|
1004
|
-
|
|
1005
|
-
if (allowOrigin) ctx.res.setHeader('Access-Control-Allow-Origin', allowOrigin);
|
|
1006
|
-
ctx.res.setHeader('Access-Control-Allow-Methods', methods.join(', '));
|
|
1007
|
-
ctx.res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
1008
|
-
if (credentials) ctx.res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
1009
|
-
ctx.res.setHeader('Access-Control-Max-Age', String(maxAge));
|
|
1010
|
-
|
|
1011
|
-
if (ctx.req.method === 'OPTIONS') {
|
|
1012
|
-
ctx.res.writeHead(204);
|
|
1013
|
-
ctx.res.end();
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
await next();
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
export function logger(options: { format?: 'simple' | 'detailed' } = {}): Middleware {
|
|
1021
|
-
const { format = 'simple' } = options;
|
|
1022
|
-
return async (ctx, next) => {
|
|
1023
|
-
const start = Date.now();
|
|
1024
|
-
const { method, url } = ctx.req;
|
|
1025
|
-
await next();
|
|
1026
|
-
const duration = Date.now() - start;
|
|
1027
|
-
const status = ctx.res.statusCode;
|
|
1028
|
-
console.log(format === 'detailed' ? `[${new Date().toISOString()}] ${method} ${url} ${status} - ${duration}ms` : `${method} ${url} - ${status} (${duration}ms)`);
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
export function errorHandler(): Middleware {
|
|
1033
|
-
return async (ctx, next) => {
|
|
1034
|
-
try {
|
|
1035
|
-
await next();
|
|
1036
|
-
} catch (error) {
|
|
1037
|
-
console.error('Error:', error);
|
|
1038
|
-
if (!ctx.res.headersSent) {
|
|
1039
|
-
ctx.res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1040
|
-
ctx.res.end(JSON.stringify({ error: 'Internal Server Error', message: error instanceof Error ? error.message : 'Unknown error' }));
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
export function rateLimit(options: { windowMs?: number; max?: number; message?: string } = {}): Middleware {
|
|
1047
|
-
const { windowMs = 60000, max = 100, message = 'Too many requests' } = options;
|
|
1048
|
-
const clients = new Map<string, { count: number; resetTime: number }>();
|
|
1049
|
-
|
|
1050
|
-
return async (ctx, next) => {
|
|
1051
|
-
const ip = ctx.req.socket.remoteAddress || 'unknown';
|
|
1052
|
-
const now = Date.now();
|
|
1053
|
-
let clientData = clients.get(ip);
|
|
1054
|
-
|
|
1055
|
-
if (!clientData || now > clientData.resetTime) {
|
|
1056
|
-
clientData = { count: 0, resetTime: now + windowMs };
|
|
1057
|
-
clients.set(ip, clientData);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
if (++clientData.count > max) {
|
|
1061
|
-
ctx.res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
1062
|
-
ctx.res.end(JSON.stringify({ error: message }));
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
await next();
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
export function bodyLimit(options: { limit?: number } = {}): Middleware {
|
|
1070
|
-
const { limit = 1024 * 1024 } = options;
|
|
1071
|
-
return async (ctx, next) => {
|
|
1072
|
-
const contentLength = ctx.req.headers['content-length'];
|
|
1073
|
-
const contentLengthStr = Array.isArray(contentLength) ? contentLength[0] : (contentLength || '0');
|
|
1074
|
-
if (parseInt(contentLengthStr, 10) > limit) {
|
|
1075
|
-
ctx.res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
1076
|
-
ctx.res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
await next();
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
export function cacheControl(options: { maxAge?: number; public?: boolean } = {}): Middleware {
|
|
1084
|
-
const { maxAge = 3600, public: isPublic = true } = options;
|
|
1085
|
-
return async (ctx, next) => {
|
|
1086
|
-
ctx.res.setHeader('Cache-Control', `${isPublic ? 'public' : 'private'}, max-age=${maxAge}`);
|
|
1087
|
-
await next();
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
export function compress(): Middleware {
|
|
1092
|
-
return async (ctx, next) => {
|
|
1093
|
-
if (isBun || !requestAcceptsGzip(ctx.req.headers['accept-encoding'])) {
|
|
1094
|
-
await next();
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Store original end method
|
|
1099
|
-
const originalEnd = ctx.res.end.bind(ctx.res);
|
|
1100
|
-
const chunks: Buffer[] = [];
|
|
1101
|
-
|
|
1102
|
-
// Intercept response data
|
|
1103
|
-
ctx.res.write = ((chunk: any) => {
|
|
1104
|
-
chunks.push(Buffer.from(chunk));
|
|
1105
|
-
return true;
|
|
1106
|
-
}) as any;
|
|
1107
|
-
|
|
1108
|
-
ctx.res.end = ((chunk?: any) => {
|
|
1109
|
-
if (chunk) chunks.push(Buffer.from(chunk));
|
|
1110
|
-
|
|
1111
|
-
const buffer = Buffer.concat(chunks);
|
|
1112
|
-
const { gzipSync } = require('zlib');
|
|
1113
|
-
const compressed = gzipSync(buffer);
|
|
1114
|
-
|
|
1115
|
-
ctx.res.setHeader('Content-Encoding', 'gzip');
|
|
1116
|
-
ctx.res.setHeader('Content-Length', compressed.length);
|
|
1117
|
-
originalEnd(compressed);
|
|
1118
|
-
return ctx.res;
|
|
1119
|
-
}) as any;
|
|
1120
|
-
|
|
1121
|
-
await next();
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
export function security(): Middleware {
|
|
1126
|
-
return async (ctx, next) => {
|
|
1127
|
-
ctx.res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
1128
|
-
ctx.res.setHeader('X-Frame-Options', 'DENY');
|
|
1129
|
-
ctx.res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
1130
|
-
ctx.res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
1131
|
-
ctx.res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
1132
|
-
ctx.res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
1133
|
-
await next();
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// ===== Proxy Handler =====
|
|
1138
|
-
|
|
1139
|
-
function rewritePath(path: string, pathRewrite?: Record<string, string>): string {
|
|
1140
|
-
if (!pathRewrite) return path;
|
|
1141
|
-
|
|
1142
|
-
for (const [from, to] of Object.entries(pathRewrite)) {
|
|
1143
|
-
const regex = new RegExp(from);
|
|
1144
|
-
if (regex.test(path)) {
|
|
1145
|
-
return path.replace(regex, to);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
return path;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
export function createProxyHandler(proxyConfigs: ProxyConfig[]) {
|
|
1152
|
-
return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
|
|
1153
|
-
const url = req.url || '/';
|
|
1154
|
-
const path = url.split('?')[0];
|
|
1155
|
-
|
|
1156
|
-
// Find matching proxy configuration (first match wins)
|
|
1157
|
-
const proxy = proxyConfigs.find(p => path.startsWith(p.context));
|
|
1158
|
-
if (!proxy) return false;
|
|
1159
|
-
|
|
1160
|
-
const { target, changeOrigin, pathRewrite, headers } = proxy;
|
|
1161
|
-
|
|
1162
|
-
try {
|
|
1163
|
-
const targetUrl = new URL(target);
|
|
1164
|
-
const isHttps = targetUrl.protocol === 'https:';
|
|
1165
|
-
const requestLib = isHttps ? httpsRequest : httpRequest;
|
|
1166
|
-
|
|
1167
|
-
// Rewrite path if needed
|
|
1168
|
-
let proxyPath = rewritePath(url, pathRewrite);
|
|
1169
|
-
|
|
1170
|
-
// Build the full proxy URL
|
|
1171
|
-
const proxyUrl = `${isHttps ? 'https' : 'http'}://${targetUrl.hostname}:${targetUrl.port || (isHttps ? 443 : 80)}${proxyPath}`;
|
|
1172
|
-
|
|
1173
|
-
// Build proxy request options
|
|
1174
|
-
const proxyReqHeaders: Record<string, string | number | string[]> = {};
|
|
1175
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
1176
|
-
if (value !== undefined) {
|
|
1177
|
-
proxyReqHeaders[key] = value;
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
if (headers) {
|
|
1181
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
1182
|
-
if (value !== undefined) {
|
|
1183
|
-
proxyReqHeaders[key] = value;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Change origin if requested (or remove host header if not)
|
|
1189
|
-
if (changeOrigin) {
|
|
1190
|
-
proxyReqHeaders.host = targetUrl.host;
|
|
1191
|
-
} else {
|
|
1192
|
-
delete proxyReqHeaders['host'];
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const proxyReqOptions = {
|
|
1196
|
-
method: req.method,
|
|
1197
|
-
headers: proxyReqHeaders
|
|
1198
|
-
};
|
|
1199
|
-
|
|
1200
|
-
// Create proxy request
|
|
1201
|
-
const proxyReq = requestLib(proxyUrl, proxyReqOptions, (proxyRes) => {
|
|
1202
|
-
// Forward status code and headers - convert incoming headers properly
|
|
1203
|
-
const outgoingHeaders: Record<string, string | number | string[]> = {};
|
|
1204
|
-
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
1205
|
-
if (value !== undefined) {
|
|
1206
|
-
outgoingHeaders[key] = value;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
res.writeHead(proxyRes.statusCode || 200, outgoingHeaders);
|
|
1210
|
-
|
|
1211
|
-
// Pipe response using read/write instead of pipe
|
|
1212
|
-
proxyRes.on('data', (chunk) => res.write(chunk));
|
|
1213
|
-
proxyRes.on('end', () => res.end());
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
|
-
// Handle errors
|
|
1217
|
-
proxyReq.on('error', (error) => {
|
|
1218
|
-
console.error('[Proxy] Error proxying %s to %s:', url, target, error.message);
|
|
1219
|
-
if (!res.headersSent) {
|
|
1220
|
-
json(res, { error: 'Bad Gateway', message: 'Proxy error' }, 502);
|
|
1221
|
-
}
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
// Forward request body
|
|
1225
|
-
req.on('data', (chunk) => proxyReq.write(chunk));
|
|
1226
|
-
req.on('end', () => proxyReq.end());
|
|
1227
|
-
|
|
1228
|
-
return true;
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
console.error('[Proxy] Invalid proxy configuration for %s:', path, error);
|
|
1231
|
-
return false;
|
|
1232
|
-
}
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// ===== State Management =====
|
|
1237
|
-
|
|
1238
|
-
export type StateChangeHandler<T = any> = (value: T, oldValue: T) => void;
|
|
1239
|
-
|
|
1240
|
-
export interface SharedStateOptions<T = any> {
|
|
1241
|
-
initial: T;
|
|
1242
|
-
persist?: boolean;
|
|
1243
|
-
validate?: (value: T) => boolean;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
export class SharedState<T = any> {
|
|
1247
|
-
private _value: T;
|
|
1248
|
-
private listeners = new Set<WebSocket>();
|
|
1249
|
-
private changeHandlers = new Set<StateChangeHandler<T>>();
|
|
1250
|
-
private options: SharedStateOptions<T>;
|
|
1251
|
-
|
|
1252
|
-
constructor(
|
|
1253
|
-
public readonly key: string,
|
|
1254
|
-
options: SharedStateOptions<T>
|
|
1255
|
-
) {
|
|
1256
|
-
this.options = options;
|
|
1257
|
-
this._value = options.initial;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
get value(): T {
|
|
1261
|
-
return this._value;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
set value(newValue: T) {
|
|
1265
|
-
if (this.options.validate && !this.options.validate(newValue)) {
|
|
1266
|
-
throw new Error(`Invalid state value for "${this.key}"`);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
const oldValue = this._value;
|
|
1270
|
-
this._value = newValue;
|
|
1271
|
-
|
|
1272
|
-
this.changeHandlers.forEach(handler => {
|
|
1273
|
-
handler(newValue, oldValue);
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
this.broadcast();
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
update(updater: (current: T) => T): void {
|
|
1280
|
-
this.value = updater(this._value);
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
subscribe(ws: WebSocket): void {
|
|
1284
|
-
this.listeners.add(ws);
|
|
1285
|
-
this.sendTo(ws);
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
unsubscribe(ws: WebSocket): void {
|
|
1289
|
-
this.listeners.delete(ws);
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
onChange(handler: StateChangeHandler<T>): () => void {
|
|
1293
|
-
this.changeHandlers.add(handler);
|
|
1294
|
-
return () => this.changeHandlers.delete(handler);
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
private broadcast(): void {
|
|
1298
|
-
const message = JSON.stringify({ type: 'state:update', key: this.key, value: this._value, timestamp: Date.now() });
|
|
1299
|
-
this.listeners.forEach(ws => ws.readyState === ReadyState.OPEN && ws.send(message));
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
private sendTo(ws: WebSocket): void {
|
|
1303
|
-
if (ws.readyState === ReadyState.OPEN) {
|
|
1304
|
-
ws.send(JSON.stringify({ type: 'state:init', key: this.key, value: this._value, timestamp: Date.now() }));
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
get subscriberCount(): number {
|
|
1309
|
-
return this.listeners.size;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
clear(): void {
|
|
1313
|
-
this.listeners.clear();
|
|
1314
|
-
this.changeHandlers.clear();
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
export class StateManager {
|
|
1319
|
-
private states = new Map<string, SharedState<any>>();
|
|
1320
|
-
|
|
1321
|
-
create<T>(key: string, options: SharedStateOptions<T>): SharedState<T> {
|
|
1322
|
-
if (this.states.has(key)) return this.states.get(key) as SharedState<T>;
|
|
1323
|
-
const state = new SharedState<T>(key, options);
|
|
1324
|
-
this.states.set(key, state);
|
|
1325
|
-
return state;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
get<T>(key: string): SharedState<T> | undefined {
|
|
1329
|
-
return this.states.get(key) as SharedState<T>;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
has(key: string): boolean {
|
|
1333
|
-
return this.states.has(key);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
delete(key: string): boolean {
|
|
1337
|
-
const state = this.states.get(key);
|
|
1338
|
-
if (state) {
|
|
1339
|
-
state.clear();
|
|
1340
|
-
return this.states.delete(key);
|
|
1341
|
-
}
|
|
1342
|
-
return false;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
subscribe(key: string, ws: WebSocket): void {
|
|
1346
|
-
this.states.get(key)?.subscribe(ws);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
unsubscribe(key: string, ws: WebSocket): void {
|
|
1350
|
-
this.states.get(key)?.unsubscribe(ws);
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
unsubscribeAll(ws: WebSocket): void {
|
|
1354
|
-
this.states.forEach(state => state.unsubscribe(ws));
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
handleStateChange(key: string, value: any): void {
|
|
1358
|
-
const state = this.states.get(key);
|
|
1359
|
-
if (state) state.value = value;
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
keys(): string[] {
|
|
1363
|
-
return Array.from(this.states.keys());
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
clear(): void {
|
|
1367
|
-
this.states.forEach(state => state.clear());
|
|
1368
|
-
this.states.clear();
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// ===== Development Server =====
|
|
1373
|
-
|
|
1374
|
-
const defaultOptions: Omit<Required<DevServerOptions>, 'api' | 'clients' | 'root' | 'fallbackRoot' | 'basePath' | 'ssr' | 'proxy' | 'index' | 'env' | 'domain' | 'ws'> = {
|
|
1375
|
-
port: 3000,
|
|
1376
|
-
host: 'localhost',
|
|
1377
|
-
https: false,
|
|
1378
|
-
open: true,
|
|
1379
|
-
standalone: false,
|
|
1380
|
-
outDir: 'dev-dist',
|
|
1381
|
-
outFile: 'index.js',
|
|
1382
|
-
watch: ['**/*.ts', '**/*.js', '**/*.html', '**/*.css'],
|
|
1383
|
-
ignore: ['node_modules/**', 'dist/**', '.git/**', '**/*.d.ts'],
|
|
1384
|
-
logging: true,
|
|
1385
|
-
worker: [],
|
|
1386
|
-
mode: 'dev'
|
|
1387
|
-
};
|
|
1388
|
-
|
|
1389
|
-
interface NormalizedClient {
|
|
1390
|
-
root: string;
|
|
1391
|
-
basePath: string;
|
|
1392
|
-
index?: string;
|
|
1393
|
-
ssr?: () => Child | string;
|
|
1394
|
-
api?: ServerRouter;
|
|
1395
|
-
ws: NormalizedWebSocketEndpoint[];
|
|
1396
|
-
proxyHandler?: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
1397
|
-
mode: 'dev' | 'preview';
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
function shouldUseClientFallbackRoot(primaryRoot: string, fallbackRoot: string | undefined, indexPath?: string): boolean {
|
|
1401
|
-
if (!fallbackRoot) {
|
|
1402
|
-
return false;
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
const resolvedPrimaryRoot = resolve(primaryRoot);
|
|
1406
|
-
const resolvedFallbackRoot = resolve(fallbackRoot);
|
|
1407
|
-
|
|
1408
|
-
if (!existsSync(resolvedFallbackRoot)) {
|
|
1409
|
-
return false;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
const normalizedIndexPath = (indexPath || '/index.html').replace(/^\//, '');
|
|
1413
|
-
const primaryHasRuntimeSources = existsSync(join(resolvedPrimaryRoot, 'src'))
|
|
1414
|
-
|| existsSync(join(resolvedPrimaryRoot, 'public'))
|
|
1415
|
-
|| existsSync(join(resolvedPrimaryRoot, normalizedIndexPath));
|
|
1416
|
-
|
|
1417
|
-
return !primaryHasRuntimeSources;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
export function createDevServer(options: DevServerOptions): DevServer {
|
|
1421
|
-
const config = { ...defaultOptions, ...options };
|
|
1422
|
-
const wsClients = new Set<WebSocket>();
|
|
1423
|
-
const stateManager = new StateManager();
|
|
1424
|
-
const transformCache = new Map<string, TransformCacheEntry>();
|
|
1425
|
-
|
|
1426
|
-
// Clear import map cache in dev mode to ensure fresh scans
|
|
1427
|
-
if (config.mode === 'dev') {
|
|
1428
|
-
clearImportMapCache();
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Normalize clients configuration - support both new API (clients array) and legacy API (root/basePath)
|
|
1432
|
-
const usesClientArray = Boolean(config.clients?.length);
|
|
1433
|
-
const clientsToNormalize = usesClientArray
|
|
1434
|
-
? config.clients!
|
|
1435
|
-
: config.root
|
|
1436
|
-
? [{ root: config.root, fallbackRoot: config.fallbackRoot, basePath: config.basePath || '', index: config.index, ssr: config.ssr, api: config.api, proxy: config.proxy, ws: config.ws, mode: config.mode }]
|
|
1437
|
-
: null;
|
|
1438
|
-
if (!clientsToNormalize) throw new Error('DevServerOptions must include either "clients" array or "root" directory');
|
|
1439
|
-
|
|
1440
|
-
const normalizedClients: NormalizedClient[] = clientsToNormalize.map(client => {
|
|
1441
|
-
let basePath = client.basePath || '';
|
|
1442
|
-
if (basePath) {
|
|
1443
|
-
// Remove leading/trailing slashes safely without ReDoS vulnerability
|
|
1444
|
-
while (basePath.startsWith('/')) basePath = basePath.slice(1);
|
|
1445
|
-
while (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
|
|
1446
|
-
basePath = basePath ? '/' + basePath : '';
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
// Normalize index path - convert ./path to /path
|
|
1450
|
-
let indexPath = client.index;
|
|
1451
|
-
if (indexPath) {
|
|
1452
|
-
// Remove leading ./ and ensure it starts with /
|
|
1453
|
-
indexPath = indexPath.replace(/^\.\//, '/');
|
|
1454
|
-
if (!indexPath.startsWith('/')) {
|
|
1455
|
-
indexPath = '/' + indexPath;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
const useFallbackRoot = shouldUseClientFallbackRoot(client.root, client.fallbackRoot, indexPath);
|
|
1460
|
-
const activeRoot = useFallbackRoot ? (client.fallbackRoot || client.root) : client.root;
|
|
1461
|
-
|
|
1462
|
-
return {
|
|
1463
|
-
root: activeRoot,
|
|
1464
|
-
basePath,
|
|
1465
|
-
index: useFallbackRoot ? undefined : indexPath,
|
|
1466
|
-
ssr: useFallbackRoot ? undefined : client.ssr,
|
|
1467
|
-
api: client.api,
|
|
1468
|
-
ws: normalizeWebSocketEndpoints(client.ws, basePath),
|
|
1469
|
-
proxyHandler: client.proxy ? createProxyHandler(client.proxy) : undefined,
|
|
1470
|
-
mode: client.mode || 'dev'
|
|
1471
|
-
};
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
const globalWebSocketEndpoints = usesClientArray ? normalizeWebSocketEndpoints(config.ws) : [];
|
|
1475
|
-
const normalizedWebSocketEndpoints = [...normalizedClients.flatMap(client => client.ws), ...globalWebSocketEndpoints];
|
|
1476
|
-
const seenWebSocketPaths = new Set<string>();
|
|
1477
|
-
|
|
1478
|
-
for (const endpoint of normalizedWebSocketEndpoints) {
|
|
1479
|
-
if (endpoint.path === ELIT_INTERNAL_WS_PATH) {
|
|
1480
|
-
throw new Error(`WebSocket path "${ELIT_INTERNAL_WS_PATH}" is reserved for Elit internals`);
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
if (seenWebSocketPaths.has(endpoint.path)) {
|
|
1484
|
-
throw new Error(`Duplicate WebSocket endpoint path: ${endpoint.path}`);
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
seenWebSocketPaths.add(endpoint.path);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
// Create global proxy handler if proxy config exists
|
|
1491
|
-
const globalProxyHandler = config.proxy ? createProxyHandler(config.proxy) : null;
|
|
1492
|
-
|
|
1493
|
-
// HTTP Server
|
|
1494
|
-
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
1495
|
-
const originalUrl = req.url || '/';
|
|
1496
|
-
const hostHeader = req.headers.host;
|
|
1497
|
-
const hostName = hostHeader ? (Array.isArray(hostHeader) ? hostHeader[0] : hostHeader).split(':')[0] : '';
|
|
1498
|
-
|
|
1499
|
-
// Handle domain mapping: redirect localhost:port to configured domain
|
|
1500
|
-
if (config.domain && hostName === (config.host || 'localhost')) {
|
|
1501
|
-
const redirectUrl = `http://${config.domain}${originalUrl}`;
|
|
1502
|
-
if (config.logging) {
|
|
1503
|
-
console.log(`[Domain Map] ${hostName}:${config.port}${originalUrl} -> ${redirectUrl}`);
|
|
1504
|
-
}
|
|
1505
|
-
res.writeHead(302, { Location: redirectUrl });
|
|
1506
|
-
res.end();
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// Find matching client based on basePath
|
|
1511
|
-
const matchedClient = normalizedClients.find(c => c.basePath && originalUrl.startsWith(c.basePath)) || normalizedClients.find(c => !c.basePath);
|
|
1512
|
-
if (!matchedClient) return send404(res, '404 Not Found');
|
|
1513
|
-
|
|
1514
|
-
// Try client-specific proxy first
|
|
1515
|
-
if (matchedClient.proxyHandler) {
|
|
1516
|
-
try {
|
|
1517
|
-
const proxied = await matchedClient.proxyHandler(req, res);
|
|
1518
|
-
if (proxied) {
|
|
1519
|
-
if (config.logging) console.log(`[Proxy] ${req.method} ${originalUrl} -> proxied (client-specific)`);
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
} catch (error) {
|
|
1523
|
-
console.error('[Proxy] Error (client-specific):', error);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// Try global proxy if client-specific didn't match
|
|
1528
|
-
if (globalProxyHandler) {
|
|
1529
|
-
try {
|
|
1530
|
-
const proxied = await globalProxyHandler(req, res);
|
|
1531
|
-
if (proxied) {
|
|
1532
|
-
if (config.logging) console.log(`[Proxy] ${req.method} ${originalUrl} -> proxied (global)`);
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
} catch (error) {
|
|
1536
|
-
console.error('[Proxy] Error (global):', error);
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
const url = matchedClient.basePath ? (originalUrl.slice(matchedClient.basePath.length) || '/') : originalUrl;
|
|
1541
|
-
|
|
1542
|
-
// Try client-specific API routes first
|
|
1543
|
-
// Strip basePath from req.url so route patterns match correctly
|
|
1544
|
-
if (matchedClient.api) {
|
|
1545
|
-
if (matchedClient.basePath) req.url = url;
|
|
1546
|
-
const handled = await matchedClient.api.handle(req, res);
|
|
1547
|
-
if (matchedClient.basePath) req.url = originalUrl;
|
|
1548
|
-
if (handled) return;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
// Try global API routes (fallback) - matches against originalUrl
|
|
1552
|
-
if (config.api) {
|
|
1553
|
-
const handled = await config.api.handle(req, res);
|
|
1554
|
-
if (handled) return;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
// If API routes are configured but none matched, return 405 for mutating methods
|
|
1558
|
-
if ((matchedClient.api || config.api) && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method || '')) {
|
|
1559
|
-
if (!res.headersSent) {
|
|
1560
|
-
if (config.logging) console.log(`[405] ${req.method} ${url} - Method not allowed`);
|
|
1561
|
-
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
1562
|
-
res.end(JSON.stringify({ error: 'Method Not Allowed', message: 'No API route found for this request' }));
|
|
1563
|
-
}
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// For root path requests, preview mode should prefer built index files and only
|
|
1568
|
-
// fall back to SSR if no index file exists. Dev mode keeps SSR-first behavior.
|
|
1569
|
-
let filePath: string;
|
|
1570
|
-
if (url === '/' && config.mode !== 'preview' && matchedClient.ssr && !matchedClient.index) {
|
|
1571
|
-
// Use SSR directly when configured and no custom index specified
|
|
1572
|
-
return await serveSSR(res, matchedClient);
|
|
1573
|
-
} else {
|
|
1574
|
-
// Use custom index file if specified, otherwise default to /index.html
|
|
1575
|
-
filePath = url === '/' ? (matchedClient.index || '/index.html') : url;
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
// Remove query string
|
|
1579
|
-
filePath = filePath.split('?')[0];
|
|
1580
|
-
|
|
1581
|
-
if (config.logging && filePath === '/src/pages') {
|
|
1582
|
-
console.log(`[DEBUG] Request for /src/pages received`);
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
// Security: Check for null bytes early
|
|
1586
|
-
if (filePath.includes('\0')) {
|
|
1587
|
-
if (config.logging) console.log(`[403] Rejected path with null byte: ${filePath}`);
|
|
1588
|
-
return send403(res, '403 Forbidden');
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
// Handle /dist/* and /node_modules/* requests - serve from parent folder
|
|
1592
|
-
const isDistRequest = filePath.startsWith('/dist/');
|
|
1593
|
-
const isNodeModulesRequest = filePath.startsWith('/node_modules/');
|
|
1594
|
-
let normalizedPath: string;
|
|
1595
|
-
|
|
1596
|
-
// Normalize and validate the path for both /dist/* and regular requests
|
|
1597
|
-
const tempPath = normalize(filePath).replace(/\\/g, '/').replace(/^\/+/, '');
|
|
1598
|
-
if (tempPath.includes('..')) {
|
|
1599
|
-
if (config.logging) console.log(`[403] Path traversal attempt: ${filePath}`);
|
|
1600
|
-
return send403(res, '403 Forbidden');
|
|
1601
|
-
}
|
|
1602
|
-
normalizedPath = tempPath;
|
|
1603
|
-
|
|
1604
|
-
// Resolve file path
|
|
1605
|
-
const rootDir = await realpath(resolve(matchedClient.root));
|
|
1606
|
-
let baseDir = rootDir;
|
|
1607
|
-
|
|
1608
|
-
// Auto-detect base directory for /dist/* and /node_modules/* requests
|
|
1609
|
-
if (isDistRequest || isNodeModulesRequest) {
|
|
1610
|
-
const targetDir = isDistRequest ? 'dist' : 'node_modules';
|
|
1611
|
-
const foundDir = await findSpecialDir(matchedClient.root, targetDir);
|
|
1612
|
-
baseDir = foundDir ? await realpath(foundDir) : rootDir;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
let fullPath;
|
|
1616
|
-
|
|
1617
|
-
try {
|
|
1618
|
-
// First check path without resolving symlinks for security
|
|
1619
|
-
const unresolvedPath = resolve(join(baseDir, normalizedPath));
|
|
1620
|
-
if (!unresolvedPath.startsWith(baseDir.endsWith(sep) ? baseDir : baseDir + sep)) {
|
|
1621
|
-
if (config.logging) console.log(`[403] File access outside of root (before symlink): ${unresolvedPath}`);
|
|
1622
|
-
return send403(res, '403 Forbidden');
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
// Then resolve symlinks to get actual file
|
|
1626
|
-
fullPath = await realpath(unresolvedPath);
|
|
1627
|
-
if (config.logging && filePath === '/src/pages') {
|
|
1628
|
-
console.log(`[DEBUG] Initial resolve succeeded: ${fullPath}`);
|
|
1629
|
-
}
|
|
1630
|
-
} catch (firstError) {
|
|
1631
|
-
// If file not found, try different extensions
|
|
1632
|
-
let resolvedPath: string | undefined;
|
|
1633
|
-
|
|
1634
|
-
if (config.logging && !normalizedPath.includes('.')) {
|
|
1635
|
-
console.log(`[DEBUG] File not found: ${normalizedPath}, trying extensions...`);
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
// If .js file not found, try .ts file
|
|
1639
|
-
if (normalizedPath.endsWith('.js')) {
|
|
1640
|
-
const tsPath = normalizedPath.replace(/\.js$/, '.ts');
|
|
1641
|
-
try {
|
|
1642
|
-
const tsFullPath = await realpath(resolve(join(baseDir, tsPath)));
|
|
1643
|
-
// Security: Ensure path is strictly within the allowed root directory
|
|
1644
|
-
if (!tsFullPath.startsWith(baseDir.endsWith(sep) ? baseDir : baseDir + sep)) {
|
|
1645
|
-
if (config.logging) console.log(`[403] Fallback TS path outside of root: ${tsFullPath}`);
|
|
1646
|
-
return send403(res, '403 Forbidden');
|
|
1647
|
-
}
|
|
1648
|
-
resolvedPath = tsFullPath;
|
|
1649
|
-
} catch {
|
|
1650
|
-
// Continue to next attempt
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
// If no extension, try adding .ts or .js, or index files
|
|
1655
|
-
if (!resolvedPath && !normalizedPath.includes('.')) {
|
|
1656
|
-
// Try .ts first
|
|
1657
|
-
try {
|
|
1658
|
-
resolvedPath = await realpath(resolve(join(baseDir, normalizedPath + '.ts')));
|
|
1659
|
-
if (config.logging) console.log(`[DEBUG] Found: ${normalizedPath}.ts`);
|
|
1660
|
-
} catch {
|
|
1661
|
-
// Try .js
|
|
1662
|
-
try {
|
|
1663
|
-
resolvedPath = await realpath(resolve(join(baseDir, normalizedPath + '.js')));
|
|
1664
|
-
if (config.logging) console.log(`[DEBUG] Found: ${normalizedPath}.js`);
|
|
1665
|
-
} catch {
|
|
1666
|
-
// Try index.ts in directory
|
|
1667
|
-
try {
|
|
1668
|
-
resolvedPath = await realpath(resolve(join(baseDir, normalizedPath, 'index.ts')));
|
|
1669
|
-
if (config.logging) console.log(`[DEBUG] Found: ${normalizedPath}/index.ts`);
|
|
1670
|
-
} catch {
|
|
1671
|
-
// Try index.js in directory
|
|
1672
|
-
try {
|
|
1673
|
-
resolvedPath = await realpath(resolve(join(baseDir, normalizedPath, 'index.js')));
|
|
1674
|
-
if (config.logging) console.log(`[DEBUG] Found: ${normalizedPath}/index.js`);
|
|
1675
|
-
} catch {
|
|
1676
|
-
if (config.logging) console.log(`[DEBUG] Not found: all attempts failed for ${normalizedPath}`);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
if (!resolvedPath) {
|
|
1684
|
-
if (!res.headersSent) {
|
|
1685
|
-
// If index.html not found but SSR function exists, use SSR
|
|
1686
|
-
if (filePath === '/index.html' && matchedClient.ssr) {
|
|
1687
|
-
return await serveSSR(res, matchedClient);
|
|
1688
|
-
}
|
|
1689
|
-
if (config.logging) console.log(`[404] ${filePath}`);
|
|
1690
|
-
return send404(res, '404 Not Found');
|
|
1691
|
-
}
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
fullPath = resolvedPath;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
// Check if resolved path is a directory, try index files
|
|
1699
|
-
try {
|
|
1700
|
-
const stats = await stat(fullPath);
|
|
1701
|
-
if (stats.isDirectory()) {
|
|
1702
|
-
if (config.logging) console.log(`[DEBUG] Path is directory: ${fullPath}, trying index files...`);
|
|
1703
|
-
let indexPath: string | undefined;
|
|
1704
|
-
|
|
1705
|
-
// Try index.ts first
|
|
1706
|
-
try {
|
|
1707
|
-
indexPath = await realpath(resolve(join(fullPath, 'index.ts')));
|
|
1708
|
-
if (config.logging) console.log(`[DEBUG] Found index.ts in directory`);
|
|
1709
|
-
} catch {
|
|
1710
|
-
// Try index.js
|
|
1711
|
-
try {
|
|
1712
|
-
indexPath = await realpath(resolve(join(fullPath, 'index.js')));
|
|
1713
|
-
if (config.logging) console.log(`[DEBUG] Found index.js in directory`);
|
|
1714
|
-
} catch {
|
|
1715
|
-
if (config.logging) console.log(`[DEBUG] No index file found in directory`);
|
|
1716
|
-
// If index.html not found in directory but SSR function exists, use SSR
|
|
1717
|
-
if (matchedClient.ssr) {
|
|
1718
|
-
return await serveSSR(res, matchedClient);
|
|
1719
|
-
}
|
|
1720
|
-
return send404(res, '404 Not Found');
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
fullPath = indexPath;
|
|
1725
|
-
}
|
|
1726
|
-
} catch (statError) {
|
|
1727
|
-
if (config.logging) console.log(`[404] ${filePath}`);
|
|
1728
|
-
return send404(res, '404 Not Found');
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
// Security check already done before resolving symlinks (line 733)
|
|
1732
|
-
// No need to check again after symlink resolution as that would block legitimate symlinks
|
|
1733
|
-
|
|
1734
|
-
try {
|
|
1735
|
-
const stats = await stat(fullPath);
|
|
1736
|
-
|
|
1737
|
-
if (stats.isDirectory()) {
|
|
1738
|
-
try {
|
|
1739
|
-
const indexPath = await realpath(resolve(join(fullPath, 'index.html')));
|
|
1740
|
-
if (!indexPath.startsWith(rootDir + sep) && indexPath !== rootDir) {
|
|
1741
|
-
return send403(res, '403 Forbidden');
|
|
1742
|
-
}
|
|
1743
|
-
await stat(indexPath);
|
|
1744
|
-
return serveFile(indexPath, req, res, matchedClient, isDistRequest || isNodeModulesRequest);
|
|
1745
|
-
} catch {
|
|
1746
|
-
return send404(res, '404 Not Found');
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
await serveFile(fullPath, req, res, matchedClient, isDistRequest || isNodeModulesRequest);
|
|
1751
|
-
} catch (error) {
|
|
1752
|
-
// Only send 404 if response hasn't been sent yet
|
|
1753
|
-
if (!res.headersSent) {
|
|
1754
|
-
if (config.logging) console.log(`[404] ${filePath}`);
|
|
1755
|
-
send404(res, '404 Not Found');
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
});
|
|
1759
|
-
|
|
1760
|
-
// Serve file helper
|
|
1761
|
-
async function serveFile(filePath: string, req: IncomingMessage, res: ServerResponse, client: NormalizedClient, isNodeModulesOrDist: boolean = false) {
|
|
1762
|
-
// Escape arbitrary text for safe embedding inside a JavaScript template literal.
|
|
1763
|
-
// This ensures that backslashes, backticks and `${` sequences are correctly escaped.
|
|
1764
|
-
function escapeForTemplateLiteral(input: string): string {
|
|
1765
|
-
return input
|
|
1766
|
-
.replace(/\\/g, '\\\\')
|
|
1767
|
-
.replace(/`/g, '\\`')
|
|
1768
|
-
.replace(/\$\{/g, '\\${');
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
try {
|
|
1772
|
-
const rootDir = await realpath(resolve(client.root));
|
|
1773
|
-
|
|
1774
|
-
// Security: Check path before resolving symlinks
|
|
1775
|
-
const unresolvedPath = resolve(filePath);
|
|
1776
|
-
|
|
1777
|
-
// Skip security check for node_modules and dist (these may be symlinks)
|
|
1778
|
-
if (!isNodeModulesOrDist) {
|
|
1779
|
-
// Check if path is within project root
|
|
1780
|
-
if (!unresolvedPath.startsWith(rootDir + sep) && unresolvedPath !== rootDir) {
|
|
1781
|
-
if (config.logging) console.log(`[403] Attempted to serve file outside allowed directories: ${filePath}`);
|
|
1782
|
-
return send403(res, '403 Forbidden');
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
// Resolve symlinks to get actual file path
|
|
1787
|
-
let resolvedPath;
|
|
1788
|
-
try {
|
|
1789
|
-
resolvedPath = await realpath(unresolvedPath);
|
|
1790
|
-
|
|
1791
|
-
// For symlinked packages (like node_modules/elit), allow serving from outside rootDir
|
|
1792
|
-
if (isNodeModulesOrDist && resolvedPath) {
|
|
1793
|
-
// Allow it - this is a symlinked package
|
|
1794
|
-
if (config.logging && !resolvedPath.startsWith(rootDir + sep)) {
|
|
1795
|
-
console.log(`[DEBUG] Serving symlinked file: ${resolvedPath}`);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
} catch {
|
|
1799
|
-
// If index.html not found but SSR function exists, use SSR
|
|
1800
|
-
if (filePath.endsWith('index.html') && client.ssr) {
|
|
1801
|
-
return await serveSSR(res, client);
|
|
1802
|
-
}
|
|
1803
|
-
return send404(res, '404 Not Found');
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
const ext = extname(resolvedPath);
|
|
1807
|
-
const urlQuery = req.url?.split('?')[1] || '';
|
|
1808
|
-
const isInlineCSS = urlQuery.includes('inline');
|
|
1809
|
-
const cacheableTransform = ext === '.ts' || ext === '.tsx' || (ext === '.css' && isInlineCSS);
|
|
1810
|
-
const resolvedStats = cacheableTransform ? await stat(resolvedPath) : undefined;
|
|
1811
|
-
let mimeType = lookup(resolvedPath) || 'application/octet-stream';
|
|
1812
|
-
let content: Buffer;
|
|
1813
|
-
|
|
1814
|
-
if (cacheableTransform && resolvedStats) {
|
|
1815
|
-
const cacheKey = createTransformCacheKey(resolvedPath, config.mode, urlQuery);
|
|
1816
|
-
const cachedTransform = getValidTransformCacheEntry(transformCache, cacheKey, resolvedStats);
|
|
1817
|
-
|
|
1818
|
-
if (cachedTransform) {
|
|
1819
|
-
content = cachedTransform.content;
|
|
1820
|
-
mimeType = cachedTransform.mimeType;
|
|
1821
|
-
} else {
|
|
1822
|
-
const sourceContent = toBuffer(await readFile(resolvedPath));
|
|
1823
|
-
|
|
1824
|
-
// Handle CSS imports as JavaScript modules (like Vite)
|
|
1825
|
-
// When CSS is imported in JS/TS with ?inline query, transform it to a JS module that injects styles
|
|
1826
|
-
if (ext === '.css' && isInlineCSS) {
|
|
1827
|
-
const cssContent = escapeForTemplateLiteral(sourceContent.toString());
|
|
1828
|
-
const jsModule = `
|
|
1829
|
-
const css = \`${cssContent}\`;
|
|
1830
|
-
const style = document.createElement('style');
|
|
1831
|
-
style.setAttribute('data-file', '${filePath}');
|
|
1832
|
-
style.textContent = css;
|
|
1833
|
-
document.head.appendChild(style);
|
|
1834
|
-
export default css;
|
|
1835
|
-
`;
|
|
1836
|
-
content = Buffer.from(jsModule);
|
|
1837
|
-
mimeType = 'application/javascript';
|
|
1838
|
-
} else {
|
|
1839
|
-
try {
|
|
1840
|
-
let transpiled: string;
|
|
1841
|
-
|
|
1842
|
-
if (isDeno) {
|
|
1843
|
-
// Deno - use Deno.emit
|
|
1844
|
-
// @ts-ignore
|
|
1845
|
-
const result = await Deno.emit(resolvedPath, {
|
|
1846
|
-
check: false,
|
|
1847
|
-
compilerOptions: {
|
|
1848
|
-
sourceMap: config.mode !== 'preview',
|
|
1849
|
-
inlineSourceMap: config.mode !== 'preview',
|
|
1850
|
-
target: 'ES2020',
|
|
1851
|
-
module: 'esnext'
|
|
1852
|
-
},
|
|
1853
|
-
sources: {
|
|
1854
|
-
[resolvedPath]: sourceContent.toString()
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
|
|
1858
|
-
transpiled = result.files[resolvedPath.replace(/\.tsx?$/, '.js')] || '';
|
|
1859
|
-
|
|
1860
|
-
} else if (isBun) {
|
|
1861
|
-
// Bun - use Bun.Transpiler
|
|
1862
|
-
// @ts-ignore
|
|
1863
|
-
const transpiler = new Bun.Transpiler({
|
|
1864
|
-
loader: ext === '.tsx' ? 'tsx' : 'ts',
|
|
1865
|
-
target: 'browser'
|
|
1866
|
-
});
|
|
1867
|
-
|
|
1868
|
-
// @ts-ignore
|
|
1869
|
-
transpiled = transpiler.transformSync(sourceContent.toString());
|
|
1870
|
-
} else {
|
|
1871
|
-
transpiled = await transpileNodeBrowserModule(sourceContent.toString(), {
|
|
1872
|
-
filename: resolvedPath,
|
|
1873
|
-
loader: ext === '.tsx' ? 'tsx' : 'ts',
|
|
1874
|
-
mode: config.mode,
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
// Rewrite .ts imports to .js for browser compatibility
|
|
1879
|
-
// This allows developers to write import './file.ts' in their source code
|
|
1880
|
-
// and the dev server will automatically rewrite it to import './file.js'
|
|
1881
|
-
transpiled = transpiled.replace(
|
|
1882
|
-
/from\s+["']([^"']+)\.ts(x?)["']/g,
|
|
1883
|
-
(_, path, tsx) => `from "${path}.js${tsx}"`
|
|
1884
|
-
);
|
|
1885
|
-
transpiled = transpiled.replace(
|
|
1886
|
-
/import\s+["']([^"']+)\.ts(x?)["']/g,
|
|
1887
|
-
(_, path, tsx) => `import "${path}.js${tsx}"`
|
|
1888
|
-
);
|
|
1889
|
-
|
|
1890
|
-
// Rewrite CSS imports to add ?inline query parameter
|
|
1891
|
-
// This tells the server to return CSS as a JavaScript module
|
|
1892
|
-
transpiled = transpiled.replace(
|
|
1893
|
-
/import\s+["']([^"']+\.css)["']/g,
|
|
1894
|
-
(_, path) => `import "${path}?inline"`
|
|
1895
|
-
);
|
|
1896
|
-
transpiled = transpiled.replace(
|
|
1897
|
-
/from\s+["']([^"']+\.css)["']/g,
|
|
1898
|
-
(_, path) => `from "${path}?inline"`
|
|
1899
|
-
);
|
|
1900
|
-
|
|
1901
|
-
content = Buffer.from(transpiled);
|
|
1902
|
-
mimeType = 'application/javascript';
|
|
1903
|
-
} catch (error) {
|
|
1904
|
-
if (config.logging) console.error('[500] TypeScript compilation error:', error);
|
|
1905
|
-
return send500(res, `TypeScript compilation error:\n${error}`);
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
transformCache.set(cacheKey, {
|
|
1910
|
-
content,
|
|
1911
|
-
mimeType,
|
|
1912
|
-
mtimeMs: resolvedStats.mtimeMs,
|
|
1913
|
-
size: resolvedStats.size,
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1916
|
-
} else {
|
|
1917
|
-
content = toBuffer(await readFile(resolvedPath));
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
// Inject HMR client and import map for HTML files
|
|
1921
|
-
if (ext === '.html') {
|
|
1922
|
-
const hmrScript = config.mode !== 'preview' ? createHMRScript(config.port) : '';
|
|
1923
|
-
let html = content.toString();
|
|
1924
|
-
|
|
1925
|
-
// If SSR is configured, extract and inject styles from SSR
|
|
1926
|
-
let ssrStyles = '';
|
|
1927
|
-
if (client.ssr) {
|
|
1928
|
-
try {
|
|
1929
|
-
const result = client.ssr();
|
|
1930
|
-
let ssrHtml: string;
|
|
1931
|
-
|
|
1932
|
-
// Convert SSR result to string
|
|
1933
|
-
if (typeof result === 'string') {
|
|
1934
|
-
ssrHtml = result;
|
|
1935
|
-
} else if (typeof result === 'object' && result !== null && 'tagName' in result) {
|
|
1936
|
-
ssrHtml = dom.renderToString(result as VNode);
|
|
1937
|
-
} else {
|
|
1938
|
-
ssrHtml = String(result);
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// Extract <style> tags from SSR output
|
|
1942
|
-
const styleMatches = ssrHtml.match(/<style[^>]*>[\s\S]*?<\/style>/g);
|
|
1943
|
-
if (styleMatches) {
|
|
1944
|
-
ssrStyles = styleMatches.join('\n');
|
|
1945
|
-
}
|
|
1946
|
-
} catch (error) {
|
|
1947
|
-
if (config.logging) console.error('[Warning] Failed to extract styles from SSR:', error);
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
// Fix relative paths to use basePath
|
|
1952
|
-
const basePath = normalizeBasePath(client.basePath);
|
|
1953
|
-
html = rewriteRelativePaths(html, basePath);
|
|
1954
|
-
|
|
1955
|
-
// Inject base tag if basePath is configured and not '/'
|
|
1956
|
-
if (client.basePath && client.basePath !== '/') {
|
|
1957
|
-
const baseTag = `<base href="${client.basePath}/">`;
|
|
1958
|
-
// Check if base tag already exists
|
|
1959
|
-
if (!html.includes('<base')) {
|
|
1960
|
-
// Try to inject after viewport meta tag
|
|
1961
|
-
if (html.includes('<meta name="viewport"')) {
|
|
1962
|
-
html = html.replace(
|
|
1963
|
-
/<meta name="viewport"[^>]*>/,
|
|
1964
|
-
(match) => `${match}\n ${baseTag}`
|
|
1965
|
-
);
|
|
1966
|
-
} else if (html.includes('<head>')) {
|
|
1967
|
-
// If no viewport, inject right after <head>
|
|
1968
|
-
html = html.replace('<head>', `<head>\n ${baseTag}`);
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
// Inject import map and SSR styles into <head>
|
|
1974
|
-
const elitImportMap = await createElitImportMap(client.root, basePath, client.mode);
|
|
1975
|
-
const modeScript = config.mode === 'preview' ? '<script>window.__ELIT_MODE__=\'preview\';</script>' : '';
|
|
1976
|
-
const headInjection = `${modeScript}${ssrStyles ? '\n' + ssrStyles : ''}\n${elitImportMap}`;
|
|
1977
|
-
html = html.includes('</head>') ? html.replace('</head>', `${headInjection}</head>`) : html;
|
|
1978
|
-
html = html.includes('</body>') ? html.replace('</body>', `${hmrScript}</body>`) : html + hmrScript;
|
|
1979
|
-
content = Buffer.from(html);
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
// Set cache headers based on file type
|
|
1983
|
-
const cacheControl = ext === '.html' || ext === '.ts' || ext === '.tsx'
|
|
1984
|
-
? 'no-cache, no-store, must-revalidate' // Don't cache HTML/TS files in dev
|
|
1985
|
-
: 'public, max-age=31536000, immutable'; // Cache static assets for 1 year
|
|
1986
|
-
|
|
1987
|
-
const headers: any = {
|
|
1988
|
-
'Content-Type': mimeType,
|
|
1989
|
-
'Cache-Control': cacheControl,
|
|
1990
|
-
'X-Content-Type-Options': 'nosniff',
|
|
1991
|
-
'X-Frame-Options': 'DENY',
|
|
1992
|
-
'X-XSS-Protection': '1; mode=block',
|
|
1993
|
-
'Referrer-Policy': 'strict-origin-when-cross-origin'
|
|
1994
|
-
};
|
|
1995
|
-
|
|
1996
|
-
// Apply gzip compression for text-based files
|
|
1997
|
-
const compressible = /^(text\/|application\/(javascript|json|xml))/.test(mimeType);
|
|
1998
|
-
const acceptsGzip = requestAcceptsGzip(req.headers['accept-encoding']);
|
|
1999
|
-
|
|
2000
|
-
if (compressible) {
|
|
2001
|
-
headers['Vary'] = 'Accept-Encoding';
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
if (!isBun && acceptsGzip && compressible && content.length > 1024) {
|
|
2005
|
-
const { gzipSync } = require('zlib');
|
|
2006
|
-
const compressed = gzipSync(content);
|
|
2007
|
-
headers['Content-Encoding'] = 'gzip';
|
|
2008
|
-
headers['Content-Length'] = compressed.length;
|
|
2009
|
-
res.writeHead(200, headers);
|
|
2010
|
-
res.end(compressed);
|
|
2011
|
-
} else {
|
|
2012
|
-
res.writeHead(200, headers);
|
|
2013
|
-
res.end(content);
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
if (config.logging) console.log(`[200] ${relative(client.root, filePath)}`);
|
|
2017
|
-
} catch (error) {
|
|
2018
|
-
if (config.logging) console.error('[500] Error reading file:', error);
|
|
2019
|
-
send500(res, '500 Internal Server Error');
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
// SSR helper - Generate HTML from SSR function
|
|
2024
|
-
async function serveSSR(res: ServerResponse, client: NormalizedClient) {
|
|
2025
|
-
try {
|
|
2026
|
-
if (!client.ssr) {
|
|
2027
|
-
return send500(res, 'SSR function not configured');
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
const result = client.ssr();
|
|
2031
|
-
let html: string;
|
|
2032
|
-
|
|
2033
|
-
// If result is a string, use it directly
|
|
2034
|
-
if (typeof result === 'string') {
|
|
2035
|
-
html = result;
|
|
2036
|
-
}
|
|
2037
|
-
// If result is a VNode, render it to HTML string
|
|
2038
|
-
else if (typeof result === 'object' && result !== null && 'tagName' in result) {
|
|
2039
|
-
const vnode = result as VNode;
|
|
2040
|
-
if (vnode.tagName === 'html') {
|
|
2041
|
-
html = dom.renderToString(vnode);
|
|
2042
|
-
} else {
|
|
2043
|
-
// Wrap in basic HTML structure if not html tag
|
|
2044
|
-
html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body>${dom.renderToString(vnode)}</body></html>`;
|
|
2045
|
-
}
|
|
2046
|
-
} else {
|
|
2047
|
-
html = String(result);
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// Fix relative paths to use basePath
|
|
2051
|
-
const basePath = normalizeBasePath(client.basePath);
|
|
2052
|
-
html = rewriteRelativePaths(html, basePath);
|
|
2053
|
-
|
|
2054
|
-
// Inject HMR script (dev mode only)
|
|
2055
|
-
const hmrScript = config.mode !== 'preview' ? createHMRScript(config.port) : '';
|
|
2056
|
-
|
|
2057
|
-
// Inject import map in head, HMR script in body
|
|
2058
|
-
const elitImportMap = await createElitImportMap(client.root, basePath, client.mode);
|
|
2059
|
-
const modeScript = config.mode === 'preview' ? '<script>window.__ELIT_MODE__=\'preview\';</script>\n' : '';
|
|
2060
|
-
html = html.includes('</head>') ? html.replace('</head>', `${modeScript}${elitImportMap}</head>`) : html;
|
|
2061
|
-
html = html.includes('</body>') ? html.replace('</body>', `${hmrScript}</body>`) : html + hmrScript;
|
|
2062
|
-
|
|
2063
|
-
res.writeHead(200, {
|
|
2064
|
-
'Content-Type': 'text/html',
|
|
2065
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
2066
|
-
'X-Content-Type-Options': 'nosniff',
|
|
2067
|
-
'X-Frame-Options': 'DENY',
|
|
2068
|
-
'X-XSS-Protection': '1; mode=block',
|
|
2069
|
-
'Referrer-Policy': 'strict-origin-when-cross-origin'
|
|
2070
|
-
});
|
|
2071
|
-
res.end(html);
|
|
2072
|
-
|
|
2073
|
-
if (config.logging) console.log(`[200] SSR rendered`);
|
|
2074
|
-
} catch (error) {
|
|
2075
|
-
if (config.logging) console.error('[500] SSR Error:', error);
|
|
2076
|
-
send500(res, '500 SSR Error');
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
// Internal WebSocket server for HMR messages and shared state sync.
|
|
2081
|
-
const wss = new WebSocketServer({ server, path: ELIT_INTERNAL_WS_PATH });
|
|
2082
|
-
const webSocketServers: WebSocketServer[] = [wss];
|
|
2083
|
-
|
|
2084
|
-
if (config.logging) {
|
|
2085
|
-
console.log(`[WebSocket] Internal server initialized at ${ELIT_INTERNAL_WS_PATH}`);
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
|
|
2089
|
-
wsClients.add(ws);
|
|
2090
|
-
|
|
2091
|
-
const message: HMRMessage = { type: 'connected', timestamp: Date.now() };
|
|
2092
|
-
ws.send(JSON.stringify(message));
|
|
2093
|
-
|
|
2094
|
-
if (config.logging) {
|
|
2095
|
-
console.log('[WebSocket] Internal client connected from', req.socket.remoteAddress);
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
// Handle incoming messages
|
|
2099
|
-
ws.on('message', (data: string) => {
|
|
2100
|
-
try {
|
|
2101
|
-
const msg = JSON.parse(data.toString());
|
|
2102
|
-
|
|
2103
|
-
// Handle state subscription
|
|
2104
|
-
if (msg.type === 'state:subscribe') {
|
|
2105
|
-
stateManager.subscribe(msg.key, ws);
|
|
2106
|
-
if (config.logging) {
|
|
2107
|
-
console.log(`[State] Client subscribed to "${msg.key}"`);
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
// Handle state unsubscribe
|
|
2112
|
-
else if (msg.type === 'state:unsubscribe') {
|
|
2113
|
-
stateManager.unsubscribe(msg.key, ws);
|
|
2114
|
-
if (config.logging) {
|
|
2115
|
-
console.log(`[State] Client unsubscribed from "${msg.key}"`);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
// Handle state change from client
|
|
2120
|
-
else if (msg.type === 'state:change') {
|
|
2121
|
-
stateManager.handleStateChange(msg.key, msg.value);
|
|
2122
|
-
if (config.logging) {
|
|
2123
|
-
console.log(`[State] Client updated "${msg.key}"`);
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
} catch (error) {
|
|
2127
|
-
if (config.logging) {
|
|
2128
|
-
console.error('[WebSocket] Message parse error:', error);
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
});
|
|
2132
|
-
|
|
2133
|
-
ws.on('close', () => {
|
|
2134
|
-
wsClients.delete(ws);
|
|
2135
|
-
stateManager.unsubscribeAll(ws);
|
|
2136
|
-
if (config.logging) {
|
|
2137
|
-
console.log('[WebSocket] Internal client disconnected');
|
|
2138
|
-
}
|
|
2139
|
-
});
|
|
2140
|
-
});
|
|
2141
|
-
|
|
2142
|
-
for (const endpoint of normalizedWebSocketEndpoints) {
|
|
2143
|
-
const endpointServer = new WebSocketServer({ server, path: endpoint.path });
|
|
2144
|
-
webSocketServers.push(endpointServer);
|
|
2145
|
-
|
|
2146
|
-
if (config.logging) {
|
|
2147
|
-
console.log(`[WebSocket] Endpoint ready at ${endpoint.path}`);
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
endpointServer.on('connection', (ws: WebSocket, req: IncomingMessage) => {
|
|
2151
|
-
const requestUrl = req.url || endpoint.path;
|
|
2152
|
-
const ctx = {
|
|
2153
|
-
ws,
|
|
2154
|
-
req,
|
|
2155
|
-
path: getRequestPath(requestUrl),
|
|
2156
|
-
query: parseRequestQuery(requestUrl),
|
|
2157
|
-
headers: req.headers as Record<string, string | string[] | undefined>
|
|
2158
|
-
};
|
|
2159
|
-
|
|
2160
|
-
void Promise.resolve(endpoint.handler(ctx)).catch((error) => {
|
|
2161
|
-
if (config.logging) {
|
|
2162
|
-
console.error(`[WebSocket] Endpoint error at ${endpoint.path}:`, error);
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
try {
|
|
2166
|
-
ws.close(CLOSE_CODES.INTERNAL_ERROR, 'Internal Server Error');
|
|
2167
|
-
} catch {
|
|
2168
|
-
// Ignore close errors while reporting endpoint failures.
|
|
2169
|
-
}
|
|
2170
|
-
});
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
// File watcher - only in dev mode (not needed for preview)
|
|
2175
|
-
let watcher: any = null;
|
|
2176
|
-
if (config.mode !== 'preview') {
|
|
2177
|
-
const watchPaths = normalizedClients.flatMap(client =>
|
|
2178
|
-
config.watch.map(pattern => join(client.root, pattern))
|
|
2179
|
-
);
|
|
2180
|
-
|
|
2181
|
-
watcher = watch(watchPaths, {
|
|
2182
|
-
ignored: (path: string) => config.ignore.some(pattern => path.includes(pattern.replace('/**', '').replace('**/', ''))),
|
|
2183
|
-
ignoreInitial: true,
|
|
2184
|
-
persistent: true
|
|
2185
|
-
});
|
|
2186
|
-
|
|
2187
|
-
watcher.on('change', (path: string) => {
|
|
2188
|
-
if (config.logging) console.log(`[HMR] File changed: ${path}`);
|
|
2189
|
-
const message = JSON.stringify({ type: 'update', path, timestamp: Date.now() } as HMRMessage);
|
|
2190
|
-
// Broadcast to all open clients with error handling
|
|
2191
|
-
wsClients.forEach(client => {
|
|
2192
|
-
if (client.readyState === ReadyState.OPEN) {
|
|
2193
|
-
client.send(message, {}, (err?: Error) => {
|
|
2194
|
-
// Silently ignore connection errors during HMR
|
|
2195
|
-
const code = (err as any)?.code;
|
|
2196
|
-
if (code === 'ECONNABORTED' || code === 'ECONNRESET' || code === 'EPIPE' || code === 'WS_NOT_OPEN') {
|
|
2197
|
-
// Client disconnected - will be removed from clients set by close event
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
});
|
|
2203
|
-
});
|
|
2204
|
-
|
|
2205
|
-
watcher.on('add', (path: string) => {
|
|
2206
|
-
if (config.logging) console.log(`[HMR] File added: ${path}`);
|
|
2207
|
-
const message = JSON.stringify({ type: 'update', path, timestamp: Date.now() } as HMRMessage);
|
|
2208
|
-
wsClients.forEach(client => {
|
|
2209
|
-
if (client.readyState === ReadyState.OPEN) client.send(message, {});
|
|
2210
|
-
});
|
|
2211
|
-
});
|
|
2212
|
-
|
|
2213
|
-
watcher.on('unlink', (path: string) => {
|
|
2214
|
-
if (config.logging) console.log(`[HMR] File removed: ${path}`);
|
|
2215
|
-
const message = JSON.stringify({ type: 'reload', path, timestamp: Date.now() } as HMRMessage);
|
|
2216
|
-
wsClients.forEach(client => {
|
|
2217
|
-
if (client.readyState === ReadyState.OPEN) client.send(message, {});
|
|
2218
|
-
});
|
|
2219
|
-
});
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
// Increase max listeners to prevent warnings
|
|
2223
|
-
server.setMaxListeners(20);
|
|
2224
|
-
|
|
2225
|
-
// Start server
|
|
2226
|
-
server.listen(config.port, config.host, () => {
|
|
2227
|
-
if (config.logging) {
|
|
2228
|
-
console.log('\nš Elit Dev Server');
|
|
2229
|
-
console.log(`\n ā Local: http://${config.host}:${config.port}`);
|
|
2230
|
-
|
|
2231
|
-
if (normalizedClients.length > 1) {
|
|
2232
|
-
console.log(` ā Clients:`);
|
|
2233
|
-
normalizedClients.forEach(client => {
|
|
2234
|
-
const clientUrl = `http://${config.host}:${config.port}${client.basePath}`;
|
|
2235
|
-
console.log(` - ${clientUrl} ā ${client.root}`);
|
|
2236
|
-
});
|
|
2237
|
-
} else {
|
|
2238
|
-
const client = normalizedClients[0];
|
|
2239
|
-
console.log(` ā Root: ${client.root}`);
|
|
2240
|
-
if (client.basePath) {
|
|
2241
|
-
console.log(` ā Base: ${client.basePath}`);
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
if (config.mode !== 'preview') console.log(`\n[HMR] Watching for file changes...\n`);
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
// Open browser to first client
|
|
2249
|
-
if (config.open && normalizedClients.length > 0) {
|
|
2250
|
-
const firstClient = normalizedClients[0];
|
|
2251
|
-
const url = `http://${config.host}:${config.port}${firstClient.basePath}`;
|
|
2252
|
-
|
|
2253
|
-
const open = async () => {
|
|
2254
|
-
const { default: openBrowser } = await import('open');
|
|
2255
|
-
await openBrowser(url);
|
|
2256
|
-
};
|
|
2257
|
-
open().catch(() => {
|
|
2258
|
-
// Fail silently if open package is not available
|
|
2259
|
-
});
|
|
2260
|
-
}
|
|
2261
|
-
});
|
|
2262
|
-
|
|
2263
|
-
// Cleanup function
|
|
2264
|
-
let isClosing = false;
|
|
2265
|
-
const close = async () => {
|
|
2266
|
-
if (isClosing) return;
|
|
2267
|
-
isClosing = true;
|
|
2268
|
-
if (config.logging) console.log('\n[Server] Shutting down...');
|
|
2269
|
-
transformCache.clear();
|
|
2270
|
-
if (watcher) await watcher.close();
|
|
2271
|
-
if (webSocketServers.length > 0) {
|
|
2272
|
-
webSocketServers.forEach(wsServer => wsServer.close());
|
|
2273
|
-
wsClients.clear();
|
|
2274
|
-
}
|
|
2275
|
-
return new Promise<void>((resolve) => {
|
|
2276
|
-
server.close(() => {
|
|
2277
|
-
if (config.logging) console.log('[Server] Closed');
|
|
2278
|
-
resolve();
|
|
2279
|
-
});
|
|
2280
|
-
});
|
|
2281
|
-
};
|
|
2282
|
-
|
|
2283
|
-
// Get the primary URL (first client's basePath)
|
|
2284
|
-
const primaryClient = normalizedClients[0];
|
|
2285
|
-
const primaryUrl = `http://${config.host}:${config.port}${primaryClient.basePath}`;
|
|
2286
|
-
|
|
2287
|
-
return {
|
|
2288
|
-
server: server as any,
|
|
2289
|
-
wss: wss as any,
|
|
2290
|
-
url: primaryUrl,
|
|
2291
|
-
state: stateManager,
|
|
2292
|
-
close
|
|
2293
|
-
};
|
|
2294
|
-
}
|