@ricsam/quickjs-runtime 0.2.20 → 0.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/README.md CHANGED
@@ -1,49 +1,291 @@
1
1
  # @ricsam/quickjs-runtime
2
2
 
3
- Umbrella package that combines all APIs.
3
+ The recommended way to create QuickJS sandboxed runtimes with web-standard APIs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @ricsam/quickjs-runtime quickjs-emscripten
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createRuntime } from "@ricsam/quickjs-runtime";
15
+
16
+ const runtime = await createRuntime({
17
+ console: {
18
+ onEntry: (entry) => {
19
+ if (entry.type === "output") {
20
+ console.log(`[sandbox:${entry.level}]`, ...entry.args);
21
+ }
22
+ },
23
+ },
24
+ fetch: async (request) => fetch(request),
25
+ });
26
+
27
+ await runtime.eval(`
28
+ const response = await fetch("https://api.example.com/data");
29
+ const data = await response.json();
30
+ console.log("Fetched:", data);
31
+ `);
32
+
33
+ await runtime.dispose();
34
+ ```
35
+
36
+ ## API
37
+
38
+ ### createRuntime(options?)
39
+
40
+ Creates a fully configured QuickJS runtime with all WHATWG APIs.
4
41
 
5
42
  ```typescript
6
- import { setupRuntime } from "@ricsam/quickjs-runtime";
43
+ const runtime = await createRuntime({
44
+ // Memory limit in bytes
45
+ memoryLimit: 1024 * 1024 * 10, // 10MB
7
46
 
8
- const handle = setupRuntime(context, {
9
- // Fetch API - pass true for defaults or options object
10
- fetch: {
11
- onFetch: async (req) => fetch(req),
47
+ // Console output handler
48
+ console: {
49
+ onEntry: (entry) => { /* handle console output */ },
12
50
  },
13
- // File System API
51
+
52
+ // Fetch handler for outbound requests
53
+ fetch: async (request) => fetch(request),
54
+
55
+ // File system access
14
56
  fs: {
15
57
  getDirectory: async (path) => createNodeDirectoryHandle(`./sandbox${path}`),
16
58
  },
17
- // Console API - pass true for defaults or options with handlers
18
- console: {
19
- handlers: {
20
- onLog: (level, args) => console.log(`[${level}]`, ...args),
59
+
60
+ // ES module loader
61
+ moduleLoader: async (moduleName) => {
62
+ if (moduleName === "@/utils") {
63
+ return `export const add = (a, b) => a + b;`;
64
+ }
65
+ throw new Error(`Unknown module: ${moduleName}`);
66
+ },
67
+
68
+ // Custom host functions
69
+ customFunctions: {
70
+ hashPassword: {
71
+ fn: async (password) => Bun.password.hash(password),
72
+ async: true,
73
+ },
74
+ getConfig: {
75
+ fn: () => ({ environment: "production" }),
76
+ async: false,
21
77
  },
22
78
  },
23
- // Crypto API - pass true to enable
24
- crypto: true,
25
- // Encoding API (atob/btoa) - pass true to enable
26
- encoding: true,
79
+
80
+ // Enable test environment (describe, it, expect)
81
+ testEnvironment: {
82
+ onEvent: (event) => { /* handle test events */ },
83
+ },
84
+
85
+ // Playwright browser automation
86
+ playwright: {
87
+ page: playwrightPage,
88
+ baseUrl: "https://example.com",
89
+ },
27
90
  });
91
+ ```
92
+
93
+ ### RuntimeHandle
94
+
95
+ The returned handle provides:
96
+
97
+ ```typescript
98
+ interface RuntimeHandle {
99
+ // Unique runtime identifier
100
+ readonly id: string;
101
+
102
+ // Execute code as ES module (supports top-level await)
103
+ eval(code: string, filename?: string): Promise<void>;
28
104
 
29
- // Access individual handles
30
- handle.core; // CoreHandle (always present)
31
- handle.fetch; // FetchHandle (if enabled)
32
- handle.fs; // FsHandle (if enabled)
33
- handle.console; // ConsoleHandle (if enabled)
34
- handle.crypto; // CryptoHandle (if enabled)
35
- handle.encoding; // EncodingHandle (if enabled)
105
+ // Dispose all resources
106
+ dispose(): Promise<void>;
36
107
 
37
- handle.dispose(); // Cleanup all
108
+ // Sub-handles for specific features
109
+ readonly fetch: RuntimeFetchHandle;
110
+ readonly timers: RuntimeTimersHandle;
111
+ readonly console: RuntimeConsoleHandle;
112
+ readonly testEnvironment: RuntimeTestEnvironmentHandle;
113
+ readonly playwright: RuntimePlaywrightHandle;
114
+ }
38
115
  ```
39
116
 
40
- **Quick enable with defaults:**
117
+ ## Examples
118
+
119
+ ### HTTP Server
41
120
 
42
121
  ```typescript
43
- const handle = setupRuntime(context, {
44
- fetch: true, // Enable fetch with default options
45
- console: true, // Enable console with no handlers (silent)
46
- crypto: true, // Enable Web Crypto API
47
- encoding: true, // Enable atob/btoa
122
+ const runtime = await createRuntime({
123
+ console: { onEntry: (e) => e.type === "output" && console.log(...e.args) },
48
124
  });
49
- ```
125
+
126
+ await runtime.eval(`
127
+ serve({
128
+ fetch(request) {
129
+ const url = new URL(request.url);
130
+ return Response.json({ path: url.pathname });
131
+ },
132
+ });
133
+ `);
134
+
135
+ // Dispatch requests to the sandboxed server
136
+ const response = await runtime.fetch.dispatchRequest(
137
+ new Request("http://localhost/api/users")
138
+ );
139
+ console.log(await response.json()); // { path: "/api/users" }
140
+
141
+ await runtime.dispose();
142
+ ```
143
+
144
+ ### Running Tests
145
+
146
+ ```typescript
147
+ const runtime = await createRuntime({
148
+ testEnvironment: {
149
+ onEvent: (event) => {
150
+ if (event.type === "testEnd") {
151
+ const icon = event.test.status === "pass" ? "✓" : "✗";
152
+ console.log(`${icon} ${event.test.fullName}`);
153
+ }
154
+ },
155
+ },
156
+ });
157
+
158
+ await runtime.eval(`
159
+ describe("Math", () => {
160
+ it("adds numbers", () => {
161
+ expect(1 + 1).toBe(2);
162
+ });
163
+ });
164
+ `);
165
+
166
+ const results = await runtime.testEnvironment.runTests();
167
+ console.log(`${results.passed}/${results.total} passed`);
168
+
169
+ await runtime.dispose();
170
+ ```
171
+
172
+ ### Browser Automation with Playwright
173
+
174
+ ```typescript
175
+ import { chromium } from "playwright";
176
+
177
+ const browser = await chromium.launch();
178
+ const page = await browser.newPage();
179
+
180
+ const runtime = await createRuntime({
181
+ testEnvironment: true,
182
+ playwright: {
183
+ page,
184
+ baseUrl: "https://example.com",
185
+ },
186
+ });
187
+
188
+ await runtime.eval(`
189
+ describe("Homepage", () => {
190
+ it("displays welcome message", async () => {
191
+ await page.goto("/");
192
+ await expect(page.getByRole("heading")).toContainText("Welcome");
193
+ });
194
+ });
195
+ `);
196
+
197
+ await runtime.testEnvironment.runTests();
198
+ await runtime.dispose();
199
+ await browser.close();
200
+ ```
201
+
202
+ ### Custom Functions
203
+
204
+ ```typescript
205
+ const runtime = await createRuntime({
206
+ customFunctions: {
207
+ // Async function
208
+ hashPassword: {
209
+ fn: async (password) => Bun.password.hash(password),
210
+ async: true,
211
+ },
212
+ // Sync function
213
+ generateId: {
214
+ fn: () => crypto.randomUUID(),
215
+ async: false,
216
+ },
217
+ },
218
+ });
219
+
220
+ await runtime.eval(`
221
+ const hash = await hashPassword("secret123");
222
+ const id = generateId();
223
+ console.log({ hash, id });
224
+ `);
225
+ ```
226
+
227
+ ### ES Modules
228
+
229
+ ```typescript
230
+ const runtime = await createRuntime({
231
+ moduleLoader: async (moduleName) => {
232
+ const modules = {
233
+ "@/utils": `export const double = (n) => n * 2;`,
234
+ "@/config": `export default { apiUrl: "https://api.example.com" };`,
235
+ };
236
+ if (moduleName in modules) {
237
+ return modules[moduleName];
238
+ }
239
+ throw new Error(`Module not found: ${moduleName}`);
240
+ },
241
+ });
242
+
243
+ await runtime.eval(`
244
+ import { double } from "@/utils";
245
+ import config from "@/config";
246
+
247
+ console.log(double(21)); // 42
248
+ console.log(config.apiUrl);
249
+ `);
250
+ ```
251
+
252
+ ## Included APIs
253
+
254
+ When you use `createRuntime()`, the following globals are automatically available in the sandbox:
255
+
256
+ - **Console**: `console.log`, `console.warn`, `console.error`, etc.
257
+ - **Fetch**: `fetch`, `Request`, `Response`, `Headers`, `FormData`, `AbortController`
258
+ - **Server**: `serve()` with WebSocket support
259
+ - **Crypto**: `crypto.getRandomValues()`, `crypto.randomUUID()`, `crypto.subtle`
260
+ - **Encoding**: `atob()`, `btoa()`
261
+ - **Timers**: `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`
262
+ - **Streams**: `ReadableStream`, `WritableStream`, `TransformStream`
263
+ - **Blob/File**: `Blob`, `File`
264
+ - **Path**: `path.join()`, `path.resolve()`, etc.
265
+
266
+ Optional (when configured):
267
+ - **File System**: `fs.getDirectory()` (requires `fs` option)
268
+ - **Test Environment**: `describe`, `it`, `expect` (requires `testEnvironment` option)
269
+ - **Playwright**: `page` object (requires `playwright` option)
270
+
271
+ ## Security
272
+
273
+ - **No automatic network access** - `fetch` option must be explicitly provided
274
+ - **File system isolation** - `fs.getDirectory` controls all path access
275
+ - **Memory limits** - Use `memoryLimit` option to prevent resource exhaustion
276
+ - **No access to host** - Code runs in isolated QuickJS VM
277
+
278
+ ## Advanced: Low-level API
279
+
280
+ For advanced use cases requiring direct context manipulation, you can use the low-level `setupRuntime()` function or individual package setup functions. See the individual package READMEs for details:
281
+
282
+ - [@ricsam/quickjs-core](../core)
283
+ - [@ricsam/quickjs-console](../console)
284
+ - [@ricsam/quickjs-fetch](../fetch)
285
+ - [@ricsam/quickjs-fs](../fs)
286
+ - [@ricsam/quickjs-crypto](../crypto)
287
+ - [@ricsam/quickjs-encoding](../encoding)
288
+ - [@ricsam/quickjs-timers](../timers)
289
+ - [@ricsam/quickjs-path](../path)
290
+ - [@ricsam/quickjs-test-environment](../test-environment)
291
+ - [@ricsam/quickjs-playwright](../playwright)
@@ -0,0 +1,329 @@
1
+ // @bun @bun-cjs
2
+ (function(exports, require, module, __filename, __dirname) {var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
7
+ var __toCommonJS = (from) => {
8
+ var entry = __moduleCache.get(from), desc;
9
+ if (entry)
10
+ return entry;
11
+ entry = __defProp({}, "__esModule", { value: true });
12
+ if (from && typeof from === "object" || typeof from === "function")
13
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ }));
17
+ __moduleCache.set(from, entry);
18
+ return entry;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true,
25
+ configurable: true,
26
+ set: (newValue) => all[name] = () => newValue
27
+ });
28
+ };
29
+
30
+ // packages/runtime/src/create-runtime.ts
31
+ var exports_create_runtime = {};
32
+ __export(exports_create_runtime, {
33
+ createRuntime: () => createRuntime
34
+ });
35
+ module.exports = __toCommonJS(exports_create_runtime);
36
+ var import_quickjs_emscripten = require("quickjs-emscripten");
37
+ var import_quickjs_core = require("@ricsam/quickjs-core");
38
+ var import_quickjs_fetch = require("@ricsam/quickjs-fetch");
39
+ var import_quickjs_fs = require("@ricsam/quickjs-fs");
40
+ var import_quickjs_console = require("@ricsam/quickjs-console");
41
+ var import_quickjs_encoding = require("@ricsam/quickjs-encoding");
42
+ var import_quickjs_timers = require("@ricsam/quickjs-timers");
43
+ var import_quickjs_crypto = require("@ricsam/quickjs-crypto");
44
+ var import_quickjs_test_environment = require("@ricsam/quickjs-test-environment");
45
+ var import_quickjs_playwright = require("@ricsam/quickjs-playwright");
46
+ async function createRuntime(options) {
47
+ const opts = options ?? {};
48
+ const id = crypto.randomUUID();
49
+ const needsAsync = opts.moduleLoader !== undefined;
50
+ let context;
51
+ let runtime;
52
+ let isAsyncContext = false;
53
+ if (needsAsync) {
54
+ const asyncContext = await import_quickjs_emscripten.newAsyncContext();
55
+ context = asyncContext;
56
+ runtime = asyncContext.runtime;
57
+ isAsyncContext = true;
58
+ if (opts.moduleLoader) {
59
+ const moduleLoader = opts.moduleLoader;
60
+ runtime.setModuleLoader(async (moduleName) => {
61
+ try {
62
+ const code = await moduleLoader(moduleName);
63
+ return code;
64
+ } catch (error) {
65
+ return { error };
66
+ }
67
+ });
68
+ }
69
+ } else {
70
+ const QuickJS = await import_quickjs_emscripten.getQuickJS();
71
+ runtime = QuickJS.newRuntime();
72
+ context = runtime.newContext();
73
+ }
74
+ if (opts.memoryLimit) {
75
+ runtime.setMemoryLimit(opts.memoryLimit);
76
+ }
77
+ const stateMap = import_quickjs_core.createStateMap();
78
+ const coreHandle = import_quickjs_core.setupCore(context, { stateMap });
79
+ const consoleHandle = import_quickjs_console.setupConsole(context, {
80
+ ...opts.console,
81
+ stateMap,
82
+ coreHandle
83
+ });
84
+ const encodingHandle = import_quickjs_encoding.setupEncoding(context, { stateMap, coreHandle });
85
+ const timersHandle = import_quickjs_timers.setupTimers(context, { stateMap, coreHandle });
86
+ const cryptoHandle = import_quickjs_crypto.setupCrypto(context, { stateMap, coreHandle });
87
+ let fetchHandle;
88
+ if (opts.fetch) {
89
+ const fetchCallback = opts.fetch;
90
+ fetchHandle = import_quickjs_fetch.setupFetch(context, {
91
+ stateMap,
92
+ coreHandle,
93
+ onFetch: async (request) => {
94
+ return Promise.resolve(fetchCallback(request));
95
+ }
96
+ });
97
+ } else {
98
+ fetchHandle = import_quickjs_fetch.setupFetch(context, { stateMap, coreHandle });
99
+ }
100
+ let fsHandle;
101
+ if (opts.fs) {
102
+ fsHandle = import_quickjs_fs.setupFs(context, {
103
+ ...opts.fs,
104
+ stateMap,
105
+ coreHandle
106
+ });
107
+ }
108
+ if (opts.customFunctions) {
109
+ for (const [name, def] of Object.entries(opts.customFunctions)) {
110
+ if (def.async) {
111
+ const fn = import_quickjs_core.defineAsyncFunction(context, name, async (...args) => {
112
+ return def.fn(...args);
113
+ });
114
+ context.setProp(context.global, name, fn);
115
+ fn.dispose();
116
+ } else {
117
+ const fn = import_quickjs_core.defineFunction(context, name, (...args) => {
118
+ return def.fn(...args);
119
+ });
120
+ context.setProp(context.global, name, fn);
121
+ fn.dispose();
122
+ }
123
+ }
124
+ }
125
+ let testEnvHandle;
126
+ if (opts.testEnvironment) {
127
+ const testEnvOpts = typeof opts.testEnvironment === "object" ? opts.testEnvironment : {};
128
+ testEnvHandle = import_quickjs_test_environment.setupTestEnvironment(context, {
129
+ stateMap,
130
+ coreHandle,
131
+ onEvent: testEnvOpts.onEvent
132
+ });
133
+ }
134
+ let playwrightHandle;
135
+ if (opts.playwright) {
136
+ playwrightHandle = import_quickjs_playwright.setupPlaywright(context, {
137
+ ...opts.playwright,
138
+ stateMap,
139
+ coreHandle,
140
+ consoleCallbacks: opts.playwright.console ? opts.console : undefined
141
+ });
142
+ }
143
+ const runtimeFetchHandle = {
144
+ async dispatchRequest(request, _options) {
145
+ if (!fetchHandle) {
146
+ throw new Error("Fetch not configured");
147
+ }
148
+ return fetchHandle.dispatchRequest(request);
149
+ },
150
+ hasServeHandler() {
151
+ return fetchHandle?.hasServeHandler() ?? false;
152
+ },
153
+ hasActiveConnections() {
154
+ return fetchHandle?.hasActiveConnections() ?? false;
155
+ }
156
+ };
157
+ const runtimeTimersHandle = {
158
+ clearAll() {
159
+ timersHandle.clearAll();
160
+ }
161
+ };
162
+ const runtimeConsoleHandle = {
163
+ reset() {
164
+ consoleHandle.reset();
165
+ },
166
+ getTimers() {
167
+ return consoleHandle.getTimers();
168
+ },
169
+ getCounters() {
170
+ return consoleHandle.getCounters();
171
+ },
172
+ getGroupDepth() {
173
+ return consoleHandle.getGroupDepth();
174
+ }
175
+ };
176
+ const runtimeTestEnvironmentHandle = {
177
+ async runTests(_timeout) {
178
+ if (!testEnvHandle) {
179
+ throw new Error("Test environment not enabled. Set testEnvironment: true in createRuntime options.");
180
+ }
181
+ return testEnvHandle.run();
182
+ },
183
+ hasTests() {
184
+ return testEnvHandle?.hasTests() ?? false;
185
+ },
186
+ getTestCount() {
187
+ return testEnvHandle?.getTestCount() ?? 0;
188
+ },
189
+ reset() {
190
+ testEnvHandle?.reset();
191
+ }
192
+ };
193
+ const runtimePlaywrightHandle = {
194
+ getCollectedData() {
195
+ if (!playwrightHandle) {
196
+ return { browserConsoleLogs: [], networkRequests: [], networkResponses: [] };
197
+ }
198
+ return {
199
+ browserConsoleLogs: playwrightHandle.getBrowserConsoleLogs(),
200
+ networkRequests: playwrightHandle.getNetworkRequests(),
201
+ networkResponses: playwrightHandle.getNetworkResponses()
202
+ };
203
+ },
204
+ clearCollectedData() {
205
+ playwrightHandle?.clearCollected();
206
+ }
207
+ };
208
+ return {
209
+ id,
210
+ fetch: runtimeFetchHandle,
211
+ timers: runtimeTimersHandle,
212
+ console: runtimeConsoleHandle,
213
+ testEnvironment: runtimeTestEnvironmentHandle,
214
+ playwright: runtimePlaywrightHandle,
215
+ async eval(code, filename) {
216
+ if (isAsyncContext) {
217
+ const asyncContext = context;
218
+ const result = await asyncContext.evalCodeAsync(code, filename ?? "<eval>", {
219
+ type: "module"
220
+ });
221
+ if (result.error) {
222
+ const err = context.dump(result.error);
223
+ result.error.dispose();
224
+ throw new Error(`Eval failed: ${typeof err === "string" ? err : JSON.stringify(err)}`);
225
+ }
226
+ const promiseState = context.getPromiseState(result.value);
227
+ if (promiseState.type === "pending") {
228
+ const resolved = await context.resolvePromise(result.value);
229
+ result.value.dispose();
230
+ if (resolved.error) {
231
+ const err = context.dump(resolved.error);
232
+ resolved.error.dispose();
233
+ throw new Error(`Promise rejected: ${typeof err === "string" ? err : JSON.stringify(err)}`);
234
+ }
235
+ resolved.value.dispose();
236
+ } else {
237
+ result.value.dispose();
238
+ }
239
+ runtime.executePendingJobs();
240
+ } else {
241
+ const result = context.evalCode(code, filename ?? "<eval>", {
242
+ type: "module"
243
+ });
244
+ if (result.error) {
245
+ const err = context.dump(result.error);
246
+ result.error.dispose();
247
+ throw new Error(`Eval failed: ${typeof err === "string" ? err : JSON.stringify(err)}`);
248
+ }
249
+ const promiseState = context.getPromiseState(result.value);
250
+ if (promiseState.type === "pending") {
251
+ const resolved = await context.resolvePromise(result.value);
252
+ result.value.dispose();
253
+ runtime.executePendingJobs();
254
+ if (resolved.error) {
255
+ const err = context.dump(resolved.error);
256
+ resolved.error.dispose();
257
+ throw new Error(`Promise rejected: ${typeof err === "string" ? err : JSON.stringify(err)}`);
258
+ }
259
+ resolved.value.dispose();
260
+ } else {
261
+ result.value.dispose();
262
+ }
263
+ runtime.executePendingJobs();
264
+ }
265
+ },
266
+ async dispose() {
267
+ for (let i = 0;i < 1000; i++) {
268
+ if (!runtime.hasPendingJob())
269
+ break;
270
+ const result = runtime.executePendingJobs();
271
+ if (result.error)
272
+ result.error.dispose();
273
+ }
274
+ playwrightHandle?.dispose();
275
+ testEnvHandle?.dispose();
276
+ cryptoHandle.dispose();
277
+ timersHandle.dispose();
278
+ encodingHandle.dispose();
279
+ consoleHandle.dispose();
280
+ fsHandle?.dispose();
281
+ fetchHandle?.dispose();
282
+ for (let i = 0;i < 1000; i++) {
283
+ if (!runtime.hasPendingJob())
284
+ break;
285
+ const result = runtime.executePendingJobs();
286
+ if (result.error)
287
+ result.error.dispose();
288
+ }
289
+ import_quickjs_core.cleanupUnmarshaledHandles(context);
290
+ import_quickjs_core.clearAllInstanceState();
291
+ try {
292
+ const clearGlobals = context.evalCode(`
293
+ (function() {
294
+ const keysToDelete = Object.keys(globalThis).filter(k =>
295
+ k !== 'globalThis' && k !== 'undefined' && k !== 'NaN' && k !== 'Infinity'
296
+ );
297
+ for (const key of keysToDelete) {
298
+ try { globalThis[key] = undefined; } catch (e) {}
299
+ }
300
+ for (const key of keysToDelete) {
301
+ try { delete globalThis[key]; } catch (e) {}
302
+ }
303
+ return keysToDelete.length;
304
+ })()
305
+ `);
306
+ if (clearGlobals.error) {
307
+ clearGlobals.error.dispose();
308
+ } else {
309
+ clearGlobals.value.dispose();
310
+ }
311
+ } catch {}
312
+ for (let i = 0;i < 100; i++) {
313
+ if (!runtime.hasPendingJob())
314
+ break;
315
+ const result = runtime.executePendingJobs();
316
+ if (result.error)
317
+ result.error.dispose();
318
+ }
319
+ coreHandle.dispose();
320
+ context.dispose();
321
+ if (!isAsyncContext) {
322
+ runtime.dispose();
323
+ }
324
+ }
325
+ };
326
+ }
327
+ })
328
+
329
+ //# debugId=0041C068E93313DA64756E2164756E21