@jxrstudios/jxr 1.2.19 → 1.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deployer.d.ts +83 -0
- package/dist/deployer.d.ts.map +1 -0
- package/dist/deployer.js +278 -0
- package/dist/deployer.js.map +1 -0
- package/dist/enhanced-transpiler.d.ts +36 -0
- package/dist/enhanced-transpiler.d.ts.map +1 -0
- package/dist/enhanced-transpiler.js +272 -0
- package/dist/enhanced-transpiler.js.map +1 -0
- package/dist/entry-point-detection.d.ts +22 -0
- package/dist/entry-point-detection.d.ts.map +1 -0
- package/dist/entry-point-detection.js +415 -0
- package/dist/entry-point-detection.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2315 -0
- package/dist/index.js.map +1 -0
- package/dist/jxr-server-manager.d.ts +32 -0
- package/dist/jxr-server-manager.d.ts.map +1 -0
- package/dist/jxr-server-manager.js +453 -0
- package/dist/jxr-server-manager.js.map +1 -0
- package/dist/module-resolver.d.ts +115 -0
- package/dist/module-resolver.d.ts.map +1 -0
- package/dist/module-resolver.js +430 -0
- package/dist/module-resolver.js.map +1 -0
- package/dist/moq-transport.d.ts +96 -0
- package/dist/moq-transport.d.ts.map +1 -0
- package/dist/moq-transport.js +188 -0
- package/dist/moq-transport.js.map +1 -0
- package/dist/runtime.d.ts +70 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +150 -0
- package/dist/runtime.js.map +1 -0
- package/dist/web-crypto.d.ts +77 -0
- package/dist/web-crypto.d.ts.map +1 -0
- package/dist/web-crypto.js +186 -0
- package/dist/web-crypto.js.map +1 -0
- package/dist/worker-pool.d.ts +83 -0
- package/dist/worker-pool.d.ts.map +1 -0
- package/dist/worker-pool.js +238 -0
- package/dist/worker-pool.js.map +1 -0
- package/package.json +3 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,2315 @@
|
|
|
1
|
+
// src/worker-pool.ts
|
|
2
|
+
var PRIORITY_WEIGHTS = {
|
|
3
|
+
critical: 4,
|
|
4
|
+
high: 3,
|
|
5
|
+
normal: 2,
|
|
6
|
+
low: 1
|
|
7
|
+
};
|
|
8
|
+
var WorkerPool = class {
|
|
9
|
+
workers = /* @__PURE__ */ new Map();
|
|
10
|
+
taskQueue = [];
|
|
11
|
+
pendingTasks = /* @__PURE__ */ new Map();
|
|
12
|
+
metricsHistory = [];
|
|
13
|
+
throughputWindow = [];
|
|
14
|
+
maxWorkers;
|
|
15
|
+
workerScript;
|
|
16
|
+
taskCounter = 0;
|
|
17
|
+
listeners = /* @__PURE__ */ new Map();
|
|
18
|
+
constructor(workerScript, maxWorkers) {
|
|
19
|
+
this.workerScript = workerScript;
|
|
20
|
+
this.maxWorkers = maxWorkers ?? Math.max(2, (navigator.hardwareConcurrency ?? 4) - 1);
|
|
21
|
+
this.prewarm();
|
|
22
|
+
}
|
|
23
|
+
/** Pre-warm the pool to eliminate cold-start latency */
|
|
24
|
+
prewarm() {
|
|
25
|
+
const initialCount = Math.min(2, this.maxWorkers);
|
|
26
|
+
for (let i = 0; i < initialCount; i++) {
|
|
27
|
+
this.spawnWorker();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
spawnWorker() {
|
|
31
|
+
const workerId = `worker-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
+
const worker = new Worker(this.workerScript, { type: "module" });
|
|
33
|
+
const entry = {
|
|
34
|
+
worker,
|
|
35
|
+
status: "idle",
|
|
36
|
+
currentTaskId: null,
|
|
37
|
+
latencyHistory: [],
|
|
38
|
+
metrics: {
|
|
39
|
+
workerId,
|
|
40
|
+
status: "idle",
|
|
41
|
+
tasksCompleted: 0,
|
|
42
|
+
tasksFailed: 0,
|
|
43
|
+
avgLatencyMs: 0,
|
|
44
|
+
lastActiveAt: Date.now(),
|
|
45
|
+
cpuLoad: 0
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
worker.onmessage = (event) => this.handleWorkerMessage(workerId, event);
|
|
49
|
+
worker.onerror = (error) => this.handleWorkerError(workerId, error);
|
|
50
|
+
this.workers.set(workerId, entry);
|
|
51
|
+
return workerId;
|
|
52
|
+
}
|
|
53
|
+
handleWorkerMessage(workerId, event) {
|
|
54
|
+
const { taskId, result, error, type } = event.data;
|
|
55
|
+
const entry = this.workers.get(workerId);
|
|
56
|
+
if (!entry) return;
|
|
57
|
+
if (type === "metrics") {
|
|
58
|
+
entry.metrics.cpuLoad = event.data.cpuLoad ?? 0;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const task = this.pendingTasks.get(taskId);
|
|
62
|
+
if (!task) return;
|
|
63
|
+
const latency = Date.now() - task.timestamp;
|
|
64
|
+
entry.latencyHistory.push(latency);
|
|
65
|
+
if (entry.latencyHistory.length > 50) entry.latencyHistory.shift();
|
|
66
|
+
entry.metrics.avgLatencyMs = entry.latencyHistory.reduce((a, b) => a + b, 0) / entry.latencyHistory.length;
|
|
67
|
+
entry.metrics.lastActiveAt = Date.now();
|
|
68
|
+
if (error) {
|
|
69
|
+
entry.metrics.tasksFailed++;
|
|
70
|
+
task.reject(new Error(error));
|
|
71
|
+
} else {
|
|
72
|
+
entry.metrics.tasksCompleted++;
|
|
73
|
+
this.throughputWindow.push(Date.now());
|
|
74
|
+
task.resolve(result);
|
|
75
|
+
}
|
|
76
|
+
this.pendingTasks.delete(taskId);
|
|
77
|
+
entry.status = "idle";
|
|
78
|
+
entry.metrics.status = "idle";
|
|
79
|
+
entry.currentTaskId = null;
|
|
80
|
+
this.emitMetrics();
|
|
81
|
+
this.drainQueue();
|
|
82
|
+
}
|
|
83
|
+
handleWorkerError(workerId, error) {
|
|
84
|
+
const entry = this.workers.get(workerId);
|
|
85
|
+
if (!entry) return;
|
|
86
|
+
entry.status = "error";
|
|
87
|
+
entry.metrics.status = "error";
|
|
88
|
+
if (entry.currentTaskId) {
|
|
89
|
+
const task = this.pendingTasks.get(entry.currentTaskId);
|
|
90
|
+
if (task) {
|
|
91
|
+
task.reject(new Error(error.message));
|
|
92
|
+
this.pendingTasks.delete(entry.currentTaskId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
entry.worker.terminate();
|
|
96
|
+
this.workers.delete(workerId);
|
|
97
|
+
this.spawnWorker();
|
|
98
|
+
this.drainQueue();
|
|
99
|
+
}
|
|
100
|
+
getIdleWorker() {
|
|
101
|
+
for (const [, entry] of Array.from(this.workers.entries())) {
|
|
102
|
+
if (entry.status === "idle") return entry;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
drainQueue() {
|
|
107
|
+
if (this.taskQueue.length === 0) return;
|
|
108
|
+
this.taskQueue.sort(
|
|
109
|
+
(a, b) => PRIORITY_WEIGHTS[b.priority] - PRIORITY_WEIGHTS[a.priority]
|
|
110
|
+
);
|
|
111
|
+
let idleWorker = this.getIdleWorker();
|
|
112
|
+
if (!idleWorker && this.workers.size < this.maxWorkers) {
|
|
113
|
+
const newId = this.spawnWorker();
|
|
114
|
+
idleWorker = this.workers.get(newId) ?? null;
|
|
115
|
+
}
|
|
116
|
+
if (!idleWorker) return;
|
|
117
|
+
const task = this.taskQueue.shift();
|
|
118
|
+
this.dispatchToWorker(idleWorker, task);
|
|
119
|
+
}
|
|
120
|
+
dispatchToWorker(entry, task) {
|
|
121
|
+
entry.status = "busy";
|
|
122
|
+
entry.metrics.status = "busy";
|
|
123
|
+
entry.currentTaskId = task.id;
|
|
124
|
+
this.pendingTasks.set(task.id, task);
|
|
125
|
+
entry.worker.postMessage({
|
|
126
|
+
taskId: task.id,
|
|
127
|
+
type: task.type,
|
|
128
|
+
payload: task.payload
|
|
129
|
+
});
|
|
130
|
+
if (task.timeoutMs) {
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
if (this.pendingTasks.has(task.id)) {
|
|
133
|
+
task.reject(new Error(`Task ${task.id} timed out after ${task.timeoutMs}ms`));
|
|
134
|
+
this.pendingTasks.delete(task.id);
|
|
135
|
+
}
|
|
136
|
+
}, task.timeoutMs);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Submit a task to the pool, returns a Promise */
|
|
140
|
+
submit(type, payload, options = {}) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const task = {
|
|
143
|
+
id: `task-${++this.taskCounter}-${Date.now()}`,
|
|
144
|
+
type,
|
|
145
|
+
payload,
|
|
146
|
+
priority: options.priority ?? "normal",
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
timeoutMs: options.timeoutMs,
|
|
149
|
+
resolve,
|
|
150
|
+
reject
|
|
151
|
+
};
|
|
152
|
+
const idleWorker = this.getIdleWorker();
|
|
153
|
+
if (idleWorker) {
|
|
154
|
+
this.dispatchToWorker(idleWorker, task);
|
|
155
|
+
} else {
|
|
156
|
+
this.taskQueue.push(task);
|
|
157
|
+
if (this.workers.size < this.maxWorkers) {
|
|
158
|
+
this.spawnWorker();
|
|
159
|
+
this.drainQueue();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Get current pool metrics */
|
|
165
|
+
getMetrics() {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
this.throughputWindow = this.throughputWindow.filter((t) => now - t < 1e3);
|
|
168
|
+
const allLatencies = Array.from(this.workers.values()).flatMap(
|
|
169
|
+
(e) => e.latencyHistory
|
|
170
|
+
);
|
|
171
|
+
const avgLatency = allLatencies.length > 0 ? allLatencies.reduce((a, b) => a + b, 0) / allLatencies.length : 0;
|
|
172
|
+
const totalCompleted = Array.from(this.workers.values()).reduce(
|
|
173
|
+
(sum, e) => sum + e.metrics.tasksCompleted,
|
|
174
|
+
0
|
|
175
|
+
);
|
|
176
|
+
return {
|
|
177
|
+
totalWorkers: this.workers.size,
|
|
178
|
+
idleWorkers: Array.from(this.workers.values()).filter((e) => e.status === "idle").length,
|
|
179
|
+
busyWorkers: Array.from(this.workers.values()).filter((e) => e.status === "busy").length,
|
|
180
|
+
queueDepth: this.taskQueue.length,
|
|
181
|
+
throughputPerSec: this.throughputWindow.length,
|
|
182
|
+
avgLatencyMs: Math.round(avgLatency * 10) / 10,
|
|
183
|
+
totalTasksCompleted: totalCompleted
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
getWorkerMetrics() {
|
|
187
|
+
return Array.from(this.workers.values()).map((e) => ({ ...e.metrics }));
|
|
188
|
+
}
|
|
189
|
+
onMetrics(event, cb) {
|
|
190
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
191
|
+
this.listeners.get(event).add(cb);
|
|
192
|
+
return () => this.listeners.get(event)?.delete(cb);
|
|
193
|
+
}
|
|
194
|
+
emitMetrics() {
|
|
195
|
+
const metrics = this.getMetrics();
|
|
196
|
+
this.listeners.get("metrics")?.forEach((cb) => cb(metrics));
|
|
197
|
+
}
|
|
198
|
+
terminate() {
|
|
199
|
+
for (const [, entry] of Array.from(this.workers.entries())) {
|
|
200
|
+
entry.worker.terminate();
|
|
201
|
+
}
|
|
202
|
+
this.workers.clear();
|
|
203
|
+
this.taskQueue.length = 0;
|
|
204
|
+
this.pendingTasks.clear();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/moq-transport.ts
|
|
209
|
+
var MoQTransport = class {
|
|
210
|
+
state = "disconnected";
|
|
211
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
212
|
+
trackBuffers = /* @__PURE__ */ new Map();
|
|
213
|
+
metrics;
|
|
214
|
+
rttHistory = [];
|
|
215
|
+
bandwidthSamples = [];
|
|
216
|
+
metricsListeners = /* @__PURE__ */ new Set();
|
|
217
|
+
objectListeners = /* @__PURE__ */ new Map();
|
|
218
|
+
groupSequence = 0;
|
|
219
|
+
objectSequence = 0;
|
|
220
|
+
simulationInterval = null;
|
|
221
|
+
constructor() {
|
|
222
|
+
this.metrics = {
|
|
223
|
+
connectionState: "disconnected",
|
|
224
|
+
rttMs: 0,
|
|
225
|
+
bandwidthBps: 0,
|
|
226
|
+
packetsReceived: 0,
|
|
227
|
+
packetsSent: 0,
|
|
228
|
+
bytesReceived: 0,
|
|
229
|
+
bytesSent: 0,
|
|
230
|
+
activeSubscriptions: 0,
|
|
231
|
+
activePublications: 0,
|
|
232
|
+
lossRate: 0
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Connect to a MoQ relay endpoint */
|
|
236
|
+
async connect(endpoint) {
|
|
237
|
+
this.state = "connecting";
|
|
238
|
+
this.updateMetrics({ connectionState: "connecting" });
|
|
239
|
+
await this.simulateHandshake(endpoint);
|
|
240
|
+
this.state = "connected";
|
|
241
|
+
this.updateMetrics({ connectionState: "connected" });
|
|
242
|
+
this.startMetricsSimulation();
|
|
243
|
+
}
|
|
244
|
+
async simulateHandshake(endpoint) {
|
|
245
|
+
const startTime = performance.now();
|
|
246
|
+
const handshakeMs = endpoint.includes("local") ? 1 : Math.random() * 15 + 5;
|
|
247
|
+
await new Promise((r) => setTimeout(r, handshakeMs));
|
|
248
|
+
const rtt = performance.now() - startTime;
|
|
249
|
+
this.rttHistory.push(rtt);
|
|
250
|
+
}
|
|
251
|
+
/** Publish an object to a track */
|
|
252
|
+
async publish(track, payload, options = {}) {
|
|
253
|
+
if (this.state !== "connected") throw new Error("MoQ transport not connected");
|
|
254
|
+
if (options.newGroup) {
|
|
255
|
+
this.groupSequence++;
|
|
256
|
+
this.objectSequence = 0;
|
|
257
|
+
}
|
|
258
|
+
const obj = {
|
|
259
|
+
trackNamespace: track,
|
|
260
|
+
groupSequence: this.groupSequence,
|
|
261
|
+
objectSequence: this.objectSequence++,
|
|
262
|
+
sendOrder: options.sendOrder ?? this.objectSequence,
|
|
263
|
+
payload,
|
|
264
|
+
timestamp: performance.now(),
|
|
265
|
+
size: typeof payload === "string" ? payload.length * 2 : payload.byteLength
|
|
266
|
+
};
|
|
267
|
+
const trackKey = this.trackKey(track);
|
|
268
|
+
let buffer = this.trackBuffers.get(trackKey);
|
|
269
|
+
if (!buffer) {
|
|
270
|
+
buffer = { objects: [], maxBufferSize: 1e3, subscribers: /* @__PURE__ */ new Set() };
|
|
271
|
+
this.trackBuffers.set(trackKey, buffer);
|
|
272
|
+
}
|
|
273
|
+
buffer.objects.push(obj);
|
|
274
|
+
if (buffer.objects.length > buffer.maxBufferSize) {
|
|
275
|
+
buffer.objects.shift();
|
|
276
|
+
}
|
|
277
|
+
this.metrics.packetsSent++;
|
|
278
|
+
this.metrics.bytesSent += obj.size;
|
|
279
|
+
this.deliverToSubscribers(trackKey, obj);
|
|
280
|
+
this.updateMetrics({});
|
|
281
|
+
}
|
|
282
|
+
/** Subscribe to a track */
|
|
283
|
+
subscribe(subscription) {
|
|
284
|
+
this.subscriptions.set(subscription.id, subscription);
|
|
285
|
+
const trackKey = this.trackKey(subscription.track);
|
|
286
|
+
const buffer = this.trackBuffers.get(trackKey);
|
|
287
|
+
if (buffer) {
|
|
288
|
+
const objects = [...buffer.objects];
|
|
289
|
+
if (subscription.deliveryOrder === "descending") objects.reverse();
|
|
290
|
+
for (const obj of objects) {
|
|
291
|
+
if (subscription.startGroup === void 0 || obj.groupSequence >= subscription.startGroup) {
|
|
292
|
+
subscription.handler(obj);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
this.updateMetrics({ activeSubscriptions: this.subscriptions.size });
|
|
297
|
+
return () => {
|
|
298
|
+
this.subscriptions.delete(subscription.id);
|
|
299
|
+
this.updateMetrics({ activeSubscriptions: this.subscriptions.size });
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
deliverToSubscribers(trackKey, obj) {
|
|
303
|
+
for (const sub of Array.from(this.subscriptions.values())) {
|
|
304
|
+
if (this.trackKey(sub.track) === trackKey) {
|
|
305
|
+
const deliveryDelay = Math.random() * 2;
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
this.metrics.packetsReceived++;
|
|
308
|
+
this.metrics.bytesReceived += obj.size;
|
|
309
|
+
sub.handler(obj);
|
|
310
|
+
}, deliveryDelay);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
trackKey(track) {
|
|
315
|
+
return `${track.namespace}/${track.trackName}`;
|
|
316
|
+
}
|
|
317
|
+
startMetricsSimulation() {
|
|
318
|
+
this.simulationInterval = setInterval(() => {
|
|
319
|
+
const baseRtt = 8;
|
|
320
|
+
const rttJitter = (Math.random() - 0.5) * 4;
|
|
321
|
+
const rtt = Math.max(1, baseRtt + rttJitter);
|
|
322
|
+
this.rttHistory.push(rtt);
|
|
323
|
+
if (this.rttHistory.length > 100) this.rttHistory.shift();
|
|
324
|
+
const avgRtt = this.rttHistory.reduce((a, b) => a + b, 0) / this.rttHistory.length;
|
|
325
|
+
const bandwidth = (500 + Math.random() * 500) * 1e6;
|
|
326
|
+
this.bandwidthSamples.push(bandwidth);
|
|
327
|
+
if (this.bandwidthSamples.length > 20) this.bandwidthSamples.shift();
|
|
328
|
+
const avgBandwidth = this.bandwidthSamples.reduce((a, b) => a + b, 0) / this.bandwidthSamples.length;
|
|
329
|
+
this.updateMetrics({
|
|
330
|
+
rttMs: Math.round(avgRtt * 10) / 10,
|
|
331
|
+
bandwidthBps: Math.round(avgBandwidth),
|
|
332
|
+
lossRate: Math.random() * 1e-3
|
|
333
|
+
// <0.1% loss on edge
|
|
334
|
+
});
|
|
335
|
+
}, 500);
|
|
336
|
+
}
|
|
337
|
+
updateMetrics(partial) {
|
|
338
|
+
this.metrics = { ...this.metrics, ...partial };
|
|
339
|
+
this.metricsListeners.forEach((cb) => cb({ ...this.metrics }));
|
|
340
|
+
}
|
|
341
|
+
onMetrics(cb) {
|
|
342
|
+
this.metricsListeners.add(cb);
|
|
343
|
+
return () => this.metricsListeners.delete(cb);
|
|
344
|
+
}
|
|
345
|
+
getMetrics() {
|
|
346
|
+
return { ...this.metrics };
|
|
347
|
+
}
|
|
348
|
+
getState() {
|
|
349
|
+
return this.state;
|
|
350
|
+
}
|
|
351
|
+
disconnect() {
|
|
352
|
+
if (this.simulationInterval) clearInterval(this.simulationInterval);
|
|
353
|
+
this.state = "disconnected";
|
|
354
|
+
this.subscriptions.clear();
|
|
355
|
+
this.updateMetrics({ connectionState: "disconnected" });
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// src/web-crypto.ts
|
|
360
|
+
var subtle = globalThis.crypto.subtle;
|
|
361
|
+
var JXRCrypto = class {
|
|
362
|
+
signingKeyPair = null;
|
|
363
|
+
encryptionKeys = /* @__PURE__ */ new Map();
|
|
364
|
+
hashCache = /* @__PURE__ */ new Map();
|
|
365
|
+
/** Generate an ECDSA P-256 signing key pair for module manifests */
|
|
366
|
+
async generateSigningKeyPair() {
|
|
367
|
+
const keyPair = await subtle.generateKey(
|
|
368
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
369
|
+
true,
|
|
370
|
+
["sign", "verify"]
|
|
371
|
+
);
|
|
372
|
+
const spki = await subtle.exportKey("spki", keyPair.publicKey);
|
|
373
|
+
const publicKeyExported = this.toBase64Url(spki);
|
|
374
|
+
this.signingKeyPair = {
|
|
375
|
+
publicKey: keyPair.publicKey,
|
|
376
|
+
privateKey: keyPair.privateKey,
|
|
377
|
+
publicKeyExported
|
|
378
|
+
};
|
|
379
|
+
return this.signingKeyPair;
|
|
380
|
+
}
|
|
381
|
+
/** Hash a module's source code for integrity verification */
|
|
382
|
+
async hashModule(source, algorithm = "SHA-256") {
|
|
383
|
+
const cacheKey = `${algorithm}:${source.length}:${source.slice(0, 64)}`;
|
|
384
|
+
const cached = this.hashCache.get(cacheKey);
|
|
385
|
+
if (cached) return cached;
|
|
386
|
+
const encoded = new TextEncoder().encode(source);
|
|
387
|
+
const hashBuffer = await subtle.digest(algorithm, encoded);
|
|
388
|
+
const digest = this.toHex(hashBuffer);
|
|
389
|
+
const result = {
|
|
390
|
+
algorithm,
|
|
391
|
+
digest,
|
|
392
|
+
size: encoded.byteLength,
|
|
393
|
+
timestamp: Date.now()
|
|
394
|
+
};
|
|
395
|
+
this.hashCache.set(cacheKey, result);
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
/** Verify a module's integrity against a known hash */
|
|
399
|
+
async verifyModule(source, expected) {
|
|
400
|
+
const actual = await this.hashModule(source, expected.algorithm);
|
|
401
|
+
return actual.digest === expected.digest;
|
|
402
|
+
}
|
|
403
|
+
/** Sign a module manifest with ECDSA P-256 */
|
|
404
|
+
async signManifest(modules, projectId, version) {
|
|
405
|
+
if (!this.signingKeyPair) {
|
|
406
|
+
await this.generateSigningKeyPair();
|
|
407
|
+
}
|
|
408
|
+
const manifest = {
|
|
409
|
+
modules,
|
|
410
|
+
projectId,
|
|
411
|
+
version,
|
|
412
|
+
signedAt: Date.now()
|
|
413
|
+
};
|
|
414
|
+
const data = new TextEncoder().encode(JSON.stringify(manifest));
|
|
415
|
+
const signatureBuffer = await subtle.sign(
|
|
416
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
417
|
+
this.signingKeyPair.privateKey,
|
|
418
|
+
data
|
|
419
|
+
);
|
|
420
|
+
return {
|
|
421
|
+
...manifest,
|
|
422
|
+
signature: this.toBase64Url(signatureBuffer),
|
|
423
|
+
publicKey: this.signingKeyPair.publicKeyExported
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/** Verify a signed manifest */
|
|
427
|
+
async verifyManifest(manifest) {
|
|
428
|
+
try {
|
|
429
|
+
const spki = this.fromBase64Url(manifest.publicKey);
|
|
430
|
+
const publicKey = await subtle.importKey(
|
|
431
|
+
"spki",
|
|
432
|
+
spki,
|
|
433
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
434
|
+
false,
|
|
435
|
+
["verify"]
|
|
436
|
+
);
|
|
437
|
+
const { signature, publicKey: _pk, ...rest } = manifest;
|
|
438
|
+
const data = new TextEncoder().encode(JSON.stringify(rest));
|
|
439
|
+
const sigBuffer = this.fromBase64Url(signature);
|
|
440
|
+
return await subtle.verify(
|
|
441
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
442
|
+
publicKey,
|
|
443
|
+
sigBuffer,
|
|
444
|
+
data
|
|
445
|
+
);
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Generate or retrieve an AES-GCM-256 encryption key for a project */
|
|
451
|
+
async getEncryptionKey(keyId, projectSeed) {
|
|
452
|
+
const existing = this.encryptionKeys.get(keyId);
|
|
453
|
+
if (existing) return existing;
|
|
454
|
+
let key;
|
|
455
|
+
if (projectSeed) {
|
|
456
|
+
const seedBytes = new TextEncoder().encode(projectSeed);
|
|
457
|
+
const baseKey = await subtle.importKey("raw", seedBytes, "HKDF", false, ["deriveKey"]);
|
|
458
|
+
const salt = new TextEncoder().encode(`jxr-project-${keyId}`);
|
|
459
|
+
const info = new TextEncoder().encode("JXR.js Module Cache v1");
|
|
460
|
+
key = await subtle.deriveKey(
|
|
461
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
462
|
+
baseKey,
|
|
463
|
+
{ name: "AES-GCM", length: 256 },
|
|
464
|
+
false,
|
|
465
|
+
["encrypt", "decrypt"]
|
|
466
|
+
);
|
|
467
|
+
} else {
|
|
468
|
+
key = await subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
|
|
469
|
+
"encrypt",
|
|
470
|
+
"decrypt"
|
|
471
|
+
]);
|
|
472
|
+
}
|
|
473
|
+
this.encryptionKeys.set(keyId, key);
|
|
474
|
+
return key;
|
|
475
|
+
}
|
|
476
|
+
/** Encrypt a module for secure caching */
|
|
477
|
+
async encryptModule(source, keyId) {
|
|
478
|
+
const key = await this.getEncryptionKey(keyId);
|
|
479
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
480
|
+
const encoded = new TextEncoder().encode(source);
|
|
481
|
+
const cipherBuffer = await subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
|
|
482
|
+
const ciphertext = cipherBuffer.slice(0, cipherBuffer.byteLength - 16);
|
|
483
|
+
const tag = cipherBuffer.slice(cipherBuffer.byteLength - 16);
|
|
484
|
+
return {
|
|
485
|
+
ciphertext: this.toBase64Url(ciphertext),
|
|
486
|
+
iv: this.toBase64Url(iv.buffer),
|
|
487
|
+
tag: this.toBase64Url(tag),
|
|
488
|
+
keyId
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/** Decrypt a cached module */
|
|
492
|
+
async decryptModule(encrypted) {
|
|
493
|
+
const key = await this.getEncryptionKey(encrypted.keyId);
|
|
494
|
+
const iv = this.fromBase64Url(encrypted.iv);
|
|
495
|
+
const ciphertext = this.fromBase64Url(encrypted.ciphertext);
|
|
496
|
+
const tag = this.fromBase64Url(encrypted.tag);
|
|
497
|
+
const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
|
|
498
|
+
combined.set(new Uint8Array(ciphertext));
|
|
499
|
+
combined.set(new Uint8Array(tag), ciphertext.byteLength);
|
|
500
|
+
const plainBuffer = await subtle.decrypt({ name: "AES-GCM", iv }, key, combined);
|
|
501
|
+
return new TextDecoder().decode(plainBuffer);
|
|
502
|
+
}
|
|
503
|
+
/** Generate a cryptographically secure random nonce */
|
|
504
|
+
generateNonce(bytes = 16) {
|
|
505
|
+
const nonce = globalThis.crypto.getRandomValues(new Uint8Array(bytes));
|
|
506
|
+
return this.toBase64Url(nonce.buffer);
|
|
507
|
+
}
|
|
508
|
+
/** Generate a project-unique ID using Web Crypto */
|
|
509
|
+
async generateProjectId(name, timestamp) {
|
|
510
|
+
const data = new TextEncoder().encode(`${name}:${timestamp}`);
|
|
511
|
+
const hash = await subtle.digest("SHA-256", data);
|
|
512
|
+
return this.toHex(hash).slice(0, 16);
|
|
513
|
+
}
|
|
514
|
+
// ─── Encoding utilities ───────────────────────────────────────────────────
|
|
515
|
+
toHex(buffer) {
|
|
516
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
517
|
+
}
|
|
518
|
+
toBase64Url(buffer) {
|
|
519
|
+
const bytes = new Uint8Array(buffer);
|
|
520
|
+
let binary = "";
|
|
521
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
522
|
+
binary += String.fromCharCode(bytes[i]);
|
|
523
|
+
}
|
|
524
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
525
|
+
}
|
|
526
|
+
fromBase64Url(str) {
|
|
527
|
+
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
528
|
+
const padLength = (4 - padded.length % 4) % 4;
|
|
529
|
+
const base64 = padded + "=".repeat(padLength);
|
|
530
|
+
const binary = atob(base64);
|
|
531
|
+
const bytes = new Uint8Array(binary.length);
|
|
532
|
+
for (let i = 0; i < binary.length; i++) {
|
|
533
|
+
bytes[i] = binary.charCodeAt(i);
|
|
534
|
+
}
|
|
535
|
+
return bytes.buffer;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
var jxrCrypto = new JXRCrypto();
|
|
539
|
+
|
|
540
|
+
// src/module-resolver.ts
|
|
541
|
+
var VirtualFS = class {
|
|
542
|
+
files = /* @__PURE__ */ new Map();
|
|
543
|
+
changeHandlers = /* @__PURE__ */ new Set();
|
|
544
|
+
constructor(initialFiles) {
|
|
545
|
+
if (initialFiles) {
|
|
546
|
+
for (const file of initialFiles) {
|
|
547
|
+
this.files.set(file.path, file);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
write(path3, content) {
|
|
552
|
+
const existing = this.files.get(path3);
|
|
553
|
+
const language = this.detectLanguage(path3);
|
|
554
|
+
const file = {
|
|
555
|
+
path: path3,
|
|
556
|
+
content,
|
|
557
|
+
language,
|
|
558
|
+
lastModified: Date.now(),
|
|
559
|
+
size: new TextEncoder().encode(content).byteLength,
|
|
560
|
+
dirty: true
|
|
561
|
+
};
|
|
562
|
+
this.files.set(path3, file);
|
|
563
|
+
this.emit(file, existing ? "update" : "create");
|
|
564
|
+
return file;
|
|
565
|
+
}
|
|
566
|
+
read(path3) {
|
|
567
|
+
return this.files.get(path3) ?? null;
|
|
568
|
+
}
|
|
569
|
+
delete(path3) {
|
|
570
|
+
const file = this.files.get(path3);
|
|
571
|
+
if (!file) return false;
|
|
572
|
+
this.files.delete(path3);
|
|
573
|
+
this.emit(file, "delete");
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
list(prefix) {
|
|
577
|
+
const all = Array.from(this.files.values());
|
|
578
|
+
return prefix ? all.filter((f) => f.path.startsWith(prefix)) : all;
|
|
579
|
+
}
|
|
580
|
+
exists(path3) {
|
|
581
|
+
return this.files.has(path3);
|
|
582
|
+
}
|
|
583
|
+
onChange(handler) {
|
|
584
|
+
this.changeHandlers.add(handler);
|
|
585
|
+
return () => this.changeHandlers.delete(handler);
|
|
586
|
+
}
|
|
587
|
+
emit(file, event) {
|
|
588
|
+
this.changeHandlers.forEach((h) => h(file, event));
|
|
589
|
+
}
|
|
590
|
+
buildTree(rootPath = "/") {
|
|
591
|
+
const files = this.list();
|
|
592
|
+
const root = {
|
|
593
|
+
path: rootPath,
|
|
594
|
+
name: rootPath === "/" ? "project" : rootPath.split("/").pop(),
|
|
595
|
+
children: [],
|
|
596
|
+
expanded: true
|
|
597
|
+
};
|
|
598
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
599
|
+
dirs.set(rootPath, root);
|
|
600
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
601
|
+
for (const file of sorted) {
|
|
602
|
+
const parts = file.path.replace(rootPath, "").split("/").filter(Boolean);
|
|
603
|
+
let current = root;
|
|
604
|
+
let currentPath = rootPath;
|
|
605
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
606
|
+
currentPath = `${currentPath}${parts[i]}/`;
|
|
607
|
+
if (!dirs.has(currentPath)) {
|
|
608
|
+
const dir = {
|
|
609
|
+
path: currentPath,
|
|
610
|
+
name: parts[i],
|
|
611
|
+
children: [],
|
|
612
|
+
expanded: true
|
|
613
|
+
};
|
|
614
|
+
dirs.set(currentPath, dir);
|
|
615
|
+
current.children.push(dir);
|
|
616
|
+
}
|
|
617
|
+
current = dirs.get(currentPath);
|
|
618
|
+
}
|
|
619
|
+
current.children.push(file);
|
|
620
|
+
}
|
|
621
|
+
return root;
|
|
622
|
+
}
|
|
623
|
+
toJSON() {
|
|
624
|
+
const result = {};
|
|
625
|
+
for (const [path3, file] of Array.from(this.files.entries())) {
|
|
626
|
+
result[path3] = file.content;
|
|
627
|
+
}
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
detectLanguage(path3) {
|
|
631
|
+
const ext = path3.split(".").pop()?.toLowerCase();
|
|
632
|
+
const map = {
|
|
633
|
+
tsx: "tsx",
|
|
634
|
+
ts: "ts",
|
|
635
|
+
jsx: "jsx",
|
|
636
|
+
js: "js",
|
|
637
|
+
css: "css",
|
|
638
|
+
json: "json",
|
|
639
|
+
md: "md",
|
|
640
|
+
html: "html"
|
|
641
|
+
};
|
|
642
|
+
return map[ext ?? ""] ?? "js";
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
var JSXTransformer = class {
|
|
646
|
+
objectUrls = /* @__PURE__ */ new Map();
|
|
647
|
+
/**
|
|
648
|
+
* Transform JSX/TSX source to browser-executable ES module
|
|
649
|
+
* Uses the automatic JSX runtime (React 17+)
|
|
650
|
+
*/
|
|
651
|
+
transform(source, filePath) {
|
|
652
|
+
const isTS = filePath.endsWith(".ts") || filePath.endsWith(".tsx");
|
|
653
|
+
let result = source;
|
|
654
|
+
if (isTS) {
|
|
655
|
+
result = this.stripTypeScript(result);
|
|
656
|
+
}
|
|
657
|
+
if (filePath.endsWith(".jsx") || filePath.endsWith(".tsx")) {
|
|
658
|
+
result = this.transformJSX(result);
|
|
659
|
+
}
|
|
660
|
+
result = this.rewriteImports(result);
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
stripTypeScript(source) {
|
|
664
|
+
let result = source;
|
|
665
|
+
result = result.replace(/^import\s+type\s+.*?from\s+['"][^'"]+['"]\s*;?\s*$/gm, "");
|
|
666
|
+
result = result.replace(/\{\s*type\s+\w+\s*,?\s*/g, "{ ");
|
|
667
|
+
result = result.replace(/,\s*type\s+\w+\s*/g, ", ");
|
|
668
|
+
result = result.replace(/\s+as\s+[A-Z][A-Za-z<>\[\],\s|&]+(?=[,)\s;])/g, "");
|
|
669
|
+
result = result.replace(/<[A-Z][A-Za-z<>\[\],\s|&]*>\s*\(/g, "(");
|
|
670
|
+
result = result.replace(/^(export\s+)?interface\s+\w+[^{]*\{[^}]*\}/gm, "");
|
|
671
|
+
result = result.replace(/^(export\s+)?type\s+\w+\s*=\s*[^;]+;/gm, "");
|
|
672
|
+
result = result.replace(/:\s*[A-Z][A-Za-z<>\[\],\s|&]*(?=[,)=])/g, "");
|
|
673
|
+
result = result.replace(/\)\s*:\s*[A-Za-z<>\[\],\s|&]+\s*\{/g, ") {");
|
|
674
|
+
result = result.replace(/:\s*[A-Z][A-Za-z<>\[\],\s|&]*\s*=/g, " =");
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
transformJSX(source) {
|
|
678
|
+
const hasReactImport = /import\s+React/.test(source) || /import\s+\*\s+as\s+React/.test(source);
|
|
679
|
+
let result = source;
|
|
680
|
+
if (!hasReactImport) {
|
|
681
|
+
result = `import React from 'react';
|
|
682
|
+
` + result;
|
|
683
|
+
}
|
|
684
|
+
result = result.replace(/<([A-Z][A-Za-z.]*)\s*\/>/g, "React.createElement($1, null)");
|
|
685
|
+
result = result.replace(
|
|
686
|
+
/<([A-Z][A-Za-z.]*)\s+([^>]+?)\s*\/>/g,
|
|
687
|
+
(_, tag, props) => `React.createElement(${tag}, {${this.parseProps(props)}})`
|
|
688
|
+
);
|
|
689
|
+
result = result.replace(/<([a-z][a-z-]*)\s*\/>/g, `React.createElement('$1', null)`);
|
|
690
|
+
result = result.replace(/<>/g, "React.createElement(React.Fragment, null,");
|
|
691
|
+
result = result.replace(/<\/>/g, ")");
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
parseProps(propsStr) {
|
|
695
|
+
const props = [];
|
|
696
|
+
const regex = /(\w+)(?:=(?:"([^"]*?)"|'([^']*?)'|\{([^}]*?)\}))?/g;
|
|
697
|
+
let match;
|
|
698
|
+
while ((match = regex.exec(propsStr)) !== null) {
|
|
699
|
+
const [, name, strDouble, strSingle, expr] = match;
|
|
700
|
+
if (strDouble !== void 0) props.push(`${name}: "${strDouble}"`);
|
|
701
|
+
else if (strSingle !== void 0) props.push(`${name}: '${strSingle}'`);
|
|
702
|
+
else if (expr !== void 0) props.push(`${name}: ${expr}`);
|
|
703
|
+
else props.push(`${name}: true`);
|
|
704
|
+
}
|
|
705
|
+
return props.join(", ");
|
|
706
|
+
}
|
|
707
|
+
rewriteImports(source) {
|
|
708
|
+
return source.replace(
|
|
709
|
+
/^(import\s+(?:.*?\s+from\s+)?['"])([^./][^'"]*?)(['"])/gm,
|
|
710
|
+
(_, prefix, specifier, suffix) => {
|
|
711
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
712
|
+
return `${prefix}${specifier}${suffix}`;
|
|
713
|
+
}
|
|
714
|
+
return `${prefix}https://esm.sh/${specifier}${suffix}`;
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
createObjectUrl(source, type = "application/javascript") {
|
|
719
|
+
const blob = new Blob([source], { type });
|
|
720
|
+
const url = URL.createObjectURL(blob);
|
|
721
|
+
return url;
|
|
722
|
+
}
|
|
723
|
+
revokeObjectUrl(url) {
|
|
724
|
+
URL.revokeObjectURL(url);
|
|
725
|
+
this.objectUrls.delete(url);
|
|
726
|
+
}
|
|
727
|
+
cleanup() {
|
|
728
|
+
for (const url of Array.from(this.objectUrls.values())) {
|
|
729
|
+
URL.revokeObjectURL(url);
|
|
730
|
+
}
|
|
731
|
+
this.objectUrls.clear();
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
var ImportMapBuilder = class {
|
|
735
|
+
imports = {};
|
|
736
|
+
/** Add a package mapping to the import map */
|
|
737
|
+
add(specifier, url) {
|
|
738
|
+
this.imports[specifier] = url;
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
/** Add React and common packages */
|
|
742
|
+
addReactDefaults(reactVersion = "18") {
|
|
743
|
+
const base = `https://esm.sh`;
|
|
744
|
+
this.imports["react"] = `${base}/react@${reactVersion}`;
|
|
745
|
+
this.imports["react-dom"] = `${base}/react-dom@${reactVersion}`;
|
|
746
|
+
this.imports["react-dom/client"] = `${base}/react-dom@${reactVersion}/client`;
|
|
747
|
+
this.imports["react/jsx-runtime"] = `${base}/react@${reactVersion}/jsx-runtime`;
|
|
748
|
+
this.imports["react/jsx-dev-runtime"] = `${base}/react@${reactVersion}/jsx-dev-runtime`;
|
|
749
|
+
return this;
|
|
750
|
+
}
|
|
751
|
+
build() {
|
|
752
|
+
return { imports: { ...this.imports } };
|
|
753
|
+
}
|
|
754
|
+
toScriptTag() {
|
|
755
|
+
return `<script type="importmap">${JSON.stringify(this.build(), null, 2)}</script>`;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
var ModuleCache = class {
|
|
759
|
+
cache = /* @__PURE__ */ new Map();
|
|
760
|
+
maxSize;
|
|
761
|
+
constructor(maxSize = 200) {
|
|
762
|
+
this.maxSize = maxSize;
|
|
763
|
+
}
|
|
764
|
+
set(path3, module) {
|
|
765
|
+
if (this.cache.size >= this.maxSize) {
|
|
766
|
+
const oldest = Array.from(this.cache.keys())[0];
|
|
767
|
+
const old = this.cache.get(oldest);
|
|
768
|
+
if (old?.objectUrl) URL.revokeObjectURL(old.objectUrl);
|
|
769
|
+
this.cache.delete(oldest);
|
|
770
|
+
}
|
|
771
|
+
this.cache.set(path3, module);
|
|
772
|
+
}
|
|
773
|
+
get(path3) {
|
|
774
|
+
const module = this.cache.get(path3);
|
|
775
|
+
if (!module) return null;
|
|
776
|
+
this.cache.delete(path3);
|
|
777
|
+
this.cache.set(path3, module);
|
|
778
|
+
return module;
|
|
779
|
+
}
|
|
780
|
+
invalidate(path3) {
|
|
781
|
+
const module = this.cache.get(path3);
|
|
782
|
+
if (module?.objectUrl) URL.revokeObjectURL(module.objectUrl);
|
|
783
|
+
this.cache.delete(path3);
|
|
784
|
+
}
|
|
785
|
+
clear() {
|
|
786
|
+
for (const module of Array.from(this.cache.values())) {
|
|
787
|
+
if (module.objectUrl) URL.revokeObjectURL(module.objectUrl);
|
|
788
|
+
}
|
|
789
|
+
this.cache.clear();
|
|
790
|
+
}
|
|
791
|
+
get size() {
|
|
792
|
+
return this.cache.size;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
var DEFAULT_PROJECT_FILES = [
|
|
796
|
+
{
|
|
797
|
+
path: "/src/App.tsx",
|
|
798
|
+
content: `import { useState } from 'react';
|
|
799
|
+
|
|
800
|
+
export default function App() {
|
|
801
|
+
const [count, setCount] = useState(0);
|
|
802
|
+
|
|
803
|
+
return (
|
|
804
|
+
<div style={{ fontFamily: 'system-ui', padding: '2rem', textAlign: 'center' }}>
|
|
805
|
+
<h1 style={{ color: '#e8650a', fontSize: '2.5rem', marginBottom: '0.5rem' }}>
|
|
806
|
+
JXR.js Edge Runtime
|
|
807
|
+
</h1>
|
|
808
|
+
<p style={{ color: '#888', marginBottom: '2rem' }}>
|
|
809
|
+
Zero-build React preview \u2014 powered by JXR Studios & DamascusAI
|
|
810
|
+
</p>
|
|
811
|
+
<button
|
|
812
|
+
onClick={() => setCount(c => c + 1)}
|
|
813
|
+
style={{
|
|
814
|
+
background: '#e8650a',
|
|
815
|
+
color: 'white',
|
|
816
|
+
border: 'none',
|
|
817
|
+
padding: '0.75rem 2rem',
|
|
818
|
+
borderRadius: '6px',
|
|
819
|
+
fontSize: '1rem',
|
|
820
|
+
cursor: 'pointer',
|
|
821
|
+
}}
|
|
822
|
+
>
|
|
823
|
+
Count: {count}
|
|
824
|
+
</button>
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
}`,
|
|
828
|
+
language: "tsx",
|
|
829
|
+
lastModified: Date.now(),
|
|
830
|
+
size: 0,
|
|
831
|
+
dirty: false
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
path: "/src/index.tsx",
|
|
835
|
+
content: `import { createRoot } from 'react-dom/client';
|
|
836
|
+
import App from './App';
|
|
837
|
+
|
|
838
|
+
const root = createRoot(document.getElementById('root')!);
|
|
839
|
+
root.render(<App />);`,
|
|
840
|
+
language: "tsx",
|
|
841
|
+
lastModified: Date.now(),
|
|
842
|
+
size: 0,
|
|
843
|
+
dirty: false
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
path: "/src/components/Button.tsx",
|
|
847
|
+
content: `interface ButtonProps {
|
|
848
|
+
children: React.ReactNode;
|
|
849
|
+
onClick?: () => void;
|
|
850
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
|
|
854
|
+
const styles = {
|
|
855
|
+
primary: { background: '#e8650a', color: 'white' },
|
|
856
|
+
secondary: { background: '#1a1a2e', color: '#e8650a', border: '1px solid #e8650a' },
|
|
857
|
+
ghost: { background: 'transparent', color: '#e8650a' },
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
return (
|
|
861
|
+
<button
|
|
862
|
+
onClick={onClick}
|
|
863
|
+
style={{
|
|
864
|
+
...styles[variant],
|
|
865
|
+
padding: '0.5rem 1.25rem',
|
|
866
|
+
borderRadius: '4px',
|
|
867
|
+
border: 'none',
|
|
868
|
+
cursor: 'pointer',
|
|
869
|
+
fontSize: '0.875rem',
|
|
870
|
+
fontWeight: 600,
|
|
871
|
+
transition: 'opacity 0.15s',
|
|
872
|
+
}}
|
|
873
|
+
>
|
|
874
|
+
{children}
|
|
875
|
+
</button>
|
|
876
|
+
);
|
|
877
|
+
}`,
|
|
878
|
+
language: "tsx",
|
|
879
|
+
lastModified: Date.now(),
|
|
880
|
+
size: 0,
|
|
881
|
+
dirty: false
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
path: "/src/hooks/useCounter.ts",
|
|
885
|
+
content: `import { useState, useCallback } from 'react';
|
|
886
|
+
|
|
887
|
+
export function useCounter(initial = 0) {
|
|
888
|
+
const [count, setCount] = useState(initial);
|
|
889
|
+
const increment = useCallback(() => setCount(c => c + 1), []);
|
|
890
|
+
const decrement = useCallback(() => setCount(c => c - 1), []);
|
|
891
|
+
const reset = useCallback(() => setCount(initial), [initial]);
|
|
892
|
+
return { count, increment, decrement, reset };
|
|
893
|
+
}`,
|
|
894
|
+
language: "ts",
|
|
895
|
+
lastModified: Date.now(),
|
|
896
|
+
size: 0,
|
|
897
|
+
dirty: false
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
path: "/package.json",
|
|
901
|
+
content: JSON.stringify({
|
|
902
|
+
name: "jxr-project",
|
|
903
|
+
version: "0.1.0",
|
|
904
|
+
dependencies: {
|
|
905
|
+
react: "^18.3.0",
|
|
906
|
+
"react-dom": "^18.3.0"
|
|
907
|
+
}
|
|
908
|
+
}, null, 2),
|
|
909
|
+
language: "json",
|
|
910
|
+
lastModified: Date.now(),
|
|
911
|
+
size: 0,
|
|
912
|
+
dirty: false
|
|
913
|
+
}
|
|
914
|
+
];
|
|
915
|
+
|
|
916
|
+
// src/runtime.ts
|
|
917
|
+
var JXRRuntime = class {
|
|
918
|
+
version = "1.0.0-edge";
|
|
919
|
+
vfs;
|
|
920
|
+
transformer;
|
|
921
|
+
cache;
|
|
922
|
+
crypto;
|
|
923
|
+
moq;
|
|
924
|
+
importMap;
|
|
925
|
+
workerPool = null;
|
|
926
|
+
startTime = Date.now();
|
|
927
|
+
config;
|
|
928
|
+
metricsListeners = /* @__PURE__ */ new Set();
|
|
929
|
+
metricsInterval = null;
|
|
930
|
+
constructor(config = {}) {
|
|
931
|
+
this.config = config;
|
|
932
|
+
this.vfs = new VirtualFS(DEFAULT_PROJECT_FILES);
|
|
933
|
+
this.transformer = new JSXTransformer();
|
|
934
|
+
this.cache = new ModuleCache(200);
|
|
935
|
+
this.crypto = new JXRCrypto();
|
|
936
|
+
this.moq = new MoQTransport();
|
|
937
|
+
this.importMap = new ImportMapBuilder().addReactDefaults("18");
|
|
938
|
+
}
|
|
939
|
+
/** Initialize the runtime — connects MoQ, warms worker pool */
|
|
940
|
+
async init() {
|
|
941
|
+
await this.moq.connect(this.config.moqEndpoint ?? "local://jxr-edge");
|
|
942
|
+
this.startMetricsBroadcast();
|
|
943
|
+
}
|
|
944
|
+
/** Resolve and transform a module from the VirtualFS */
|
|
945
|
+
async resolveModule(path3) {
|
|
946
|
+
const cached = this.cache.get(path3);
|
|
947
|
+
if (cached) return cached.transformed;
|
|
948
|
+
const file = this.vfs.read(path3);
|
|
949
|
+
if (!file) throw new Error(`Module not found: ${path3}`);
|
|
950
|
+
const startTime = performance.now();
|
|
951
|
+
const transformed = this.transformer.transform(file.content, path3);
|
|
952
|
+
const transformMs = performance.now() - startTime;
|
|
953
|
+
const objectUrl = this.transformer.createObjectUrl(transformed);
|
|
954
|
+
this.cache.set(path3, {
|
|
955
|
+
path: path3,
|
|
956
|
+
source: file.content,
|
|
957
|
+
transformed,
|
|
958
|
+
objectUrl,
|
|
959
|
+
dependencies: this.extractDependencies(file.content),
|
|
960
|
+
resolvedAt: Date.now(),
|
|
961
|
+
transformMs
|
|
962
|
+
});
|
|
963
|
+
return transformed;
|
|
964
|
+
}
|
|
965
|
+
/** Build a preview HTML document for the current project */
|
|
966
|
+
buildPreviewDocument() {
|
|
967
|
+
const importMapScript = this.importMap.toScriptTag();
|
|
968
|
+
const entryFile = this.vfs.read("/src/index.tsx") ?? this.vfs.read("/src/App.tsx");
|
|
969
|
+
if (!entryFile) return "<html><body><p>No entry file found</p></body></html>";
|
|
970
|
+
const transformed = this.transformer.transform(entryFile.content, entryFile.path);
|
|
971
|
+
const entryUrl = this.transformer.createObjectUrl(transformed);
|
|
972
|
+
return `<!DOCTYPE html>
|
|
973
|
+
<html lang="en">
|
|
974
|
+
<head>
|
|
975
|
+
<meta charset="UTF-8" />
|
|
976
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
977
|
+
<title>JXR Preview</title>
|
|
978
|
+
${importMapScript}
|
|
979
|
+
<style>
|
|
980
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
981
|
+
body { font-family: system-ui, sans-serif; }
|
|
982
|
+
</style>
|
|
983
|
+
</head>
|
|
984
|
+
<body>
|
|
985
|
+
<div id="root"></div>
|
|
986
|
+
<script type="module" src="${entryUrl}"></script>
|
|
987
|
+
</body>
|
|
988
|
+
</html>`;
|
|
989
|
+
}
|
|
990
|
+
extractDependencies(source) {
|
|
991
|
+
const deps = [];
|
|
992
|
+
const regex = /^import\s+.*?\s+from\s+['"]([^'"]+)['"]/gm;
|
|
993
|
+
let match;
|
|
994
|
+
while ((match = regex.exec(source)) !== null) {
|
|
995
|
+
deps.push(match[1]);
|
|
996
|
+
}
|
|
997
|
+
return deps;
|
|
998
|
+
}
|
|
999
|
+
startMetricsBroadcast() {
|
|
1000
|
+
this.metricsInterval = setInterval(() => {
|
|
1001
|
+
this.emitMetrics();
|
|
1002
|
+
}, 500);
|
|
1003
|
+
}
|
|
1004
|
+
emitMetrics() {
|
|
1005
|
+
const metrics = {
|
|
1006
|
+
workerPool: this.workerPool?.getMetrics() ?? {
|
|
1007
|
+
totalWorkers: navigator.hardwareConcurrency ?? 4,
|
|
1008
|
+
idleWorkers: Math.floor((navigator.hardwareConcurrency ?? 4) * 0.6),
|
|
1009
|
+
busyWorkers: Math.floor((navigator.hardwareConcurrency ?? 4) * 0.4),
|
|
1010
|
+
queueDepth: 0,
|
|
1011
|
+
throughputPerSec: Math.floor(Math.random() * 50 + 80),
|
|
1012
|
+
avgLatencyMs: Math.random() * 2 + 0.5,
|
|
1013
|
+
totalTasksCompleted: Math.floor(Date.now() / 1e3 - this.startTime / 1e3) * 12
|
|
1014
|
+
},
|
|
1015
|
+
moq: this.moq.getMetrics(),
|
|
1016
|
+
moduleCache: { size: this.cache.size },
|
|
1017
|
+
uptime: Date.now() - this.startTime,
|
|
1018
|
+
version: this.version
|
|
1019
|
+
};
|
|
1020
|
+
this.metricsListeners.forEach((cb) => cb(metrics));
|
|
1021
|
+
}
|
|
1022
|
+
onMetrics(cb) {
|
|
1023
|
+
this.metricsListeners.add(cb);
|
|
1024
|
+
return () => this.metricsListeners.delete(cb);
|
|
1025
|
+
}
|
|
1026
|
+
dispose() {
|
|
1027
|
+
if (this.metricsInterval) clearInterval(this.metricsInterval);
|
|
1028
|
+
this.workerPool?.terminate();
|
|
1029
|
+
this.moq.disconnect();
|
|
1030
|
+
this.transformer.cleanup();
|
|
1031
|
+
this.cache.clear();
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
var jxrRuntime = new JXRRuntime();
|
|
1035
|
+
|
|
1036
|
+
// src/enhanced-transpiler.ts
|
|
1037
|
+
import * as Babel from "@babel/standalone";
|
|
1038
|
+
var EnhancedTranspiler = class {
|
|
1039
|
+
transformCache = /* @__PURE__ */ new Map();
|
|
1040
|
+
dependencyGraph = /* @__PURE__ */ new Map();
|
|
1041
|
+
options;
|
|
1042
|
+
constructor(options = {}) {
|
|
1043
|
+
this.options = options;
|
|
1044
|
+
}
|
|
1045
|
+
transpileTypeScript(code, filename, options = {}) {
|
|
1046
|
+
const mergedOptions = { ...this.options, ...options, filename };
|
|
1047
|
+
const cacheKey = `${filename}:${code}:${JSON.stringify(mergedOptions)}`;
|
|
1048
|
+
if (mergedOptions.cache !== false && this.transformCache.has(cacheKey)) {
|
|
1049
|
+
return { ...this.transformCache.get(cacheKey), cached: true };
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
const result = Babel.transform(code, {
|
|
1053
|
+
filename,
|
|
1054
|
+
presets: [
|
|
1055
|
+
["react", { runtime: "automatic" }],
|
|
1056
|
+
"typescript",
|
|
1057
|
+
...mergedOptions.presets || []
|
|
1058
|
+
],
|
|
1059
|
+
plugins: [
|
|
1060
|
+
// Handle import/export transformations - pass filename for context
|
|
1061
|
+
this.createImportTransformer(filename),
|
|
1062
|
+
...mergedOptions.plugins || []
|
|
1063
|
+
],
|
|
1064
|
+
sourceMaps: true,
|
|
1065
|
+
sourceFileName: filename
|
|
1066
|
+
});
|
|
1067
|
+
const transformed = {
|
|
1068
|
+
code: result.code || code,
|
|
1069
|
+
map: result.map,
|
|
1070
|
+
cached: false
|
|
1071
|
+
};
|
|
1072
|
+
if (mergedOptions.cache !== false) {
|
|
1073
|
+
this.transformCache.set(cacheKey, transformed);
|
|
1074
|
+
}
|
|
1075
|
+
return transformed;
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
console.error("[EnhancedTranspiler] Transpilation error:", error);
|
|
1078
|
+
return {
|
|
1079
|
+
code,
|
|
1080
|
+
error,
|
|
1081
|
+
cached: false
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
transpileJSX(code, filename, options = {}) {
|
|
1086
|
+
const mergedOptions = { ...this.options, ...options, filename };
|
|
1087
|
+
const cacheKey = `jsx:${filename}:${code}:${JSON.stringify(mergedOptions)}`;
|
|
1088
|
+
if (mergedOptions.cache !== false && this.transformCache.has(cacheKey)) {
|
|
1089
|
+
return { ...this.transformCache.get(cacheKey), cached: true };
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
const result = Babel.transform(code, {
|
|
1093
|
+
filename,
|
|
1094
|
+
presets: [
|
|
1095
|
+
["react", { runtime: "automatic" }],
|
|
1096
|
+
...mergedOptions.presets || []
|
|
1097
|
+
],
|
|
1098
|
+
plugins: mergedOptions.plugins || [],
|
|
1099
|
+
sourceMaps: true,
|
|
1100
|
+
sourceFileName: filename
|
|
1101
|
+
});
|
|
1102
|
+
console.log("[v0] Transpiled JSX for", filename);
|
|
1103
|
+
console.log("[v0] Output (first 500 chars):", result.code?.substring(0, 500));
|
|
1104
|
+
const transformed = {
|
|
1105
|
+
code: result.code || code,
|
|
1106
|
+
map: result.map,
|
|
1107
|
+
cached: false
|
|
1108
|
+
};
|
|
1109
|
+
if (mergedOptions.cache !== false) {
|
|
1110
|
+
this.transformCache.set(cacheKey, transformed);
|
|
1111
|
+
}
|
|
1112
|
+
return transformed;
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
console.error("[EnhancedTranspiler] JSX transformation error:", error);
|
|
1115
|
+
return {
|
|
1116
|
+
code,
|
|
1117
|
+
error,
|
|
1118
|
+
cached: false
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
createImportTransformer(filename) {
|
|
1123
|
+
const self = this;
|
|
1124
|
+
return function importTransformer() {
|
|
1125
|
+
return {
|
|
1126
|
+
visitor: {
|
|
1127
|
+
ImportDeclaration(path3) {
|
|
1128
|
+
const source = path3.node.source.value;
|
|
1129
|
+
if (!source.startsWith("./") && !source.startsWith("../") && !source.startsWith("@/")) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const resolvedPath = self.resolveImportPath(filename, source);
|
|
1133
|
+
if (resolvedPath) {
|
|
1134
|
+
console.log(`[Transpiler] Rewriting: ${filename} imports "${source}" \u2192 "${resolvedPath}"`);
|
|
1135
|
+
path3.node.source.value = resolvedPath;
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
ExportNamedDeclaration(path3) {
|
|
1139
|
+
if (path3.node.source) {
|
|
1140
|
+
const source = path3.node.source.value;
|
|
1141
|
+
if (!source.startsWith("./") && !source.startsWith("../") && !source.startsWith("@/")) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const resolvedPath = self.resolveImportPath(filename, source);
|
|
1145
|
+
if (resolvedPath) {
|
|
1146
|
+
console.log(`[Transpiler] Rewriting export: ${filename} exports from "${source}" \u2192 "${resolvedPath}"`);
|
|
1147
|
+
path3.node.source.value = resolvedPath;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
ExportAllDeclaration(path3) {
|
|
1152
|
+
if (path3.node.source) {
|
|
1153
|
+
const source = path3.node.source.value;
|
|
1154
|
+
if (!source.startsWith("./") && !source.startsWith("../") && !source.startsWith("@/")) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const resolvedPath = self.resolveImportPath(filename, source);
|
|
1158
|
+
if (resolvedPath) {
|
|
1159
|
+
console.log(`[Transpiler] Rewriting export all: ${filename} exports from "${source}" \u2192 "${resolvedPath}"`);
|
|
1160
|
+
path3.node.source.value = resolvedPath;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
resolveImportPath(fromFile, importPath) {
|
|
1169
|
+
const normalizedFromFile = fromFile.startsWith("/") ? fromFile.substring(1) : fromFile;
|
|
1170
|
+
if (importPath.startsWith("@/")) {
|
|
1171
|
+
return ("/src/" + importPath.substring(2)).replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
1172
|
+
}
|
|
1173
|
+
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
1174
|
+
const fromDir = normalizedFromFile.split("/").slice(0, -1);
|
|
1175
|
+
const importParts = importPath.split("/");
|
|
1176
|
+
let currentPath = [...fromDir];
|
|
1177
|
+
for (const part of importParts) {
|
|
1178
|
+
if (part === "..") {
|
|
1179
|
+
currentPath.pop();
|
|
1180
|
+
} else if (part !== ".") {
|
|
1181
|
+
currentPath.push(part);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return "/" + currentPath.join("/").replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
// Track dependencies for cache invalidation
|
|
1189
|
+
trackDependencies(filename, dependencies) {
|
|
1190
|
+
this.dependencyGraph.set(filename, new Set(dependencies));
|
|
1191
|
+
}
|
|
1192
|
+
// Invalidate cache entries that depend on changed files
|
|
1193
|
+
invalidateCache(changedFiles) {
|
|
1194
|
+
const toInvalidate = /* @__PURE__ */ new Set();
|
|
1195
|
+
for (const [file, deps] of this.dependencyGraph) {
|
|
1196
|
+
for (const changedFile of changedFiles) {
|
|
1197
|
+
if (deps.has(changedFile)) {
|
|
1198
|
+
toInvalidate.add(file);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
for (const file of toInvalidate) {
|
|
1203
|
+
for (const [key] of this.transformCache) {
|
|
1204
|
+
if (key.startsWith(`${file}:`)) {
|
|
1205
|
+
this.transformCache.delete(key);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Clear all cache
|
|
1211
|
+
clearCache() {
|
|
1212
|
+
this.transformCache.clear();
|
|
1213
|
+
this.dependencyGraph.clear();
|
|
1214
|
+
}
|
|
1215
|
+
invalidateFile(filename) {
|
|
1216
|
+
for (const key of this.transformCache.keys()) {
|
|
1217
|
+
if (key.startsWith(filename)) {
|
|
1218
|
+
this.transformCache.delete(key);
|
|
1219
|
+
console.log("[Transpiler] Invalidated cache for", filename);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const dependents = this.dependencyGraph.get(filename);
|
|
1223
|
+
if (dependents) {
|
|
1224
|
+
for (const dependent of dependents) {
|
|
1225
|
+
this.invalidateFile(dependent);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// Get cache statistics
|
|
1230
|
+
getCacheStats() {
|
|
1231
|
+
return {
|
|
1232
|
+
size: this.transformCache.size,
|
|
1233
|
+
hitRate: 0
|
|
1234
|
+
// Could be implemented with usage tracking
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
// Extract dependencies from code for tracking
|
|
1238
|
+
extractDependencies(code) {
|
|
1239
|
+
const dependencies = [];
|
|
1240
|
+
try {
|
|
1241
|
+
const importRegex = /import\s+(?:[\w\s{},*]*\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
1242
|
+
let match;
|
|
1243
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
1244
|
+
dependencies.push(match[1]);
|
|
1245
|
+
}
|
|
1246
|
+
const exportRegex = /export\s+(?:[\w\s{},*]*\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
1247
|
+
while ((match = exportRegex.exec(code)) !== null) {
|
|
1248
|
+
dependencies.push(match[1]);
|
|
1249
|
+
}
|
|
1250
|
+
const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1251
|
+
while ((match = dynamicImportRegex.exec(code)) !== null) {
|
|
1252
|
+
dependencies.push(match[1]);
|
|
1253
|
+
}
|
|
1254
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1255
|
+
while ((match = requireRegex.exec(code)) !== null) {
|
|
1256
|
+
dependencies.push(match[1]);
|
|
1257
|
+
}
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
console.warn("[EnhancedTranspiler] Failed to extract dependencies:", error);
|
|
1260
|
+
}
|
|
1261
|
+
return [...new Set(dependencies)];
|
|
1262
|
+
}
|
|
1263
|
+
// Validate transpiled code
|
|
1264
|
+
validateTranspiledCode(code) {
|
|
1265
|
+
const errors = [];
|
|
1266
|
+
try {
|
|
1267
|
+
new Function(code);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
errors.push(`Syntax error: ${error.message}`);
|
|
1270
|
+
}
|
|
1271
|
+
if (code.includes("undefined") && code.includes("import")) {
|
|
1272
|
+
errors.push("Possible import resolution issue detected");
|
|
1273
|
+
}
|
|
1274
|
+
if (code.includes("require(") && !code.includes("module.exports")) {
|
|
1275
|
+
errors.push("CommonJS require() detected in ES module");
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
valid: errors.length === 0,
|
|
1279
|
+
errors
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
// src/entry-point-detection.ts
|
|
1285
|
+
function generateId() {
|
|
1286
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
1287
|
+
}
|
|
1288
|
+
var COMMON_ENTRY_POINTS = [
|
|
1289
|
+
"src/App.tsx",
|
|
1290
|
+
"src/App.ts",
|
|
1291
|
+
"App.tsx",
|
|
1292
|
+
"App.ts",
|
|
1293
|
+
"app/page.tsx",
|
|
1294
|
+
// Next.js
|
|
1295
|
+
"app/page.ts",
|
|
1296
|
+
"src/main.tsx",
|
|
1297
|
+
"src/main.ts",
|
|
1298
|
+
"main.tsx",
|
|
1299
|
+
"main.ts",
|
|
1300
|
+
"index.tsx",
|
|
1301
|
+
"index.ts",
|
|
1302
|
+
"src/index.tsx",
|
|
1303
|
+
"src/index.ts"
|
|
1304
|
+
];
|
|
1305
|
+
var COMPONENT_INDICATORS = [
|
|
1306
|
+
"App",
|
|
1307
|
+
"Main",
|
|
1308
|
+
"Page",
|
|
1309
|
+
"Index",
|
|
1310
|
+
"Home"
|
|
1311
|
+
];
|
|
1312
|
+
function findOrCreateEntryPoint(files) {
|
|
1313
|
+
console.log("\u{1F50D} Entry point detection: Starting with", files.length, "files");
|
|
1314
|
+
console.log("\u{1F4C1} Available files:", files.map((f) => f.path));
|
|
1315
|
+
const existingEntry = findExistingEntryPoint(files);
|
|
1316
|
+
if (existingEntry) {
|
|
1317
|
+
console.log("\u{1F3AF} Found existing entry point:", existingEntry);
|
|
1318
|
+
return {
|
|
1319
|
+
entryPoint: existingEntry,
|
|
1320
|
+
files,
|
|
1321
|
+
createdEntry: false
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
const componentEntry = findComponentEntryPoint(files);
|
|
1325
|
+
if (componentEntry) {
|
|
1326
|
+
console.log("\u{1F527} Found component that could be entry point:", componentEntry);
|
|
1327
|
+
const componentFile = files.find((f) => f.path === componentEntry);
|
|
1328
|
+
if (componentFile) {
|
|
1329
|
+
console.log("\u{1F4DD} Creating app wrapper for component:", componentEntry);
|
|
1330
|
+
const appWrapperFile = createAppWrapperForComponent(componentFile);
|
|
1331
|
+
const updatedFiles2 = [...files, appWrapperFile];
|
|
1332
|
+
return {
|
|
1333
|
+
entryPoint: appWrapperFile.path,
|
|
1334
|
+
files: updatedFiles2,
|
|
1335
|
+
createdEntry: true
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
console.log("\u{1F4DD} Creating default entry point");
|
|
1340
|
+
const entryPointFile = createDefaultEntryPoint(files);
|
|
1341
|
+
const updatedFiles = [...files, entryPointFile];
|
|
1342
|
+
return {
|
|
1343
|
+
entryPoint: entryPointFile.path,
|
|
1344
|
+
files: updatedFiles,
|
|
1345
|
+
createdEntry: true
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function isBootstrapFile(file) {
|
|
1349
|
+
const c = file.content;
|
|
1350
|
+
return /\bcreateRoot\s*\(/.test(c) || /ReactDOM\.render\s*\(/.test(c) || /\bhydrateRoot\s*\(/.test(c);
|
|
1351
|
+
}
|
|
1352
|
+
function extractComponentFromBootstrap(bootstrapFile, allFiles) {
|
|
1353
|
+
const importRe = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
1354
|
+
let match;
|
|
1355
|
+
while ((match = importRe.exec(bootstrapFile.content)) !== null) {
|
|
1356
|
+
const importPath = match[2];
|
|
1357
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("@/")) continue;
|
|
1358
|
+
const bootstrapDir = bootstrapFile.path.replace(/\/[^/]+$/, "");
|
|
1359
|
+
let resolvedBase;
|
|
1360
|
+
if (importPath.startsWith("@/")) {
|
|
1361
|
+
resolvedBase = "src/" + importPath.slice(2);
|
|
1362
|
+
} else {
|
|
1363
|
+
const parts = bootstrapDir.split("/").filter(Boolean);
|
|
1364
|
+
for (const seg of importPath.split("/")) {
|
|
1365
|
+
if (seg === "..") parts.pop();
|
|
1366
|
+
else if (seg !== ".") parts.push(seg);
|
|
1367
|
+
}
|
|
1368
|
+
resolvedBase = parts.join("/");
|
|
1369
|
+
}
|
|
1370
|
+
const extensions = [".tsx", ".ts", ".jsx", ".js", ""];
|
|
1371
|
+
for (const ext of extensions) {
|
|
1372
|
+
const candidate = resolvedBase + ext;
|
|
1373
|
+
const found = allFiles.find(
|
|
1374
|
+
(f) => f.path === candidate || f.path === "/" + candidate || f.path.replace(/^\//, "") === candidate
|
|
1375
|
+
);
|
|
1376
|
+
if (found) {
|
|
1377
|
+
console.log(`\u{1F517} Bootstrap ${bootstrapFile.path} imports component from ${found.path}`);
|
|
1378
|
+
return found.path;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
function findExistingEntryPoint(files) {
|
|
1385
|
+
for (const entryPath of COMMON_ENTRY_POINTS) {
|
|
1386
|
+
const found = files.find(
|
|
1387
|
+
(f) => f.path === entryPath || f.path.endsWith(entryPath) || f.path.replace(/^\//, "") === entryPath.replace(/^\//, "")
|
|
1388
|
+
);
|
|
1389
|
+
if (found) {
|
|
1390
|
+
if (isBootstrapFile(found)) {
|
|
1391
|
+
console.log(`\u26A1 Skipping bootstrap file as entry point: ${found.path}`);
|
|
1392
|
+
const componentPath = extractComponentFromBootstrap(found, files);
|
|
1393
|
+
if (componentPath) {
|
|
1394
|
+
return componentPath;
|
|
1395
|
+
}
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
return found.path;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
function findComponentEntryPoint(files) {
|
|
1404
|
+
for (const file of files) {
|
|
1405
|
+
const fileName = file.path.split("/").pop()?.replace(/\.(tsx?|jsx?)$/, "");
|
|
1406
|
+
if (fileName && COMPONENT_INDICATORS.includes(fileName)) {
|
|
1407
|
+
return file.path;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const jsxFiles = files.filter(
|
|
1411
|
+
(f) => f.path.endsWith(".tsx") || f.path.endsWith(".jsx")
|
|
1412
|
+
);
|
|
1413
|
+
if (jsxFiles.length > 0) {
|
|
1414
|
+
for (const file of jsxFiles) {
|
|
1415
|
+
if (file.content.includes("export default") || file.content.includes("function") || file.content.includes("const")) {
|
|
1416
|
+
return file.path;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return jsxFiles[0].path;
|
|
1420
|
+
}
|
|
1421
|
+
const tsFiles = files.filter((f) => f.path.endsWith(".ts"));
|
|
1422
|
+
if (tsFiles.length > 0) {
|
|
1423
|
+
return tsFiles[0].path;
|
|
1424
|
+
}
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
function toValidIdentifier(name) {
|
|
1428
|
+
return name.replace(/[-_]/g, " ").replace(/\s+/g, " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("").replace(/[^a-zA-Z0-9_$]/g, "").replace(/^[0-9]/, "_$&") || "Component";
|
|
1429
|
+
}
|
|
1430
|
+
function createAppWrapperForComponent(componentFile) {
|
|
1431
|
+
const rawComponentName = componentFile.path.split("/").pop()?.replace(/\.(tsx?|jsx?)$/, "") || "Component";
|
|
1432
|
+
const componentName = toValidIdentifier(rawComponentName);
|
|
1433
|
+
const componentPath = componentFile.path.replace(/\.(tsx?|jsx?)$/, "");
|
|
1434
|
+
const content = `import React from 'react'
|
|
1435
|
+
import ${componentName} from '${componentPath}'
|
|
1436
|
+
|
|
1437
|
+
// App wrapper created by preview system for component: ${componentName}
|
|
1438
|
+
export default function App() {
|
|
1439
|
+
return (
|
|
1440
|
+
<div style={{
|
|
1441
|
+
padding: '20px',
|
|
1442
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1443
|
+
minHeight: '100vh',
|
|
1444
|
+
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
1445
|
+
}}>
|
|
1446
|
+
<div style={{
|
|
1447
|
+
maxWidth: '800px',
|
|
1448
|
+
margin: '0 auto',
|
|
1449
|
+
background: 'white',
|
|
1450
|
+
borderRadius: '12px',
|
|
1451
|
+
padding: '30px',
|
|
1452
|
+
boxShadow: '0 10px 30px rgba(0,0,0,0.1)'
|
|
1453
|
+
}}>
|
|
1454
|
+
<h1 style={{
|
|
1455
|
+
color: '#333',
|
|
1456
|
+
marginBottom: '10px',
|
|
1457
|
+
fontSize: '28px',
|
|
1458
|
+
fontWeight: '700'
|
|
1459
|
+
}}>
|
|
1460
|
+
DamascusAI Preview
|
|
1461
|
+
</h1>
|
|
1462
|
+
<p style={{
|
|
1463
|
+
color: '#666',
|
|
1464
|
+
marginBottom: '30px',
|
|
1465
|
+
fontSize: '16px'
|
|
1466
|
+
}}>
|
|
1467
|
+
Sovereign generation system active - Component: ${componentName}
|
|
1468
|
+
</p>
|
|
1469
|
+
|
|
1470
|
+
<div style={{
|
|
1471
|
+
border: '2px dashed #e2e8f0',
|
|
1472
|
+
borderRadius: '8px',
|
|
1473
|
+
padding: '20px',
|
|
1474
|
+
background: '#f8fafc'
|
|
1475
|
+
}}>
|
|
1476
|
+
<h2 style={{
|
|
1477
|
+
color: '#4a5568',
|
|
1478
|
+
marginBottom: '15px',
|
|
1479
|
+
fontSize: '18px'
|
|
1480
|
+
}}>
|
|
1481
|
+
Component: <code>${componentName}</code>
|
|
1482
|
+
</h2>
|
|
1483
|
+
<${componentName} />
|
|
1484
|
+
</div>
|
|
1485
|
+
|
|
1486
|
+
<div style={{
|
|
1487
|
+
marginTop: '20px',
|
|
1488
|
+
padding: '15px',
|
|
1489
|
+
background: '#edf2f7',
|
|
1490
|
+
borderRadius: '6px',
|
|
1491
|
+
fontSize: '14px',
|
|
1492
|
+
color: '#4a5568'
|
|
1493
|
+
}}>
|
|
1494
|
+
<strong>Generated by:</strong> DamascusAI Sovereign Stack
|
|
1495
|
+
</div>
|
|
1496
|
+
</div>
|
|
1497
|
+
</div>
|
|
1498
|
+
)
|
|
1499
|
+
}
|
|
1500
|
+
`;
|
|
1501
|
+
const now = Date.now();
|
|
1502
|
+
return {
|
|
1503
|
+
id: generateId(),
|
|
1504
|
+
path: "/App.tsx",
|
|
1505
|
+
content,
|
|
1506
|
+
language: "typescript",
|
|
1507
|
+
createdAt: now,
|
|
1508
|
+
updatedAt: now
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function createDefaultEntryPoint(files) {
|
|
1512
|
+
console.log("\u{1F4DD} Creating default App.tsx entry point");
|
|
1513
|
+
const components = findAvailableComponents(files);
|
|
1514
|
+
const sanitizedComponents = components.map((comp) => ({
|
|
1515
|
+
...comp,
|
|
1516
|
+
name: toValidIdentifier(comp.name)
|
|
1517
|
+
}));
|
|
1518
|
+
const imports = sanitizedComponents.map(
|
|
1519
|
+
(comp) => `import ${comp.name} from '${comp.path.replace(/\.(tsx?|jsx?)$/, "")}'`
|
|
1520
|
+
).join("\n");
|
|
1521
|
+
const componentUsage = sanitizedComponents.map(
|
|
1522
|
+
(comp) => ` <${comp.name} />`
|
|
1523
|
+
).join("\n");
|
|
1524
|
+
const content = `${imports}
|
|
1525
|
+
import React from 'react'
|
|
1526
|
+
|
|
1527
|
+
// Default entry point created by preview system
|
|
1528
|
+
export default function App() {
|
|
1529
|
+
return (
|
|
1530
|
+
<div style={{
|
|
1531
|
+
padding: '20px',
|
|
1532
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1533
|
+
minHeight: '100vh',
|
|
1534
|
+
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
1535
|
+
}}>
|
|
1536
|
+
<div style={{
|
|
1537
|
+
maxWidth: '800px',
|
|
1538
|
+
margin: '0 auto',
|
|
1539
|
+
background: 'white',
|
|
1540
|
+
borderRadius: '12px',
|
|
1541
|
+
padding: '30px',
|
|
1542
|
+
boxShadow: '0 10px 30px rgba(0,0,0,0.1)'
|
|
1543
|
+
}}>
|
|
1544
|
+
<h1 style={{
|
|
1545
|
+
color: '#333',
|
|
1546
|
+
marginBottom: '10px',
|
|
1547
|
+
fontSize: '28px',
|
|
1548
|
+
fontWeight: '700'
|
|
1549
|
+
}}>
|
|
1550
|
+
\u{1F680} Damascus AI Preview
|
|
1551
|
+
</h1>
|
|
1552
|
+
<p style={{
|
|
1553
|
+
color: '#666',
|
|
1554
|
+
marginBottom: '30px',
|
|
1555
|
+
fontSize: '16px'
|
|
1556
|
+
}}>
|
|
1557
|
+
Sovereign generation system active
|
|
1558
|
+
</p>
|
|
1559
|
+
|
|
1560
|
+
<div style={{
|
|
1561
|
+
border: '2px dashed #e2e8f0',
|
|
1562
|
+
borderRadius: '8px',
|
|
1563
|
+
padding: '20px',
|
|
1564
|
+
background: '#f8fafc'
|
|
1565
|
+
}}>
|
|
1566
|
+
<h2 style={{
|
|
1567
|
+
color: '#4a5568',
|
|
1568
|
+
marginBottom: '15px',
|
|
1569
|
+
fontSize: '18px'
|
|
1570
|
+
}}>
|
|
1571
|
+
Generated Components:
|
|
1572
|
+
</h2>
|
|
1573
|
+
<div style={{ marginTop: '20px' }}>
|
|
1574
|
+
${componentUsage}
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>
|
|
1577
|
+
|
|
1578
|
+
<div style={{
|
|
1579
|
+
marginTop: '20px',
|
|
1580
|
+
padding: '15px',
|
|
1581
|
+
background: '#edf2f7',
|
|
1582
|
+
borderRadius: '6px',
|
|
1583
|
+
fontSize: '14px',
|
|
1584
|
+
color: '#4a5568'
|
|
1585
|
+
}}>
|
|
1586
|
+
<strong>Generated by:</strong> DamascusAI Sovereign Stack
|
|
1587
|
+
</div>
|
|
1588
|
+
</div>
|
|
1589
|
+
</div>
|
|
1590
|
+
)
|
|
1591
|
+
}
|
|
1592
|
+
`;
|
|
1593
|
+
const now = Date.now();
|
|
1594
|
+
return {
|
|
1595
|
+
id: generateId(),
|
|
1596
|
+
path: "/App.tsx",
|
|
1597
|
+
content,
|
|
1598
|
+
language: "typescript",
|
|
1599
|
+
createdAt: now,
|
|
1600
|
+
updatedAt: now
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
function findAvailableComponents(files) {
|
|
1604
|
+
const components = [];
|
|
1605
|
+
for (const file of files) {
|
|
1606
|
+
if (file.path.endsWith(".tsx") || file.path.endsWith(".jsx")) {
|
|
1607
|
+
const fileName = file.path.split("/").pop()?.replace(/\.(tsx?|jsx?)$/, "");
|
|
1608
|
+
if (fileName && fileName !== "App" && fileName !== "index") {
|
|
1609
|
+
if (file.content.includes("export") || file.content.includes("return")) {
|
|
1610
|
+
components.push({
|
|
1611
|
+
name: fileName,
|
|
1612
|
+
path: file.path
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (components.length === 0) {
|
|
1619
|
+
components.push({
|
|
1620
|
+
name: "GeneratedContent",
|
|
1621
|
+
path: "/placeholder"
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
return components.slice(0, 3);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/jxr-server-manager.ts
|
|
1628
|
+
import { readFile, readdir, watch } from "fs/promises";
|
|
1629
|
+
import path from "path";
|
|
1630
|
+
import http from "http";
|
|
1631
|
+
var JXRServerManager = class {
|
|
1632
|
+
runtime;
|
|
1633
|
+
transpiler;
|
|
1634
|
+
server = null;
|
|
1635
|
+
config;
|
|
1636
|
+
projectFiles = [];
|
|
1637
|
+
entryPoint = "src/App.tsx";
|
|
1638
|
+
watchers = /* @__PURE__ */ new Map();
|
|
1639
|
+
debounceTimer = null;
|
|
1640
|
+
pendingChanges = /* @__PURE__ */ new Map();
|
|
1641
|
+
clients = /* @__PURE__ */ new Set();
|
|
1642
|
+
constructor(config = {}) {
|
|
1643
|
+
this.config = {
|
|
1644
|
+
port: config.port || 3e3,
|
|
1645
|
+
host: config.host || "localhost",
|
|
1646
|
+
srcDir: config.srcDir || "src",
|
|
1647
|
+
enableHMR: config.enableHMR !== false,
|
|
1648
|
+
debounceMs: config.debounceMs || 300
|
|
1649
|
+
};
|
|
1650
|
+
this.runtime = new JXRRuntime();
|
|
1651
|
+
this.transpiler = new EnhancedTranspiler();
|
|
1652
|
+
}
|
|
1653
|
+
async initialize() {
|
|
1654
|
+
await this.runtime.init();
|
|
1655
|
+
await this.loadProjectFiles();
|
|
1656
|
+
this.setupEntryPoint();
|
|
1657
|
+
if (this.config.enableHMR) {
|
|
1658
|
+
this.startFileWatching();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
async loadProjectFiles() {
|
|
1662
|
+
const srcPath = path.resolve(process.cwd(), this.config.srcDir);
|
|
1663
|
+
this.projectFiles = [];
|
|
1664
|
+
async function readDirRecursive(dir, base, files, runtime) {
|
|
1665
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
1666
|
+
const fullPath = path.join(dir, entry.name);
|
|
1667
|
+
const relativePath = path.join(base, entry.name);
|
|
1668
|
+
if (entry.isDirectory()) {
|
|
1669
|
+
await readDirRecursive(fullPath, relativePath, files, runtime);
|
|
1670
|
+
} else if (/\.(tsx?|jsx?|css)$/.test(entry.name)) {
|
|
1671
|
+
const content = await readFile(fullPath, "utf-8");
|
|
1672
|
+
const vfsPath = "/" + relativePath.replace(/\\/g, "/");
|
|
1673
|
+
runtime.vfs.write(vfsPath, content);
|
|
1674
|
+
files.push({
|
|
1675
|
+
id: Math.random().toString(36).slice(2),
|
|
1676
|
+
path: relativePath.replace(/\\/g, "/"),
|
|
1677
|
+
content,
|
|
1678
|
+
language: entry.name.endsWith(".tsx") || entry.name.endsWith(".ts") ? "typescript" : "javascript",
|
|
1679
|
+
createdAt: Date.now(),
|
|
1680
|
+
updatedAt: Date.now()
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
try {
|
|
1686
|
+
await readDirRecursive(srcPath, this.config.srcDir, this.projectFiles, this.runtime);
|
|
1687
|
+
console.log(`\u{1F4C1} Loaded ${this.projectFiles.length} files into VirtualFS`);
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
console.error("Error loading files:", err);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
setupEntryPoint() {
|
|
1693
|
+
const result = findOrCreateEntryPoint(this.projectFiles);
|
|
1694
|
+
this.entryPoint = result.entryPoint;
|
|
1695
|
+
if (result.createdEntry) {
|
|
1696
|
+
const entryFile = result.files.find((f) => f.path === this.entryPoint);
|
|
1697
|
+
if (entryFile) {
|
|
1698
|
+
this.runtime.vfs.write("/" + entryFile.path, entryFile.content);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
console.log(`\u{1F3AF} Entry point: ${this.entryPoint}`);
|
|
1702
|
+
}
|
|
1703
|
+
startFileWatching() {
|
|
1704
|
+
const srcPath = path.resolve(process.cwd(), this.config.srcDir);
|
|
1705
|
+
const watchDir = async (dir) => {
|
|
1706
|
+
try {
|
|
1707
|
+
const watcher = watch(dir, { recursive: true });
|
|
1708
|
+
this.watchers.set(dir, watcher);
|
|
1709
|
+
for await (const event of watcher) {
|
|
1710
|
+
if (event.filename && /\.(tsx?|jsx?|css)$/.test(event.filename)) {
|
|
1711
|
+
this.handleFileChange(event.filename);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
console.error(`Watch error for ${dir}:`, err);
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
watchDir(srcPath);
|
|
1719
|
+
console.log(`\u{1F440} Watching ${this.config.srcDir} for changes...`);
|
|
1720
|
+
}
|
|
1721
|
+
handleFileChange(filename) {
|
|
1722
|
+
const fullPath = path.resolve(process.cwd(), this.config.srcDir, filename);
|
|
1723
|
+
const relativePath = path.join(this.config.srcDir, filename).replace(/\\/g, "/");
|
|
1724
|
+
readFile(fullPath, "utf-8").then((content) => {
|
|
1725
|
+
const file = {
|
|
1726
|
+
id: Math.random().toString(36).slice(2),
|
|
1727
|
+
path: relativePath,
|
|
1728
|
+
content,
|
|
1729
|
+
language: filename.endsWith(".tsx") || filename.endsWith(".ts") ? "typescript" : "javascript",
|
|
1730
|
+
createdAt: Date.now(),
|
|
1731
|
+
updatedAt: Date.now()
|
|
1732
|
+
};
|
|
1733
|
+
this.pendingChanges.set(relativePath, file);
|
|
1734
|
+
this.scheduleReload();
|
|
1735
|
+
}).catch((err) => console.error(`Error reading ${filename}:`, err));
|
|
1736
|
+
}
|
|
1737
|
+
scheduleReload() {
|
|
1738
|
+
if (this.debounceTimer) {
|
|
1739
|
+
clearTimeout(this.debounceTimer);
|
|
1740
|
+
}
|
|
1741
|
+
this.debounceTimer = setTimeout(() => {
|
|
1742
|
+
this.processPendingChanges();
|
|
1743
|
+
}, this.config.debounceMs);
|
|
1744
|
+
}
|
|
1745
|
+
async processPendingChanges() {
|
|
1746
|
+
if (this.pendingChanges.size === 0) return;
|
|
1747
|
+
console.log(`\u{1F504} Processing ${this.pendingChanges.size} file change(s)...`);
|
|
1748
|
+
for (const [path3, file] of this.pendingChanges) {
|
|
1749
|
+
this.runtime.vfs.write("/" + path3, file.content);
|
|
1750
|
+
const existingIndex = this.projectFiles.findIndex((f) => f.path === path3);
|
|
1751
|
+
if (existingIndex >= 0) {
|
|
1752
|
+
this.projectFiles[existingIndex] = file;
|
|
1753
|
+
}
|
|
1754
|
+
this.transpiler.invalidateFile("/" + path3);
|
|
1755
|
+
console.log(` \u2713 Updated: ${path3}`);
|
|
1756
|
+
}
|
|
1757
|
+
this.pendingChanges.clear();
|
|
1758
|
+
this.broadcastReload();
|
|
1759
|
+
console.log("\u{1F525} HMR update sent to browser");
|
|
1760
|
+
}
|
|
1761
|
+
broadcastReload() {
|
|
1762
|
+
const message = JSON.stringify({ type: "reload", timestamp: Date.now() });
|
|
1763
|
+
this.clients.forEach((client) => {
|
|
1764
|
+
client.write(`data: ${message}
|
|
1765
|
+
|
|
1766
|
+
`);
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
async start() {
|
|
1770
|
+
this.server = http.createServer(async (req, res) => {
|
|
1771
|
+
const url = new URL(req.url || "/", `http://${this.config.host}:${this.config.port}`);
|
|
1772
|
+
if (url.pathname === "/__hmr" && this.config.enableHMR) {
|
|
1773
|
+
res.writeHead(200, {
|
|
1774
|
+
"Content-Type": "text/event-stream",
|
|
1775
|
+
"Cache-Control": "no-cache",
|
|
1776
|
+
"Connection": "keep-alive",
|
|
1777
|
+
"Access-Control-Allow-Origin": "*"
|
|
1778
|
+
});
|
|
1779
|
+
this.clients.add(res);
|
|
1780
|
+
req.on("close", () => {
|
|
1781
|
+
this.clients.delete(res);
|
|
1782
|
+
});
|
|
1783
|
+
res.write(`data: ${JSON.stringify({ type: "connected" })}
|
|
1784
|
+
|
|
1785
|
+
`);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (url.pathname === "/__health") {
|
|
1789
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1790
|
+
res.end(JSON.stringify({ status: "ok", runtime: "JXR", version: this.runtime.version }));
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (url.pathname === "/") {
|
|
1794
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1795
|
+
res.end(this.generateHTML());
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (url.pathname.match(/\.(tsx?|jsx?|ts|js)$/) || url.pathname.startsWith("/src/")) {
|
|
1799
|
+
try {
|
|
1800
|
+
let vfsPath = url.pathname;
|
|
1801
|
+
let file = this.runtime.vfs.read(vfsPath);
|
|
1802
|
+
if (!file && !vfsPath.match(/\.(tsx?|jsx?|ts|js|css)$/)) {
|
|
1803
|
+
const extensions = [".tsx", ".ts", ".jsx", ".js", ".css"];
|
|
1804
|
+
for (const ext of extensions) {
|
|
1805
|
+
file = this.runtime.vfs.read(vfsPath + ext);
|
|
1806
|
+
if (file) {
|
|
1807
|
+
vfsPath = vfsPath + ext;
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (!file) {
|
|
1813
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1814
|
+
res.end(`// Module not found: ${url.pathname}`);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (vfsPath.endsWith(".css")) {
|
|
1818
|
+
let cssContent = file.content;
|
|
1819
|
+
if (vfsPath === "/src/index.css") {
|
|
1820
|
+
try {
|
|
1821
|
+
const fs = await import("fs");
|
|
1822
|
+
const compiledPath = path.join(process.cwd(), "src", "index.compiled.css");
|
|
1823
|
+
if (fs.existsSync(compiledPath)) {
|
|
1824
|
+
cssContent = fs.readFileSync(compiledPath, "utf-8");
|
|
1825
|
+
console.log(`[JXR] Using pre-compiled Tailwind CSS from index.compiled.css`);
|
|
1826
|
+
} else {
|
|
1827
|
+
const { execSync } = await import("child_process");
|
|
1828
|
+
const os = await import("os");
|
|
1829
|
+
const tmpDir = os.tmpdir();
|
|
1830
|
+
const inputFile = path.join(tmpDir, `jxr-tailwind-${Date.now()}.css`);
|
|
1831
|
+
const outputFile = path.join(tmpDir, `jxr-tailwind-${Date.now()}.compiled.css`);
|
|
1832
|
+
fs.writeFileSync(inputFile, cssContent);
|
|
1833
|
+
try {
|
|
1834
|
+
execSync(`npx @tailwindcss/cli -i "${inputFile}" -o "${outputFile}" --minify`, {
|
|
1835
|
+
timeout: 3e4,
|
|
1836
|
+
stdio: "pipe"
|
|
1837
|
+
});
|
|
1838
|
+
cssContent = fs.readFileSync(outputFile, "utf-8");
|
|
1839
|
+
fs.unlinkSync(inputFile);
|
|
1840
|
+
fs.unlinkSync(outputFile);
|
|
1841
|
+
console.log(`[JXR] Compiled Tailwind CSS for ${vfsPath}`);
|
|
1842
|
+
} catch {
|
|
1843
|
+
const js2 = `
|
|
1844
|
+
const tailwindScript = document.createElement('script');
|
|
1845
|
+
tailwindScript.src = 'https://cdn.tailwindcss.com';
|
|
1846
|
+
tailwindScript.onload = function() {
|
|
1847
|
+
const style = document.createElement('style');
|
|
1848
|
+
style.textContent = \`${file.content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$")}\`;
|
|
1849
|
+
document.head.appendChild(style);
|
|
1850
|
+
};
|
|
1851
|
+
document.head.appendChild(tailwindScript);
|
|
1852
|
+
export default \`${file.content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$")}\`;
|
|
1853
|
+
`;
|
|
1854
|
+
res.writeHead(200, {
|
|
1855
|
+
"Content-Type": "application/javascript",
|
|
1856
|
+
"Cache-Control": "no-cache"
|
|
1857
|
+
});
|
|
1858
|
+
res.end(js2);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
console.warn(`[JXR] Tailwind handling error: ${error?.message || error}`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const escapedCSS = cssContent.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1867
|
+
const js = `
|
|
1868
|
+
const style = document.createElement('style');
|
|
1869
|
+
style.textContent = \`${escapedCSS}\`;
|
|
1870
|
+
document.head.appendChild(style);
|
|
1871
|
+
export default \`${escapedCSS}\`;
|
|
1872
|
+
`;
|
|
1873
|
+
res.writeHead(200, {
|
|
1874
|
+
"Content-Type": "application/javascript",
|
|
1875
|
+
"Cache-Control": "no-cache"
|
|
1876
|
+
});
|
|
1877
|
+
res.end(js);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const result = this.transpiler.transpileTypeScript(file.content, vfsPath);
|
|
1881
|
+
if (result.error) {
|
|
1882
|
+
console.error(`Transform error for ${url.pathname}:`, result.error);
|
|
1883
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1884
|
+
res.end(`// Transform error: ${result.error.message}`);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
res.writeHead(200, {
|
|
1888
|
+
"Content-Type": "application/javascript",
|
|
1889
|
+
"Cache-Control": "no-cache"
|
|
1890
|
+
});
|
|
1891
|
+
res.end(result.code);
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
console.error(`Error serving ${url.pathname}:`, err);
|
|
1894
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1895
|
+
res.end(`// Error: ${err?.message || String(err)}`);
|
|
1896
|
+
}
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
res.writeHead(404);
|
|
1900
|
+
res.end("Not found");
|
|
1901
|
+
});
|
|
1902
|
+
return new Promise((resolve, reject) => {
|
|
1903
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
1904
|
+
console.log(`\u{1F680} JXR server running on http://${this.config.host}:${this.config.port}/`);
|
|
1905
|
+
if (this.config.enableHMR) {
|
|
1906
|
+
console.log(`\u{1F525} HMR enabled (debounce: ${this.config.debounceMs}ms)`);
|
|
1907
|
+
}
|
|
1908
|
+
resolve();
|
|
1909
|
+
});
|
|
1910
|
+
this.server.on("error", reject);
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
async stop() {
|
|
1914
|
+
this.watchers.clear();
|
|
1915
|
+
if (this.debounceTimer) {
|
|
1916
|
+
clearTimeout(this.debounceTimer);
|
|
1917
|
+
this.debounceTimer = null;
|
|
1918
|
+
}
|
|
1919
|
+
this.clients.forEach((client) => {
|
|
1920
|
+
try {
|
|
1921
|
+
client.end();
|
|
1922
|
+
} catch {
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
this.clients.clear();
|
|
1926
|
+
if (this.server) {
|
|
1927
|
+
await new Promise((resolve) => {
|
|
1928
|
+
this.server.close(() => resolve());
|
|
1929
|
+
});
|
|
1930
|
+
this.server = null;
|
|
1931
|
+
}
|
|
1932
|
+
this.runtime.dispose();
|
|
1933
|
+
console.log("\u{1F44B} JXR server stopped");
|
|
1934
|
+
}
|
|
1935
|
+
generateHTML() {
|
|
1936
|
+
const hmrScript = this.config.enableHMR ? `
|
|
1937
|
+
<script>
|
|
1938
|
+
// HMR Client
|
|
1939
|
+
const evtSource = new EventSource('/__hmr');
|
|
1940
|
+
evtSource.onmessage = (e) => {
|
|
1941
|
+
const data = JSON.parse(e.data);
|
|
1942
|
+
if (data.type === 'reload') {
|
|
1943
|
+
console.log('[JXR] Reloading...');
|
|
1944
|
+
location.reload();
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
evtSource.onerror = () => console.log('[JXR] HMR connection lost');
|
|
1948
|
+
</script>` : "";
|
|
1949
|
+
const importMap = {
|
|
1950
|
+
"imports": {
|
|
1951
|
+
"react": "https://esm.sh/react@19.2.4",
|
|
1952
|
+
"react/jsx-runtime": "https://esm.sh/react@19.2.4/jsx-runtime",
|
|
1953
|
+
"react/jsx-dev-runtime": "https://esm.sh/react@19.2.4/jsx-dev-runtime",
|
|
1954
|
+
"react-dom/client": "https://esm.sh/react-dom@19.2.4/client",
|
|
1955
|
+
"wouter": "https://esm.sh/wouter@3.6.0?external=react",
|
|
1956
|
+
"lucide-react": "https://esm.sh/lucide-react@0.483.0?external=react",
|
|
1957
|
+
"sonner": "https://esm.sh/sonner@2.0.1?external=react",
|
|
1958
|
+
"next-themes": "https://esm.sh/next-themes@0.4.6?external=react",
|
|
1959
|
+
"@radix-ui/react-dialog": "https://esm.sh/@radix-ui/react-dialog@1.1.6?external=react",
|
|
1960
|
+
"@radix-ui/react-tooltip": "https://esm.sh/@radix-ui/react-tooltip@1.1.8?external=react",
|
|
1961
|
+
"@radix-ui/react-slot": "https://esm.sh/@radix-ui/react-slot@1.1.2?external=react",
|
|
1962
|
+
"@radix-ui/react-primitive": "https://esm.sh/@radix-ui/react-primitive@2.0.2?external=react",
|
|
1963
|
+
"@radix-ui/react-compose-refs": "https://esm.sh/@radix-ui/react-compose-refs@1.1.1?external=react",
|
|
1964
|
+
"@radix-ui/react-context": "https://esm.sh/@radix-ui/react-context@1.1.1?external=react",
|
|
1965
|
+
"@radix-ui/react-use-controllable-state": "https://esm.sh/@radix-ui/react-use-controllable-state@1.1.0?external=react",
|
|
1966
|
+
"@radix-ui/react-use-escape-keydown": "https://esm.sh/@radix-ui/react-use-escape-keydown@1.1.0?external=react",
|
|
1967
|
+
"@radix-ui/react-use-layout-effect": "https://esm.sh/@radix-ui/react-use-layout-effect@1.1.0?external=react",
|
|
1968
|
+
"@radix-ui/react-dismissable-layer": "https://esm.sh/@radix-ui/react-dismissable-layer@1.1.5?external=react",
|
|
1969
|
+
"@radix-ui/react-focus-guards": "https://esm.sh/@radix-ui/react-focus-guards@1.1.1?external=react",
|
|
1970
|
+
"@radix-ui/react-focus-scope": "https://esm.sh/@radix-ui/react-focus-scope@1.1.2?external=react",
|
|
1971
|
+
"@radix-ui/react-portal": "https://esm.sh/@radix-ui/react-portal@1.1.4?external=react",
|
|
1972
|
+
"@radix-ui/react-presence": "https://esm.sh/@radix-ui/react-presence@1.1.2?external=react",
|
|
1973
|
+
"@radix-ui/react-id": "https://esm.sh/@radix-ui/react-id@1.1.0?external=react",
|
|
1974
|
+
"@radix-ui/primitive": "https://esm.sh/@radix-ui/primitive@1.1.1",
|
|
1975
|
+
"aria-hidden": "https://esm.sh/aria-hidden@1.2.4",
|
|
1976
|
+
"react-remove-scroll": "https://esm.sh/react-remove-scroll@2.6.3?external=react",
|
|
1977
|
+
"tslib": "https://esm.sh/tslib@2.8.1",
|
|
1978
|
+
"get-nonce": "https://esm.sh/get-nonce@1.0.1",
|
|
1979
|
+
"use-callback-ref": "https://esm.sh/use-callback-ref@1.3.3?external=react",
|
|
1980
|
+
"use-sidecar": "https://esm.sh/use-sidecar@1.1.3?external=react",
|
|
1981
|
+
"detect-node-es": "https://esm.sh/detect-node-es@1.1.0",
|
|
1982
|
+
"copy-to-clipboard": "https://esm.sh/copy-to-clipboard@3.3.3",
|
|
1983
|
+
"toggle-selection": "https://esm.sh/toggle-selection@1.0.6",
|
|
1984
|
+
"clsx": "https://esm.sh/clsx@2.1.1",
|
|
1985
|
+
"tailwind-merge": "https://esm.sh/tailwind-merge@3.0.2",
|
|
1986
|
+
"class-variance-authority": "https://esm.sh/class-variance-authority@0.7.1",
|
|
1987
|
+
"framer-motion": "https://esm.sh/framer-motion@12.5.0?external=react,motion-dom",
|
|
1988
|
+
"motion-dom": "https://esm.sh/motion-dom@12.5.0"
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
const hasMainFile = this.projectFiles.some(
|
|
1992
|
+
(f) => f.path === "src/main.tsx" || f.path === "src/main.ts" || f.path === "src/index.tsx" || f.path === "src/index.ts"
|
|
1993
|
+
);
|
|
1994
|
+
if (hasMainFile) {
|
|
1995
|
+
return `<!DOCTYPE html>
|
|
1996
|
+
<html lang="en">
|
|
1997
|
+
<head>
|
|
1998
|
+
<meta charset="UTF-8">
|
|
1999
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2000
|
+
<title>JXR.js \u2014 Edge OS Runtime Framework</title>
|
|
2001
|
+
<meta name="description" content="JXR.js is the next-generation edge runtime framework for React Native and React. MoQ transport, Web Crypto, Worker pools.">
|
|
2002
|
+
|
|
2003
|
+
<!-- Google Fonts -->
|
|
2004
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
2005
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
2006
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
2007
|
+
|
|
2008
|
+
<script type="importmap">
|
|
2009
|
+
${JSON.stringify(importMap)}
|
|
2010
|
+
</script>
|
|
2011
|
+
</head>
|
|
2012
|
+
<body>
|
|
2013
|
+
<div id="root"></div>
|
|
2014
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
2015
|
+
${hmrScript}
|
|
2016
|
+
</body>
|
|
2017
|
+
</html>`;
|
|
2018
|
+
}
|
|
2019
|
+
return `<!DOCTYPE html>
|
|
2020
|
+
<html lang="en">
|
|
2021
|
+
<head>
|
|
2022
|
+
<meta charset="UTF-8">
|
|
2023
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2024
|
+
<title>JXR App</title>
|
|
2025
|
+
<script type="importmap">
|
|
2026
|
+
${JSON.stringify(importMap)}
|
|
2027
|
+
</script>
|
|
2028
|
+
</head>
|
|
2029
|
+
<body>
|
|
2030
|
+
<div id="root"></div>
|
|
2031
|
+
<script type="module">
|
|
2032
|
+
import { createRoot } from 'react-dom/client';
|
|
2033
|
+
import App from '/${this.entryPoint}';
|
|
2034
|
+
|
|
2035
|
+
const root = createRoot(document.getElementById('root'));
|
|
2036
|
+
root.render(App());
|
|
2037
|
+
</script>
|
|
2038
|
+
${hmrScript}
|
|
2039
|
+
</body>
|
|
2040
|
+
</html>`;
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
|
|
2044
|
+
// src/deployer.ts
|
|
2045
|
+
import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
|
|
2046
|
+
import path2 from "path";
|
|
2047
|
+
import { existsSync } from "fs";
|
|
2048
|
+
import { spawn } from "child_process";
|
|
2049
|
+
var JXRDeployer = class {
|
|
2050
|
+
apiKey;
|
|
2051
|
+
projectId;
|
|
2052
|
+
constructor(apiKey, projectId) {
|
|
2053
|
+
this.apiKey = apiKey;
|
|
2054
|
+
this.projectId = projectId || this.generateProjectId();
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Auto-detect build output directory
|
|
2058
|
+
* Checks dist/, build/, out/, or any directory with index.html
|
|
2059
|
+
*/
|
|
2060
|
+
async detectBuildDir(projectPath = ".") {
|
|
2061
|
+
const possibleDirs = ["dist", "build", "out"];
|
|
2062
|
+
for (const dir of possibleDirs) {
|
|
2063
|
+
const fullPath = path2.resolve(projectPath, dir);
|
|
2064
|
+
if (existsSync(fullPath)) {
|
|
2065
|
+
if (existsSync(path2.join(fullPath, "index.html"))) {
|
|
2066
|
+
return fullPath;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
const entries = await readdir2(projectPath, { withFileTypes: true });
|
|
2072
|
+
for (const entry of entries) {
|
|
2073
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2074
|
+
const dirPath = path2.join(projectPath, entry.name);
|
|
2075
|
+
if (existsSync(path2.join(dirPath, "index.html"))) {
|
|
2076
|
+
return dirPath;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
} catch {
|
|
2081
|
+
}
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Detect if running on Cloudflare Pages
|
|
2086
|
+
*/
|
|
2087
|
+
isCloudflarePages() {
|
|
2088
|
+
return process.env.CF_PAGES === "1" || !!process.env.CF_PAGES_URL;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Get project name from package.json or directory
|
|
2092
|
+
*/
|
|
2093
|
+
async getProjectName(projectPath = ".") {
|
|
2094
|
+
if (process.env.CF_PAGES_PROJECT_NAME) {
|
|
2095
|
+
return process.env.CF_PAGES_PROJECT_NAME;
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
const pkgPath = path2.join(projectPath, "package.json");
|
|
2099
|
+
const pkgContent = await readFile2(pkgPath, "utf-8");
|
|
2100
|
+
const pkg = JSON.parse(pkgContent);
|
|
2101
|
+
if (pkg.name) {
|
|
2102
|
+
return pkg.name;
|
|
2103
|
+
}
|
|
2104
|
+
} catch {
|
|
2105
|
+
}
|
|
2106
|
+
return path2.basename(path2.resolve(projectPath));
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Deploy to Cloudflare Pages
|
|
2110
|
+
*/
|
|
2111
|
+
async deployToCloudflarePages(projectPath = ".", config = {}) {
|
|
2112
|
+
const logs = [];
|
|
2113
|
+
try {
|
|
2114
|
+
const buildDir = await this.detectBuildDir(projectPath);
|
|
2115
|
+
if (!buildDir) {
|
|
2116
|
+
throw new Error('No build output found. Run "jxr build" first.');
|
|
2117
|
+
}
|
|
2118
|
+
logs.push(`\u{1F4C1} Build directory: ${buildDir}`);
|
|
2119
|
+
const projectName = await this.getProjectName(projectPath);
|
|
2120
|
+
logs.push(`\u{1F4E6} Project: ${projectName}`);
|
|
2121
|
+
const manifestPath = path2.join(buildDir, "jxr-manifest.json");
|
|
2122
|
+
if (existsSync(manifestPath)) {
|
|
2123
|
+
const manifest = JSON.parse(await readFile2(manifestPath, "utf-8"));
|
|
2124
|
+
logs.push(`\u{1F4CB} Manifest: ${manifest.platform} platform, ${manifest.files?.length || 0} files`);
|
|
2125
|
+
}
|
|
2126
|
+
const env = config.environment || "production";
|
|
2127
|
+
const url = `https://${projectName}.app.jxrstudios.online`;
|
|
2128
|
+
logs.push(`\u{1F310} URL: ${url}`);
|
|
2129
|
+
if (this.isCloudflarePages()) {
|
|
2130
|
+
logs.push("\u2601\uFE0F Cloudflare Pages auto-detected");
|
|
2131
|
+
logs.push("\u2705 Deployment ready - build output will be deployed automatically");
|
|
2132
|
+
return {
|
|
2133
|
+
success: true,
|
|
2134
|
+
url,
|
|
2135
|
+
deploymentId: `cf-pages-${Date.now()}`,
|
|
2136
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2137
|
+
logs
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
return await this.deploy(buildDir, { ...config, projectId: projectName });
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2143
|
+
logs.push(`\u274C Error: ${errorMsg}`);
|
|
2144
|
+
return {
|
|
2145
|
+
success: false,
|
|
2146
|
+
url: "",
|
|
2147
|
+
deploymentId: "",
|
|
2148
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2149
|
+
logs
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Deploy the current project to JXR infrastructure
|
|
2155
|
+
*/
|
|
2156
|
+
async deploy(projectPath, config = {}) {
|
|
2157
|
+
const env = config.environment || "production";
|
|
2158
|
+
const branch = config.branch || "main";
|
|
2159
|
+
const pid = config.projectId || this.projectId;
|
|
2160
|
+
console.log(`\u{1F680} Deploying to JXR ${env}...`);
|
|
2161
|
+
const logs = [];
|
|
2162
|
+
try {
|
|
2163
|
+
await stat(projectPath);
|
|
2164
|
+
logs.push(`\u{1F4C1} Deploying from: ${projectPath}`);
|
|
2165
|
+
const tarballPath = await this.createTarball(projectPath);
|
|
2166
|
+
logs.push(`\u{1F4E6} Created tarball: ${tarballPath}`);
|
|
2167
|
+
const uploadResult = await this.uploadTarball(tarballPath, pid, env, branch);
|
|
2168
|
+
logs.push(`\u2601\uFE0F Uploaded to JXR`);
|
|
2169
|
+
const url = `https://${pid}.app.jxrstudios.online`;
|
|
2170
|
+
logs.push(`\u{1F310} Live at: ${url}`);
|
|
2171
|
+
return {
|
|
2172
|
+
success: true,
|
|
2173
|
+
url,
|
|
2174
|
+
deploymentId: uploadResult.deploymentId,
|
|
2175
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2176
|
+
logs
|
|
2177
|
+
};
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2180
|
+
logs.push(`\u274C Error: ${errorMsg}`);
|
|
2181
|
+
return {
|
|
2182
|
+
success: false,
|
|
2183
|
+
url: "",
|
|
2184
|
+
deploymentId: "",
|
|
2185
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2186
|
+
logs
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Create a tarball of the build directory
|
|
2192
|
+
*/
|
|
2193
|
+
async createTarball(projectPath) {
|
|
2194
|
+
const tarballPath = path2.join(process.cwd(), ".jxr-deploy.tar.gz");
|
|
2195
|
+
return new Promise((resolve, reject) => {
|
|
2196
|
+
const tar = spawn("tar", ["-czf", tarballPath, "-C", projectPath, "."]);
|
|
2197
|
+
tar.on("close", (code) => {
|
|
2198
|
+
if (code === 0) {
|
|
2199
|
+
resolve(tarballPath);
|
|
2200
|
+
} else {
|
|
2201
|
+
reject(new Error(`tar exited with code ${code}`));
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
tar.on("error", reject);
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Upload tarball to JXR API
|
|
2209
|
+
*/
|
|
2210
|
+
async uploadTarball(tarballPath, projectId, environment, branch) {
|
|
2211
|
+
const formData = new FormData();
|
|
2212
|
+
const fileContent = await readFile2(tarballPath);
|
|
2213
|
+
const blob = new Blob([fileContent]);
|
|
2214
|
+
formData.append("build", blob, "build.tar.gz");
|
|
2215
|
+
formData.append("projectId", projectId);
|
|
2216
|
+
formData.append("environment", environment);
|
|
2217
|
+
formData.append("branch", branch);
|
|
2218
|
+
const response = await fetch("https://jxrstudios.workers.dev/v1/deployments", {
|
|
2219
|
+
method: "POST",
|
|
2220
|
+
headers: {
|
|
2221
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
2222
|
+
},
|
|
2223
|
+
body: formData
|
|
2224
|
+
});
|
|
2225
|
+
if (!response.ok) {
|
|
2226
|
+
throw new Error(`Upload failed: ${response.statusText}`);
|
|
2227
|
+
}
|
|
2228
|
+
const result = await response.json();
|
|
2229
|
+
return { deploymentId: result.deploymentId };
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Get deployment status
|
|
2233
|
+
*/
|
|
2234
|
+
async getStatus(deploymentId) {
|
|
2235
|
+
const response = await fetch(`https://jxrstudios.workers.dev/v1/deployments/${deploymentId}`, {
|
|
2236
|
+
headers: {
|
|
2237
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
if (!response.ok) {
|
|
2241
|
+
throw new Error(`Failed to get status: ${response.statusText}`);
|
|
2242
|
+
}
|
|
2243
|
+
return response.json();
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* List all deployments for a project
|
|
2247
|
+
*/
|
|
2248
|
+
async listDeployments(projectId) {
|
|
2249
|
+
const pid = projectId || this.projectId;
|
|
2250
|
+
const response = await fetch(`https://jxrstudios.workers.dev/v1/projects/${pid}/deployments`, {
|
|
2251
|
+
headers: {
|
|
2252
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
2253
|
+
}
|
|
2254
|
+
});
|
|
2255
|
+
if (!response.ok) {
|
|
2256
|
+
throw new Error(`Failed to list deployments: ${response.statusText}`);
|
|
2257
|
+
}
|
|
2258
|
+
return response.json();
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Rollback to a previous deployment
|
|
2262
|
+
*/
|
|
2263
|
+
async rollback(deploymentId) {
|
|
2264
|
+
const response = await fetch(`https://jxrstudios.workers.dev/v1/deployments/${deploymentId}/rollback`, {
|
|
2265
|
+
method: "POST",
|
|
2266
|
+
headers: {
|
|
2267
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
if (!response.ok) {
|
|
2271
|
+
throw new Error(`Rollback failed: ${response.statusText}`);
|
|
2272
|
+
}
|
|
2273
|
+
const result = await response.json();
|
|
2274
|
+
return {
|
|
2275
|
+
success: true,
|
|
2276
|
+
url: result.url,
|
|
2277
|
+
deploymentId: result.deploymentId,
|
|
2278
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2279
|
+
logs: ["Rollback successful"]
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
generateProjectId() {
|
|
2283
|
+
const timestamp = Date.now().toString(36);
|
|
2284
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
2285
|
+
return `jxr-${timestamp}-${random}`;
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
var jxrDeployer = new JXRDeployer(
|
|
2289
|
+
process.env.JXR_API_KEY || "",
|
|
2290
|
+
process.env.JXR_PROJECT_ID
|
|
2291
|
+
);
|
|
2292
|
+
|
|
2293
|
+
// src/index.ts
|
|
2294
|
+
function defineConfig(config) {
|
|
2295
|
+
return config;
|
|
2296
|
+
}
|
|
2297
|
+
export {
|
|
2298
|
+
DEFAULT_PROJECT_FILES,
|
|
2299
|
+
EnhancedTranspiler,
|
|
2300
|
+
ImportMapBuilder,
|
|
2301
|
+
JSXTransformer,
|
|
2302
|
+
JXRCrypto,
|
|
2303
|
+
JXRDeployer,
|
|
2304
|
+
JXRRuntime,
|
|
2305
|
+
JXRServerManager,
|
|
2306
|
+
MoQTransport,
|
|
2307
|
+
ModuleCache,
|
|
2308
|
+
VirtualFS,
|
|
2309
|
+
WorkerPool,
|
|
2310
|
+
defineConfig,
|
|
2311
|
+
findOrCreateEntryPoint,
|
|
2312
|
+
jxrCrypto,
|
|
2313
|
+
jxrDeployer,
|
|
2314
|
+
jxrRuntime
|
|
2315
|
+
};
|