Skip to content

Commit

Permalink
Revamp importing to enable bumping Undici, and fixing instanceof test…
Browse files Browse the repository at this point in the history
…s within the edge runtime (#309)

Co-authored-by: Kiko Beats <josefrancisco.verdu@gmail.com>
Co-authored-by: Gal Schlezinger <gal@spitfire.co.il>
  • Loading branch information
3 people authored May 2, 2023
1 parent c13ddb8 commit ed225b3
Show file tree
Hide file tree
Showing 43 changed files with 794 additions and 281 deletions.
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@edge-runtime/docs"]
"ignore": ["@edge-runtime/docs", "@edge-runtime/integration-tests"]
}
15 changes: 15 additions & 0 deletions .changeset/orange-colts-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@edge-runtime/jest-environment': minor
'@edge-runtime/jest-expect': minor
'@edge-runtime/node-utils': minor
'@edge-runtime/primitives': minor
'@edge-runtime/user-agent': minor
'@edge-runtime/ponyfill': minor
'@edge-runtime/cookies': minor
'edge-runtime': minor
'@edge-runtime/format': minor
'@edge-runtime/types': minor
'@edge-runtime/vm': minor
---

Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ coverage
docs/.next
docs/node_modules
packages/*/dist
packages/*/*.tgz
packages/runtime/src/version.ts
.vercel
1 change: 1 addition & 0 deletions docs/pages/features/available-apis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The following APIs are available in the Edge Runtime.
- [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
- [File](https://developer.mozilla.org/en-US/docs/Web/API/File)
- [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
- [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)

## Encoding APIs

Expand Down
3 changes: 0 additions & 3 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
global.AbortSignal =
require('./packages/primitives/dist/abort-controller').AbortSignal

/**
* Jest uses a VM under the covers but it is setup to look like Node.js.
* Those globals that are missing in the VM but exist in Node.js will be
Expand Down
2 changes: 1 addition & 1 deletion packages/cookies/test/response-cookies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test('reflect .delete into `set-cookie`', async () => {
})

cookies.set('fooz', 'barz')
expect(Object.fromEntries(headers.entries())['set-cookie']).toBe(
expect(Object.fromEntries(headers)['set-cookie']).toBe(
'foo=bar; Path=/, fooz=barz; Path=/'
)

Expand Down
8 changes: 8 additions & 0 deletions packages/integration-tests/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import buildConfig from '../../jest.config'
import type { Config } from '@jest/types'

const config: Config.InitialOptions = {
...buildConfig(__dirname),
testEnvironment: '@edge-runtime/jest-environment',
}
export default config
16 changes: 16 additions & 0 deletions packages/integration-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@edge-runtime/integration-tests",
"private": true,
"version": "1.0.0",
"scripts": {
"test": "pnpm run test:node && pnpm run test:edge",
"test:edge": "jest --testEnvironment @edge-runtime/jest-environment",
"test:node": "jest --testEnvironment node"
},
"license": "MPL-2.0",
"devDependencies": {
"@edge-runtime/jest-environment": "workspace:*",
"@edge-runtime/ponyfill": "workspace:*",
"@edge-runtime/primitives": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AbortController, AbortSignal, DOMException } from '../abort-controller'
import { fetch } from '../fetch'
import { AbortController, fetch, DOMException } from '@edge-runtime/ponyfill'

describe('AbortController', () => {
it('allows to abort fetch', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { console as konsole } from '../console'
import { console } from '@edge-runtime/ponyfill'

it.each([
{ method: 'assert' },
Expand All @@ -14,7 +14,7 @@ it.each([
{ method: 'warn' },
])('$method', ({ method }) => {
const key = method.toString()
expect(konsole).toHaveProperty(key, expect.any(Function))
const fn = konsole[key as keyof typeof konsole]
expect(console).toHaveProperty(key, expect.any(Function))
const fn = console[key as keyof typeof console]
expect(typeof fn.bind(console)).toBe('function')
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { crypto } from '../crypto'
import { crypto } from '@edge-runtime/ponyfill'

test('crypto.randomUUID', () => {
expect(crypto.randomUUID()).toEqual(expect.stringMatching(/^[a-f0-9-]+$/))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TextDecoder } from '../encoding'
import { TextDecoder } from '@edge-runtime/ponyfill'

test('TextDecoder', () => {
const input = new Uint8Array([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Headers } from '../fetch'
import { Headers } from '@edge-runtime/ponyfill'

test('sets header calling Headers constructor', async () => {
const headers = new Headers({ cookie: 'hello=world' })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetch, Headers, Request } from '../fetch'
import { fetch, Request, Headers } from '@edge-runtime/ponyfill'

test('combine with fetch', async () => {
const request = new Request('https://example.vercel.sh')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetch, Response } from '../fetch'
import { fetch, Response } from '@edge-runtime/ponyfill'

test('allow to set `set-cookie` header', async () => {
const response = new Response(null)
Expand All @@ -10,7 +10,10 @@ test('allow to append multiple `set-cookie` header', async () => {
const response = new Response(null)
response.headers.append('set-cookie', 'foo=bar')
response.headers.append('set-cookie', 'bar=baz')
expect(response.headers.getAll('set-cookie')).toEqual(['foo=bar', 'bar=baz'])
expect(response.headers.getAll?.('set-cookie')).toEqual([
'foo=bar',
'bar=baz',
])
})

test('disallow mutate response headers for redirects', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { URL } from '../url'
import { URL } from '@edge-runtime/ponyfill'

test('URL', async () => {
const url = new URL('https://edge-ping.vercel.app/')
Expand Down
4 changes: 2 additions & 2 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export = class EdgeEnvironment implements JestEnvironment<number> {
context.Buffer = Buffer
return context
},
...((config as any).projectConfig ?? config)?.testEnvironmentOptions
...((config as any).projectConfig ?? config)?.testEnvironmentOptions,
})

revealPrimitives(vm)
Expand Down Expand Up @@ -68,7 +68,7 @@ export = class EdgeEnvironment implements JestEnvironment<number> {
}

exportConditions(): string[] {
return ['edge']
return ['edge', 'edge-light']
}

getVmContext(): Context | null {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-utils/test/node-to-edge/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ it('does not allow to read the body twice', async () => {

expect(request.method).toEqual('POST')
expect(await request.text()).toEqual('Hello World')
await expect(request.text()).rejects.toThrowError('The body has already been consumed.')
await expect(request.text()).rejects.toThrowError('Body is unusable')
})

async function mapRequest(input: string, init: RequestInit = {}) {
Expand Down
1 change: 1 addition & 0 deletions packages/ponyfill/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function edge() {
URL,
URLPattern,
URLSearchParams,
WebSocket,
WritableStream,
WritableStreamDefaultWriter,
}
Expand Down
5 changes: 0 additions & 5 deletions packages/primitives/jest.config.ts

This file was deleted.

8 changes: 3 additions & 5 deletions packages/primitives/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
"@edge-runtime/format": "workspace:2.0.1",
"@peculiar/webcrypto": "1.4.3",
"@stardazed/streams-text-encoding": "1.0.2",
"@types/multer": "1.4.7",
"@types/test-listen": "1.1.0",
"@ungap/structured-clone": "1.0.2",
"aggregate-error-ponyfill": "1.1.0",
"blob-polyfill": "7.0.20220408",
Expand All @@ -39,7 +37,7 @@
"test-listen": "1.1.0",
"text-encoding": "0.7.0",
"tsup": "6",
"undici": "5.11.0",
"undici": "5.22.0",
"urlpattern-polyfill": "8.0.2",
"web-streams-polyfill": "4.0.0-beta.3",
"whatwg-url": "12.0.1"
Expand All @@ -58,15 +56,15 @@
"fetch",
"streams",
"structured-clone",
"text-encoding-streams",
"types",
"url"
],
"scripts": {
"build": "ts-node scripts/build.ts",
"clean:build": "rm -rf dist abort-controller blob console crypto encoding events fetch streams structured-clone url",
"clean:node": "rm -rf node_modules",
"prebuild": "pnpm run clean:build",
"test": "jest"
"prebuild": "pnpm run clean:build"
},
"license": "MPL-2.0",
"publishConfig": {
Expand Down
2 changes: 0 additions & 2 deletions packages/primitives/src/primitives/abort-controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { EventTarget, Event } from './events'

const kSignal = Symbol('kSignal')
const kAborted = Symbol('kAborted')
const kReason = Symbol('kReason')
Expand Down
114 changes: 40 additions & 74 deletions packages/primitives/src/primitives/fetch.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { AbortController } from './abort-controller'
import { AbortSignal } from './abort-controller'

import * as CoreSymbols from 'undici/lib/core/symbols'
import * as FetchSymbols from 'undici/lib/fetch/symbols'
import * as HeadersModule from 'undici/lib/fetch/headers'
import * as ResponseModule from 'undici/lib/fetch/response'
import * as UtilModule from 'undici/lib/fetch/util'
import * as WebIDLModule from 'undici/lib/fetch/webidl'
import { Request as BaseRequest } from 'undici/lib/fetch/request'

import { fetch as fetchImpl } from 'undici/lib/fetch'
import Agent from 'undici/lib/agent'
Expand All @@ -17,79 +17,33 @@ global.AbortSignal = AbortSignal
// undici uses `process.nextTick`,
// but process APIs doesn't exist in a runtime context.
process.nextTick = setImmediate

/**
* A symbol used to store cookies in the headers module.
*/
const SCookies = Symbol('set-cookie')

/**
* Patch HeadersList.append so that when a `set-cookie` header is appended
* we keep it in an list to allow future retrieval of all values.
*/
const __append = HeadersModule.HeadersList.prototype.append
HeadersModule.HeadersList.prototype.append = function (name, value) {
const result = __append.call(this, name, value)
if (!this[SCookies]) {
Object.defineProperty(this, SCookies, {
configurable: false,
enumerable: false,
writable: true,
value: [],
})
process.emitWarning = () => {}

const Request = new Proxy(BaseRequest, {
construct(target, args) {
const [input, init] = args
return new target(input, addDuplexToInit(init))
},
})

const __entries = HeadersModule.Headers.prototype.entries
HeadersModule.Headers.prototype.entries = function* () {
for (const [key, value] of __entries.call(this)) {
if (key === 'set-cookie') {
const cookies = this.getSetCookie()
yield [key, cookies.join(', ')]
} else {
yield [key, value]
}
}

const _name = normalizeAndValidateHeaderValue(name, 'Header.append')
if (_name === 'set-cookie') {
this[SCookies].push(normalizeAndValidateHeaderValue(value, 'Header.append'))
}

return result
}

/**
* Patch HeadersList.set to make sure that when a new value for `set-cookie`
* is set it will also entirely replace the internal list of values.
*/
const __set = HeadersModule.HeadersList.prototype.set
HeadersModule.HeadersList.prototype.set = function (name, value) {
const result = __set.call(this, name, value)
if (!this[SCookies]) {
Object.defineProperty(this, SCookies, {
configurable: false,
enumerable: false,
writable: true,
value: [],
})
}
HeadersModule.Headers.prototype[Symbol.iterator] =
HeadersModule.Headers.prototype.entries

const _name = normalizeAndValidateHeaderName(name)
if (_name === 'set-cookie') {
this[SCookies] = [normalizeAndValidateHeaderValue(value, 'HeadersList.set')]
}

return result
}

/**
* Patch HeaderList.delete to make sure that when `set-cookie` is cleared
* we also remove the internal list values.
*/
const __delete = HeadersModule.HeadersList.prototype.delete
HeadersModule.HeadersList.prototype.delete = function (name) {
__delete.call(this, name)
if (!this[SCookies]) {
Object.defineProperty(this, SCookies, {
configurable: false,
enumerable: false,
writable: true,
value: [],
})
}

const _name = normalizeAndValidateHeaderName(name, 'Headers.delete')
if (_name === 'set-cookie') {
this[SCookies] = []
HeadersModule.Headers.prototype.values = function* () {
for (const [, value] of __entries.call(this)) {
yield value
}
}

Expand All @@ -104,7 +58,7 @@ HeadersModule.Headers.prototype.getAll = function (name) {
throw new Error(`getAll can only be used with 'set-cookie'`)
}

return this[CoreSymbols.kHeadersList][SCookies] || []
return this.getSetCookie()
}

/**
Expand Down Expand Up @@ -184,13 +138,24 @@ export function setGlobalDispatcher(agent) {
globalDispatcher = agent
}

/**
* Add `duplex: 'half'` by default to all requests
*/
function addDuplexToInit(init) {
if (typeof init === 'undefined' || typeof init === 'object') {
return { duplex: 'half', ...init }
}
return init
}

/**
* Export fetch with an implementation that uses a default global dispatcher.
* It also re-cretates a new Response object in order to allow mutations on
* the Response headers.
*/
export async function fetch() {
const res = await fetchImpl.apply(getGlobalDispatcher(), arguments)
export async function fetch(info, init) {
init = addDuplexToInit(init)
const res = await fetchImpl.call(getGlobalDispatcher(), info, init)
const response = new Response(res.body, res)
Object.defineProperty(response, 'url', { value: res.url })
return response
Expand All @@ -200,5 +165,6 @@ export const Headers = HeadersModule.Headers
export const Response = ResponseModule.Response

export { FormData } from 'undici/lib/fetch/formdata'
export { Request } from 'undici/lib/fetch/request'
export { File } from 'undici/lib/fetch/file'
export { WebSocket } from 'undici/lib/websocket/websocket'
export { Request }
Loading

1 comment on commit ed225b3

@vercel
Copy link

@vercel vercel bot commented on ed225b3 May 2, 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.