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.
Files changed (113) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +1 -1
  3. package/desktop/build.rs +83 -0
  4. package/desktop/icon.rs +106 -0
  5. package/desktop/lib.rs +2 -0
  6. package/desktop/main.rs +235 -0
  7. package/desktop/native_main.rs +128 -0
  8. package/desktop/native_renderer/action_widgets.rs +184 -0
  9. package/desktop/native_renderer/app_models.rs +171 -0
  10. package/desktop/native_renderer/app_runtime.rs +140 -0
  11. package/desktop/native_renderer/container_rendering.rs +610 -0
  12. package/desktop/native_renderer/content_widgets.rs +634 -0
  13. package/desktop/native_renderer/css_models.rs +371 -0
  14. package/desktop/native_renderer/embedded_surfaces.rs +414 -0
  15. package/desktop/native_renderer/form_controls.rs +516 -0
  16. package/desktop/native_renderer/interaction_dispatch.rs +89 -0
  17. package/desktop/native_renderer/runtime_support.rs +135 -0
  18. package/desktop/native_renderer/utilities.rs +495 -0
  19. package/desktop/native_renderer/vector_drawing.rs +491 -0
  20. package/desktop/native_renderer.rs +4122 -0
  21. package/desktop/runtime/external.rs +422 -0
  22. package/desktop/runtime/mod.rs +67 -0
  23. package/desktop/runtime/quickjs.rs +106 -0
  24. package/desktop/window.rs +383 -0
  25. package/package.json +6 -3
  26. package/dist/build.d.mts +0 -20
  27. package/dist/chokidar.d.mts +0 -134
  28. package/dist/cli.d.mts +0 -81
  29. package/dist/config.d.mts +0 -254
  30. package/dist/coverage.d.mts +0 -85
  31. package/dist/database.d.mts +0 -52
  32. package/dist/desktop.d.mts +0 -68
  33. package/dist/dom.d.mts +0 -87
  34. package/dist/el.d.mts +0 -208
  35. package/dist/fs.d.mts +0 -255
  36. package/dist/hmr.d.mts +0 -38
  37. package/dist/http.d.mts +0 -169
  38. package/dist/https.d.mts +0 -108
  39. package/dist/index.d.mts +0 -13
  40. package/dist/mime-types.d.mts +0 -48
  41. package/dist/native.d.mts +0 -136
  42. package/dist/path.d.mts +0 -163
  43. package/dist/router.d.mts +0 -49
  44. package/dist/runtime.d.mts +0 -97
  45. package/dist/server-D0Dp4R5z.d.mts +0 -449
  46. package/dist/server.d.mts +0 -7
  47. package/dist/state.d.mts +0 -117
  48. package/dist/style.d.mts +0 -232
  49. package/dist/test-reporter.d.mts +0 -77
  50. package/dist/test-runtime.d.mts +0 -122
  51. package/dist/test.d.mts +0 -39
  52. package/dist/types.d.mts +0 -586
  53. package/dist/universal.d.mts +0 -21
  54. package/dist/ws.d.mts +0 -200
  55. package/dist/wss.d.mts +0 -108
  56. package/src/build.ts +0 -362
  57. package/src/chokidar.ts +0 -427
  58. package/src/cli.ts +0 -1162
  59. package/src/config.ts +0 -509
  60. package/src/coverage.ts +0 -1479
  61. package/src/database.ts +0 -1410
  62. package/src/desktop-auto-render.ts +0 -317
  63. package/src/desktop-cli.ts +0 -1533
  64. package/src/desktop.ts +0 -99
  65. package/src/dev-build.ts +0 -340
  66. package/src/dom.ts +0 -901
  67. package/src/el.ts +0 -183
  68. package/src/fs.ts +0 -609
  69. package/src/hmr.ts +0 -149
  70. package/src/http.ts +0 -856
  71. package/src/https.ts +0 -411
  72. package/src/index.ts +0 -16
  73. package/src/mime-types.ts +0 -222
  74. package/src/mobile-cli.ts +0 -2313
  75. package/src/native-background.ts +0 -444
  76. package/src/native-border.ts +0 -343
  77. package/src/native-canvas.ts +0 -260
  78. package/src/native-cli.ts +0 -414
  79. package/src/native-color.ts +0 -904
  80. package/src/native-estimation.ts +0 -194
  81. package/src/native-grid.ts +0 -590
  82. package/src/native-interaction.ts +0 -1289
  83. package/src/native-layout.ts +0 -568
  84. package/src/native-link.ts +0 -76
  85. package/src/native-render-support.ts +0 -361
  86. package/src/native-spacing.ts +0 -231
  87. package/src/native-state.ts +0 -318
  88. package/src/native-strings.ts +0 -46
  89. package/src/native-transform.ts +0 -120
  90. package/src/native-types.ts +0 -439
  91. package/src/native-typography.ts +0 -254
  92. package/src/native-units.ts +0 -441
  93. package/src/native-vector.ts +0 -910
  94. package/src/native.ts +0 -5606
  95. package/src/path.ts +0 -493
  96. package/src/pm-cli.ts +0 -2498
  97. package/src/preview-build.ts +0 -294
  98. package/src/render-context.ts +0 -138
  99. package/src/router.ts +0 -260
  100. package/src/runtime.ts +0 -97
  101. package/src/server.ts +0 -2294
  102. package/src/state.ts +0 -556
  103. package/src/style.ts +0 -1790
  104. package/src/test-globals.d.ts +0 -184
  105. package/src/test-reporter.ts +0 -609
  106. package/src/test-runtime.ts +0 -1359
  107. package/src/test.ts +0 -368
  108. package/src/types.ts +0 -381
  109. package/src/universal.ts +0 -81
  110. package/src/wapk-cli.ts +0 -3213
  111. package/src/workspace-package.ts +0 -102
  112. package/src/ws.ts +0 -648
  113. 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
- }