@jxrstudios/jxr 1.0.4 → 1.0.6

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 (41) hide show
  1. package/README.md +6 -2
  2. package/bin/jxr.js +114 -76
  3. package/dist/deployer.d.ts +8 -12
  4. package/dist/deployer.d.ts.map +1 -1
  5. package/dist/deployer.js +69 -106
  6. package/dist/deployer.js.map +1 -1
  7. package/dist/enhanced-transpiler.d.ts +36 -0
  8. package/dist/enhanced-transpiler.d.ts.map +1 -0
  9. package/dist/enhanced-transpiler.js +272 -0
  10. package/dist/enhanced-transpiler.js.map +1 -0
  11. package/dist/entry-point-detection.d.ts +22 -0
  12. package/dist/entry-point-detection.d.ts.map +1 -0
  13. package/dist/entry-point-detection.js +415 -0
  14. package/dist/entry-point-detection.js.map +1 -0
  15. package/dist/index.d.ts +19 -12
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1025 -126
  18. package/dist/index.js.map +1 -1
  19. package/dist/jxr-server-manager.d.ts +32 -0
  20. package/dist/jxr-server-manager.d.ts.map +1 -0
  21. package/dist/jxr-server-manager.js +353 -0
  22. package/dist/jxr-server-manager.js.map +1 -0
  23. package/dist/runtime.d.ts +9 -9
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +3 -3
  26. package/package.json +9 -2
  27. package/src/deployer.ts +231 -0
  28. package/src/enhanced-transpiler.ts +331 -0
  29. package/src/entry-point-detection.ts +470 -0
  30. package/src/index.ts +63 -0
  31. package/src/jxr-server-manager.ts +419 -0
  32. package/src/module-resolver.ts +520 -0
  33. package/src/moq-transport.ts +267 -0
  34. package/src/runtime.ts +188 -0
  35. package/src/web-crypto.ts +279 -0
  36. package/src/worker-pool.ts +321 -0
  37. package/zzz_react_template/App.tsx +160 -0
  38. package/zzz_react_template/index.css +16 -0
  39. package/zzz_react_template/index.html +12 -0
  40. package/zzz_react_template/main.tsx +10 -0
  41. package/zzz_react_template/package.json +25 -0
