@scriptdb/vm 1.0.0
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 +96 -0
- package/dist/index.js +111 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ScriptDB VM
|
|
2
|
+
|
|
3
|
+
Virtual Machine package for executing TypeScript and JavaScript code in a secure sandboxed environment.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @scriptdb/vm
|
|
9
|
+
# or
|
|
10
|
+
yarn add @scriptdb/vm
|
|
11
|
+
# or
|
|
12
|
+
bun add @scriptdb/vm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { VM } from '@scriptdb/vm';
|
|
19
|
+
|
|
20
|
+
// Create a VM instance
|
|
21
|
+
const vm = new VM();
|
|
22
|
+
|
|
23
|
+
// Execute TypeScript code
|
|
24
|
+
const result = await vm.run(`
|
|
25
|
+
const greet = (name: string) => \`Hello, \${name}!\`;
|
|
26
|
+
greet("World");
|
|
27
|
+
`);
|
|
28
|
+
|
|
29
|
+
console.log(result); // "Hello, World!"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API Reference
|
|
33
|
+
|
|
34
|
+
### Constructor
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
new VM(options?)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Creates a new VM instance with the specified options.
|
|
41
|
+
|
|
42
|
+
#### Options
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface VMOptions {
|
|
46
|
+
language?: 'ts' | 'js'; // Code language (default: 'ts')
|
|
47
|
+
registerModules?: { [key: string]: any }; // Modules to register in the VM context
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Methods
|
|
52
|
+
|
|
53
|
+
#### run(code, options?)
|
|
54
|
+
|
|
55
|
+
Executes code in the VM context.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
await vm.run(code: string, options?: vm.RunningCodeOptions | string): Promise<any>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `code` (string): The TypeScript or JavaScript code to execute
|
|
62
|
+
- `options` (optional): Execution options or filename
|
|
63
|
+
|
|
64
|
+
Returns a promise that resolves with the result of the code execution.
|
|
65
|
+
|
|
66
|
+
#### register(context)
|
|
67
|
+
|
|
68
|
+
Registers modules and values in the VM context.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
vm.register(context: { [key: string]: any }): void
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `context` (object): Object with keys that will be available in the VM context
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Install dependencies
|
|
80
|
+
bun install
|
|
81
|
+
|
|
82
|
+
# Run in development mode
|
|
83
|
+
bun run dev
|
|
84
|
+
|
|
85
|
+
# Build the package
|
|
86
|
+
bun run build:all
|
|
87
|
+
|
|
88
|
+
# Run tests
|
|
89
|
+
bun run test
|
|
90
|
+
|
|
91
|
+
# Run linting
|
|
92
|
+
bun run lint
|
|
93
|
+
|
|
94
|
+
# Type checking
|
|
95
|
+
bun run typecheck
|
|
96
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
var Bun = globalThis.Bun;
|
|
4
|
+
import vm from "vm";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import os from "os";
|
|
8
|
+
|
|
9
|
+
class VM {
|
|
10
|
+
transpiler;
|
|
11
|
+
ctx;
|
|
12
|
+
registerModules;
|
|
13
|
+
DATABASE_DIR = path.join(os.homedir(), ".scriptdb", "databases");
|
|
14
|
+
PLUGIN_DIR = path.join(os.homedir(), ".scriptdb", "plugins");
|
|
15
|
+
pkgPlugin = {};
|
|
16
|
+
constructor(options) {
|
|
17
|
+
if (!fs.existsSync(this.DATABASE_DIR)) {
|
|
18
|
+
fs.mkdirSync(this.DATABASE_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
if (!fs.existsSync(this.PLUGIN_DIR)) {
|
|
21
|
+
fs.mkdirSync(this.PLUGIN_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
const pkgPath = path.join(this.PLUGIN_DIR, "package.json");
|
|
24
|
+
if (fs.existsSync(pkgPath)) {
|
|
25
|
+
this.pkgPlugin = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
this.transpiler = new Bun.Transpiler({
|
|
28
|
+
loader: options?.language || "ts"
|
|
29
|
+
});
|
|
30
|
+
this.registerModules = options?.registerModules || {};
|
|
31
|
+
this.ctx = vm.createContext(this.registerModules);
|
|
32
|
+
}
|
|
33
|
+
register(context) {
|
|
34
|
+
this.registerModules = { ...this.registerModules, ...context };
|
|
35
|
+
this.ctx = vm.createContext(this.registerModules);
|
|
36
|
+
}
|
|
37
|
+
resolvePath(fileList, query) {
|
|
38
|
+
const aliases = { "@db": this.DATABASE_DIR };
|
|
39
|
+
let resolvedPath = query;
|
|
40
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
41
|
+
if (resolvedPath.startsWith(alias + "/")) {
|
|
42
|
+
resolvedPath = resolvedPath.replace(alias, target);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
resolvedPath = path.normalize(resolvedPath);
|
|
47
|
+
return fileList.find((file) => {
|
|
48
|
+
const normalizedFile = path.normalize(file);
|
|
49
|
+
const fileWithoutExt = normalizedFile.replace(/\.[^/.]+$/, "");
|
|
50
|
+
return normalizedFile === resolvedPath || fileWithoutExt === resolvedPath || normalizedFile === resolvedPath + ".ts" || normalizedFile === resolvedPath + ".js";
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async moduleLinker(specifier, referencingModule) {
|
|
54
|
+
const dbFiles = fs.readdirSync(this.DATABASE_DIR).filter((f) => f.endsWith(".ts")).map((f) => path.join(this.DATABASE_DIR, f));
|
|
55
|
+
const dbResult = this.resolvePath(dbFiles, specifier);
|
|
56
|
+
if (dbResult) {
|
|
57
|
+
try {
|
|
58
|
+
const actualModule = await import(dbResult);
|
|
59
|
+
const exportNames = Object.keys(actualModule);
|
|
60
|
+
return new vm.SyntheticModule(exportNames, function() {
|
|
61
|
+
exportNames.forEach((key) => {
|
|
62
|
+
this.setExport(key, actualModule[key]);
|
|
63
|
+
});
|
|
64
|
+
}, { identifier: specifier, context: referencingModule.context });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Failed to load database module ${specifier}:`, err);
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const allowedPackages = Object.keys(this.pkgPlugin.dependencies || {});
|
|
71
|
+
if (allowedPackages.includes(specifier)) {
|
|
72
|
+
try {
|
|
73
|
+
const modulePath = path.join(this.PLUGIN_DIR, "node_modules", specifier);
|
|
74
|
+
const actualModule = await import(modulePath);
|
|
75
|
+
const exportNames = Object.keys(actualModule);
|
|
76
|
+
return new vm.SyntheticModule(exportNames, function() {
|
|
77
|
+
exportNames.forEach((key) => {
|
|
78
|
+
this.setExport(key, actualModule[key]);
|
|
79
|
+
});
|
|
80
|
+
}, { identifier: specifier, context: referencingModule.context });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`Failed to load plugin module ${specifier}:`, err);
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`Module ${specifier} is not allowed or not found.`);
|
|
87
|
+
}
|
|
88
|
+
async run(code, options) {
|
|
89
|
+
const logs = [];
|
|
90
|
+
const customConsole = ["log", "error", "warn", "info", "debug", "trace"].reduce((acc, type) => {
|
|
91
|
+
acc[type] = (...args) => logs.push({ type, args });
|
|
92
|
+
return acc;
|
|
93
|
+
}, {});
|
|
94
|
+
this.register({
|
|
95
|
+
console: customConsole
|
|
96
|
+
});
|
|
97
|
+
const js = this.transpiler.transformSync(code);
|
|
98
|
+
const mod = new vm.SourceTextModule(js, { context: this.ctx, identifier: path.join(this.PLUGIN_DIR, "virtual-entry.js") });
|
|
99
|
+
await mod.link(this.moduleLinker.bind(this));
|
|
100
|
+
await mod.evaluate();
|
|
101
|
+
return {
|
|
102
|
+
namespace: mod.namespace,
|
|
103
|
+
logs
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
var src_default = VM;
|
|
108
|
+
export {
|
|
109
|
+
src_default as default,
|
|
110
|
+
VM
|
|
111
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scriptdb/vm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Virtual machine package for script database",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "bun --watch src/index.ts",
|
|
20
|
+
"build": "bun build src/index.ts --outdir dist --target bun --format esm --splitting",
|
|
21
|
+
"build:cjs": "bun build src/index.ts --outdir dist --target bun --format cjs --outfile dist/index.js",
|
|
22
|
+
"build:types": "tsc --emitDeclarationOnly --project tsconfig.build.json",
|
|
23
|
+
"build:all": "bun run build && bun run build:cjs && bun run build:types",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"lint": "bun run lint:src",
|
|
26
|
+
"lint:src": "eslint src --ext .ts,.tsx",
|
|
27
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"clean": "rm -rf dist"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "^1.3.2",
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
35
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
36
|
+
"bun-types": "latest",
|
|
37
|
+
"eslint": "^8.0.0",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|