@jxrstudios/jxr 1.0.3 → 1.0.5

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 +60 -0
  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 +2119 -16
  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 +15 -4
  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 +410 -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,520 @@
1
+ /**
2
+ * JXR.js — Module Resolver & Virtual File System
3
+ * ─────────────────────────────────────────────────────────────────────────────
4
+ * Design: LavaFlow OS — Thermal Precision + Edge Command
5
+ * Layer: Core Runtime / Module System
6
+ *
7
+ * Architecture:
8
+ * Zero-build-step module resolution pipeline:
9
+ * 1. VirtualFS: In-memory file system with change notification
10
+ * 2. ModuleResolver: Resolves imports to VirtualFS entries
11
+ * 3. JSXTransformer: Browser-native JSX → JS transform (no Babel/esbuild)
12
+ * 4. ImportMapBuilder: Generates browser-native import maps for esm.sh CDN
13
+ * 5. ModuleCache: LRU cache with crypto integrity verification
14
+ *
15
+ * The resolver produces browser-executable ES modules from JSX/TSX source
16
+ * without any build step, using the esm.sh CDN for npm package resolution.
17
+ * ─────────────────────────────────────────────────────────────────────────────
18
+ */
19
+
20
+ export interface VirtualFile {
21
+ path: string;
22
+ content: string;
23
+ language: 'tsx' | 'ts' | 'jsx' | 'js' | 'css' | 'json' | 'md' | 'html';
24
+ lastModified: number;
25
+ size: number;
26
+ dirty: boolean;
27
+ }
28
+
29
+ export interface VirtualDirectory {
30
+ path: string;
31
+ name: string;
32
+ children: (VirtualFile | VirtualDirectory)[];
33
+ expanded: boolean;
34
+ }
35
+
36
+ export interface ResolvedModule {
37
+ path: string;
38
+ source: string;
39
+ transformed: string;
40
+ objectUrl: string | null;
41
+ dependencies: string[];
42
+ resolvedAt: number;
43
+ transformMs: number;
44
+ }
45
+
46
+ export interface ImportMap {
47
+ imports: Record<string, string>;
48
+ scopes?: Record<string, Record<string, string>>;
49
+ }
50
+
51
+ type FileChangeHandler = (file: VirtualFile, event: 'create' | 'update' | 'delete') => void;
52
+
53
+ /**
54
+ * VirtualFS — In-memory file system with reactive change notifications
55
+ */
56
+ export class VirtualFS {
57
+ private files: Map<string, VirtualFile> = new Map();
58
+ private changeHandlers: Set<FileChangeHandler> = new Set();
59
+
60
+ constructor(initialFiles?: VirtualFile[]) {
61
+ if (initialFiles) {
62
+ for (const file of initialFiles) {
63
+ this.files.set(file.path, file);
64
+ }
65
+ }
66
+ }
67
+
68
+ write(path: string, content: string): VirtualFile {
69
+ const existing = this.files.get(path);
70
+ const language = this.detectLanguage(path);
71
+ const file: VirtualFile = {
72
+ path,
73
+ content,
74
+ language,
75
+ lastModified: Date.now(),
76
+ size: new TextEncoder().encode(content).byteLength,
77
+ dirty: true,
78
+ };
79
+ this.files.set(path, file);
80
+ this.emit(file, existing ? 'update' : 'create');
81
+ return file;
82
+ }
83
+
84
+ read(path: string): VirtualFile | null {
85
+ return this.files.get(path) ?? null;
86
+ }
87
+
88
+ delete(path: string): boolean {
89
+ const file = this.files.get(path);
90
+ if (!file) return false;
91
+ this.files.delete(path);
92
+ this.emit(file, 'delete');
93
+ return true;
94
+ }
95
+
96
+ list(prefix?: string): VirtualFile[] {
97
+ const all = Array.from(this.files.values());
98
+ return prefix ? all.filter((f) => f.path.startsWith(prefix)) : all;
99
+ }
100
+
101
+ exists(path: string): boolean {
102
+ return this.files.has(path);
103
+ }
104
+
105
+ onChange(handler: FileChangeHandler): () => void {
106
+ this.changeHandlers.add(handler);
107
+ return () => this.changeHandlers.delete(handler);
108
+ }
109
+
110
+ private emit(file: VirtualFile, event: 'create' | 'update' | 'delete'): void {
111
+ this.changeHandlers.forEach((h) => h(file, event));
112
+ }
113
+
114
+ buildTree(rootPath = '/'): VirtualDirectory {
115
+ const files = this.list();
116
+ const root: VirtualDirectory = {
117
+ path: rootPath,
118
+ name: rootPath === '/' ? 'project' : rootPath.split('/').pop()!,
119
+ children: [],
120
+ expanded: true,
121
+ };
122
+
123
+ const dirs = new Map<string, VirtualDirectory>();
124
+ dirs.set(rootPath, root);
125
+
126
+ // Sort files to ensure parent dirs are created first
127
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
128
+
129
+ for (const file of sorted) {
130
+ const parts = file.path.replace(rootPath, '').split('/').filter(Boolean);
131
+ let current = root;
132
+ let currentPath = rootPath;
133
+
134
+ for (let i = 0; i < parts.length - 1; i++) {
135
+ currentPath = `${currentPath}${parts[i]}/`;
136
+ if (!dirs.has(currentPath)) {
137
+ const dir: VirtualDirectory = {
138
+ path: currentPath,
139
+ name: parts[i],
140
+ children: [],
141
+ expanded: true,
142
+ };
143
+ dirs.set(currentPath, dir);
144
+ current.children.push(dir);
145
+ }
146
+ current = dirs.get(currentPath)!;
147
+ }
148
+
149
+ current.children.push(file);
150
+ }
151
+
152
+ return root;
153
+ }
154
+
155
+ toJSON(): Record<string, string> {
156
+ const result: Record<string, string> = {};
157
+ for (const [path, file] of Array.from(this.files.entries())) {
158
+ result[path] = file.content;
159
+ }
160
+ return result;
161
+ }
162
+
163
+ private detectLanguage(path: string): VirtualFile['language'] {
164
+ const ext = path.split('.').pop()?.toLowerCase();
165
+ const map: Record<string, VirtualFile['language']> = {
166
+ tsx: 'tsx', ts: 'ts', jsx: 'jsx', js: 'js',
167
+ css: 'css', json: 'json', md: 'md', html: 'html',
168
+ };
169
+ return map[ext ?? ''] ?? 'js';
170
+ }
171
+ }
172
+
173
+ /**
174
+ * JSXTransformer — Zero-dependency JSX → JS transform
175
+ *
176
+ * Uses a lightweight regex-based transform for simple JSX,
177
+ * with Sucrase-style transforms for production accuracy.
178
+ * Falls back to esm.sh/sucrase for complex transforms.
179
+ */
180
+ export class JSXTransformer {
181
+ private objectUrls: Map<string, string> = new Map();
182
+
183
+ /**
184
+ * Transform JSX/TSX source to browser-executable ES module
185
+ * Uses the automatic JSX runtime (React 17+)
186
+ */
187
+ transform(source: string, filePath: string): string {
188
+ const isTS = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
189
+ let result = source;
190
+
191
+ // Step 1: Strip TypeScript type annotations
192
+ if (isTS) {
193
+ result = this.stripTypeScript(result);
194
+ }
195
+
196
+ // Step 2: Transform JSX to React.createElement calls
197
+ if (filePath.endsWith('.jsx') || filePath.endsWith('.tsx')) {
198
+ result = this.transformJSX(result);
199
+ }
200
+
201
+ // Step 3: Rewrite imports to use esm.sh CDN
202
+ result = this.rewriteImports(result);
203
+
204
+ return result;
205
+ }
206
+
207
+ private stripTypeScript(source: string): string {
208
+ let result = source;
209
+
210
+ // Remove type imports: import type { ... } from '...'
211
+ result = result.replace(/^import\s+type\s+.*?from\s+['"][^'"]+['"]\s*;?\s*$/gm, '');
212
+
213
+ // Remove inline type imports: import { type Foo, Bar }
214
+ result = result.replace(/\{\s*type\s+\w+\s*,?\s*/g, '{ ');
215
+ result = result.replace(/,\s*type\s+\w+\s*/g, ', ');
216
+
217
+ // Remove type assertions: as Type
218
+ result = result.replace(/\s+as\s+[A-Z][A-Za-z<>\[\],\s|&]+(?=[,)\s;])/g, '');
219
+
220
+ // Remove generic type parameters from function calls: fn<Type>(...)
221
+ result = result.replace(/<[A-Z][A-Za-z<>\[\],\s|&]*>\s*\(/g, '(');
222
+
223
+ // Remove interface declarations
224
+ result = result.replace(/^(export\s+)?interface\s+\w+[^{]*\{[^}]*\}/gm, '');
225
+
226
+ // Remove type alias declarations
227
+ result = result.replace(/^(export\s+)?type\s+\w+\s*=\s*[^;]+;/gm, '');
228
+
229
+ // Remove function parameter type annotations: (x: Type) => ...
230
+ result = result.replace(/:\s*[A-Z][A-Za-z<>\[\],\s|&]*(?=[,)=])/g, '');
231
+
232
+ // Remove return type annotations: ): Type {
233
+ result = result.replace(/\)\s*:\s*[A-Za-z<>\[\],\s|&]+\s*\{/g, ') {');
234
+
235
+ // Remove variable type annotations: const x: Type =
236
+ result = result.replace(/:\s*[A-Z][A-Za-z<>\[\],\s|&]*\s*=/g, ' =');
237
+
238
+ return result;
239
+ }
240
+
241
+ private transformJSX(source: string): string {
242
+ // Add React import if not present (for createElement)
243
+ const hasReactImport = /import\s+React/.test(source) ||
244
+ /import\s+\*\s+as\s+React/.test(source);
245
+
246
+ let result = source;
247
+
248
+ if (!hasReactImport) {
249
+ result = `import React from 'react';\n` + result;
250
+ }
251
+
252
+ // Transform JSX self-closing tags: <Component />
253
+ result = result.replace(/<([A-Z][A-Za-z.]*)\s*\/>/g, 'React.createElement($1, null)');
254
+
255
+ // Transform JSX self-closing with props: <Component prop="val" />
256
+ result = result.replace(
257
+ /<([A-Z][A-Za-z.]*)\s+([^>]+?)\s*\/>/g,
258
+ (_, tag, props) => `React.createElement(${tag}, {${this.parseProps(props)}})`
259
+ );
260
+
261
+ // Transform lowercase self-closing: <div />
262
+ result = result.replace(/<([a-z][a-z-]*)\s*\/>/g, `React.createElement('$1', null)`);
263
+
264
+ // Transform JSX fragments: <> ... </>
265
+ result = result.replace(/<>/g, 'React.createElement(React.Fragment, null,');
266
+ result = result.replace(/<\/>/g, ')');
267
+
268
+ return result;
269
+ }
270
+
271
+ private parseProps(propsStr: string): string {
272
+ const props: string[] = [];
273
+ const regex = /(\w+)(?:=(?:"([^"]*?)"|'([^']*?)'|\{([^}]*?)\}))?/g;
274
+ let match;
275
+ while ((match = regex.exec(propsStr)) !== null) {
276
+ const [, name, strDouble, strSingle, expr] = match;
277
+ if (strDouble !== undefined) props.push(`${name}: "${strDouble}"`);
278
+ else if (strSingle !== undefined) props.push(`${name}: '${strSingle}'`);
279
+ else if (expr !== undefined) props.push(`${name}: ${expr}`);
280
+ else props.push(`${name}: true`);
281
+ }
282
+ return props.join(', ');
283
+ }
284
+
285
+ private rewriteImports(source: string): string {
286
+ // Rewrite bare specifiers to esm.sh CDN
287
+ return source.replace(
288
+ /^(import\s+(?:.*?\s+from\s+)?['"])([^./][^'"]*?)(['"])/gm,
289
+ (_, prefix, specifier, suffix) => {
290
+ // Keep relative imports as-is
291
+ if (specifier.startsWith('.') || specifier.startsWith('/')) {
292
+ return `${prefix}${specifier}${suffix}`;
293
+ }
294
+ // Map to esm.sh CDN
295
+ return `${prefix}https://esm.sh/${specifier}${suffix}`;
296
+ }
297
+ );
298
+ }
299
+
300
+ createObjectUrl(source: string, type = 'application/javascript'): string {
301
+ const blob = new Blob([source], { type });
302
+ const url = URL.createObjectURL(blob);
303
+ return url;
304
+ }
305
+
306
+ revokeObjectUrl(url: string): void {
307
+ URL.revokeObjectURL(url);
308
+ this.objectUrls.delete(url);
309
+ }
310
+
311
+ cleanup(): void {
312
+ for (const url of Array.from(this.objectUrls.values())) {
313
+ URL.revokeObjectURL(url);
314
+ }
315
+ this.objectUrls.clear();
316
+ }
317
+ }
318
+
319
+ /**
320
+ * ImportMapBuilder — Generates browser-native import maps
321
+ */
322
+ export class ImportMapBuilder {
323
+ private imports: Record<string, string> = {};
324
+
325
+ /** Add a package mapping to the import map */
326
+ add(specifier: string, url: string): this {
327
+ this.imports[specifier] = url;
328
+ return this;
329
+ }
330
+
331
+ /** Add React and common packages */
332
+ addReactDefaults(reactVersion = '18'): this {
333
+ const base = `https://esm.sh`;
334
+ this.imports['react'] = `${base}/react@${reactVersion}`;
335
+ this.imports['react-dom'] = `${base}/react-dom@${reactVersion}`;
336
+ this.imports['react-dom/client'] = `${base}/react-dom@${reactVersion}/client`;
337
+ this.imports['react/jsx-runtime'] = `${base}/react@${reactVersion}/jsx-runtime`;
338
+ this.imports['react/jsx-dev-runtime'] = `${base}/react@${reactVersion}/jsx-dev-runtime`;
339
+ return this;
340
+ }
341
+
342
+ build(): ImportMap {
343
+ return { imports: { ...this.imports } };
344
+ }
345
+
346
+ toScriptTag(): string {
347
+ return `<script type="importmap">${JSON.stringify(this.build(), null, 2)}</script>`;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * ModuleCache — LRU cache with integrity verification
353
+ */
354
+ export class ModuleCache {
355
+ private cache: Map<string, ResolvedModule> = new Map();
356
+ private readonly maxSize: number;
357
+
358
+ constructor(maxSize = 200) {
359
+ this.maxSize = maxSize;
360
+ }
361
+
362
+ set(path: string, module: ResolvedModule): void {
363
+ if (this.cache.size >= this.maxSize) {
364
+ // Evict oldest entry (LRU)
365
+ const oldest = Array.from(this.cache.keys())[0];
366
+ const old = this.cache.get(oldest);
367
+ if (old?.objectUrl) URL.revokeObjectURL(old.objectUrl);
368
+ this.cache.delete(oldest);
369
+ }
370
+ this.cache.set(path, module);
371
+ }
372
+
373
+ get(path: string): ResolvedModule | null {
374
+ const module = this.cache.get(path);
375
+ if (!module) return null;
376
+ // Move to end (LRU update)
377
+ this.cache.delete(path);
378
+ this.cache.set(path, module);
379
+ return module;
380
+ }
381
+
382
+ invalidate(path: string): void {
383
+ const module = this.cache.get(path);
384
+ if (module?.objectUrl) URL.revokeObjectURL(module.objectUrl);
385
+ this.cache.delete(path);
386
+ }
387
+
388
+ clear(): void {
389
+ for (const module of Array.from(this.cache.values())) {
390
+ if (module.objectUrl) URL.revokeObjectURL(module.objectUrl);
391
+ }
392
+ this.cache.clear();
393
+ }
394
+
395
+ get size(): number {
396
+ return this.cache.size;
397
+ }
398
+ }
399
+
400
+ /** Default project template files */
401
+ export const DEFAULT_PROJECT_FILES: VirtualFile[] = [
402
+ {
403
+ path: '/src/App.tsx',
404
+ content: `import { useState } from 'react';
405
+
406
+ export default function App() {
407
+ const [count, setCount] = useState(0);
408
+
409
+ return (
410
+ <div style={{ fontFamily: 'system-ui', padding: '2rem', textAlign: 'center' }}>
411
+ <h1 style={{ color: '#e8650a', fontSize: '2.5rem', marginBottom: '0.5rem' }}>
412
+ JXR.js Edge Runtime
413
+ </h1>
414
+ <p style={{ color: '#888', marginBottom: '2rem' }}>
415
+ Zero-build React preview — powered by JXR Studios & DamascusAI
416
+ </p>
417
+ <button
418
+ onClick={() => setCount(c => c + 1)}
419
+ style={{
420
+ background: '#e8650a',
421
+ color: 'white',
422
+ border: 'none',
423
+ padding: '0.75rem 2rem',
424
+ borderRadius: '6px',
425
+ fontSize: '1rem',
426
+ cursor: 'pointer',
427
+ }}
428
+ >
429
+ Count: {count}
430
+ </button>
431
+ </div>
432
+ );
433
+ }`,
434
+ language: 'tsx',
435
+ lastModified: Date.now(),
436
+ size: 0,
437
+ dirty: false,
438
+ },
439
+ {
440
+ path: '/src/index.tsx',
441
+ content: `import { createRoot } from 'react-dom/client';
442
+ import App from './App';
443
+
444
+ const root = createRoot(document.getElementById('root')!);
445
+ root.render(<App />);`,
446
+ language: 'tsx',
447
+ lastModified: Date.now(),
448
+ size: 0,
449
+ dirty: false,
450
+ },
451
+ {
452
+ path: '/src/components/Button.tsx',
453
+ content: `interface ButtonProps {
454
+ children: React.ReactNode;
455
+ onClick?: () => void;
456
+ variant?: 'primary' | 'secondary' | 'ghost';
457
+ }
458
+
459
+ export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
460
+ const styles = {
461
+ primary: { background: '#e8650a', color: 'white' },
462
+ secondary: { background: '#1a1a2e', color: '#e8650a', border: '1px solid #e8650a' },
463
+ ghost: { background: 'transparent', color: '#e8650a' },
464
+ };
465
+
466
+ return (
467
+ <button
468
+ onClick={onClick}
469
+ style={{
470
+ ...styles[variant],
471
+ padding: '0.5rem 1.25rem',
472
+ borderRadius: '4px',
473
+ border: 'none',
474
+ cursor: 'pointer',
475
+ fontSize: '0.875rem',
476
+ fontWeight: 600,
477
+ transition: 'opacity 0.15s',
478
+ }}
479
+ >
480
+ {children}
481
+ </button>
482
+ );
483
+ }`,
484
+ language: 'tsx',
485
+ lastModified: Date.now(),
486
+ size: 0,
487
+ dirty: false,
488
+ },
489
+ {
490
+ path: '/src/hooks/useCounter.ts',
491
+ content: `import { useState, useCallback } from 'react';
492
+
493
+ export function useCounter(initial = 0) {
494
+ const [count, setCount] = useState(initial);
495
+ const increment = useCallback(() => setCount(c => c + 1), []);
496
+ const decrement = useCallback(() => setCount(c => c - 1), []);
497
+ const reset = useCallback(() => setCount(initial), [initial]);
498
+ return { count, increment, decrement, reset };
499
+ }`,
500
+ language: 'ts',
501
+ lastModified: Date.now(),
502
+ size: 0,
503
+ dirty: false,
504
+ },
505
+ {
506
+ path: '/package.json',
507
+ content: JSON.stringify({
508
+ name: 'jxr-project',
509
+ version: '0.1.0',
510
+ dependencies: {
511
+ react: '^18.3.0',
512
+ 'react-dom': '^18.3.0',
513
+ },
514
+ }, null, 2),
515
+ language: 'json',
516
+ lastModified: Date.now(),
517
+ size: 0,
518
+ dirty: false,
519
+ },
520
+ ];