feat(ScriptExecutor): implement resolveIsolatedVmModule function and add unit tests
This commit is contained in:
parent
aa408841a5
commit
004edc9d27
2 changed files with 54 additions and 7 deletions
|
|
@ -1,10 +1,34 @@
|
||||||
import * as ivm from 'isolated-vm';
|
import ivmImport from 'isolated-vm';
|
||||||
|
import type { Context, Isolate, Reference } from 'isolated-vm';
|
||||||
import { ScriptContextData } from '../../../shared/types';
|
import { ScriptContextData } from '../../../shared/types';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const SCRIPT_TIMEOUT_MS = 5000;
|
const SCRIPT_TIMEOUT_MS = 5000;
|
||||||
const MEMORY_LIMIT_MB = 50;
|
const MEMORY_LIMIT_MB = 50;
|
||||||
|
|
||||||
|
type IsolatedVmModule = typeof import('isolated-vm');
|
||||||
|
|
||||||
|
export function resolveIsolatedVmModule(moduleValue: unknown): IsolatedVmModule {
|
||||||
|
if (moduleValue && typeof moduleValue === 'object' && 'Isolate' in moduleValue) {
|
||||||
|
return moduleValue as IsolatedVmModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
moduleValue &&
|
||||||
|
typeof moduleValue === 'object' &&
|
||||||
|
'default' in moduleValue &&
|
||||||
|
moduleValue.default &&
|
||||||
|
typeof moduleValue.default === 'object' &&
|
||||||
|
'Isolate' in moduleValue.default
|
||||||
|
) {
|
||||||
|
return moduleValue.default as IsolatedVmModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('isolated-vm module did not expose an Isolate constructor');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ivm = resolveIsolatedVmModule(ivmImport);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip `export` keywords from user script code so it can run in eval() context.
|
* Strip `export` keywords from user script code so it can run in eval() context.
|
||||||
* Supports: `export const`, `export function`, `export default`, `export {`.
|
* Supports: `export const`, `export function`, `export default`, `export {`.
|
||||||
|
|
@ -19,10 +43,10 @@ function preprocessScript(code: string): string {
|
||||||
*/
|
*/
|
||||||
export class CompiledScript {
|
export class CompiledScript {
|
||||||
private constructor(
|
private constructor(
|
||||||
private isolate: ivm.Isolate,
|
private isolate: Isolate,
|
||||||
private ctx: ivm.Context,
|
private ctx: Context,
|
||||||
private onRequestRef: ivm.Reference<Function> | null,
|
private onRequestRef: Reference<Function> | null,
|
||||||
private onResponseRef: ivm.Reference<Function> | null,
|
private onResponseRef: Reference<Function> | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get hasOnRequest(): boolean {
|
get hasOnRequest(): boolean {
|
||||||
|
|
@ -78,10 +102,10 @@ export class CompiledScript {
|
||||||
) as boolean;
|
) as boolean;
|
||||||
|
|
||||||
const onRequestRef = hasOnRequest
|
const onRequestRef = hasOnRequest
|
||||||
? await ctx.eval('onRequest', { timeout: SCRIPT_TIMEOUT_MS, reference: true }) as ivm.Reference<Function>
|
? await ctx.eval('onRequest', { timeout: SCRIPT_TIMEOUT_MS, reference: true }) as Reference<Function>
|
||||||
: null;
|
: null;
|
||||||
const onResponseRef = hasOnResponse
|
const onResponseRef = hasOnResponse
|
||||||
? await ctx.eval('onResponse', { timeout: SCRIPT_TIMEOUT_MS, reference: true }) as ivm.Reference<Function>
|
? await ctx.eval('onResponse', { timeout: SCRIPT_TIMEOUT_MS, reference: true }) as Reference<Function>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return new CompiledScript(isolate, ctx, onRequestRef, onResponseRef);
|
return new CompiledScript(isolate, ctx, onRequestRef, onResponseRef);
|
||||||
|
|
|
||||||
23
server/tests/unit/script-executor.test.ts
Normal file
23
server/tests/unit/script-executor.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { resolveIsolatedVmModule } from '../../src/services/ScriptExecutor';
|
||||||
|
|
||||||
|
describe('resolveIsolatedVmModule', () => {
|
||||||
|
it('accepts a direct isolated-vm export object', () => {
|
||||||
|
const fakeModule = { Isolate: class FakeIsolate {} };
|
||||||
|
|
||||||
|
expect(resolveIsolatedVmModule(fakeModule)).toBe(fakeModule);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a default-wrapped isolated-vm export object', () => {
|
||||||
|
const fakeModule = { Isolate: class FakeIsolate {} };
|
||||||
|
const wrappedModule = { default: fakeModule };
|
||||||
|
|
||||||
|
expect(resolveIsolatedVmModule(wrappedModule)).toBe(fakeModule);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when Isolate is unavailable', () => {
|
||||||
|
expect(() => resolveIsolatedVmModule({})).toThrow(
|
||||||
|
'isolated-vm module did not expose an Isolate constructor',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue