Skip to content

Commit

Permalink
remove dynamism in vm package (#355)
Browse files Browse the repository at this point in the history
  • Loading branch information
Schniz authored May 28, 2023
1 parent 56ce6d4 commit a594675
Show file tree
Hide file tree
Showing 7 changed files with 16 additions and 157 deletions.
9 changes: 9 additions & 0 deletions .changeset/mighty-impalas-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@edge-runtime/vm': major
---

remove `.require` helpers. This is not necessary as people can add dependencies
to the context and instanceof should just work.

we don't use the vm as a security boundary, so we don't need to worry about
people adding malicious code to the context.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parse } from 'acorn-loose'
import { createRequire } from './create-require'
import { promises as fs } from 'fs'
import { simple } from 'acorn-walk'
import { EdgeVM } from '@edge-runtime/vm'
Expand All @@ -17,7 +18,11 @@ test('exports all primitives in Edge Runtime', async () => {
const runtime = new EdgeVM({
codeGeneration: { strings: false, wasm: false },
})
const result = runtime.require(require.resolve('..'))

const moduleRequire = createRequire(runtime.context, new Map())
runtime.context.require = moduleRequire

const result = moduleRequire(require.resolve('..'), require.resolve('..'))

for (const key of LIMBO_STATE) {
delete anyObject[key]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,6 @@
import type { Context } from 'vm'
import { readFileSync } from 'fs'
import { runInContext } from 'vm'
import { dirname } from 'path'

/**
* Allows to require a series of dependencies provided by their path
* into a provided module context. It fills and accepts a require
* cache to ensure each module is loaded once.
*/
export function requireDependencies(params: {
context: Context
requireCache: Map<string, Record<string | number, any>>
dependencies: Array<{
mapExports: { [key: string]: string }
path: string
}>
}): void {
const { context, requireCache, dependencies } = params
const requireFn = createRequire(context, requireCache)
for (const { path, mapExports } of dependencies) {
const mod = requireFn(path, path)
for (const mapKey of Object.keys(mapExports)) {
context[mapExports[mapKey]] = mod[mapKey]
}
}
}
import { Context, runInContext } from 'vm'

export function createRequire(
context: Context,
Expand Down Expand Up @@ -74,18 +50,3 @@ export function createRequire(
return module.exports
}
}

export function requireWithCache(params: {
cache?: Map<string, any>
context: Context
path: string
references?: Set<string>
scopedContext?: Record<string, any>
}) {
return createRequire(
params.context,
params.cache ?? new Map(),
params.references,
params.scopedContext
).call(null, params.path, params.path)
}
5 changes: 0 additions & 5 deletions packages/vm/src/edge-vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ export interface EdgeVMOptions<T extends EdgeContext> {
* evaluating.
*/
initialCode?: string
/**
* Provides an initial map to the require cache.
* If none is given, it will be initialized to an empty map.
*/
requireCache?: VMOptions<T>['requireCache']
}

/**
Expand Down
18 changes: 0 additions & 18 deletions packages/vm/src/temp-file.ts

This file was deleted.

44 changes: 0 additions & 44 deletions packages/vm/src/vm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { CreateContextOptions } from 'vm'
import { createContext, runInContext } from 'vm'
import { createRequire } from './require'
import { tempFile } from './temp-file'

export interface VMOptions<T> {
/**
Expand All @@ -14,11 +12,6 @@ export interface VMOptions<T> {
* object so ideally it should return the same reference it receives.
*/
extend?: (context: VMContext) => VMContext & T
/**
* Provides an initial map to the require cache.
* If none is given, it will be initialized to an empty map.
*/
requireCache?: Map<string, Record<string | number, any>>
}

/**
Expand All @@ -27,8 +20,6 @@ export interface VMOptions<T> {
* modules in multiple ways.
*/
export class VM<T extends Record<string | number, any>> {
private readonly requireFn: (referrer: string, specifier: string) => any
public readonly requireCache: Map<string, Record<string | number, any>>
public readonly context: VMContext & T

constructor(options: VMOptions<T> = {}) {
Expand All @@ -43,9 +34,7 @@ export class VM<T extends Record<string | number, any>> {
}
) as VMContext

this.requireCache = options.requireCache ?? new Map()
this.context = options.extend?.(context) ?? (context as VMContext & T)
this.requireFn = createRequire(this.context, this.requireCache)
}

/**
Expand All @@ -54,39 +43,6 @@ export class VM<T extends Record<string | number, any>> {
evaluate<T = any>(code: string): T {
return runInContext(code, this.context)
}

/**
* Allows to require a CommonJS module referenced in the provided file
* path within the VM context. It will return its exports.
*/
require<T extends Record<string | number, any> = any>(filepath: string): T {
return this.requireFn(filepath, filepath)
}

/**
* Same as `require` but it will copy each of the exports in the context
* of the vm. Then exports can be used inside of the vm with an
* evaluated script.
*/
requireInContext<T extends Record<string | number, any> = any>(
filepath: string
): void {
const moduleLoaded = this.require<T>(filepath)
for (const [key, value] of Object.entries(moduleLoaded)) {
this.context[key as keyof typeof this.context] = value
}
}

/**
* Same as `requireInContext` but allows to pass the code instead of a
* reference to a file. It will create a temporary file and then load
* it in the VM Context.
*/
requireInlineInContext(code: string): void {
const file = tempFile(code)
this.requireInContext(file.path)
file.remove()
}
}

export interface VMContext {
Expand Down
49 changes: 0 additions & 49 deletions packages/vm/tests/vm.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { VM } from '../src/vm'
import path from 'path'

it('creates a VM with empty context', () => {
const vm = new VM()
Expand Down Expand Up @@ -62,54 +61,6 @@ it('allows to extend the context with code evaluation', () => {
expect(vm.context.URL.name).toEqual('MockURL')
})

it('allows to require a CJS module file from the vm context', () => {
const vm = new VM()
const modulepath = path.resolve(__dirname, './fixtures/cjs-module.js')
const moduleLoaded = vm.require<{ URL: URL }>(modulepath)

vm.context.URL = moduleLoaded.URL

vm.evaluate('this.hasURL = !!URL')
vm.evaluate('this.url = new URL("https://edge-ping.vercel.app")')

expect(vm.context.hasURL).toBeTruthy()
expect(vm.context.url.href).toEqual('https://edge-ping.vercel.app')
expect(vm.context.URL.name).toEqual('MockURL')
})

it('allows to require a CJS module file and load it into the vm context', () => {
const vm = new VM()
const modulepath = path.resolve(__dirname, './fixtures/cjs-module.js')
vm.requireInContext(modulepath)

vm.evaluate('this.hasURL = !!URL')
vm.evaluate('this.url = new URL("https://edge-ping.vercel.app")')

expect(vm.context.hasURL).toBeTruthy()
expect(vm.context.url.href).toEqual('https://edge-ping.vercel.app')
expect(vm.context.URL.name).toEqual('MockURL')
})

it('allows to require CJS module code from the vm context', () => {
const vm = new VM()

const script = `function MockURL (href) {
if (!(this instanceof MockURL)) return new MockURL(href)
this.href = href
}
module.exports.URL = MockURL`

vm.requireInlineInContext(script)

vm.evaluate('this.hasURL = !!URL')
vm.evaluate('this.url = new URL("https://edge-ping.vercel.app")')

expect(vm.context.hasURL).toBeTruthy()
expect(vm.context.url.href).toEqual('https://edge-ping.vercel.app')
expect(vm.context.URL.name).toEqual('MockURL')
})

it('does not allow to run `new Function`', () => {
const vm = new VM()
expect(() => {
Expand Down

1 comment on commit a594675

@vercel
Copy link

@vercel vercel bot commented on a594675 May 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

edge-runtime – ./

edge-runtime.vercel.sh
edge-runtime-git-main.vercel.sh
edge-runtime.vercel.app

Please sign in to comment.