Skip to content

Commit

Permalink
simplify imports in edge-vm (#324)
Browse files Browse the repository at this point in the history
  • Loading branch information
Schniz committed May 3, 2023
1 parent 9129868 commit a9054f7
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 188 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-pans-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@edge-runtime/primitives': patch
---

Don't use require.resolve for the custom import resolution
5 changes: 5 additions & 0 deletions .changeset/hip-hairs-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@edge-runtime/vm': patch
---

simplify primitives loading in VM
1 change: 0 additions & 1 deletion packages/primitives/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"http-body": "1.0.4",
"multer": "1.4.5-lts.1",
"test-listen": "1.1.0",
"text-encoding": "0.7.0",
"tsup": "6",
"undici": "5.22.0",
"urlpattern-polyfill": "8.0.2",
Expand Down
10 changes: 7 additions & 3 deletions packages/primitives/src/primitives/encoding.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export { TextEncoder, TextDecoder } from 'text-encoding'
export const atob = enc => Buffer.from(enc, 'base64').toString('binary')
export const btoa = str => Buffer.from(str, 'binary').toString('base64')
export const atob = (enc) => Buffer.from(enc, 'base64').toString('binary')
export const btoa = (str) => Buffer.from(str, 'binary').toString('base64')

const TE = TextEncoder
const TD = TextDecoder

export { TE as TextEncoder, TD as TextDecoder }
16 changes: 9 additions & 7 deletions packages/primitives/src/primitives/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @ts-check

const path = require('path')

function load() {
/** @type {Record<string, any>} */
const context = {}
Expand All @@ -14,7 +16,7 @@ function load() {

const consoleImpl = requireWithFakeGlobalScope({
context,
path: require.resolve('./console'),
path: path.resolve(__dirname, './console.js'),
scopedContext: {},
})
Object.assign(context, { console: consoleImpl.console })
Expand All @@ -30,7 +32,7 @@ function load() {
const streamsImpl = require('./streams')
const textEncodingStreamImpl = requireWithFakeGlobalScope({
context,
path: require.resolve('./text-encoding-streams'),
path: path.resolve(__dirname, './text-encoding-streams.js'),
scopedContext: streamsImpl,
})

Expand All @@ -47,7 +49,7 @@ function load() {

const abortControllerImpl = requireWithFakeGlobalScope({
context,
path: require.resolve('./abort-controller'),
path: path.resolve(__dirname, './abort-controller.js'),
scopedContext: eventsImpl,
})
Object.assign(context, abortControllerImpl)
Expand All @@ -61,15 +63,15 @@ function load() {

const blobImpl = requireWithFakeGlobalScope({
context,
path: require.resolve('./blob'),
path: path.resolve(__dirname, './blob.js'),
scopedContext: streamsImpl,
})
Object.assign(context, {
Blob: blobImpl.Blob,
})

const structuredCloneImpl = requireWithFakeGlobalScope({
path: require.resolve('./structured-clone'),
path: path.resolve(__dirname, './structured-clone.js'),
context,
scopedContext: streamsImpl,
})
Expand All @@ -79,7 +81,7 @@ function load() {

const fetchImpl = requireWithFakeGlobalScope({
context,
path: require.resolve('./fetch'),
path: path.resolve(__dirname, './fetch.js'),
cache: new Map([
['abort-controller', { exports: abortControllerImpl }],
['streams', { exports: streamsImpl }],
Expand Down Expand Up @@ -129,7 +131,7 @@ import { readFileSync } from 'fs'
* @returns {any}
*/
function requireWithFakeGlobalScope(params) {
const resolved = require.resolve(params.path)
const resolved = path.resolve(params.path)
const getModuleCode = `(function(module,exports,require,__dirname,__filename,globalThis,${Object.keys(
params.scopedContext
).join(',')}) {${readFileSync(resolved, 'utf-8')}\n})`
Expand Down
152 changes: 42 additions & 110 deletions packages/vm/src/edge-vm.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import type * as EdgePrimitives from '@edge-runtime/primitives'
import * as EdgePrimitives from '@edge-runtime/primitives'
import type { DispatchFetch, ErrorHandler, RejectionHandler } from './types'
import { requireWithCache, requireWithFakeGlobalScope } from './require'
import { runInContext } from 'vm'
import { VM, type VMContext, type VMOptions } from './vm'
import * as streamsImpl from '@edge-runtime/primitives/streams'
import * as urlImpl from '@edge-runtime/primitives/url'
import * as cryptoImpl from '@edge-runtime/primitives/crypto'
import * as eventsImpl from '@edge-runtime/primitives/events'

export interface EdgeVMOptions<T extends EdgeContext> {
/**
Expand Down Expand Up @@ -317,45 +312,41 @@ function addPrimitives(context: VMContext) {
defineProperty(context, 'setTimeout', { value: setTimeout })
defineProperty(context, 'EdgeRuntime', { value: 'edge-runtime' })

// Console
defineProperties(context, {
exports: requireWithCache({
path: require.resolve('@edge-runtime/primitives/console'),
context,
}),
nonenumerable: ['console'],
})
exports: EdgePrimitives,
enumerable: ['crypto'],
nonenumerable: [
// Crypto
'Crypto',
'CryptoKey',
'SubtleCrypto',

const atob = (str: string) => Buffer.from(str, 'base64').toString('binary')
const btoa = (str: string) => Buffer.from(str, 'binary').toString('base64')
// Fetch APIs
'fetch',
'File',
'FormData',
'Headers',
'Request',
'Response',
'WebSocket',

// Events
defineProperties(context, {
exports: eventsImpl,
nonenumerable: [
'Event',
'EventTarget',
'FetchEvent',
'PromiseRejectionEvent',
],
})
// Structured Clone
'structuredClone',

// Encoding APIs
defineProperties(context, {
exports: { atob, btoa, TextEncoder, TextDecoder },
nonenumerable: ['atob', 'btoa', 'TextEncoder', 'TextDecoder'],
})
// Blob
'Blob',

const textEncodingStreamImpl = requireWithFakeGlobalScope({
context,
path: require.resolve('@edge-runtime/primitives/text-encoding-streams'),
scopedContext: streamsImpl,
})
// URL
'URL',
'URLSearchParams',
'URLPattern',

// Streams
defineProperties(context, {
exports: { ...streamsImpl, ...textEncodingStreamImpl },
nonenumerable: [
// AbortController
'AbortController',
'AbortSignal',
'DOMException',

// Streams
'ReadableStream',
'ReadableStreamBYOBReader',
'ReadableStreamDefaultReader',
Expand All @@ -364,83 +355,24 @@ function addPrimitives(context: VMContext) {
'TransformStream',
'WritableStream',
'WritableStreamDefaultWriter',
],
})

// AbortController
const abortControllerImpl = requireWithFakeGlobalScope({
path: require.resolve('@edge-runtime/primitives/abort-controller'),
context,
scopedContext: eventsImpl,
})
defineProperties(context, {
exports: abortControllerImpl,
nonenumerable: ['AbortController', 'AbortSignal', 'DOMException'],
})

// URL
defineProperties(context, {
exports: urlImpl,
nonenumerable: ['URL', 'URLSearchParams', 'URLPattern'],
})

// Blob
defineProperties(context, {
exports: requireWithFakeGlobalScope({
context,
path: require.resolve('@edge-runtime/primitives/blob'),
scopedContext: streamsImpl,
}),
nonenumerable: ['Blob'],
})
// Encoding
'atob',
'btoa',
'TextEncoder',
'TextDecoder',

// Structured Clone
defineProperties(context, {
exports: requireWithFakeGlobalScope({
path: require.resolve('@edge-runtime/primitives/structured-clone'),
context,
scopedContext: streamsImpl,
}),
nonenumerable: ['structuredClone'],
})
// Events
'Event',
'EventTarget',
'FetchEvent',
'PromiseRejectionEvent',

// Fetch APIs
defineProperties(context, {
exports: requireWithFakeGlobalScope({
context,
cache: new Map([
['abort-controller', { exports: abortControllerImpl }],
['streams', { exports: streamsImpl }],
]),
path: require.resolve('@edge-runtime/primitives/fetch'),
scopedContext: {
...streamsImpl,
...urlImpl,
structuredClone: context.structuredClone,
...eventsImpl,
AbortController: context.AbortController,
DOMException: context.DOMException,
AbortSignal: context.AbortSignal,
},
}),
nonenumerable: [
'fetch',
'File',
'FormData',
'Headers',
'Request',
'Response',
'WebSocket',
// Console
'console',
],
})

// Crypto
defineProperties(context, {
exports: cryptoImpl,
enumerable: ['crypto'],
nonenumerable: ['Crypto', 'CryptoKey', 'SubtleCrypto'],
})

return context as EdgeContext
}

Expand Down
48 changes: 0 additions & 48 deletions packages/vm/src/require.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Context } from 'vm'
import { readFileSync } from 'fs'
import { runInContext } from 'vm'
import { dirname } from 'path'
import Module from 'module'

/**
* Allows to require a series of dependencies provided by their path
Expand Down Expand Up @@ -90,50 +89,3 @@ export function requireWithCache(params: {
params.scopedContext
).call(null, params.path, params.path)
}

export function requireWithFakeGlobalScope(params: {
context: Context
cache?: Map<string, any>
path: string
references?: Set<string>
scopedContext: Record<string, any>
}) {
const resolved = require.resolve(params.path)
const getModuleCode = `(function(module,exports,require,__dirname,__filename,globalThis,${Object.keys(
params.scopedContext
).join(',')}) {${readFileSync(resolved, 'utf-8')}\n})`
const module = {
exports: {},
loaded: false,
id: resolved,
}

const moduleRequire = (Module.createRequire || Module.createRequireFromPath)(
resolved
)

function throwingRequire(path: string) {
if (path.startsWith('./')) {
const moduleName = path.replace(/^\.\//, '')
if (!params.cache?.has(moduleName)) {
throw new Error(`Cannot find module '${moduleName}'`)
}
return params.cache.get(moduleName).exports
}
return moduleRequire(path)
}

throwingRequire.resolve = moduleRequire.resolve.bind(moduleRequire)

eval(getModuleCode)(
module,
module.exports,
throwingRequire,
dirname(resolved),
resolved,
params.context,
...Object.values(params.scopedContext)
)

return module.exports
}
42 changes: 42 additions & 0 deletions packages/vm/tests/fetch-within-vm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createServer } from 'http'
import listen from 'test-listen'
import { EdgeVM } from '../src'

test('fetch within vm', async () => {
const server = createServer((req, res) => {
res.write(`Hello from ${req.url}`)
res.end()
})
try {
const url = await listen(server)
const vm = new EdgeVM()

const result = await vm.evaluate(`fetch("${url}/foo")`)
expect(await result.text()).toBe(`Hello from /foo`)
} finally {
server.close()
}
})

test('sends a Uint8Array', async () => {
const server = createServer(async (req, res) => {
const chunks = [] as Buffer[]
for await (const chunk of req) {
chunks.push(chunk)
}
const body = Buffer.concat(chunks).toString()
res.write(`Hello from ${req.url} with body ${body}`)
res.end()
})
try {
const url = await listen(server)
const vm = new EdgeVM()

const result = await vm.evaluate(
`fetch("${url}/foo", { method: "POST", body: new Uint8Array([104, 105, 33]) })`
)
expect(await result.text()).toBe(`Hello from /foo with body hi!`)
} finally {
server.close()
}
})
Loading

1 comment on commit a9054f7

@vercel
Copy link

@vercel vercel bot commented on a9054f7 May 3, 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.app
edge-runtime.vercel.sh
edge-runtime-git-main.vercel.sh

Please sign in to comment.