vmlive 1.0.2 → 1.0.4
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/README.md +9 -3
- package/package.json +1 -1
- package/src/cli.js +150 -18
- package/src/dashboard-worker.js +149 -0
- package/src/string-shim.js +8 -4
package/README.md
CHANGED
|
@@ -29,9 +29,16 @@ Interactively scaffolds additional serverless endpoints natively into your `src/
|
|
|
29
29
|
### `npx vmlive dev`
|
|
30
30
|
Starts the local emulation environment.
|
|
31
31
|
- Iteratively hosts your entire multi-function footprint locally via predictable slugs: `http://<workspace_id>-<project_id>-<function_name>.localhost:8787`.
|
|
32
|
+
- **Local Dashboard:** Automatically spawns a web dashboard at `http://dashboard.localhost:8787` for visualizing offline databases, global KV, and Object Storage.
|
|
32
33
|
- Provides local proxy bindings for your SQL Database (`env.DB`), Global KV (`env.KV`), and Object Storage (`env.BUCKET`) using the `.vmlive/` directory for persistent storage.
|
|
33
34
|
- Supports hot-reloading for `.js` and `.ts` files.
|
|
34
35
|
|
|
36
|
+
### `npx vmlive test`
|
|
37
|
+
Executes your Vitest suites against a strictly ephemeral **in-memory** engine.
|
|
38
|
+
- Spins up `VirtualMachine` in memory-only mode without touching `.vmlive/` disk databases.
|
|
39
|
+
- Runs `npx vitest run` and gracefully destroys the engine upon exit.
|
|
40
|
+
- Allows test suites to deeply mutate state predictably without sandbox bleeding.
|
|
41
|
+
|
|
35
42
|
### `npx vmlive deploy`
|
|
36
43
|
Uploads the local code to the vm.live edge platform.
|
|
37
44
|
- Bundles the code using `esbuild`.
|
|
@@ -42,6 +49,5 @@ Uploads the local code to the vm.live edge platform.
|
|
|
42
49
|
If you are using an AI assistant (such as Cursor, Copilot, or Aider) to write code, provide it with the platform context to ensure it adheres to the V8 Isolate execution model and uses the correct infrastructure bindings.
|
|
43
50
|
|
|
44
51
|
**Instructions:**
|
|
45
|
-
1. Download the
|
|
46
|
-
2.
|
|
47
|
-
3. If using Cursor, rename the file to `.cursorrules` to force the AI to apply the constraints automatically.
|
|
52
|
+
1. Download the rules seamlessly via the API directly into your local root footprint: `curl -O https://api.vm.live/vmlive-ai-rules.md`
|
|
53
|
+
2. Configure your AI Assistant (e.g. Cursor, Copilot) to ingest this manifest so it dynamically enforces the strict PaaS architectural primitives across your local logic scope!
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -28,31 +28,51 @@ const buildResourcesConfig = () => {
|
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const buildProxyDispatcher = (functions, workspaceId, projectId) => ({
|
|
31
|
+
const buildProxyDispatcher = (functions, workspaceId, projectId, includeDashboard = true) => ({
|
|
32
32
|
name: "vm-gateway-proxy",
|
|
33
33
|
modules: true,
|
|
34
34
|
script: `
|
|
35
35
|
export default {
|
|
36
36
|
async fetch(request, env) {
|
|
37
37
|
const url = new URL(request.url);
|
|
38
|
+
|
|
39
|
+
if (url.pathname === '/favicon.ico') {
|
|
40
|
+
return new Response(null, { status: 204 });
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
const hostHeader = request.headers.get("Host") || url.hostname;
|
|
39
44
|
let targetName = hostHeader.split('.')[0];
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
let res;
|
|
47
|
+
|
|
48
|
+
if (targetName === 'dashboard' && env.DASHBOARD) {
|
|
49
|
+
res = await env.DASHBOARD.fetch(request);
|
|
50
|
+
} else if (targetName && env[targetName]) {
|
|
51
|
+
res = await env[targetName].fetch(request);
|
|
52
|
+
} else {
|
|
53
|
+
res = new Response(
|
|
54
|
+
\`Function Not Found: \${targetName}.\\n\\nExpected format: http://${workspaceId}-${projectId}-<function_name>.localhost\`,
|
|
55
|
+
{ status: 404 }
|
|
56
|
+
);
|
|
43
57
|
}
|
|
58
|
+
|
|
59
|
+
const method = request.method;
|
|
60
|
+
const path = url.pathname;
|
|
61
|
+
const status = res.status;
|
|
62
|
+
const color = status >= 500 ? "\\x1b[31m" : status >= 400 ? "\\x1b[33m" : "\\x1b[32m";
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ status: 404 }
|
|
48
|
-
);
|
|
64
|
+
console.log(\`\\x1b[90m[\${targetName}]\\x1b[0m \${method} \${path} \${color}\${status}\\x1b[0m\`);
|
|
65
|
+
return res;
|
|
49
66
|
}
|
|
50
67
|
}
|
|
51
68
|
`,
|
|
52
|
-
serviceBindings:
|
|
53
|
-
acc
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
serviceBindings: {
|
|
70
|
+
...functions.reduce((acc, fn) => {
|
|
71
|
+
acc[`${workspaceId}-${projectId}-${fn.name}`] = fn.name;
|
|
72
|
+
return acc;
|
|
73
|
+
}, {}),
|
|
74
|
+
...(includeDashboard ? { DASHBOARD: "vm-dashboard-worker" } : {})
|
|
75
|
+
}
|
|
56
76
|
});
|
|
57
77
|
|
|
58
78
|
const runInit = async () => {
|
|
@@ -138,7 +158,7 @@ const runInit = async () => {
|
|
|
138
158
|
|
|
139
159
|
if (stripeRes.ok) {
|
|
140
160
|
const stripeData = await stripeRes.json();
|
|
141
|
-
console.log(`\n\x1b[34m${stripeData.url}\x1b[0m\n`);
|
|
161
|
+
console.log(`\n\x1b]8;;${stripeData.url}\x1b\\\x1b[34m${stripeData.url}\x1b[0m\x1b]8;;\x1b\\\n`);
|
|
142
162
|
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
143
163
|
exec(`${openCmd} "${stripeData.url}"`);
|
|
144
164
|
await input({ message: 'Press Enter once verified to continue:' });
|
|
@@ -401,10 +421,21 @@ const runDev = async () => {
|
|
|
401
421
|
const shadowSource = fs.readFileSync(path.join(__dirname, 'shadow-dos.js'), 'utf-8');
|
|
402
422
|
fs.writeFileSync(path.join(WORK_DIR, 'shadow-dos.mjs'), shadowSource);
|
|
403
423
|
|
|
424
|
+
const dashSource = fs.readFileSync(path.join(__dirname, 'dashboard-worker.js'), 'utf-8');
|
|
425
|
+
fs.writeFileSync(path.join(WORK_DIR, 'dashboard-worker.mjs'), dashSource);
|
|
426
|
+
|
|
404
427
|
const mfPort = process.env.PORT ? parseInt(process.env.PORT) : (config.port || 8787);
|
|
405
428
|
|
|
406
429
|
const miniflareWorkers = [
|
|
407
430
|
buildProxyDispatcher(config.functions, workspaceId, projectId),
|
|
431
|
+
{
|
|
432
|
+
name: "vm-dashboard-worker",
|
|
433
|
+
modules: true,
|
|
434
|
+
scriptPath: path.join(WORK_DIR, 'dashboard-worker.mjs'),
|
|
435
|
+
d1Databases: resourcesConfig.d1Databases,
|
|
436
|
+
kvNamespaces: resourcesConfig.kvNamespaces,
|
|
437
|
+
r2Buckets: resourcesConfig.r2Buckets
|
|
438
|
+
},
|
|
408
439
|
{
|
|
409
440
|
name: "vm-shadow-worker",
|
|
410
441
|
modules: true,
|
|
@@ -436,9 +467,14 @@ const runDev = async () => {
|
|
|
436
467
|
r2Persist: path.join(DATA_DIR, 'bucket')
|
|
437
468
|
});
|
|
438
469
|
await mf.ready;
|
|
439
|
-
console.log('
|
|
470
|
+
console.log('\nLocal endpoints available:');
|
|
471
|
+
|
|
472
|
+
const dashboardUrl = `http://dashboard.localhost:${mfPort}`;
|
|
473
|
+
console.log(` \x1b]8;;${dashboardUrl}\x1b\\${dashboardUrl}\x1b]8;;\x1b\\ \x1b[32m[Dashboard]\x1b[0m`);
|
|
474
|
+
|
|
440
475
|
config.functions.forEach(fn => {
|
|
441
|
-
|
|
476
|
+
const endpointUrl = `http://${workspaceId}-${projectId}-${fn.name}.localhost:${mfPort}`;
|
|
477
|
+
console.log(` \x1b]8;;${endpointUrl}\x1b\\${endpointUrl}\x1b]8;;\x1b\\`);
|
|
442
478
|
});
|
|
443
479
|
|
|
444
480
|
setInterval(() => {
|
|
@@ -489,6 +525,100 @@ const runDev = async () => {
|
|
|
489
525
|
});
|
|
490
526
|
};
|
|
491
527
|
|
|
528
|
+
const runTest = async () => {
|
|
529
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
530
|
+
throw new Error("Missing vm.json in project root. Run 'vm init' to scaffold a new project workspace.");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const testArgs = process.argv.slice(3);
|
|
534
|
+
console.log('\x1b[36mInitializing Ephemeral Target Engine for Integration Tests...\x1b[0m');
|
|
535
|
+
|
|
536
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
537
|
+
if (!fs.existsSync(WORK_DIR)) fs.mkdirSync(WORK_DIR, { recursive: true });
|
|
538
|
+
|
|
539
|
+
const sharedBindings = fs.existsSync(path.resolve('.env'))
|
|
540
|
+
? dotenv.parse(fs.readFileSync(path.resolve('.env')))
|
|
541
|
+
: {};
|
|
542
|
+
|
|
543
|
+
const resourcesConfig = buildResourcesConfig();
|
|
544
|
+
|
|
545
|
+
const entryPoints = config.functions.reduce((acc, fn) => {
|
|
546
|
+
acc[`${fn.name}-out`] = path.resolve(fn.entry);
|
|
547
|
+
return acc;
|
|
548
|
+
}, {});
|
|
549
|
+
|
|
550
|
+
const workspaceId = config.workspaceId || "ws_local";
|
|
551
|
+
const projectId = config.projectId || "prj_local";
|
|
552
|
+
|
|
553
|
+
config.functions.forEach(fn => {
|
|
554
|
+
const relativeTarget = `./${fn.name}-out.mjs`;
|
|
555
|
+
fs.writeFileSync(path.join(WORK_DIR, `${fn.name}-shim.mjs`), generateShim(relativeTarget, workspaceId, projectId, fn.name));
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const builder = await esbuild.context({
|
|
559
|
+
entryPoints,
|
|
560
|
+
bundle: true,
|
|
561
|
+
format: 'esm',
|
|
562
|
+
outdir: WORK_DIR,
|
|
563
|
+
outExtension: { '.js': '.mjs' },
|
|
564
|
+
external: ['cloudflare:*'],
|
|
565
|
+
logLevel: 'silent'
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await builder.rebuild();
|
|
569
|
+
|
|
570
|
+
const shadowSource = fs.readFileSync(path.join(__dirname, 'shadow-dos.js'), 'utf-8');
|
|
571
|
+
fs.writeFileSync(path.join(WORK_DIR, 'shadow-dos.mjs'), shadowSource);
|
|
572
|
+
|
|
573
|
+
const mfPort = process.env.PORT ? parseInt(process.env.PORT) : 8787;
|
|
574
|
+
|
|
575
|
+
// Exact parity workers but omitting the Dashboard intentionally
|
|
576
|
+
const miniflareWorkers = [
|
|
577
|
+
buildProxyDispatcher(config.functions, workspaceId, projectId, false),
|
|
578
|
+
{
|
|
579
|
+
name: "vm-shadow-worker",
|
|
580
|
+
modules: true,
|
|
581
|
+
scriptPath: path.join(WORK_DIR, 'shadow-dos.mjs'),
|
|
582
|
+
bindings: { PORT: mfPort },
|
|
583
|
+
durableObjects: { LocalTaskManagerDO: "LocalTaskManagerDO", LocalChannelRoomDO: "LocalChannelRoomDO" }
|
|
584
|
+
},
|
|
585
|
+
...config.functions.map(fn => ({
|
|
586
|
+
name: fn.name,
|
|
587
|
+
modules: true,
|
|
588
|
+
scriptPath: path.join(WORK_DIR, `${fn.name}-shim.mjs`),
|
|
589
|
+
bindings: sharedBindings,
|
|
590
|
+
...resourcesConfig,
|
|
591
|
+
durableObjects: {
|
|
592
|
+
TASK_DO: { className: "LocalTaskManagerDO", scriptName: "vm-shadow-worker" },
|
|
593
|
+
CHANNEL_DO: { className: "LocalChannelRoomDO", scriptName: "vm-shadow-worker" }
|
|
594
|
+
}
|
|
595
|
+
}))
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
// IN-MEMORY EMULATOR (NO PERSIST PATHS)
|
|
599
|
+
const mf = new Emulator({
|
|
600
|
+
workers: miniflareWorkers,
|
|
601
|
+
port: mfPort
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await mf.ready;
|
|
605
|
+
console.log('\x1b[32m✔ Ephemeral environment completely provisioned and bound.\x1b[0m\\n');
|
|
606
|
+
|
|
607
|
+
const { spawn } = await import('child_process');
|
|
608
|
+
|
|
609
|
+
const testProcess = spawn('npx', ['vitest', 'run', ...testArgs], {
|
|
610
|
+
stdio: 'inherit',
|
|
611
|
+
env: { ...process.env, VMLIVE_TESTING: "true" }
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
testProcess.on('exit', async (code) => {
|
|
615
|
+
console.log('\\n\x1b[36mTerminating ephemeral engine execution context...\x1b[0m');
|
|
616
|
+
await builder.dispose();
|
|
617
|
+
await mf.dispose();
|
|
618
|
+
process.exit(code || 0);
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
|
|
492
622
|
const runDeploy = async () => {
|
|
493
623
|
console.log('\x1b[36mDeploying...\x1b[0m');
|
|
494
624
|
|
|
@@ -633,7 +763,7 @@ const runLogin = async () => {
|
|
|
633
763
|
const PORT = server.address().port;
|
|
634
764
|
const EDITOR_URL = process.env.EDITOR_URL || 'http://localhost:5174';
|
|
635
765
|
const authUrl = `${EDITOR_URL}/?challenge=${codeChallenge}&port=${PORT}`;
|
|
636
|
-
console.log(`\x1b[34mOpening browser to:
|
|
766
|
+
console.log(`\x1b[34mOpening browser to: \x1b]8;;${authUrl}\x1b\\${authUrl}\x1b]8;;\x1b\\\x1b[0m`);
|
|
637
767
|
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
638
768
|
exec(`${openCmd} "${authUrl}"`);
|
|
639
769
|
});
|
|
@@ -645,15 +775,17 @@ const main = async () => {
|
|
|
645
775
|
runInit();
|
|
646
776
|
} else if (command === 'add') {
|
|
647
777
|
await runAdd();
|
|
648
|
-
} else if (command === 'dev'
|
|
778
|
+
} else if (command === 'dev') {
|
|
649
779
|
await runDev();
|
|
780
|
+
} else if (command === 'test') {
|
|
781
|
+
await runTest();
|
|
650
782
|
} else if (command === 'login') {
|
|
651
783
|
await runLogin();
|
|
652
784
|
} else if (command === 'deploy') {
|
|
653
785
|
await runDeploy();
|
|
654
786
|
} else {
|
|
655
|
-
console.error(`\x1b[31m❌ Unknown command: ${command}\x1b[0m`);
|
|
656
|
-
console.log('Usage: vm init | vm add | vm dev | vm login | vm deploy');
|
|
787
|
+
console.error(`\x1b[31m❌ Unknown command: ${command || 'missing'}\x1b[0m`);
|
|
788
|
+
console.log('Usage: vm init | vm add | vm dev | vm test | vm login | vm deploy');
|
|
657
789
|
process.exit(1);
|
|
658
790
|
}
|
|
659
791
|
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const UI_HTML = `
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>vm.live — Local Dashboard</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
body { font-family: 'Inter', sans-serif; background-color: #09090b; color: #f4f4f5; }
|
|
11
|
+
.tab-active { border-bottom: 2px solid #10b981; color: #10b981; }
|
|
12
|
+
.tab-inactive { color: #a1a1aa; }
|
|
13
|
+
.tab-inactive:hover { color: #f4f4f5; }
|
|
14
|
+
pre { background: #18181b; padding: 1rem; border-radius: 0.5rem; border: 1px solid #27272a; overflow-x: auto; font-size: 0.875rem; color: #a1a1aa; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body class="h-screen flex flex-col">
|
|
18
|
+
<header class="border-b border-zinc-800 bg-zinc-950 p-4 shrink-0 flex items-center justify-between">
|
|
19
|
+
<div class="flex items-center gap-3">
|
|
20
|
+
<div class="w-8 h-8 rounded-full bg-emerald-500/10 text-emerald-500 flex items-center justify-center font-bold">V</div>
|
|
21
|
+
<h1 class="font-semibold tracking-tight">Local Sandbox</h1>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex gap-6 text-sm font-medium">
|
|
24
|
+
<button onclick="switchTab('db')" id="tab-db" class="tab-active pb-4 -mb-4">Database (D1)</button>
|
|
25
|
+
<button onclick="switchTab('kv')" id="tab-kv" class="tab-inactive pb-4 -mb-4">Global KV</button>
|
|
26
|
+
<button onclick="switchTab('r2')" id="tab-r2" class="tab-inactive pb-4 -mb-4">Storage (R2)</button>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<main class="flex-1 overflow-hidden relative">
|
|
31
|
+
<!-- D1 View -->
|
|
32
|
+
<div id="view-db" class="absolute inset-0 flex flex-col p-6 gap-4">
|
|
33
|
+
<h2 class="text-lg font-semibold">Execute SQL Query</h2>
|
|
34
|
+
<div class="flex gap-2">
|
|
35
|
+
<input type="text" id="sql-input" placeholder="SELECT * FROM users;" class="flex-1 bg-zinc-900 border border-zinc-800 rounded-md px-4 py-2 font-mono text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500">
|
|
36
|
+
<button onclick="runQuery()" class="bg-emerald-600 hover:bg-emerald-500 text-white px-6 py-2 rounded-md font-medium transition-colors">Run</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="flex-1 overflow-auto rounded-md border border-zinc-800 bg-zinc-900/50">
|
|
39
|
+
<pre id="db-results" class="h-full m-0 rounded-none border-0 text-sm">Awaiting query...</pre>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- KV View -->
|
|
44
|
+
<div id="view-kv" class="absolute inset-0 flex flex-col p-6 gap-4 hidden">
|
|
45
|
+
<div class="flex items-center justify-between">
|
|
46
|
+
<h2 class="text-lg font-semibold">Key-Value Pairs</h2>
|
|
47
|
+
<button onclick="loadKV()" class="text-xs bg-zinc-800 hover:bg-zinc-700 px-3 py-1.5 rounded-md transition-colors">Refresh</button>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="flex-1 overflow-auto rounded-md border border-zinc-800 bg-zinc-900/50">
|
|
50
|
+
<pre id="kv-results" class="h-full m-0 rounded-none border-0 text-sm">Loading keys...</pre>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<!-- R2 View -->
|
|
55
|
+
<div id="view-r2" class="absolute inset-0 flex flex-col p-6 gap-4 hidden">
|
|
56
|
+
<div class="flex items-center justify-between">
|
|
57
|
+
<h2 class="text-lg font-semibold">Object Storage</h2>
|
|
58
|
+
<button onclick="loadR2()" class="text-xs bg-zinc-800 hover:bg-zinc-700 px-3 py-1.5 rounded-md transition-colors">Refresh</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="flex-1 overflow-auto rounded-md border border-zinc-800 bg-zinc-900/50">
|
|
61
|
+
<pre id="r2-results" class="h-full m-0 rounded-none border-0 text-sm">Loading objects...</pre>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</main>
|
|
65
|
+
|
|
66
|
+
<script>
|
|
67
|
+
const views = ['db', 'kv', 'r2'];
|
|
68
|
+
function switchTab(target) {
|
|
69
|
+
views.forEach(v => {
|
|
70
|
+
document.getElementById('view-' + v).classList.add('hidden');
|
|
71
|
+
document.getElementById('tab-' + v).className = 'tab-inactive pb-4 -mb-4';
|
|
72
|
+
});
|
|
73
|
+
document.getElementById('view-' + target).classList.remove('hidden');
|
|
74
|
+
document.getElementById('tab-' + target).className = 'tab-active pb-4 -mb-4';
|
|
75
|
+
|
|
76
|
+
if (target === 'kv') loadKV();
|
|
77
|
+
if (target === 'r2') loadR2();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runQuery() {
|
|
81
|
+
const q = document.getElementById('sql-input').value;
|
|
82
|
+
if (!q) return;
|
|
83
|
+
document.getElementById('db-results').innerText = 'Executing...';
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch('/api/db', { method: 'POST', body: JSON.stringify({ query: q }) });
|
|
86
|
+
const json = await res.json();
|
|
87
|
+
document.getElementById('db-results').innerText = JSON.stringify(json, null, 2);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
document.getElementById('db-results').innerText = 'Network Error: ' + e.message;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadKV() {
|
|
94
|
+
document.getElementById('kv-results').innerText = 'Fetching keys...';
|
|
95
|
+
const res = await fetch('/api/kv');
|
|
96
|
+
const json = await res.json();
|
|
97
|
+
document.getElementById('kv-results').innerText = JSON.stringify(json, null, 2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function loadR2() {
|
|
101
|
+
document.getElementById('r2-results').innerText = 'Fetching objects...';
|
|
102
|
+
const res = await fetch('/api/bucket');
|
|
103
|
+
const json = await res.json();
|
|
104
|
+
document.getElementById('r2-results').innerText = JSON.stringify(json, null, 2);
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
\`;
|
|
110
|
+
|
|
111
|
+
export default {
|
|
112
|
+
async fetch(req, env) {
|
|
113
|
+
const url = new URL(req.url);
|
|
114
|
+
|
|
115
|
+
if (req.method === 'POST' && url.pathname === '/api/db') {
|
|
116
|
+
if (!env.DB) return Response.json({ error: "DB binding not configured in emulator" }, { status: 400 });
|
|
117
|
+
const body = await req.json();
|
|
118
|
+
try {
|
|
119
|
+
const res = await env.DB.prepare(body.query).all();
|
|
120
|
+
return Response.json(res);
|
|
121
|
+
} catch(e) {
|
|
122
|
+
return Response.json({ error: e.message }, { status: 400 });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (req.method === 'GET' && url.pathname === '/api/kv') {
|
|
127
|
+
if (!env.__RAW_KV) return Response.json({ error: "KV binding not configured in emulator" }, { status: 400 });
|
|
128
|
+
try {
|
|
129
|
+
const res = await env.__RAW_KV.list();
|
|
130
|
+
return Response.json(res);
|
|
131
|
+
} catch(e) {
|
|
132
|
+
return Response.json({ error: e.message }, { status: 400 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (req.method === 'GET' && url.pathname === '/api/bucket') {
|
|
137
|
+
if (!env.R2) return Response.json({ error: "BUCKET binding not configured in emulator" }, { status: 400 });
|
|
138
|
+
try {
|
|
139
|
+
const res = await env.R2.list();
|
|
140
|
+
return Response.json(res);
|
|
141
|
+
} catch(e) {
|
|
142
|
+
return Response.json({ error: e.message }, { status: 400 });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Default to the Editor UI Payload
|
|
147
|
+
return new Response(UI_HTML, { headers: { 'Content-Type': 'text/html; charset=utf-8' }});
|
|
148
|
+
}
|
|
149
|
+
};
|
package/src/string-shim.js
CHANGED
|
@@ -3,8 +3,8 @@ import * as UserCode from '${outFileUrl}';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
async fetch(req, env, ctx) {
|
|
6
|
-
// Isolate the global KV
|
|
7
|
-
const { __RAW_KV, ...safeEnv } = env;
|
|
6
|
+
// Isolate the global KV and Storage
|
|
7
|
+
const { __RAW_KV, R2, ...safeEnv } = env;
|
|
8
8
|
const KV_PREFIX = \`${workspaceId}:${projectId}:\`;
|
|
9
9
|
|
|
10
10
|
// 1. Air-Gapped Virtual KV Multiplexer
|
|
@@ -80,9 +80,12 @@ export default {
|
|
|
80
80
|
const Channels = {
|
|
81
81
|
accept: async (roomName, metadata) => {
|
|
82
82
|
console.log(\`\\x1b[36m[\${env.PROJECT_ID || '${projectId}'}]\\x1b[0m 🔌 Channels.accept: [\${roomName}]\`, metadata);
|
|
83
|
-
|
|
83
|
+
if (req.headers.get("Upgrade") !== "websocket") {
|
|
84
|
+
return new Response("Upgrade Required: Endpoint expects a WebSocket connection.", { status: 426 });
|
|
85
|
+
}
|
|
86
|
+
const doReq = new Request("http://platform/.internal", { headers: req.headers });
|
|
84
87
|
const stub = env.CHANNEL_DO.idFromName(roomName);
|
|
85
|
-
return await env.CHANNEL_DO.get(stub).fetch(
|
|
88
|
+
return await env.CHANNEL_DO.get(stub).fetch(doReq);
|
|
86
89
|
},
|
|
87
90
|
broadcast: async (roomName, payload) => {
|
|
88
91
|
console.log(\`\\x1b[36m[\${env.PROJECT_ID || '${projectId}'}]\\x1b[0m 📡 Channels.broadcast: [\${roomName}]\`, payload);
|
|
@@ -110,6 +113,7 @@ export default {
|
|
|
110
113
|
const customEnv = {
|
|
111
114
|
...safeEnv,
|
|
112
115
|
KV: virtualKV,
|
|
116
|
+
BUCKET: R2,
|
|
113
117
|
Tasks,
|
|
114
118
|
Channels,
|
|
115
119
|
Discord,
|