@@ -0,0 +1,419 @@
1
+ import { readFile, readdir, watch } from "fs/promises";
2
+ import path from "path";
3
+ import http from "http";
4
+ import { JXRRuntime, findOrCreateEntryPoint, EnhancedTranspiler } from "./index.ts";
5
+ import type { ProjectFile } from "./index.ts";
6
+
7
+ export interface JXRServerConfig {
8
+ port?: number;
9
+ host?: string;
10
+ srcDir?: string;
11
+ enableHMR?: boolean;
12
+ debounceMs?: number;
13
+ }
14
+
15
+ export class JXRServerManager {
16
+ private runtime: JXRRuntime;
17
+ private transpiler: EnhancedTranspiler;
18
+ private server: http.Server | null = null;
19
+ private config: Required<JXRServerConfig>;
20
+ private projectFiles: ProjectFile[] = [];
21
+ private entryPoint: string = "src/App.tsx";
22
+ private watchers: Map<string, ReturnType<typeof watch>> = new Map();
23
+ private debounceTimer: NodeJS.Timeout | null = null;
24
+ private pendingChanges: Map<string, ProjectFile> = new Map();
25
+ private clients: Set<http.ServerResponse> = new Set();
26
+
27
+ constructor(config: JXRServerConfig = {}) {
28
+ this.config = {
29
+ port: config.port || 3000,
30
+ host: config.host || "localhost",
31
+ srcDir: config.srcDir || "src",
32
+ enableHMR: config.enableHMR !== false,
33
+ debounceMs: config.debounceMs || 300,
34
+ };
35
+ this.runtime = new JXRRuntime();
36
+ this.transpiler = new EnhancedTranspiler();
37
+ }
38
+
39
+ async initialize(): Promise<void> {
40
+ await this.runtime.init();
41
+ await this.loadProjectFiles();
42
+ this.setupEntryPoint();
43
+ if (this.config.enableHMR) {
44
+ this.startFileWatching();
45
+ }
46
+ }
47
+
48
+ private async loadProjectFiles(): Promise<void> {
49
+ const srcPath = path.resolve(process.cwd(), this.config.srcDir);
50
+ this.projectFiles = [];
51
+
52
+ async function readDirRecursive(dir: string, base: string, files: ProjectFile[], runtime: JXRRuntime) {
53
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
54
+ const fullPath = path.join(dir, entry.name);
55
+ const relativePath = path.join(base, entry.name);
56
+
57
+ if (entry.isDirectory()) {
58
+ await readDirRecursive(fullPath, relativePath, files, runtime);
59
+ } else if (/\.(tsx?|jsx?|css)$/.test(entry.name)) {
60
+ const content = await readFile(fullPath, "utf-8");
61
+ const vfsPath = "/" + relativePath.replace(/\\/g, "/");
62
+ runtime.vfs.write(vfsPath, content);
63
+
64
+ files.push({
65
+ id: Math.random().toString(36).slice(2),
66
+ path: relativePath.replace(/\\/g, "/"),
67
+ content,
68
+ language: entry.name.endsWith(".tsx") || entry.name.endsWith(".ts") ? "typescript" : "javascript",
69
+ createdAt: Date.now(),
70
+ updatedAt: Date.now(),
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ try {
77
+ await readDirRecursive(srcPath, this.config.srcDir, this.projectFiles, this.runtime);
78
+ console.log(`📁 Loaded ${this.projectFiles.length} files into VirtualFS`);
79
+ } catch (err) {
80
+ console.error("Error loading files:", err);
81
+ }
82
+ }
83
+
84
+ private setupEntryPoint(): void {
85
+ const result = findOrCreateEntryPoint(this.projectFiles);
86
+ this.entryPoint = result.entryPoint;
87
+
88
+ // If a new entry was created, add it to VFS
89
+ if (result.createdEntry) {
90
+ const entryFile = result.files.find(f => f.path === this.entryPoint);
91
+ if (entryFile) {
92
+ this.runtime.vfs.write("/" + entryFile.path, entryFile.content);
93
+ }
94
+ }
95
+
96
+ console.log(`🎯 Entry point: ${this.entryPoint}`);
97
+ }
98
+
99
+ private startFileWatching(): void {
100
+ const srcPath = path.resolve(process.cwd(), this.config.srcDir);
101
+
102
+ const watchDir = async (dir: string) => {
103
+ try {
104
+ const watcher = watch(dir, { recursive: true });
105
+ this.watchers.set(dir, watcher);
106
+
107
+ for await (const event of watcher) {
108
+ if (event.filename && /\.(tsx?|jsx?|css)$/.test(event.filename)) {
109
+ this.handleFileChange(event.filename);
110
+ }
111
+ }
112
+ } catch (err) {
113
+ console.error(`Watch error for ${dir}:`, err);
114
+ }
115
+ };
116
+
117
+ watchDir(srcPath);
118
+ console.log(`👀 Watching ${this.config.srcDir} for changes...`);
119
+ }
120
+
121
+ private handleFileChange(filename: string): void {
122
+ const fullPath = path.resolve(process.cwd(), this.config.srcDir, filename);
123
+ const relativePath = path.join(this.config.srcDir, filename).replace(/\\/g, "/");
124
+
125
+ readFile(fullPath, "utf-8")
126
+ .then(content => {
127
+ const file: ProjectFile = {
128
+ id: Math.random().toString(36).slice(2),
129
+ path: relativePath,
130
+ content,
131
+ language: filename.endsWith(".tsx") || filename.endsWith(".ts") ? "typescript" : "javascript",
132
+ createdAt: Date.now(),
133
+ updatedAt: Date.now(),
134
+ };
135
+
136
+ this.pendingChanges.set(relativePath, file);
137
+ this.scheduleReload();
138
+ })
139
+ .catch(err => console.error(`Error reading ${filename}:`, err));
140
+ }
141
+
142
+ private scheduleReload(): void {
143
+ if (this.debounceTimer) {
144
+ clearTimeout(this.debounceTimer);
145
+ }
146
+
147
+ this.debounceTimer = setTimeout(() => {
148
+ this.processPendingChanges();
149
+ }, this.config.debounceMs);
150
+ }
151
+
152
+ private async processPendingChanges(): Promise<void> {
153
+ if (this.pendingChanges.size === 0) return;
154
+
155
+ console.log(`🔄 Processing ${this.pendingChanges.size} file changes...`);
156
+
157
+ for (const [path, file] of this.pendingChanges) {
158
+ this.runtime.vfs.write("/" + path, file.content);
159
+
160
+ // Update project files array
161
+ const existingIndex = this.projectFiles.findIndex(f => f.path === path);
162
+ if (existingIndex >= 0) {
163
+ this.projectFiles[existingIndex] = file;
164
+ }
165
+ }
166
+
167
+ this.pendingChanges.clear();
168
+
169
+ // Notify connected clients
170
+ this.broadcastReload();
171
+ }
172
+
173
+ private broadcastReload(): void {
174
+ const message = JSON.stringify({ type: "reload", timestamp: Date.now() });
175
+ this.clients.forEach(client => {
176
+ client.write(`data: ${message}\n\n`);
177
+ });
178
+ }
179
+
180
+ async start(): Promise<void> {
181
+ this.server = http.createServer(async (req, res) => {
182
+ const url = new URL(req.url || "/", `http://${this.config.host}:${this.config.port}`);
183
+
184
+ // SSE endpoint for HMR
185
+ if (url.pathname === "/__hmr" && this.config.enableHMR) {
186
+ res.writeHead(200, {
187
+ "Content-Type": "text/event-stream",
188
+ "Cache-Control": "no-cache",
189
+ "Connection": "keep-alive",
190
+ "Access-Control-Allow-Origin": "*",
191
+ });
192
+
193
+ this.clients.add(res);
194
+
195
+ req.on("close", () => {
196
+ this.clients.delete(res);
197
+ });
198
+
199
+ // Send initial connection message
200
+ res.write(`data: ${JSON.stringify({ type: "connected" })}\n\n`);
201
+ return;
202
+ }
203
+
204
+ // Health check
205
+ if (url.pathname === "/__health") {
206
+ res.writeHead(200, { "Content-Type": "application/json" });
207
+ res.end(JSON.stringify({ status: "ok", runtime: "JXR", version: this.runtime.version }));
208
+ return;
209
+ }
210
+
211
+ // Serve index.html
212
+ if (url.pathname === "/") {
213
+ res.writeHead(200, { "Content-Type": "text/html" });
214
+ res.end(this.generateHTML());
215
+ return;
216
+ }
217
+
218
+ // Serve transformed TSX/TS files (with or without extension)
219
+ if (url.pathname.match(/\.(tsx?|jsx?|ts|js)$/) || url.pathname.startsWith('/src/')) {
220
+ try {
221
+ let vfsPath = url.pathname;
222
+
223
+ // Try to resolve the file with various extensions
224
+ let file = this.runtime.vfs.read(vfsPath);
225
+
226
+ // If not found and no extension, try adding extensions
227
+ if (!file && !vfsPath.match(/\.(tsx?|jsx?|ts|js|css)$/)) {
228
+ const extensions = ['.tsx', '.ts', '.jsx', '.js', '.css'];
229
+ for (const ext of extensions) {
230
+ file = this.runtime.vfs.read(vfsPath + ext);
231
+ if (file) {
232
+ vfsPath = vfsPath + ext;
233
+ break;
234
+ }
235
+ }
236
+ }
237
+
238
+ if (!file) {
239
+ res.writeHead(404, { "Content-Type": "text/plain" });
240
+ res.end(`// Module not found: ${url.pathname}`);
241
+ return;
242
+ }
243
+
244
+ // Serve CSS files as JavaScript that injects the CSS
245
+ if (vfsPath.endsWith('.css')) {
246
+ const escapedCSS = file.content
247
+ .replace(/\\/g, '\\\\')
248
+ .replace(/`/g, '\\`')
249
+ .replace(/\$/g, '\\$');
250
+ const js = `
251
+ const style = document.createElement('style');
252
+ style.textContent = \`${escapedCSS}\`;
253
+ document.head.appendChild(style);
254
+ export default \`${escapedCSS}\`;
255
+ `;
256
+ res.writeHead(200, {
257
+ "Content-Type": "application/javascript",
258
+ "Cache-Control": "no-cache"
259
+ });
260
+ res.end(js);
261
+ return;
262
+ }
263
+
264
+ // Use EnhancedTranspiler (Babel) for proper JSX transformation
265
+ const result = this.transpiler.transpileTypeScript(file.content, vfsPath);
266
+ if (result.error) {
267
+ console.error(`Transform error for ${url.pathname}:`, result.error);
268
+ res.writeHead(500, { "Content-Type": "text/plain" });
269
+ res.end(`// Transform error: ${result.error.message}`);
270
+ return;
271
+ }
272
+
273
+ res.writeHead(200, {
274
+ "Content-Type": "application/javascript",
275
+ "Cache-Control": "no-cache"
276
+ });
277
+ res.end(result.code);
278
+ } catch (err: any) {
279
+ console.error(`Error serving ${url.pathname}:`, err);
280
+ res.writeHead(500, { "Content-Type": "text/plain" });
281
+ res.end(`// Error: ${err?.message || String(err)}`);
282
+ }
283
+ return;
284
+ }
285
+
286
+ res.writeHead(404);
287
+ res.end("Not found");
288
+ });
289
+
290
+ return new Promise((resolve, reject) => {
291
+ this.server!.listen(this.config.port, this.config.host, () => {
292
+ console.log(`🚀 JXR server running on http://${this.config.host}:${this.config.port}/`);
293
+ if (this.config.enableHMR) {
294
+ console.log(`🔥 HMR enabled (debounce: ${this.config.debounceMs}ms)`);
295
+ }
296
+ resolve();
297
+ });
298
+
299
+ this.server!.on("error", reject);
300
+ });
301
+ }
302
+
303
+ async stop(): Promise<void> {
304
+ // Stop watchers - we can't easily abort async iterators, but we can clear the map
305
+ // The watch() iterator will naturally stop when the process exits
306
+ this.watchers.clear();
307
+
308
+ // Clear any pending timers
309
+ if (this.debounceTimer) {
310
+ clearTimeout(this.debounceTimer);
311
+ this.debounceTimer = null;
312
+ }
313
+
314
+ // Close all SSE connections
315
+ this.clients.forEach(client => {
316
+ try { client.end(); } catch {}
317
+ });
318
+ this.clients.clear();
319
+
320
+ // Close server
321
+ if (this.server) {
322
+ await new Promise<void>((resolve) => {
323
+ this.server!.close(() => resolve());
324
+ });
325
+ this.server = null;
326
+ }
327
+
328
+ // Dispose runtime
329
+ this.runtime.dispose();
330
+
331
+ console.log("👋 JXR server stopped");
332
+ }
333
+
334
+ private generateHTML(): string {
335
+ const hmrScript = this.config.enableHMR ? `
336
+ <script>
337
+ // HMR Client
338
+ const evtSource = new EventSource('/__hmr');
339
+ evtSource.onmessage = (e) => {
340
+ const data = JSON.parse(e.data);
341
+ if (data.type === 'reload') {
342
+ console.log('[JXR] Reloading...');
343
+ location.reload();
344
+ }
345
+ };
346
+ evtSource.onerror = () => console.log('[JXR] HMR connection lost');
347
+ </script>` : '';
348
+
349
+ // Import map with react/jsx-runtime for Babel's automatic runtime
350
+ // Includes common dependencies used by JXR projects
351
+ const importMap = {
352
+ "imports": {
353
+ "react": "https://esm.sh/react@19.2.4",
354
+ "react/jsx-runtime": "https://esm.sh/react@19.2.4/jsx-runtime",
355
+ "react/jsx-dev-runtime": "https://esm.sh/react@19.2.4/jsx-dev-runtime",
356
+ "react-dom/client": "https://esm.sh/react-dom@19.2.4/client",
357
+ "wouter": "https://esm.sh/wouter@3.6.0?external=react",
358
+ "lucide-react": "https://esm.sh/lucide-react@0.483.0?external=react",
359
+ "sonner": "https://esm.sh/sonner@2.0.1?external=react",
360
+ "@radix-ui/react-dialog": "https://esm.sh/@radix-ui/react-dialog@1.1.6?external=react",
361
+ "@radix-ui/react-tooltip": "https://esm.sh/@radix-ui/react-tooltip@1.1.8?external=react",
362
+ "clsx": "https://esm.sh/clsx@2.1.1",
363
+ "tailwind-merge": "https://esm.sh/tailwind-merge@3.0.2",
364
+ "class-variance-authority": "https://esm.sh/class-variance-authority@0.7.1"
365
+ }
366
+ };
367
+
368
+ // Check if we have a main.tsx/bootstrap file - if so, use it directly
369
+ // Otherwise use the component entry point pattern
370
+ const hasMainFile = this.projectFiles.some(f =>
371
+ f.path === 'src/main.tsx' || f.path === 'src/main.ts' ||
372
+ f.path === 'src/index.tsx' || f.path === 'src/index.ts'
373
+ );
374
+
375
+ if (hasMainFile) {
376
+ // Use the bootstrap file pattern (like the react template)
377
+ return `<!DOCTYPE html>
378
+ <html lang="en">
379
+ <head>
380
+ <meta charset="UTF-8">
381
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
382
+ <title>JXR App</title>
383
+ <script type="importmap">
384
+ ${JSON.stringify(importMap)}
385
+ </script>
386
+ </head>
387
+ <body>
388
+ <div id="root"></div>
389
+ <script type="module" src="/src/main.tsx"></script>
390
+ ${hmrScript}
391
+ </body>
392
+ </html>`;
393
+ }
394
+
395
+ // Fallback: use component entry point directly
396
+ return `<!DOCTYPE html>
397
+ <html lang="en">
398
+ <head>
399
+ <meta charset="UTF-8">
400
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
401
+ <title>JXR App</title>
402
+ <script type="importmap">
403
+ ${JSON.stringify(importMap)}
404
+ </script>
405
+ </head>
406
+ <body>
407
+ <div id="root"></div>
408
+ <script type="module">
409
+ import { createRoot } from 'react-dom/client';
410
+ import App from '/${this.entryPoint}';
411
+
412
+ const root = createRoot(document.getElementById('root'));
413
+ root.render(App());
414
+ </script>
415
+ ${hmrScript}
416
+ </body>
417
+ </html>`;
418
+ }
419
+ }