├── .changeset
├── README.md
└── config.json
├── .editorconfig
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── questions_answers.md
├── dependabot.yml
└── workflows
│ ├── publish.yml
│ ├── test.yml
│ └── update.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.json
├── LICENSE.md
├── README.md
├── docs
├── .gitignore
├── components
│ ├── features.js
│ └── footer.tsx
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.mdx
│ ├── _document.js
│ ├── _meta.json
│ ├── cli.mdx
│ ├── features
│ │ ├── _meta.json
│ │ ├── available-apis.mdx
│ │ ├── polyfills.mdx
│ │ ├── typescript-support.mdx
│ │ └── upgrading-nextjs.mdx
│ ├── getting-started.mdx
│ ├── index.mdx
│ └── packages
│ │ ├── _meta.json
│ │ ├── cookies.mdx
│ │ ├── format.mdx
│ │ ├── jest-environment.mdx
│ │ ├── jest-expect.mdx
│ │ ├── node-utils.mdx
│ │ ├── ponyfill.mdx
│ │ ├── primitives.mdx
│ │ ├── runtime.mdx
│ │ ├── types.mdx
│ │ ├── user-agent.mdx
│ │ └── vm.mdx
├── postcss.config.js
├── public
│ ├── logo-dark.svg
│ ├── logo.svg
│ ├── og-image-dark.png
│ └── og-image.png
├── styles.css
├── tailwind.config.js
├── theme.config.js
└── vercel.json
├── jest.config.ts
├── jest.setup.js
├── media
└── logo.sketch
├── package.json
├── packages
├── cookies
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── request-cookies.ts
│ │ ├── response-cookies.ts
│ │ ├── serialize.ts
│ │ └── types.ts
│ ├── test
│ │ ├── index.test.ts
│ │ ├── request-cookies.test.ts
│ │ └── response-cookies.test.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── format
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── primordials.ts
│ ├── tests
│ │ └── index.test.ts
│ ├── tsconfig.prod.json
│ └── tsup.config.ts
├── integration-tests
│ ├── jest.config.ts
│ ├── package.json
│ └── test
│ │ ├── abort-controller.test.ts
│ │ ├── body.test.ts
│ │ ├── console.test.ts
│ │ ├── crypto.test.ts
│ │ ├── encoding.test.ts
│ │ ├── env.d.ts
│ │ ├── fetch.test.ts
│ │ ├── global.test.ts
│ │ ├── headers.test.ts
│ │ ├── request.test.ts
│ │ ├── response.test.ts
│ │ ├── test-if.ts
│ │ ├── url-pattern.test.ts
│ │ └── url.test.ts
├── jest-environment
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── test
│ │ ├── index.test.ts
│ │ └── next.test.ts
│ └── tsconfig.prod.json
├── jest-expect
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── index.d.ts
│ ├── index.js
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── response.ts
│ │ ├── shared.ts
│ │ └── types.ts
│ ├── test
│ │ └── index.test.ts
│ └── tsconfig.prod.json
├── node-utils
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ ├── edge-to-node
│ │ │ ├── handler.ts
│ │ │ ├── headers.ts
│ │ │ ├── index.ts
│ │ │ └── stream.ts
│ │ ├── index.ts
│ │ ├── node-to-edge
│ │ │ ├── fetch-event.ts
│ │ │ ├── headers.ts
│ │ │ ├── index.ts
│ │ │ ├── request.ts
│ │ │ └── stream.ts
│ │ └── types.ts
│ ├── test
│ │ ├── edge-to-node
│ │ │ ├── handler.test.ts
│ │ │ ├── headers.test.ts
│ │ │ └── stream.test.ts
│ │ ├── node-to-edge
│ │ │ ├── fetch-event.test.ts
│ │ │ └── request.test.ts
│ │ └── test-utils
│ │ │ ├── get-kill-server.ts
│ │ │ ├── run-test-server.ts
│ │ │ └── serialize-response.ts
│ ├── tsconfig.prod.json
│ └── tsup.config.ts
├── ponyfill
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ ├── index.d.ts
│ │ └── index.js
│ └── test
│ │ ├── EdgeRuntime.test.ts
│ │ ├── acorn.d.ts
│ │ ├── compliance-with-primitives.node.test.ts
│ │ └── create-require.ts
├── primitives
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── load
│ │ └── package.json
│ ├── package.json
│ ├── scripts
│ │ └── build.ts
│ ├── src
│ │ ├── injectSourceCode.d.ts
│ │ ├── patches
│ │ │ └── undici-core-request.js
│ │ └── primitives
│ │ │ ├── abort-controller.js
│ │ │ ├── console.js
│ │ │ ├── crypto.js
│ │ │ ├── events.js
│ │ │ ├── fetch.js
│ │ │ ├── index.js
│ │ │ ├── load.js
│ │ │ ├── stream.js
│ │ │ ├── timers.js
│ │ │ └── url.js
│ └── type-definitions
│ │ ├── abort-controller.d.ts
│ │ ├── blob.d.ts
│ │ ├── console.d.ts
│ │ ├── crypto.d.ts
│ │ ├── encoding.d.ts
│ │ ├── events.d.ts
│ │ ├── fetch.d.ts
│ │ ├── index.d.ts
│ │ ├── load.d.ts
│ │ ├── performance.d.ts
│ │ ├── streams.d.ts
│ │ ├── structured-clone.d.ts
│ │ ├── text-encoding-streams.d.ts
│ │ ├── timers.d.ts
│ │ └── url.d.ts
├── runtime
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── examples
│ │ ├── cache.js
│ │ ├── crypto.js
│ │ ├── empty.js
│ │ ├── error.js
│ │ ├── fetch.js
│ │ ├── html.js
│ │ ├── unhandledrejection.js
│ │ └── urlpattern.js
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ ├── cli
│ │ │ ├── eval.ts
│ │ │ ├── help.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.ts
│ │ │ └── repl.ts
│ │ ├── edge-runtime.ts
│ │ ├── index.ts
│ │ ├── server
│ │ │ ├── body-streams.ts
│ │ │ ├── create-handler.ts
│ │ │ ├── index.ts
│ │ │ └── run-server.ts
│ │ └── types.ts
│ ├── tests
│ │ ├── body-stream.test.ts
│ │ ├── fixtures
│ │ │ ├── pull-error.ts
│ │ │ └── unhandled-rejection.ts
│ │ ├── rejections-and-errors.test.ts
│ │ ├── repl.test.ts
│ │ └── server.test.ts
│ └── tsconfig.prod.json
├── types
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.d.ts
├── user-agent
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── test
│ │ └── index.test.ts
│ ├── tsconfig.prod.json
│ └── tsup.config.ts
└── vm
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── src
│ ├── edge-vm.ts
│ ├── index.ts
│ ├── types.ts
│ └── vm.ts
│ ├── tests
│ ├── edge-runtime.test.ts
│ ├── fetch-within-vm.test.ts
│ ├── fixtures
│ │ ├── cjs-module.js
│ │ ├── legit-uncaught-exception.ts
│ │ ├── legit-unhandled-rejection.ts
│ │ └── uncaught-exception.ts
│ ├── instanceof.test.ts
│ ├── integration
│ │ ├── crypto.test.ts
│ │ └── error.test.ts
│ ├── rejections-and-errors.test.ts
│ ├── vm.test.ts
│ └── websocket.test.ts
│ └── tsconfig.prod.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tsconfig.json
└── turbo.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json",
3 | "changelog": [
4 | "@svitejs/changesets-changelog-github-compact",
5 | { "repo": "vercel/edge-runtime" }
6 | ],
7 | "commit": false,
8 | "fixed": [],
9 | "linked": [],
10 | "access": "public",
11 | "baseBranch": "main",
12 | "updateInternalDependencies": "patch",
13 | "ignore": ["@edge-runtime/docs", "@edge-runtime/integration-tests"]
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | max_line_length = 80
13 | indent_brace_style = 1TBS
14 | spaces_around_operators = true
15 | quote_type = auto
16 |
17 | [package.json]
18 | indent_style = space
19 | indent_size = 2
20 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: If something isn't working as expected.
4 | ---
5 |
6 | ## Bug Report
7 |
8 | **Current behavior**
9 |
10 | A clear and concise description of the behavior.
11 |
12 | **Expected behavior/code**
13 |
14 | A clear and concise description of what you expected to happen (or code).
15 |
16 | **Possible solution**
17 |
18 |
19 |
20 | **Additional context/screenshots**
21 |
22 | Add any other context about the problem here. If applicable, add screenshots to help explain.
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🚀 Feature Request
3 | about: If you have a suggestion.
4 | ---
5 |
6 | ## Feature Request
7 |
8 | **Is your feature request related to a problem? Please describe.**
9 | A clear and concise description of what the problem is. Ex. I have an issue when [...]
10 |
11 | **Describe the solution you'd like**
12 | A clear and concise description of what you want to happen. Add any considered drawbacks.
13 |
14 | **Describe alternatives you've considered**
15 | A clear and concise description of any alternative solutions or features you've considered.
16 |
17 | **Teachability, Documentation, Adoption, Migration Strategy**
18 | If you can, explain how users will be able to use this and possibly write out a version the docs.
19 | Maybe a screenshot or design?
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/questions_answers.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🤔 Questions & Answers
3 | about: Ask anything you want to know
4 | ---
5 |
6 | ## Questions & Answers
7 |
8 | **Context of your question**
9 | Help us to understand the mental roadmap we need to follow to reach the same question.
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: npm
8 | directory: '/docs'
9 | schedule:
10 | interval: daily
11 | - package-ecosystem: 'github-actions'
12 | directory: '/'
13 | schedule:
14 | # Check for updates to GitHub Actions every weekday
15 | interval: 'daily'
16 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - '!*'
9 |
10 | env:
11 | NODE_VERSION: '18'
12 |
13 | concurrency: ${{ github.workflow }}-${{ github.ref }}
14 |
15 | jobs:
16 | release:
17 | name: Release
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout Repo
21 | uses: actions/checkout@v4
22 |
23 | - name: Setup Cache
24 | uses: actions/cache@v4
25 | with:
26 | path: |
27 | ~/.pnpm-store
28 | node_modules/.cache/turbo
29 | node_modules/.pnpm
30 | key: ${{ runner.os }}-node${{ env.NODE_VERSION }}-${{ hashFiles('**/pnpm-lock.yaml') }}
31 |
32 | - name: Setup Node.js
33 | uses: actions/setup-node@v4
34 | with:
35 | node-version: ${{ env.NODE_VERSION }}
36 |
37 | - run: corepack enable && pnpm --version
38 |
39 | - run: pnpm install --recursive --no-frozen-lockfile --loglevel=error
40 |
41 | - run: pnpm build
42 |
43 | - name: Create Release Pull Request
44 | uses: changesets/action@v1
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
48 | with:
49 | version: pnpm version:prepare
50 | publish: pnpm version:publish
51 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | # https://nodejs.org/en/about/releases/
17 | # https://pnpm.io/installation#compatibility
18 | version:
19 | - node: 18
20 | - node: 20
21 | - node: 22
22 |
23 | name: Node.js ${{ matrix.version.node }}
24 | env:
25 | NODE_VERSION: ${{ matrix.version.node }}
26 |
27 | steps:
28 | - uses: actions/checkout@v4
29 |
30 | - name: Setup Cache
31 | uses: actions/cache@v4
32 | with:
33 | path: |
34 | ~/.pnpm-store
35 | node_modules/.cache/turbo
36 | node_modules/.pnpm
37 | key: ${{ runner.os }}-node${{ matrix.version.node }}-${{ hashFiles('**/pnpm-lock.yaml') }}
38 |
39 | - name: Setup Node.js
40 | uses: actions/setup-node@v4
41 | with:
42 | node-version: ${{ matrix.version.node }}
43 |
44 | - run: corepack enable && pnpm --version
45 |
46 | - run: pnpm install --recursive --loglevel=error
47 |
48 | - run: pnpm build --filter='!@edge-runtime/docs'
49 |
50 | - run: pnpm run test --filter='!@edge-runtime/docs'
51 |
52 | - name: Generate coverage
53 | run: pnpm coverage
54 |
--------------------------------------------------------------------------------
/.github/workflows/update.yml:
--------------------------------------------------------------------------------
1 | # https://gist.github.com/Purpzie/8ed86ae38c73f440881bbee0523a324b
2 | # https://github.com/dependabot/dependabot-core/issues/1736
3 | name: Dependabot
4 | on: pull_request_target
5 | permissions: read-all
6 | env:
7 | NODE_VERSION: '18'
8 | jobs:
9 | update-lockfile:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.actor == 'dependabot[bot]' && github.event.pull_request.head.repo.fork == false }}
12 | permissions:
13 | pull-requests: write
14 | contents: write
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | ref: ${{ github.event.pull_request.head.ref }}
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: ${{ env.NODE_VERSION }}
22 | - run: corepack enable && pnpm --version
23 | - run: pnpm i --lockfile-only
24 | - run: |
25 | git config --global user.name github-actions[bot]
26 | git config --global user.email github-actions[bot]@users.noreply.github.com
27 | git add pnpm-lock.yaml
28 | git commit -m "Update pnpm-lock.yaml" || echo "No changes to commit"
29 | git push origin ${{ github.head_ref }}
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # npm
2 | node_modules
3 | npm-debug.log
4 | .node_history
5 | yarn.lock
6 |
7 | # tmp
8 | .tmp
9 | *.swo
10 | *.swp
11 | *.swn
12 | *.swm
13 | .DS_Store
14 | *#
15 | *~
16 | .idea
17 | *sublime*
18 | nbproject
19 |
20 |
21 | # test
22 | testApp
23 | coverage
24 | .nyc_output
25 |
26 | # env
27 | .env
28 | .envrc
29 |
30 | # build files
31 | .pnpm-debug.log
32 | .turbo
33 | docs/.next
34 | docs/node_modules
35 | packages/*/dist
36 | packages/*/*.tgz
37 | packages/runtime/src/version.ts
38 | .vercel
39 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # https://pnpm.io/npmrc
2 | enable-pre-post-scripts=true
3 | strict-peer-dependencies=false
4 | save-exact=true
5 | unsafe-perm=true
6 | prefer-offline=true
7 | prefer-workspace-packages=true
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | coverage
2 | node_modules
3 | .next
4 | build
5 | dist
6 | *.tsbuildinfo
7 | *.gitignore
8 | *.svg
9 | *.lock
10 | *.npmignore
11 | *.sql
12 | *.png
13 | *.jpg
14 | *.jpeg
15 | *.gif
16 | *.ico
17 | *.sh
18 | Dockerfile
19 | Dockerfile.*
20 | .env
21 | .env.*
22 | LICENSE
23 | *.log
24 | .DS_Store
25 | .dockerignore
26 | *.patch
27 | *.toml
28 | *.prisma
29 | packages/primitives/load
30 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "jsxSingleQuote": true,
3 | "quoteProps": "preserve",
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Vercel, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ###### [Documentation](https://edge-runtime.vercel.app/) | [CLI](https://edge-runtime.vercel.app/cli)
7 |
8 | ## License
9 |
10 | **edge-runtime** © [Vercel](https://vercel.com), released under the [MIT](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
11 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
12 |
13 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · X [@vercel](https://x.com/vercel)
14 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 |
--------------------------------------------------------------------------------
/docs/components/features.js:
--------------------------------------------------------------------------------
1 | import {
2 | ArrowPathIcon,
3 | FingerPrintIcon,
4 | CloudArrowUpIcon,
5 | BoltIcon,
6 | CpuChipIcon,
7 | ArrowsPointingOutIcon,
8 | } from '@heroicons/react/24/outline'
9 |
10 | const features = [
11 | {
12 | name: 'Web APIs',
13 | icon: ArrowPathIcon,
14 | },
15 | {
16 | name: 'Context isolation',
17 | icon: FingerPrintIcon,
18 | },
19 | {
20 | name: 'Easy to extend',
21 | icon: CloudArrowUpIcon,
22 | },
23 | {
24 | name: 'Lightweight',
25 | description: `Execute builds using every core at maximum parallelism without wasting idle CPUs.`,
26 | icon: BoltIcon,
27 | },
28 | {
29 | name: 'Written in TypeScript',
30 | description: `Define the relationships between your tasks and then let Turborepo optimize what to build and when.`,
31 | icon: ArrowsPointingOutIcon,
32 | },
33 | {
34 | name: 'Node.js v16 or higher',
35 | description: `Turborepo doesn't interfere with your runtime code or touch your sourcemaps. It does what it does and then gets out of your way.`,
36 | icon: CpuChipIcon,
37 | },
38 | ]
39 |
40 | function Features() {
41 | return (
42 | <>
43 |
44 | {features.map(({ icon: Icon, ...feature }, i) => (
45 |
49 |
50 |
55 |
56 |
57 |
58 | {feature.name}
59 |
60 |
61 |
62 | ))}
63 |
64 | >
65 | )
66 | }
67 |
68 | export default Features
69 |
--------------------------------------------------------------------------------
/docs/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/docs/next.config.js:
--------------------------------------------------------------------------------
1 | const withNextra = require('nextra')({
2 | theme: 'nextra-theme-docs',
3 | themeConfig: './theme.config.js',
4 | defaultShowCopyCode: true,
5 | })
6 |
7 | module.exports = withNextra({
8 | reactStrictMode: true,
9 | })
10 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/docs",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@heroicons/react": "~2.2.0",
6 | "next": "~15.2.4",
7 | "next-themes": "~0.4.6",
8 | "nextra": "2",
9 | "nextra-theme-docs": "2",
10 | "react": "18",
11 | "react-dom": "18",
12 | "swr": "~2.3.3"
13 | },
14 | "devDependencies": {
15 | "autoprefixer": "~10.4.21",
16 | "postcss": "~8.5.3",
17 | "tailwindcss": "3"
18 | },
19 | "engines": {
20 | "node": ">=18"
21 | },
22 | "scripts": {
23 | "build": "next build ",
24 | "clean": "pnpm run clean:node && pnpm run clean:build",
25 | "clean:build": "rm -rf dist",
26 | "clean:node": "rm -rf node_modules",
27 | "dev": "next",
28 | "lint": "next lint",
29 | "start": "next start"
30 | },
31 | "private": true,
32 | "license": "MIT"
33 | }
34 |
--------------------------------------------------------------------------------
/docs/pages/_app.mdx:
--------------------------------------------------------------------------------
1 | import '../styles.css'
2 | import 'nextra-theme-docs/style.css'
3 |
4 | export default function Nextra({ Component, pageProps }) {
5 | const getLayout = Component.getLayout || ((page) => page)
6 |
7 | return getLayout(
8 | <>
9 |
10 | >,
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/docs/pages/_document.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/google-font-display */
2 |
3 | import Document, { Html, Head, Main, NextScript } from 'next/document'
4 |
5 | // We use display=block to remove the jank
6 |
7 | class MyDocument extends Document {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 | }
31 |
32 | export default MyDocument
33 |
--------------------------------------------------------------------------------
/docs/pages/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "*": {
3 | "theme": {
4 | "footer": false
5 | }
6 | },
7 | "index": "Introduction",
8 | "getting-started": "Getting Started",
9 | "cli": "Command-line Interface",
10 | "features": "Features",
11 | "packages": "Packages",
12 | "changelog": {
13 | "title": "Changelog",
14 | "href": "https://github.com/vercel/edge-runtime/releases",
15 | "newWindow": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/docs/pages/cli.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Command-line Interface
3 | description: Learn how to install and run the Edge Runtime as a CLI.
4 | ---
5 |
6 | # Using the CLI
7 |
8 | ## Installation
9 |
10 | The **Edge Runtime** can be also consumed from your terminal when you install it globally in your system:
11 |
12 | ```sh npm2yarn
13 | npm -g install edge-runtime
14 | ```
15 |
16 | ## Usage
17 |
18 | The CLI provides you different ways to evaluate an script with [Edge Runtime APIs](/features/available-apis) constraints.
19 |
20 | You can just start an interactive session with `--repl`:
21 |
22 | ```bash
23 | edge-runtime --repl
24 | ```
25 |
26 | Evaluate an inline script with `--eval`:
27 |
28 | ```bash
29 | edge-runtime --eval "Object.getOwnPropertyNames(this)"
30 | ```
31 |
32 | Run a local HTTP server:
33 |
34 | ```bash
35 | edge-runtime --listen examples/fetch.js
36 | ```
37 |
38 | and more. In any case, you can see all this information typing `edge-runtime --help`.
39 |
--------------------------------------------------------------------------------
/docs/pages/features/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "available-apis": "Edge Runtime APIs",
3 | "polyfills": "Polyfills",
4 | "typescript-support": "TypeScript support"
5 | }
6 |
--------------------------------------------------------------------------------
/docs/pages/features/polyfills.mdx:
--------------------------------------------------------------------------------
1 | # Polyfills
2 |
3 | The **Edge Runtime** is built on top of Web APIs available in Node.js.
4 |
5 | The minimum Node.js version supported is **v14.6.0** that is mapped to **ES2019**.
6 |
7 | Under the hood, the following Web APIs are used by the **Edge Runtime**:
8 |
9 | | polyfill | node14 | node16 | node18 |
10 | | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ |
11 | | [util.types](https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/patches/util-types.js) | x | | |
12 | | [WebCrypto](https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/crypto.js) | x | | |
13 | | [AbortController, AbortSignal, DOMException](https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/abort-controller.js) | x | | |
14 | | [base64](https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/encoding.js) | x | | |
15 | | [fetch, Request, Response](https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/fetch.js) | x | x | |
16 | | [URLPattern](https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/url.js) | x | x | x |
17 | | [WebStreams](https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/streams.js) | x | x | x |
18 |
19 | The Edge Runtime polyfills missing APIs for backward compatibility with older Node.js versions.
20 |
21 | In the future, Node.js will become a superset of the Edge Runtime with built-in support for the [available APIs](/features/available-apis).
22 |
--------------------------------------------------------------------------------
/docs/pages/features/typescript-support.mdx:
--------------------------------------------------------------------------------
1 | # TypeScript support
2 |
3 | The **Edge Runtime** includes TypeScript types. In fact, the library is written in TypeScript!
4 |
5 | Just in case you need to have these types loaded as part of the global context, you can add them inside [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html):
6 |
7 | ```json
8 | {
9 | "compilerOptions": {
10 | "types": ["@edge-runtime/types"]
11 | }
12 | }
13 | ```
14 |
15 | Alternatively, you can load them using [triple-slash directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) as well:
16 |
17 | ```js
18 | ///
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/pages/features/upgrading-nextjs.mdx:
--------------------------------------------------------------------------------
1 | # Upgrading in Next.js
2 |
3 | [Next.js](https://github.com/vercel/next.js/) is using [Edge Runtime](https://edge-runtime.vercel.app/) and the dependency is updated very often to get the latest changes.
4 |
5 | If you want to upgrade the Edge Runtime version there, follow the steps:
6 |
7 | - Find for `"edge-runtime"` and `"@edge-runtime"` versions inside `"package.json"` files.
8 | - Upgrade the version numbers to the latest version available (You can find [npm semver calculator](https://semver.npmjs.com/) helpful to determine what's the latest version).
9 | - Perform a `pnpm install` on the root folder to install them but also update `'pnpm-lock.yaml'` properly.
10 | - Once the new version has been installed, you should to precompile them, entering to `packages/next` and executing `npm run ncc-compiled`.
11 | - Commit all the thing as a single commit, and that's it!
12 |
13 | Since Next.js has a lot of tests, the best way for testing your changes is to make a Next.js draft pull request.
14 |
15 | Then, if a test failed, you can reproduce it locally to find the gap.
16 |
--------------------------------------------------------------------------------
/docs/pages/getting-started.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | description: Learn how to install and run the Edge Runtime as a Node.js package.
4 | ---
5 |
6 | # Quick overview
7 |
8 | ## Installation
9 |
10 | The **Edge Runtime** is available as Node.js package. You can install it with your favorite package manager:
11 |
12 | ```sh npm2yarn
13 | npm install edge-runtime
14 | ```
15 |
16 | ## Usage
17 |
18 | Once it's installed, you can evaluate any script into the runtime context:
19 |
20 | ```js
21 | import { EdgeRuntime } from 'edge-runtime'
22 |
23 | const runtime = new EdgeRuntime()
24 | const result = await runtime.evaluate("fetch('https://example.vercel.sh')")
25 |
26 | console.log(result)
27 | ```
28 |
29 | The runtime provides you with ready to use Web APIs that can be extended if needed:
30 |
31 | ```js
32 | import { EdgeRuntime } from 'edge-runtime'
33 |
34 | const runtime = new EdgeRuntime({
35 | extend: (context) => {
36 | const rawFetch = context.fetch.bind(context.fetch)
37 | context.fetch = async (input: RequestInfo | URL, init?: RequestInit) =>
38 | rawFetch(
39 | typeof input === 'string' && !input.startsWith('https://')
40 | ? `https://${input}`
41 | : String(input),
42 | init
43 | )
44 |
45 | return context
46 | },
47 | })
48 |
49 | const result = await runtime.evaluate("fetch('example.com')")
50 |
51 | console.log(result)
52 | ```
53 |
54 | You can load some initial code, for example, to be ready for receiving fetch events:
55 |
56 | ```js
57 | import { EdgeRuntime } from 'edge-runtime'
58 |
59 | const initialCode = `
60 | addEventListener('fetch', event => {
61 | return event.respondWith(fetch(event.request.url))
62 | })`
63 |
64 | const edgeRuntime = new EdgeRuntime({ initialCode })
65 | const response = await edgeRuntime.dispatchFetch('https://example.vercel.sh')
66 |
67 | // If your code logic performs asynchronous tasks, you should await them.
68 | // https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil
69 | await response.waitUntil()
70 |
71 | console.log(await response.text())
72 | ```
73 |
74 | and expose it locally to be used via HTTP:
75 |
76 | ```js
77 | import { EdgeRuntime, runServer } from 'edge-runtime'
78 | import { onExit } from 'signal-exit'
79 |
80 | const initialCode = `
81 | addEventListener('fetch', event => {
82 | const { searchParams } = new URL(event.request.url)
83 | const url = searchParams.get('url')
84 | return event.respondWith(fetch(url))
85 | })`
86 |
87 | const edgeRuntime = new EdgeRuntime({ initialCode })
88 |
89 | const server = await runServer({ runtime: edgeRuntime, port: 3000 })
90 | console.log(`> Edge server running at ${server.url}`)
91 | onExit(() => server.close())
92 | ```
93 |
94 | After that, you will be ready to hit your local server from your terminal:
95 |
96 | ```
97 | curl http://[::]:3000?url=https://example.vercel.sh
98 | ```
99 |
--------------------------------------------------------------------------------
/docs/pages/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: What is Edge Runtime
3 | description: The Edge Runtime is a toolkit for developing, testing, and defining the runtime Web APIs for Edge infrastructure.
4 | ---
5 |
6 | import Features from '../components/features'
7 |
8 | # The Edge Runtime
9 |
10 | The Edge Runtime is designed to help framework authors adopt edge computing and provide open-source tooling built on Web standards. It’s designed to be integrated into frameworks (like Next.js) and not for usage in application code.
11 |
12 | The Edge Runtime is a subset of Node.js APIs, giving you compatibility and interoperability between multiple web environments. The project is designed to be compliant with standards developed by [WinterCG](https://wintercg.org) - a community group between Vercel, Cloudflare, Deno, Shopify, and more. The term “Edge” refers to the orientation toward instant serverless compute environments and not a specific set of locations.
13 |
14 |
15 |
16 | ## Using the Edge Runtime Locally
17 |
18 | When developing and testing locally, the Edge Runtime will polyfill Web APIs and ensure compatibility with the Node.js layer.
19 |
20 | In production, the Edge Runtime uses the [JavaScript V8 engine](https://v8.dev/), **not** Node.js, so there is **no access** to Node.js APIs.
21 |
22 | Get started using Edge Runtime:
23 |
24 | - [Explore the available APIs](features/available-apis)
25 | - [Integrate it in your project](/packages/runtime)
26 | - [Test your code with Jest](/packages/jest-environment) or [Vitest](https://vitest.dev/config/#environment)
27 |
--------------------------------------------------------------------------------
/docs/pages/packages/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "cookies": "@edge-runtime/cookies",
3 | "format": "@edge-runtime/format",
4 | "jest-environment": "@edge-runtime/jest-environment",
5 | "jest-expect": "@edge-runtime/jest-expect",
6 | "node-utils": "@edge-runtime/node-utils",
7 | "ponyfill": "@edge-runtime/ponyfill",
8 | "primitives": "@edge-runtime/primitives",
9 | "runtime": "edge-runtime",
10 | "types": "@edge-runtime/types",
11 | "user-agent": "@edge-runtime/user-agent",
12 | "vm": "@edge-runtime/vm"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/pages/packages/cookies.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime Cookies
2 |
3 | The **@edge-runtime/cookies** package provides high-level HTTP Cookie
4 | abstractions for Edge-compatible environments, such as Edge Runtime or Vercel
5 | Edge Functions and Edge Middleware.
6 |
7 | The available methods are based on the [`CookieStore API`](https://wicg.github.io/cookie-store/#CookieStore).
8 | The main difference is that the methods are not asynchronous so they do not return a `Promise`.
9 |
10 | ## Installation
11 |
12 | ```sh npm2yarn
13 | npm install @edge-runtime/cookies
14 | ```
15 |
16 | ## Usage
17 |
18 | ### for Request
19 |
20 | To access and manipulate request cookies, use the exported `RequestCookies` constructor:
21 |
22 | ```ts
23 | import { RequestCookies } from '@edge-runtime/cookies'
24 |
25 | function handleRequest(req: Request) {
26 | const cookies = new RequestCookies(req.headers)
27 | cookies.get('cookie-name')?.value // undefined | string
28 | cookies.has('cookie-name') // boolean
29 | // do something...
30 | }
31 | ```
32 |
33 | Notes:
34 |
35 | - All mutations are performed in place and will update the `Cookie` header in
36 | the provided `Request` object.
37 |
38 | - When mutating the request cookies, the client won't be able to read the
39 | updated/deleted cookies. Please use `ResponseCookies` for that.
40 |
41 | #### Available methods
42 |
43 | - `get` - A method that takes a cookie `name` and returns an object with `name` and `value`. If a cookie with `name` isn't found, it returns `undefined`. If multiple cookies match, it will only return the first match. The cookie configuration (Max-Age, Path etc) is not being passed by the HTTP client, therefore it's not possible to determine the cookie expiration date.
44 | - `getAll` - A method that is similar to `get`, but returns a list of all the cookies with a matching `name`. If `name` is unspecified, it returns all the available cookies.
45 | - `set` - A method that takes an object with properties of `CookieListItem` as defined in the [W3C CookieStore API](https://wicg.github.io/cookie-store/#dictdef-cookielistitem) spec.
46 | - `delete` - A method that takes either a cookie `name` or a list of names. and removes the cookies matching the name(s). Returns `true` for deleted and `false` for undeleted cookies.
47 | - `has` - A method that takes a cookie `name` and returns a `boolean` based on if the cookie exists (`true`) or not (`false`).
48 | - `clear` - A method that takes no argument and will effectively remove the `Cookie` header.
49 |
50 | ### for Response
51 |
52 | To access and manipulate response cookies that will persist in the HTTP client,
53 | use the exported `ResponseCookies` constructor:
54 |
55 | ```ts
56 | import { ResponseCookies } from '@edge-runtime/cookies'
57 |
58 | function handleRequest(req: Request) {
59 | const cookieKey = 'cookie-name'
60 | const headers = new Headers()
61 | const responseCookies = new ResponseCookies(headers)
62 |
63 | cookies.set('cookie-name', 'cookie-value', { maxAge: 1000 }) // make cookie persistent for 1000 seconds
64 | cookies.delete('old-cookie')
65 |
66 | return new Response(null, {
67 | headers,
68 | status: 200,
69 | })
70 | }
71 | ```
72 |
73 | #### Available methods
74 |
75 | - `get` - A method that takes a cookie `name` and returns an object with `name` and `value`. If a cookie with `name` isn't found, it returns `undefined`. If multiple cookies match, it will only return the first match.
76 | - `getAll` - A method that is similar to `get`, but returns a list of all the cookies with a matching `name`. If `name` is unspecified, it returns all the available cookies.
77 | - `set` - A method that takes an object with properties of `CookieListItem` as defined in the [W3C CookieStore API](https://wicg.github.io/cookie-store/#dictdef-cookielistitem) spec.
78 | - `delete` - A method that takes either a cookie `name` or a list of names. and removes the cookies matching the name(s). Returns `true` for deleted and `false` for undeleted cookies.
79 |
--------------------------------------------------------------------------------
/docs/pages/packages/format.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime Format
2 |
3 | The **@edge-runtime/format** package receives any JavaScript primitive or Object as input, returning the beauty string representation as output.
4 |
5 | It follows the [formatter spec](https://console.spec.whatwg.org/#formatter) and can be used as drop-in replacement for [util.inspect](https://nodejs.org/api/util.html#utilinspectobject-options).
6 |
7 | ## Installation
8 |
9 | ```sh npm2yarn
10 | npm install @edge-runtime/format
11 | ```
12 |
13 | This package includes built-in TypeScript support.
14 |
15 | ## Usage
16 |
17 | First, call `createFormat` method for initialize your formatter:
18 |
19 | ```js
20 | import { createFormat } from '@edge-runtime/format'
21 | const format = createFormat()
22 | ```
23 |
24 | After that, you can interact with your formatter:
25 |
26 | ```js
27 | const obj = { [Symbol.for('foo')]: 'bar' }
28 |
29 | format(obj)
30 |
31 | // => '{ [Symbol(foo)]: 'bar' }'
32 | ```
33 |
34 | You can output multiple objects by listing them:
35 |
36 | ```js
37 | format('The PI number is', Math.PI, '(more or less)')
38 |
39 | // => 'The PI number is 3.141592653589793 (more or less)'
40 | ```
41 |
42 | The string substitutions (printf style) is supported, and can be combined:
43 |
44 | ```js
45 | format('The PI number is %i', Math.PI, '(rounded)')
46 |
47 | // => 'The PI number is 3 (rounded)'
48 | ```
49 |
50 | In case you need to hide implementation details or want to customize how something should be printed, you can use the `customInspectSymbol` option for that:
51 |
52 | ```js
53 | const customInspectSymbol = Symbol.for('edge-runtime.inspect.custom')
54 |
55 | class Password {
56 | constructor(value) {
57 | Object.defineProperty(this, 'password', {
58 | value,
59 | enumerable: false,
60 | })
61 | }
62 |
63 | toString() {
64 | return 'xxx'
65 | }
66 |
67 | [customInspectSymbol]() {
68 | return {
69 | password: `<${this.toString()}>`,
70 | }
71 | }
72 | }
73 |
74 | format(new Password('r0sebud'))
75 |
76 | // => { password: '' }
77 | ```
78 |
79 | ## API
80 |
81 | ### createFormat([options])
82 |
83 | It returns a formatter method.
84 |
85 | #### options
86 |
87 | ##### formatError?: (error: Error) => string
88 |
89 | It customizes how errors should be printed.
90 |
91 | The default behavior is `error.toString()`.
92 |
93 | ##### customInspectSymbol?: symbol
94 |
95 | It sets the symbol to be used for printing custom behavior.
96 |
97 | The default value is `edge-runtime.inspect.custom`.
98 |
--------------------------------------------------------------------------------
/docs/pages/packages/jest-environment.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime Jest Environment
2 |
3 | The **@edge-runtime/jest-environment** package enables you to run [Jest](https://jestjs.io) tests against the Edge Runtime environment.
4 |
5 | It helps you to write tests assertion without being worried about the environment setup.
6 |
7 | ## Installation
8 |
9 | ```sh npm2yarn
10 | npm install @edge-runtime/jest-environment
11 | ```
12 |
13 | ## Usage
14 |
15 | Jest enables you to define the test environment through a [code comment](https://jestjs.io/docs/configuration#testenvironment-string) or as a [CLI option](https://jestjs.io/docs/cli#--envenvironment).
16 |
17 | For example, the following test would **pass**:
18 |
19 | ```js
20 | // jest --env node
21 | // ✅ Pass
22 | it('should return the correct value', () => {
23 | let val = eval('2 + 2')
24 | expect(val).toBe(4)
25 | })
26 | ```
27 |
28 | The following test would **fail** when using the Edge Runtime:
29 |
30 | ```js
31 | // jest --env @edge-runtime/jest-environment
32 | // ❌ Fail
33 | // Error name: "EvalError"
34 | // Error message: "Code generation from strings disallowed for this context"
35 | it('should return the correct value', () => {
36 | let val = eval('2 + 2')
37 | expect(val).toBe(4)
38 | })
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/pages/packages/jest-expect.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime Jest Expect
2 |
3 | The **@edge-runtime/jest-expect** package comes with useful [Jest matchers](https://jestjs.io/docs/expect#expectextendmatchers) to help testing `Request` and `Response` instances.
4 |
5 | ## Installation
6 |
7 | ```sh npm2yarn
8 | npm install @edge-runtime/jest-expect
9 | ```
10 |
11 | ### Usage
12 |
13 | Add the following in your `jest.config.js` file:
14 |
15 | ```js
16 | // See https://jestjs.io/docs/configuration#setupfilesafterenv-array
17 | setupFilesAfterEnv: ['@edge-runtime/jest-expect'],
18 | ```
19 |
20 | To include the TypeScript types, add the following in your `tsconfig.json` file:
21 |
22 | ```json
23 | // See https://www.typescriptlang.org/tsconfig#include
24 | "include": [
25 | "node_modules/@edge-runtime/jest-expect"
26 | ]
27 | ```
28 |
29 | ### API
30 |
31 | #### .toHaveStatus
32 |
33 | It checks for HTTP status code or message correctness:
34 |
35 | ```js
36 | const response = new Response('OK')
37 |
38 | expect(response).toHaveStatus(200)
39 | expect(response).toHaveStatus('Successful')
40 | expect(response).not.toHaveStatus(201)
41 | ```
42 |
43 | #### .toHaveJSONBody
44 |
45 | It checks if an HTTP request/response body is a JSON:
46 |
47 | ```js
48 | const response = Response.json({ hello: 'world' })
49 |
50 | expect(response).toHaveJSONBody({ hello: 'world' })
51 | expect(response).not.toHaveJSONBody({ foo: 'baz' })
52 | ```
53 |
54 | #### .toHaveTextBody
55 |
56 | It checks if an HTTP request/response body is a text:
57 |
58 | ```js
59 | const request = new Request('http://example.com', {
60 | method: 'POST',
61 | body: 'Hello World',
62 | })
63 |
64 | expect(request).toHaveTextBody('hello world')
65 | expect(request).not.toHaveTextBody('foo bar')
66 | ```
67 |
--------------------------------------------------------------------------------
/docs/pages/packages/ponyfill.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime Ponyfill
2 |
3 | The **@edge-runtime/ponyfill** package helps to have the Edge Runtime APIs available in your environment:
4 |
5 | - When running on Edge Runtime, no polyfills will be loaded, and the native implementations will be used.
6 | - When running on Node.js runtimes, this package will load the polyfills from [@edge-runtime/primitives](/packages/primitives).
7 |
8 | Note this is just necessary if you want to run the same code across different environments.
9 |
10 | ## Installation
11 |
12 | ```sh npm2yarn
13 | npm install @edge-runtime/ponyfill
14 | ```
15 |
16 | This package includes built-in TypeScript support.
17 |
18 | ## Usage
19 |
20 | Rather than use APIs from the global scope:
21 |
22 | ```js
23 | const data = new TextEncoder().encode('Hello, world')
24 | const digest = await crypto.subtle.digest('SHA-256', content)
25 | ```
26 |
27 | Load them from the package instead:
28 |
29 | ```js
30 | import { crypto, TextEncoder } from '@edge-runtime/ponyfill'
31 |
32 | const data = new TextEncoder().encode('Hello, world')
33 | const digest = await crypto.subtle.digest('SHA-256', content)
34 | ```
35 |
36 | Any [Edge Runtime API](/features/available-apis) is available.
37 |
--------------------------------------------------------------------------------
/docs/pages/packages/primitives.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime Primitives
2 |
3 | The **@edge-runtime/primitives** package contains all the Web Standard APIs that represent the Edge environment.
4 |
5 | These APIs are a subset of modern browser APIs (such as `fetch`, `URLPattern`, `structuredClone`, etc).
6 |
7 | See full list is available at [Edge Runtime APIs](/features/available-apis).
8 |
9 | ## Installation
10 |
11 | ```sh npm2yarn
12 | npm install @edge-runtime/primitives --save
13 | ```
14 |
15 | This package includes built-in TypeScript support.
16 |
17 | ## Usage
18 |
19 | The **@edge-runtime/primitives** package exports a context containing all the web primitives:
20 |
21 | ```js
22 | import primitives from '@edge-runtime/primitives'
23 |
24 | console.log(primitives)
25 |
26 | // {
27 | // AbortController: [Getter],
28 | // AbortSignal: [Getter],
29 | // Blob: [Getter],
30 | // console: [Getter],
31 | // Crypto: [Getter],
32 | // CryptoKey: [Getter],
33 | // SubtleCrypto: [Getter],
34 | // crypto: [Getter],
35 | // TextDecoder: [Getter],
36 | // TextDecoderStream: [Getter],
37 | // …
38 | // }
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/pages/packages/types.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime Types
2 |
3 | The **@edge-runtime/types** package has the TypeScript global types for using Edge Runtime.
4 |
5 | ## Installation
6 |
7 | ```sh npm2yarn
8 | npm install @edge-runtime/types
9 | ```
10 |
11 | ## Usage
12 |
13 | If you need to have these types loaded as part of the global context, you can add them inside [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html):
14 |
15 | ```json
16 | {
17 | "compilerOptions": {
18 | "types": ["@edge-runtime/types"]
19 | }
20 | }
21 | ```
22 |
23 | Alternatively, you can load them using [triple-slash directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) as well:
24 |
25 | ```js
26 | ///
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/pages/packages/user-agent.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra-theme-docs'
2 |
3 | # Edge Runtime User Agent
4 |
5 | The **@edge-runtime/user-agent** package gives some utilities on top of [ua-parser-js](https://faisalman.github.io/ua-parser-js/).
6 |
7 | ## Installation
8 |
9 | ```sh npm2yarn
10 | npm install @edge-runtime/user-agent
11 | ```
12 |
13 | This package includes built-in TypeScript support.
14 |
15 | ## Usage
16 |
17 | Just call the exported `.userAgentFromString` method for getting the parsed data from an [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) HTTP header:
18 |
19 | ```js
20 | import { userAgentFromString } from '@edge-runtime/user-agent'
21 |
22 | export default (request: Request) => {
23 | const userAgent = request.headers.get('user-agent')
24 | userAgentFromString(userAgent)
25 |
26 | return Response.json(userAgent(request))
27 | // => {
28 | // browser: {
29 | // major: '83',
30 | // name: 'Chrome',
31 | // version: '83.0.4103.116',
32 | // },
33 | // cpu: { architecture: 'amd64' },
34 | // engine: {
35 | // name: 'Blink',
36 | // version: '83.0.4103.116',
37 | // },
38 | // isBot: false,
39 | // os: {
40 | // name: 'Windows',
41 | // version: '10',
42 | // },
43 | // ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)…'
44 | // }
45 | }
46 | ```
47 |
48 | or alternatively, you can call `.userAgent` and pass the `Request` instance:
49 |
50 | ```ts
51 | import { userAgent } from '@edge-runtime/user-agent'
52 |
53 | export default (request: Request) => {
54 | return Response.json(userAgent(request))
55 | // => {
56 | // browser: {
57 | // major: '83',
58 | // name: 'Chrome',
59 | // version: '83.0.4103.116',
60 | // },
61 | // cpu: { architecture: 'amd64' },
62 | // engine: {
63 | // name: 'Blink',
64 | // version: '83.0.4103.116',
65 | // },
66 | // isBot: false,
67 | // os: {
68 | // name: 'Windows',
69 | // version: '10',
70 | // },
71 | // ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)…'
72 | // }
73 | }
74 | ```
75 |
--------------------------------------------------------------------------------
/docs/pages/packages/vm.mdx:
--------------------------------------------------------------------------------
1 | # Edge Runtime VM
2 |
3 | The **@edge-runtime/vm** package is the low level bindings for creating Web Standard isolated VM contexts under the same [Vercel Edge Functions](https://vercel.com/edge) conditions.
4 |
5 | It has been designed to simulate Edge conditions locally in your machine.
6 |
7 | ## Installation
8 |
9 | ```sh npm2yarn
10 | npm install @edge-runtime/vm
11 | ```
12 |
13 | This package includes built-in TypeScript support.
14 |
15 | ## Usage
16 |
17 | You can evaluate arbitrary code isolated inside a VM context, getting output back from the output:
18 |
19 | ```js
20 | import { EdgeVM } from '@edge-runtime/vm'
21 |
22 | const edgeVM = new EdgeVM()
23 | const promise = edgeVM.evaluate("fetch('https://edge-ping.vercel.app')")
24 | const { url, status } = await promise
25 |
26 | console.log(`ping to ${url} returned ${status}`)
27 | ```
28 |
29 | The **@edge-runtime/vm** has [@edge-runtime/primitives](/packages/primitives) preloaded.
30 |
31 | In case you need, you can extend the VM context:
32 |
33 | ```js
34 | const edgeVM = new EdgeVM({
35 | extend: (context) => {
36 | const rawFetch = context.fetch.bind(context.fetch)
37 | context.fetch = async (input: RequestInfo | URL, init?: RequestInit) =>
38 | rawFetch(
39 | typeof input === 'string' && !input.startsWith('https://')
40 | ? `https://${input}`
41 | : String(input),
42 | init
43 | )
44 |
45 | return context
46 | },
47 | })
48 |
49 | const { url, status } = await edgeVM.evaluate("fetch('edge-ping.vercel.app')")
50 |
51 | console.log(`ping to ${url} returned ${status}`)
52 | ```
53 |
54 | ## API
55 |
56 | ### new EdgeVM([options])
57 |
58 | It creates a new EdgeVM instance.
59 |
60 | #### options
61 |
62 | ##### codeGeneration?: object
63 |
64 | Provides code generation options to [vm.createContext#options](https://nodejs.org/api/vm.html#vmcreatecontextcontextobject-options).
65 |
66 | If you don't provide any option, `{ strings: false, wasm: true }` will be used.
67 |
68 | ##### extend?: (context: VMContext) => ExtendedDictionary\
69 |
70 | Allows to extend the VMContext.
71 |
72 | ```js
73 | const vm = new EdgeVM({
74 | extend: (context) =>
75 | Object.assign(context, {
76 | process: { env: { NODE_ENV: 'development' } },
77 | }),
78 | })
79 | ```
80 |
81 | > Note that it must return a contextified object so ideally it should return the same reference it receives.
82 |
83 | ##### requireCache?: Map\
84 |
85 | Provides an initial map to the require cache, if none is given, it will be initialized to an empty map.
86 |
87 | #### methods
88 |
89 | ##### evaluate: (code: string): () => Promise\
90 |
91 | Allows you to run arbitrary code within the VM.
92 |
93 | ##### require: (filepath: string)
94 |
95 | Allows you to require a CommonJS module referenced in the provided file path within the VM context. It will return its exports.
96 |
97 | ##### requireInContext: (filepath: string)
98 |
99 | Same as `require` but it will copy each of the exports in the context of the VM. Exports can then be used inside of the VM with an evaluated script.
100 |
101 | ##### requireInlineInContext: (code: string)
102 |
103 | Same as `requireInContext` but allows you to pass the code instead of a reference to a file.
104 |
105 | It will create a temporary file and then load it in the VM Context.
106 |
--------------------------------------------------------------------------------
/docs/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'tailwindcss/nesting': {},
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/docs/public/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/docs/public/logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/docs/public/og-image-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/edge-runtime/77be36e84fbeb20ecd83ad065243ded353a3f803/docs/public/og-image-dark.png
--------------------------------------------------------------------------------
/docs/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/edge-runtime/77be36e84fbeb20ecd83ad065243ded353a3f803/docs/public/og-image.png
--------------------------------------------------------------------------------
/docs/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind utilities;
2 |
3 | code {
4 | @apply text-sm;
5 | }
6 |
7 | body {
8 | background:
9 | linear-gradient(
10 | to bottom,
11 | rgba(255, 255, 255, 0) 0%,
12 | rgba(255, 255, 255, 1) 300px
13 | ),
14 | fixed 0 0 / 20px 20px radial-gradient(#d1d1d1 1px, transparent 0),
15 | fixed 10px 10px / 20px 20px radial-gradient(#d1d1d1 1px, transparent 0);
16 | }
17 |
18 | .dark .invert-on-dark {
19 | filter: invert(1) brightness(1.8);
20 | }
21 |
22 | #__next .nextra-body > main h1 {
23 | margin-top: 0;
24 | }
25 |
26 | #__next article :not(pre) > code {
27 | color: var(--shiki-token-string-expression);
28 | background-color: transparent;
29 | border: 0;
30 | font-size: 90%;
31 | }
32 |
33 | #__next article :not(pre) > code::before,
34 | #__next article :not(pre) > code::after {
35 | content: '\`';
36 | }
37 |
38 | .dark body article pre {
39 | background-color: transparent;
40 | border: 1px solid rgba(224, 243, 255, 0.1);
41 | border-radius: 5px;
42 | }
43 |
44 | .light body article pre {
45 | background-color: transparent;
46 | border: 1px solid #e5e7eb;
47 | border-radius: 5px;
48 | }
49 |
50 | .light body {
51 | --shiki-color-text: #111;
52 | --shiki-token-constant: var(--shiki-color-text);
53 | --shiki-token-comment: #999;
54 | --shiki-token-keyword: #ff0078;
55 | --shiki-token-function: #0077ff;
56 | --shiki-token-string-expression: #028265;
57 | --shiki-token-punctuation: var(--shiki-color-text);
58 | }
59 |
60 | .dark body {
61 | --shiki-color-text: #eaeaea;
62 | --shiki-token-constant: var(--shiki-color-text);
63 | --shiki-token-comment: #666;
64 | --shiki-token-keyword: #ff0078;
65 | --shiki-token-function: #0099ff;
66 | --shiki-token-string-expression: #50e3c2;
67 | --shiki-token-punctuation: var(--shiki-color-text);
68 |
69 | background:
70 | linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #111 300px),
71 | fixed 0 0 / 20px 20px radial-gradient(#313131 1px, transparent 0),
72 | fixed 10px 10px / 20px 20px radial-gradient(#313131 1px, transparent 0);
73 | }
74 |
75 | code[data-language='sh'] {
76 | --shiki-token-string: var(--shiki-color-text);
77 | }
78 |
79 | #__next article.nextra-body h1 {
80 | font-size: 2em;
81 | }
82 |
83 | #__next article.nextra-body h2 {
84 | font-size: 1.5em;
85 | }
86 |
87 | #__next article.nextra-body h3 {
88 | font-size: 1.25em;
89 | }
90 |
91 | #__next article.nextra-body h4 {
92 | font-size: 1em;
93 | }
94 |
95 | #__next article.nextra-body h5 {
96 | font-size: 0.875em;
97 | }
98 |
99 | #__next article.nextra-body h5 {
100 | font-size: 0.85em;
101 | }
102 |
--------------------------------------------------------------------------------
/docs/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 |
3 | module.exports = {
4 | content: [
5 | './components/**/*.js',
6 | './components/**/*.tsx',
7 | './nextra-theme-docs/**/*.js',
8 | './nextra-theme-docs/**/*.tsx',
9 | './nextra-theme-docs/**/*.css',
10 | './pages/**/*.md',
11 | './pages/**/*.mdx',
12 | './pages/**/*.tsx',
13 | './theme.config.js',
14 | './styles.css',
15 | ],
16 | theme: {
17 | extend: {
18 | fontFamily: {
19 | sans: [`"Inter"`, 'sans-serif'],
20 | mono: [
21 | 'Menlo',
22 | 'Monaco',
23 | 'Lucida Console',
24 | 'Liberation Mono',
25 | 'DejaVu Sans Mono',
26 | 'Bitstream Vera Sans Mono',
27 | 'Courier New',
28 | 'monospace',
29 | ],
30 | },
31 | colors: {
32 | dark: '#000',
33 | gray: colors.neutral,
34 | blue: colors.blue,
35 | orange: colors.orange,
36 | green: colors.green,
37 | red: colors.red,
38 | yellow: colors.yellow,
39 | },
40 | screens: {
41 | sm: '640px',
42 | md: '768px',
43 | lg: '1024px',
44 | betterhover: { raw: '(hover: hover)' },
45 | },
46 | },
47 | },
48 | darkMode: 'class',
49 | }
50 |
--------------------------------------------------------------------------------
/docs/theme.config.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { useRouter } from 'next/router'
3 | import { useTheme } from 'next-themes'
4 | import { useConfig } from 'nextra-theme-docs'
5 | const useDark = () => {
6 | const { resolvedTheme } = useTheme()
7 | const [isDark, setIsDark] = useState(false)
8 | useEffect(() => {
9 | setIsDark(resolvedTheme === 'dark')
10 | return () => false
11 | }, [resolvedTheme])
12 | return isDark
13 | }
14 | /** @type import('nextra-theme-docs').DocsThemeConfig */
15 | const theme = {
16 | project: {
17 | link: 'https://github.com/vercel/edge-runtime',
18 | },
19 | editLink: {
20 | text: 'Edit this page on GitHub',
21 | },
22 | feedback: {
23 | content: 'Question? Give us feedback →',
24 | },
25 | toc: {
26 | float: true,
27 | },
28 | docsRepositoryBase: 'https://github.com/vercel/edge-runtime/blob/main/docs',
29 | useNextSeoProps() {
30 | return {
31 | titleTemplate: '%s | Edge Runtime',
32 | }
33 | },
34 | logo: function Logo() {
35 | const isDark = useDark()
36 | return (
37 | <>
38 |
43 | Edge Runtime
44 | >
45 | )
46 | },
47 | head: function Head() {
48 | const router = useRouter()
49 | const isDark = useDark()
50 | const { frontMatter, title } = useConfig()
51 | return (
52 | <>
53 |
54 |
59 |
60 |
61 |
62 |
63 |
67 |
73 |
74 | >
75 | )
76 | },
77 | }
78 | export default theme
79 |
--------------------------------------------------------------------------------
/docs/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "env": {
4 | "ENABLE_ROOT_PATH_BUILD_CACHE": "1",
5 | "FORCE_RUNTIME_TAG": "canary"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types'
2 | import path from 'path'
3 |
4 | export default (rootDir: string): Config.InitialOptions => {
5 | return {
6 | rootDir,
7 | setupFilesAfterEnv: [path.join(__dirname, 'jest.setup.js')],
8 | transform: {
9 | '^.+\\.tsx?$': [
10 | 'ts-jest',
11 | {
12 | 'diagnostics': true,
13 | 'isolatedModules': true,
14 | },
15 | ],
16 | },
17 | preset: 'ts-jest/presets/default',
18 | testEnvironment: 'node',
19 | watchPlugins: [
20 | 'jest-watch-typeahead/filename',
21 | 'jest-watch-typeahead/testname',
22 | ],
23 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Jest uses a VM under the covers but it is setup to look like Node.js.
3 | * Those globals that are missing in the VM but exist in Node.js will be
4 | * copied over but they can produce objects that use prototypes that do
5 | * not exist in the VM. This causes instanceof checks to fail.
6 | *
7 | * For example, `TextEncoder` does not exist in the VM so it gets copied
8 | * from Node.js, but it can produce objects that use `Uint8Array` prototype.
9 | * This one differs from the one in the VM so what Jest does is to copy the
10 | * Node.js one and override one in the VM.
11 | *
12 | * The problem is that `new Uint8Array instanceof Object` will return false
13 | * because the underlying object is from a different realm. To fix this we
14 | * can patch `instanceof` to check against both for every prototype that we
15 | * duplicate in the Jest Runtime.
16 | */
17 | global.Object = new Proxy(Object, {
18 | get(target, prop, receiver) {
19 | if (prop === Symbol.hasInstance) {
20 | const ObjectConstructor = Object.getPrototypeOf(
21 | Object.getPrototypeOf(Uint8Array.prototype),
22 | ).constructor
23 |
24 | return function (instance) {
25 | return (
26 | instance instanceof target || instance instanceof ObjectConstructor
27 | )
28 | }
29 | }
30 |
31 | return Reflect.get(target, prop, receiver)
32 | },
33 | })
34 |
--------------------------------------------------------------------------------
/media/logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/edge-runtime/77be36e84fbeb20ecd83ad065243ded353a3f803/media/logo.sketch
--------------------------------------------------------------------------------
/packages/cookies/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/cookies: An Edge Runtime compatible cookie helpers.
8 |
See @edge-runtime/cookies section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/cookies --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/cookies --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/cookies --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/cookies** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/cookies/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | config.testEnvironment = '@edge-runtime/jest-environment'
6 | export default config
7 |
--------------------------------------------------------------------------------
/packages/cookies/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/cookies",
3 | "description": "Cookie helpers compatible with Edge Runtime",
4 | "homepage": "https://edge-runtime.vercel.app/packages/cookies",
5 | "version": "6.0.0",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "repository": {
9 | "directory": "packages/cookies",
10 | "type": "git",
11 | "url": "git+https://github.com/vercel/edge-runtime.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/vercel/edge-runtime/issues"
15 | },
16 | "keywords": [
17 | "cookie",
18 | "cookies",
19 | "edge",
20 | "edge-runtime",
21 | "functions",
22 | "runtime",
23 | "set-cookie",
24 | "standard",
25 | "web"
26 | ],
27 | "devDependencies": {
28 | "@edge-runtime/format": "workspace:*",
29 | "@edge-runtime/jest-environment": "workspace:*",
30 | "@types/cookie": "0.6.0",
31 | "tsup": "8"
32 | },
33 | "engines": {
34 | "node": ">=18"
35 | },
36 | "files": [
37 | "dist"
38 | ],
39 | "scripts": {
40 | "build": "tsup",
41 | "clean:build": "rm -rf dist",
42 | "clean:node": "rm -rf node_modules",
43 | "prebuild": "pnpm run clean:build",
44 | "test": "jest"
45 | },
46 | "license": "MIT",
47 | "publishConfig": {
48 | "access": "public"
49 | },
50 | "types": "dist/index.d.ts"
51 | }
52 |
--------------------------------------------------------------------------------
/packages/cookies/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { CookieListItem, RequestCookie, ResponseCookie } from './types'
2 | export { RequestCookies } from './request-cookies'
3 | export { ResponseCookies } from './response-cookies'
4 | export { stringifyCookie, parseCookie, parseSetCookie } from './serialize'
5 |
--------------------------------------------------------------------------------
/packages/cookies/src/request-cookies.ts:
--------------------------------------------------------------------------------
1 | import type { RequestCookie } from './types'
2 | import { parseCookie, stringifyCookie } from './serialize'
3 |
4 | /**
5 | * A class for manipulating {@link Request} cookies (`Cookie` header).
6 | */
7 | export class RequestCookies {
8 | /** @internal */
9 | readonly _headers: Headers
10 | /** @internal */
11 | _parsed: Map = new Map()
12 |
13 | constructor(requestHeaders: Headers) {
14 | this._headers = requestHeaders
15 | const header = requestHeaders.get('cookie')
16 | if (header) {
17 | const parsed = parseCookie(header)
18 | for (const [name, value] of parsed) {
19 | this._parsed.set(name, { name, value })
20 | }
21 | }
22 | }
23 |
24 | [Symbol.iterator]() {
25 | return this._parsed[Symbol.iterator]()
26 | }
27 |
28 | /**
29 | * The amount of cookies received from the client
30 | */
31 | get size(): number {
32 | return this._parsed.size
33 | }
34 |
35 | get(...args: [name: string] | [RequestCookie]) {
36 | const name = typeof args[0] === 'string' ? args[0] : args[0].name
37 | return this._parsed.get(name)
38 | }
39 |
40 | getAll(...args: [name: string] | [RequestCookie] | []) {
41 | const all = Array.from(this._parsed)
42 | if (!args.length) {
43 | return all.map(([_, value]) => value)
44 | }
45 |
46 | const name = typeof args[0] === 'string' ? args[0] : args[0]?.name
47 | return all.filter(([n]) => n === name).map(([_, value]) => value)
48 | }
49 |
50 | has(name: string) {
51 | return this._parsed.has(name)
52 | }
53 |
54 | set(...args: [key: string, value: string] | [options: RequestCookie]): this {
55 | const [name, value] =
56 | args.length === 1 ? [args[0].name, args[0].value] : args
57 |
58 | const map = this._parsed
59 | map.set(name, { name, value })
60 |
61 | this._headers.set(
62 | 'cookie',
63 | Array.from(map)
64 | .map(([_, value]) => stringifyCookie(value))
65 | .join('; '),
66 | )
67 | return this
68 | }
69 |
70 | /**
71 | * Delete the cookies matching the passed name or names in the request.
72 | */
73 | delete(
74 | /** Name or names of the cookies to be deleted */
75 | names: string | string[],
76 | ): boolean | boolean[] {
77 | const map = this._parsed
78 | const result = !Array.isArray(names)
79 | ? map.delete(names)
80 | : names.map((name) => map.delete(name))
81 | this._headers.set(
82 | 'cookie',
83 | Array.from(map)
84 | .map(([_, value]) => stringifyCookie(value))
85 | .join('; '),
86 | )
87 | return result
88 | }
89 |
90 | /**
91 | * Delete all the cookies in the cookies in the request.
92 | */
93 | clear(): this {
94 | this.delete(Array.from(this._parsed.keys()))
95 | return this
96 | }
97 |
98 | /**
99 | * Format the cookies in the request as a string for logging
100 | */
101 | [Symbol.for('edge-runtime.inspect.custom')]() {
102 | return `RequestCookies ${JSON.stringify(Object.fromEntries(this._parsed))}`
103 | }
104 |
105 | toString() {
106 | return [...this._parsed.values()]
107 | .map((v) => `${v.name}=${encodeURIComponent(v.value)}`)
108 | .join('; ')
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/packages/cookies/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { CookieSerializeOptions } from 'cookie'
2 |
3 | /**
4 | * {@link https://wicg.github.io/cookie-store/#dictdef-cookielistitem CookieListItem}
5 | * as specified by W3C.
6 | */
7 | export interface CookieListItem
8 | extends Pick<
9 | CookieSerializeOptions,
10 | 'domain' | 'path' | 'secure' | 'sameSite' | 'partitioned'
11 | > {
12 | /** A string with the name of a cookie. */
13 | name: string
14 | /** A string containing the value of the cookie. */
15 | value: string
16 | /** A number of milliseconds or Date interface containing the expires of the cookie. */
17 | expires?: number | CookieSerializeOptions['expires']
18 | }
19 |
20 | /**
21 | * Superset of {@link CookieListItem} extending it with
22 | * the `httpOnly`, `maxAge` and `priority` properties.
23 | */
24 | export type ResponseCookie = CookieListItem &
25 | Pick
26 |
27 | /**
28 | * Subset of {@link CookieListItem}, only containing `name` and `value`
29 | * since other cookie attributes aren't be available on a `Request`.
30 | */
31 | export type RequestCookie = Pick
32 |
--------------------------------------------------------------------------------
/packages/cookies/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { stringifyCookie, parseCookie, parseSetCookie } from '../src'
2 |
3 | test('.stringifyCookie is exported', async () => {
4 | expect(typeof stringifyCookie).toBe('function')
5 | })
6 |
7 | test('.parseCookie is exported', async () => {
8 | expect(typeof parseCookie).toBe('function')
9 | })
10 |
11 | test('.parseSetCookie is exported', async () => {
12 | expect(typeof parseSetCookie).toBe('function')
13 | })
14 |
--------------------------------------------------------------------------------
/packages/cookies/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/cookies/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 | export default defineConfig({
3 | dts: {
4 | resolve: true,
5 | },
6 | entry: ['src/index.ts'],
7 | format: ['cjs', 'esm'],
8 | outDir: 'dist',
9 | })
10 |
--------------------------------------------------------------------------------
/packages/format/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @edge-runtime/format
2 |
3 | ## 4.0.0
4 |
5 | ### Major Changes
6 |
7 | - drop node16 ([#1045](https://github.com/vercel/edge-runtime/pull/1045))
8 |
9 | ## 3.0.1
10 |
11 | ### Patch Changes
12 |
13 | - print stack trace by default when logging errors ([#1023](https://github.com/vercel/edge-runtime/pull/1023))
14 |
15 | ## 3.0.0
16 |
17 | ### Major Changes
18 |
19 | - use MIT license ([#909](https://github.com/vercel/edge-runtime/pull/909))
20 |
21 | ## 2.2.1
22 |
23 | ### Patch Changes
24 |
25 | - build: upgrade tsup ([#773](https://github.com/vercel/edge-runtime/pull/773))
26 |
27 | - fix: expose `performance` constructor ([#772](https://github.com/vercel/edge-runtime/pull/772))
28 |
29 | ## 2.2.0
30 |
31 | ### Minor Changes
32 |
33 | - drop node14 support ([`7cc92cc`](https://github.com/vercel/edge-runtime/commit/7cc92ccd190c2d96483202d9f2e1a523778d1f48))
34 |
35 | ## 2.1.0
36 |
37 | ### Minor Changes
38 |
39 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
40 |
41 | ## 2.1.0-beta.0
42 |
43 | ### Minor Changes
44 |
45 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
46 |
47 | ## 2.0.1
48 |
49 | ### Patch Changes
50 |
51 | - Use valid SPDX license expression ([#276](https://github.com/vercel/edge-runtime/pull/276))
52 |
53 | ## 2.0.0
54 |
55 | ### Major Changes
56 |
57 | - feat: provide formatter inside custom symbol ([#221](https://github.com/vercel/edge-runtime/pull/221))
58 |
59 | ## 1.1.0
60 |
61 | ### Patch Changes
62 |
63 | - fix: handle objects with null prototype ([#172](https://github.com/vercel/edge-runtime/pull/172))
64 |
65 | - Change release method to Changesets ([#110](https://github.com/vercel/edge-runtime/pull/110))
66 |
67 | - upgrading undici ([`3207fa2`](https://github.com/vercel/edge-runtime/commit/3207fa224783fecc70ac63aef4cd49a8404ecbc0))
68 |
69 | ## 1.1.0-beta.34
70 |
71 | ### Patch Changes
72 |
73 | - fix: handle objects with null prototype ([#172](https://github.com/vercel/edge-runtime/pull/172))
74 |
75 | ## 1.1.0-beta.33
76 |
77 | ### Patch Changes
78 |
79 | - upgrading undici ([`3207fa2`](https://github.com/vercel/edge-runtime/commit/3207fa224783fecc70ac63aef4cd49a8404ecbc0))
80 |
81 | ## 1.1.0-beta.32
82 |
83 | ### Patch Changes
84 |
85 | - Change release method to Changesets ([#110](https://github.com/vercel/edge-runtime/pull/110))
86 |
--------------------------------------------------------------------------------
/packages/format/README.md:
--------------------------------------------------------------------------------
1 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/format --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/format --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/format --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/format** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License, based on [Node.js source code](https://github.com/nodejs/node/blob/v18.7.0/lib/util.js).
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/format/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | export default config
6 |
--------------------------------------------------------------------------------
/packages/format/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/format",
3 | "description": "A printf-like string formatter for Edge Runtime",
4 | "homepage": "https://github.com/vercel/edge-runtime#readme",
5 | "version": "4.0.0",
6 | "main": "dist/index.js",
7 | "repository": {
8 | "directory": "packages/format",
9 | "type": "git",
10 | "url": "git+https://github.com/vercel/edge-runtime.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/vercel/edge-runtime/issues"
14 | },
15 | "keywords": [
16 | "edge",
17 | "edge-runtime",
18 | "format",
19 | "formatter",
20 | "functions",
21 | "printf",
22 | "runtime",
23 | "standard",
24 | "web"
25 | ],
26 | "devDependencies": {
27 | "tsup": "8"
28 | },
29 | "engines": {
30 | "node": ">=18"
31 | },
32 | "files": [
33 | "dist"
34 | ],
35 | "scripts": {
36 | "build": "tsup",
37 | "clean": "pnpm run clean:node && pnpm run clean:build",
38 | "clean:build": "rm -rf dist",
39 | "clean:node": "rm -rf node_modules",
40 | "prebuild": "pnpm run clean:build",
41 | "test": "TZ=UTC jest"
42 | },
43 | "license": "MIT",
44 | "publishConfig": {
45 | "access": "public"
46 | },
47 | "types": "dist/index.d.ts"
48 | }
49 |
--------------------------------------------------------------------------------
/packages/format/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/format/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig({
4 | dts: true,
5 | entry: ['./src/index.ts'],
6 | format: ['cjs', 'esm'],
7 | tsconfig: './tsconfig.prod.json',
8 | })
9 |
--------------------------------------------------------------------------------
/packages/integration-tests/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = {
5 | ...buildConfig(__dirname),
6 | testEnvironment: '@edge-runtime/jest-environment',
7 | }
8 | export default config
9 |
--------------------------------------------------------------------------------
/packages/integration-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/integration-tests",
3 | "private": true,
4 | "version": "1.0.0",
5 | "scripts": {
6 | "test": "pnpm run test:node && pnpm run test:edge",
7 | "test:edge": "jest --testEnvironment @edge-runtime/jest-environment",
8 | "test:node": "jest --testEnvironment node"
9 | },
10 | "license": "MIT",
11 | "devDependencies": {
12 | "multer": "1.4.5-lts.1",
13 | "test-listen": "1.1.0",
14 | "@types/test-listen": "1.1.2",
15 | "@edge-runtime/jest-environment": "workspace:*",
16 | "@edge-runtime/ponyfill": "workspace:*"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/console.test.ts:
--------------------------------------------------------------------------------
1 | it.each([
2 | { method: 'assert' },
3 | { method: 'count' },
4 | { method: 'debug' },
5 | { method: 'dir' },
6 | { method: 'error' },
7 | { method: 'info' },
8 | { method: 'time' },
9 | { method: 'timeEnd' },
10 | { method: 'timeLog' },
11 | { method: 'trace' },
12 | { method: 'warn' },
13 | ])('$method', ({ method }) => {
14 | const key = method.toString()
15 | expect(console).toHaveProperty(key, expect.any(Function))
16 | const fn = console[key as keyof typeof console]
17 | expect(typeof fn.bind(console)).toBe('function')
18 | })
19 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/crypto.test.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from 'crypto'
2 |
3 | if (!globalThis.crypto) {
4 | globalThis.crypto = require('crypto')
5 | }
6 |
7 | function toHex(buffer: ArrayBuffer) {
8 | return Array.from(new Uint8Array(buffer))
9 | .map((b) => b.toString(16).padStart(2, '0'))
10 | .join('')
11 | }
12 |
13 | test('crypto.randomUUID', async () => {
14 | expect(crypto.randomUUID()).toEqual(expect.stringMatching(/^[a-f0-9-]+$/))
15 | })
16 |
17 | test('crypto.subtle.digest returns a SHA-256 hash', async () => {
18 | const digest = await crypto.subtle.digest(
19 | 'SHA-256',
20 | new Uint8Array([104, 105, 33]),
21 | )
22 | expect(toHex(digest)).toEqual(
23 | createHash('sha256').update('hi!').digest('hex'),
24 | )
25 | })
26 |
27 | test('Ed25519', async () => {
28 | const kp = await crypto.subtle.generateKey('Ed25519', false, [
29 | 'sign',
30 | 'verify',
31 | ])
32 | expect(kp).toHaveProperty('privateKey')
33 | expect(kp).toHaveProperty('publicKey')
34 | })
35 |
36 | test('X25519', async () => {
37 | const kp = await crypto.subtle.generateKey('X25519', false, [
38 | 'deriveBits',
39 | 'deriveKey',
40 | ])
41 | expect(kp).toHaveProperty('privateKey')
42 | expect(kp).toHaveProperty('publicKey')
43 | })
44 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/encoding.test.ts:
--------------------------------------------------------------------------------
1 | test('TextDecoder', () => {
2 | const input = new Uint8Array([
3 | 101, 100, 103, 101, 45, 112, 105, 110, 103, 46, 118, 101, 114, 99, 101, 108,
4 | 46, 97, 112, 112,
5 | ])
6 |
7 | const utf8Decoder = new TextDecoder('utf-8', { ignoreBOM: true })
8 | const output = utf8Decoder.decode(input)
9 | expect(output).toBe('edge-ping.vercel.app')
10 | })
11 |
12 | test('TextDecoder with stream', () => {
13 | const input = new Uint8Array([
14 | 123, 34, 103, 114, 101, 101, 116, 105, 110, 103, 34, 58, 34, 104, 101, 108,
15 | 108, 111, 34, 125,
16 | ])
17 |
18 | const textDecoder = new TextDecoder()
19 | let result = textDecoder.decode(input, { stream: true })
20 |
21 | expect(result).toBe('{"greeting":"hello"}')
22 | })
23 |
24 | test('btoa', async () => {
25 | expect(btoa('Hello, world')).toBe('SGVsbG8sIHdvcmxk')
26 | expect(btoa(new Uint8Array([1, 2, 3]).toString())).toBe('MSwyLDM=')
27 | expect(btoa(new Uint8Array([1, 2, 3]))).toBe('MSwyLDM=')
28 | expect(btoa([1, 2, 3])).toBe('MSwyLDM=')
29 | expect(btoa(123)).toBe('MTIz')
30 | expect(btoa('123')).toBe('MTIz')
31 | })
32 |
33 | test('atob', async () => {
34 | expect(atob('SGVsbG8sIHdvcmxk')).toBe('Hello, world')
35 | })
36 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'multer' {
2 | declare const multer: {
3 | (args: any): any
4 | memoryStorage: (args?: any) => any
5 | }
6 | export = multer
7 | }
8 |
9 | interface Headers {
10 | /**
11 | * This method is polyfilled in Edge Runtime,
12 | * and therefore might exist and might not exist in runtime.
13 | *
14 | * @deprecated use {@link Headers.getSetCookie}
15 | */
16 | getAll?(name: 'set-cookie' | (string & {})): string[]
17 | }
18 |
19 | class WeakRef {
20 | deref(): T | undefined
21 | constructor(value: T)
22 | }
23 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/global.test.ts:
--------------------------------------------------------------------------------
1 | test('performance', () => {
2 | expect(performance.now()).toBeGreaterThan(0)
3 | })
4 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/headers.test.ts:
--------------------------------------------------------------------------------
1 | describe('headers', () => {
2 | it.each([
3 | ['host', 'vercel.com'],
4 | ['content-length', '1234'],
5 | ['content-type', 'application/json'],
6 | ['transfer-encoding', 'chunked'],
7 | ['connection', 'keep-alive'],
8 | ['keep-alive', 'timeout=5'],
9 | ['upgrade', 'websocket'],
10 | ['expect', '100-continue'],
11 | ])("sets '%s' header in the constructor", async (name, value) => {
12 | const headers = new Headers({ [name]: value })
13 | expect(headers.get(name)).toBe(value)
14 | })
15 |
16 | it('sets header calling Headers constructor', async () => {
17 | const headers = new Headers()
18 | headers.set('cookie', 'hello=world')
19 | expect(headers.get('cookie')).toBe('hello=world')
20 | })
21 |
22 | it('multiple headers', async () => {
23 | const headers = new Headers()
24 | headers.append('set-cookie', 'foo=chocochip')
25 | headers.append('set-cookie', 'bar=chocochip')
26 | expect(headers.get('set-cookie')).toBe('foo=chocochip, bar=chocochip')
27 | expect([...headers]).toEqual([
28 | ['set-cookie', 'foo=chocochip'],
29 | ['set-cookie', 'bar=chocochip'],
30 | ])
31 | })
32 |
33 | describe('iterators', () => {
34 | const generate = () => {
35 | const headers = new Headers()
36 | headers.append('a', '1')
37 | headers.append('b', '2')
38 | headers.append('set-cookie', 'c=3')
39 | headers.append('set-cookie', 'd=4')
40 | return headers
41 | }
42 |
43 | test('#Symbol.iterator', () => {
44 | const entries = [...generate()]
45 | expect(entries).toEqual([
46 | ['a', '1'],
47 | ['b', '2'],
48 | ['set-cookie', 'c=3'],
49 | ['set-cookie', 'd=4'],
50 | ])
51 | })
52 |
53 | test('#entries', () => {
54 | const entries = [...generate().entries()]
55 | expect(entries).toEqual([
56 | ['a', '1'],
57 | ['b', '2'],
58 | ['set-cookie', 'c=3'],
59 | ['set-cookie', 'd=4'],
60 | ])
61 | })
62 |
63 | test('#values', () => {
64 | const values = [...generate().values()]
65 | expect(values).toEqual(['1', '2', 'c=3', 'd=4'])
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/response.test.ts:
--------------------------------------------------------------------------------
1 | import { guard, isEdgeRuntime } from './test-if'
2 |
3 | describe('Response', () => {
4 | test('create a response', async () => {
5 | const res1 = new Response('Hello world!')
6 | expect(await res1.text()).toEqual('Hello world!')
7 | })
8 |
9 | test('clones responses', async () => {
10 | const { readable, writable } = new TransformStream()
11 | const encoder = new TextEncoder()
12 | const writer = writable.getWriter()
13 |
14 | void writer.write(encoder.encode('Hello '))
15 | void writer.write(encoder.encode('world!'))
16 | void writer.close()
17 |
18 | const res1 = new Response(readable)
19 | const res2 = res1.clone()
20 |
21 | expect(await res1.text()).toEqual('Hello world!')
22 | expect(await res2.text()).toEqual('Hello world!')
23 | })
24 |
25 | test('reads response body as buffer', async () => {
26 | const response = await fetch('https://example.vercel.sh')
27 | const arrayBuffer = await response.arrayBuffer()
28 | const text = new TextDecoder().decode(arrayBuffer)
29 | expect(text).toMatch(/^/i)
30 |
31 | const doctype = new TextEncoder().encode('')
32 | const partial = new Uint8Array(arrayBuffer).slice(0, doctype.length)
33 |
34 | expect([...partial]).toEqual([...new Uint8Array(doctype)])
35 | })
36 |
37 | test('allow to set `set-cookie` header', async () => {
38 | const response = new Response(null)
39 | response.headers.set('set-cookie', 'foo=bar')
40 | expect(response.headers.get('set-cookie')).toEqual('foo=bar')
41 | })
42 |
43 | guard(test, isEdgeRuntime)(
44 | 'allow to append multiple `set-cookie` header',
45 | async () => {
46 | const response = new Response(null)
47 | response.headers.append('set-cookie', 'foo=bar')
48 | response.headers.append('set-cookie', 'bar=baz')
49 | expect(response.headers.getSetCookie()).toEqual(['foo=bar', 'bar=baz'])
50 | },
51 | )
52 |
53 | test('disallow mutate response headers for redirects', async () => {
54 | const response = Response.redirect('https://edge-ping.vercel.app/')
55 | expect(() => response.headers.set('foo', 'bar')).toThrow('immutable')
56 | })
57 |
58 | guard(test, isEdgeRuntime)(
59 | 'allow to mutate response headers for error',
60 | async () => {
61 | {
62 | const response = new Response()
63 | response.headers.set('foo', 'bar')
64 | expect(response.headers.get('foo')).toEqual('bar')
65 | }
66 | {
67 | const response = Response.error()
68 | response.headers.set('foo', 'bar')
69 | expect(response.headers.get('foo')).toEqual('bar')
70 | }
71 | },
72 | )
73 | })
74 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/test-if.ts:
--------------------------------------------------------------------------------
1 | export const guard = (
2 | t: T,
3 | conditional: boolean | (() => boolean),
4 | ): T =>
5 | (typeof conditional === 'function' ? conditional() : conditional) ? t : t.skip
6 |
7 | export const isEdgeRuntime = () =>
8 | (globalThis as { EdgeRuntime?: unknown }).EdgeRuntime !== undefined
9 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/url-pattern.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * URLPattern is missing in Node.js
3 | * TODO: remove this when this issue is addressed
4 | * https://github.com/nodejs/node/issues/40844
5 | */
6 | if (!globalThis.URLPattern) {
7 | globalThis.URLPattern = require('@edge-runtime/ponyfill').URLPattern
8 | }
9 |
10 | test('URLPattern', () => {
11 | const urlPattern = new URLPattern('/:foo/:bar', 'https://example.vercel.sh')
12 | const urlPatternAsType: URLPattern = urlPattern
13 | const result = urlPatternAsType.exec('https://example.vercel.sh/1/2')
14 | expect(result?.pathname.groups).toEqual({
15 | foo: '1',
16 | bar: '2',
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/packages/integration-tests/test/url.test.ts:
--------------------------------------------------------------------------------
1 | test('URL', async () => {
2 | const url = new URL('https://edge-ping.vercel.app/')
3 | expect(typeof url).toBe('object')
4 | expect(url.toString()).toBe('https://edge-ping.vercel.app/')
5 | })
6 |
--------------------------------------------------------------------------------
/packages/jest-environment/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/jest-environment: A Jest integration to run assertions in Edge Runtime context.
8 |
See @edge-runtime/jest-environment section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/jest-environment --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/jest-environment --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/jest-environment --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/vm** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/jest-environment/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | export default config
6 |
--------------------------------------------------------------------------------
/packages/jest-environment/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/jest-environment",
3 | "description": "A Jest integration to run assertions in Edge Runtime context.",
4 | "homepage": "https://edge-runtime.vercel.app/packages/jest-environment",
5 | "version": "4.0.0",
6 | "main": "dist/index.js",
7 | "repository": {
8 | "directory": "packages/jest-environment",
9 | "type": "git",
10 | "url": "git+https://github.com/vercel/edge-runtime.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/vercel/edge-runtime/issues"
14 | },
15 | "keywords": [
16 | "edge",
17 | "edge-runtime",
18 | "functions",
19 | "jest",
20 | "runtime",
21 | "standard",
22 | "testing",
23 | "web"
24 | ],
25 | "dependencies": {
26 | "@edge-runtime/vm": "workspace:*",
27 | "@jest/environment": "29.5.0",
28 | "@jest/fake-timers": "29.5.0",
29 | "jest-mock": "29.5.0",
30 | "jest-util": "29.5.0"
31 | },
32 | "engines": {
33 | "node": ">=18"
34 | },
35 | "files": [
36 | "dist"
37 | ],
38 | "scripts": {
39 | "build": "tsc --project tsconfig.prod.json",
40 | "clean": "pnpm run clean:node && pnpm run clean:build",
41 | "clean:build": "rm -rf dist",
42 | "clean:node": "rm -rf node_modules",
43 | "prebuild": "pnpm run clean:build",
44 | "test": "jest"
45 | },
46 | "license": "MIT",
47 | "publishConfig": {
48 | "access": "public"
49 | },
50 | "types": "dist/index.d.ts"
51 | }
52 |
--------------------------------------------------------------------------------
/packages/jest-environment/test/index.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment ./dist
3 | */
4 |
5 | test('TextEncoder references the same global Uint8Array constructor', () => {
6 | expect(new TextEncoder().encode('abc')).toBeInstanceOf(Uint8Array)
7 | })
8 |
9 | test('allows to run fetch', async () => {
10 | const response = await fetch('https://example.vercel.sh')
11 | expect(response.status).toEqual(200)
12 | })
13 |
14 | test('allows to run crypto', async () => {
15 | const array = new Uint32Array(10)
16 | expect(crypto.getRandomValues(array)).toHaveLength(array.length)
17 | })
18 |
19 | test('has EdgeRuntime global', () => {
20 | expect(EdgeRuntime).toEqual('edge-runtime')
21 | })
22 |
--------------------------------------------------------------------------------
/packages/jest-environment/test/next.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment ./dist
3 | */
4 |
5 | import { redirect } from 'next/navigation'
6 |
7 | test('it should works fine with next internals', async () => {
8 | expect(redirect).toBeDefined()
9 | })
10 |
--------------------------------------------------------------------------------
/packages/jest-environment/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "outDir": "./dist"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/jest-expect/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @edge-runtime/jest-expect
2 |
3 | ## 3.0.0
4 |
5 | ### Major Changes
6 |
7 | - drop node16 ([#1045](https://github.com/vercel/edge-runtime/pull/1045))
8 |
9 | ## 2.0.0
10 |
11 | ### Major Changes
12 |
13 | - use MIT license ([#909](https://github.com/vercel/edge-runtime/pull/909))
14 |
15 | ## 1.2.0
16 |
17 | ### Minor Changes
18 |
19 | - drop node14 support ([`7cc92cc`](https://github.com/vercel/edge-runtime/commit/7cc92ccd190c2d96483202d9f2e1a523778d1f48))
20 |
21 | ## 1.1.2
22 |
23 | ### Patch Changes
24 |
25 | - fix: exclude react-server ([#413](https://github.com/vercel/edge-runtime/pull/413))
26 |
27 | ## 1.1.1
28 |
29 | ### Patch Changes
30 |
31 | - build: remove duplicate dependency ([#407](https://github.com/vercel/edge-runtime/pull/407))
32 |
33 | ## 1.1.0
34 |
35 | ### Minor Changes
36 |
37 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
38 |
39 | ## 1.1.0-beta.0
40 |
41 | ### Minor Changes
42 |
43 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
44 |
45 | ## 1.0.1
46 |
47 | ### Patch Changes
48 |
49 | - Use valid SPDX license expression ([#276](https://github.com/vercel/edge-runtime/pull/276))
50 |
51 | ## 1.0.0
52 |
53 | ### Major Changes
54 |
55 | - initial release ([#164](https://github.com/vercel/edge-runtime/pull/164))
56 |
--------------------------------------------------------------------------------
/packages/jest-expect/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/jest-expect: Custom matchers for Jest's expect to help test Request/Response instances.
8 |
See @edge-runtime/jest-expect section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/jest-expect --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/jest-expect --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/jest-expect --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/expect** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/jest-expect/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './dist'
2 |
--------------------------------------------------------------------------------
/packages/jest-expect/index.js:
--------------------------------------------------------------------------------
1 | require('./dist')
2 |
--------------------------------------------------------------------------------
/packages/jest-expect/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | config.testEnvironment = '@edge-runtime/jest-environment'
6 | config.setupFilesAfterEnv = ['./dist']
7 | export default config
8 |
--------------------------------------------------------------------------------
/packages/jest-expect/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/jest-expect",
3 | "description": "Custom matchers for Jest's expect to help test Request/Response instances",
4 | "homepage": "https://edge-runtime.vercel.app/packages/jest-expect",
5 | "version": "3.0.0",
6 | "main": "dist/index.js",
7 | "repository": {
8 | "directory": "packages/jest-expect",
9 | "type": "git",
10 | "url": "git+https://github.com/vercel/edge-runtime.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/vercel/edge-runtime/issues"
14 | },
15 | "keywords": [
16 | "edge",
17 | "edge-runtime",
18 | "functions",
19 | "jest",
20 | "matchers",
21 | "runtime",
22 | "standard",
23 | "testing",
24 | "web"
25 | ],
26 | "dependencies": {
27 | "@jest/environment": "29.5.0",
28 | "@jest/fake-timers": "29.5.0",
29 | "@jest/globals": "29.7.0"
30 | },
31 | "devDependencies": {
32 | "@edge-runtime/jest-environment": "workspace:*"
33 | },
34 | "engines": {
35 | "node": ">=18"
36 | },
37 | "files": [
38 | "dist",
39 | "index.d.ts",
40 | "index.js"
41 | ],
42 | "scripts": {
43 | "build": "tsc --project tsconfig.prod.json",
44 | "clean": "pnpm run clean:node && pnpm run clean:build",
45 | "clean:build": "rm -rf dist",
46 | "clean:node": "rm -rf node_modules",
47 | "prebuild": "pnpm run clean:build",
48 | "test": "jest --no-colors"
49 | },
50 | "license": "MIT",
51 | "publishConfig": {
52 | "access": "public"
53 | },
54 | "types": "dist/index.d.ts"
55 | }
56 |
--------------------------------------------------------------------------------
/packages/jest-expect/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './response'
2 | export * from './shared'
3 | export * from './types'
4 |
--------------------------------------------------------------------------------
/packages/jest-expect/src/response.ts:
--------------------------------------------------------------------------------
1 | import type { StatusParams } from './types'
2 |
3 | function assertResponse(actual: unknown): asserts actual is Response {
4 | if (actual instanceof Response) return
5 | throw new Error('Expected a Response instance')
6 | }
7 |
8 | const HTTP_STATUS_CODE_RANGES = {
9 | '1': 'Informational',
10 | '2': 'Successful',
11 | '3': 'Redirection',
12 | '4': 'Client Error',
13 | '5': 'Server Error',
14 | }
15 |
16 | type HttpStatusCodeRange = keyof typeof HTTP_STATUS_CODE_RANGES
17 |
18 | expect.extend({
19 | toHaveStatus(actual, ...args: StatusParams) {
20 | assertResponse(actual)
21 | const [status] = args
22 | if (typeof status === 'string') {
23 | const httpStatusCodeRange =
24 | actual.status.toString()[0] as HttpStatusCodeRange
25 | const matchedRange = HTTP_STATUS_CODE_RANGES[httpStatusCodeRange]
26 | const statusRange = Object.keys(httpStatusCodeRange).find(
27 | (k) => httpStatusCodeRange[k as HttpStatusCodeRange] === status,
28 | )
29 |
30 | const pass = matchedRange === status
31 | return {
32 | message: () =>
33 | `expected ${actual.status} (${matchedRange})${
34 | pass ? ' not' : ''
35 | } to be in status range (${statusRange}xx)`,
36 | pass,
37 | }
38 | }
39 | const pass = actual.status === status
40 | if (pass) {
41 | return {
42 | message: () => `expected ${actual.status} not to be ${status}`,
43 | pass,
44 | }
45 | }
46 | return {
47 | message: () => `expected ${actual.status} to be ${status}`,
48 | pass,
49 | }
50 | },
51 | })
52 |
--------------------------------------------------------------------------------
/packages/jest-expect/src/shared.ts:
--------------------------------------------------------------------------------
1 | import { JSONBodyParams } from './types'
2 |
3 | export function assertRequestOrResponse(
4 | actual: unknown,
5 | ): asserts actual is Request | Response {
6 | if (actual instanceof Request || actual instanceof Response) return
7 | throw new Error('Expected a Request or Response instance')
8 | }
9 |
10 | expect.extend({
11 | async toHaveJSONBody(actual, ...args: JSONBodyParams) {
12 | assertRequestOrResponse(actual)
13 | const [body] = args
14 | const contentType = actual.headers.get('Content-Type')
15 |
16 | if (!contentType?.includes('application/json')) {
17 | const type = actual instanceof Request ? 'request' : 'response'
18 | return {
19 | message: () =>
20 | [
21 | `${this.utils.matcherHint('toHaveJSONBody')}\n`,
22 | `Expected ${type} to have "Content-Type": ${this.utils.printExpected(
23 | 'application/json',
24 | )}`,
25 | `Received: ${this.utils.printReceived(contentType)}`,
26 | ].join('\n'),
27 | pass: false,
28 | }
29 | }
30 | const json = await actual.clone().json()
31 | const pass = this.equals(json, body)
32 | if (pass) {
33 | return {
34 | message: () => `expected ${json} not to be ${body}`,
35 | pass: true,
36 | }
37 | }
38 | return {
39 | message: () =>
40 | `expected JSON body '${JSON.stringify(json)}' to be '${JSON.stringify(
41 | body,
42 | )}'`,
43 | pass: false,
44 | }
45 | },
46 | async toHaveTextBody(actual, body: string) {
47 | assertRequestOrResponse(actual)
48 | const text = await actual.clone().text()
49 | const pass = this.equals(text, body)
50 | if (pass) {
51 | return {
52 | message: () => `expected ${text} not to be ${body}`,
53 | pass: true,
54 | }
55 | }
56 | return {
57 | message: () => `expected text body '${text}' to be '${body}'`,
58 | pass: false,
59 | }
60 | },
61 | })
62 |
--------------------------------------------------------------------------------
/packages/jest-expect/test/index.test.ts:
--------------------------------------------------------------------------------
1 | describe('Request matchers', () => {
2 | test('`expect.toHaveJSONBody` available', async () => {
3 | const json = { foo: 'bar' }
4 | const request = new Request('http://n', {
5 | body: JSON.stringify(json),
6 | headers: { 'Content-Type': 'application/json' },
7 | method: 'POST',
8 | })
9 |
10 | await expect(request).toHaveJSONBody(json)
11 | await expect(request).not.toHaveJSONBody({ foo: 'baz' })
12 | })
13 |
14 | test('`expect.toHaveTextBody` available', async () => {
15 | const request1 = new Request('http://n', { body: '', method: 'POST' })
16 | await expect(
17 | expect(request1).toHaveTextBody('Does not have this text'),
18 | ).rejects.toThrowErrorMatchingInlineSnapshot(
19 | `"expected text body '' to be 'Does not have this text'"`,
20 | )
21 |
22 | const text = 'Does have this text'
23 | const request2 = new Request('http://n', { body: text, method: 'POST' })
24 |
25 | await expect(request2).toHaveTextBody(text)
26 | await expect(request2).not.toHaveTextBody('Does not have this text')
27 | })
28 | })
29 |
30 | describe('Response matchers', () => {
31 | test('`expect.toHaveStatus` available', () => {
32 | const okResponse = new Response('OK')
33 |
34 | expect(okResponse).toHaveStatus(200)
35 | expect(okResponse).toHaveStatus('Successful')
36 | expect(okResponse).not.toHaveStatus(201)
37 |
38 | expect(new Response('Internal Server Error', { status: 500 })).toHaveStatus(
39 | 'Server Error',
40 | )
41 | })
42 |
43 | test('`expect.toHaveJSONBody` available', async () => {
44 | await expect(
45 | expect(new Response('Without Content-Type')).toHaveJSONBody(null),
46 | ).rejects.toThrowErrorMatchingInlineSnapshot(`
47 | "expect(received).toHaveJSONBody(expected)
48 |
49 | Expected response to have "Content-Type": "application/json"
50 | Received: "text/plain;charset=UTF-8""
51 | `)
52 |
53 | const json = { foo: 'bar' }
54 | // @ts-expect-error See https://developer.mozilla.org/en-US/docs/Web/API/Response/json
55 | const response = Response.json(json)
56 |
57 | await expect(response).toHaveJSONBody(json)
58 | await expect(response).not.toHaveJSONBody({ foo: 'baz' })
59 | })
60 |
61 | test('`expect.toHaveTextBody` available', async () => {
62 | const text = 'Does have this text'
63 | const response = new Response(text)
64 |
65 | await expect(response).toHaveTextBody(text)
66 | await expect(response).not.toHaveTextBody("Doesn't have this text")
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/packages/jest-expect/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "outDir": "./dist"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/node-utils/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/node-utils: A set of helpers for running edge-compliant code in Node.js environment.
8 |
See @edge-runtime/node-utils section in our website for more information.
9 |
10 |
11 |
12 | > **Note**: This is an alpha version.
13 |
14 | ## Install
15 |
16 | Using npm:
17 |
18 | ```sh
19 | npm install @edge-runtime/node-utils --save
20 | ```
21 |
22 | or using yarn:
23 |
24 | ```sh
25 | yarn add @edge-runtime/node-utils --dev
26 | ```
27 |
28 | or using pnpm:
29 |
30 | ```sh
31 | pnpm install @edge-runtime/node-utils --save
32 | ```
33 |
34 | ## License
35 |
36 | **@edge-runtime/node-utils** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
37 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
38 |
39 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
40 |
--------------------------------------------------------------------------------
/packages/node-utils/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | export default config
6 |
--------------------------------------------------------------------------------
/packages/node-utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/node-utils",
3 | "description": "A set of helpers for running edge-compliant code in Node.js environment",
4 | "homepage": "https://edge-runtime.vercel.app/packages/node-utils",
5 | "version": "4.0.0",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "repository": {
9 | "directory": "packages/node-utils",
10 | "type": "git",
11 | "url": "git+https://github.com/vercel/edge-runtime.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/vercel/edge-runtime/issues"
15 | },
16 | "keywords": [
17 | "edge",
18 | "edge-runtime",
19 | "functions",
20 | "node",
21 | "runtime",
22 | "standard",
23 | "utils",
24 | "web"
25 | ],
26 | "devDependencies": {
27 | "@edge-runtime/primitives": "workspace:*",
28 | "@types/test-listen": "1.1.2",
29 | "test-listen": "1.1.0",
30 | "tsup": "8"
31 | },
32 | "engines": {
33 | "node": ">=18"
34 | },
35 | "files": [
36 | "dist"
37 | ],
38 | "scripts": {
39 | "build": "tsup",
40 | "clean:build": "rm -rf dist",
41 | "clean:node": "rm -rf node_modules",
42 | "prebuild": "pnpm run clean:build",
43 | "test": "jest"
44 | },
45 | "license": "MIT",
46 | "publishConfig": {
47 | "access": "public"
48 | },
49 | "types": "dist/index.d.ts"
50 | }
51 |
--------------------------------------------------------------------------------
/packages/node-utils/src/edge-to-node/handler.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage, ServerResponse } from 'node:http'
2 | import type {
3 | WebHandler,
4 | NodeHandler,
5 | BuildDependencies,
6 | RequestOptions,
7 | } from '../types'
8 | import { buildToFetchEvent } from '../node-to-edge/fetch-event'
9 | import { buildToRequest } from '../node-to-edge/request'
10 | import { mergeIntoServerResponse, toOutgoingHeaders } from './headers'
11 | import { toToReadable } from './stream'
12 |
13 | export function buildToNodeHandler(
14 | dependencies: BuildDependencies,
15 | options: RequestOptions,
16 | ) {
17 | const toRequest = buildToRequest(dependencies)
18 | const toFetchEvent = buildToFetchEvent(dependencies)
19 | return function toNodeHandler(webHandler: WebHandler): NodeHandler {
20 | return (
21 | incomingMessage: IncomingMessage,
22 | serverResponse: ServerResponse,
23 | ) => {
24 | const request = toRequest(incomingMessage, options)
25 | const maybePromise = webHandler(request, toFetchEvent(request))
26 | if (maybePromise instanceof Promise) {
27 | maybePromise.then((response) =>
28 | toServerResponse(response, serverResponse),
29 | )
30 | } else {
31 | toServerResponse(maybePromise, serverResponse)
32 | }
33 | }
34 | }
35 | }
36 |
37 | function toServerResponse(
38 | webResponse: Response | null | undefined,
39 | serverResponse: ServerResponse,
40 | ) {
41 | if (!webResponse) {
42 | serverResponse.end()
43 | return
44 | }
45 | mergeIntoServerResponse(
46 | toOutgoingHeaders(webResponse.headers),
47 | serverResponse,
48 | )
49 |
50 | serverResponse.statusCode = webResponse.status
51 | serverResponse.statusMessage = webResponse.statusText
52 | if (!webResponse.body) {
53 | serverResponse.end()
54 | return
55 | }
56 | toToReadable(webResponse.body).pipe(serverResponse)
57 | }
58 |
--------------------------------------------------------------------------------
/packages/node-utils/src/edge-to-node/headers.ts:
--------------------------------------------------------------------------------
1 | import { Headers } from '@edge-runtime/primitives'
2 | import type { OutgoingHttpHeaders, ServerResponse } from 'node:http'
3 |
4 | export function toOutgoingHeaders(headers?: Headers): OutgoingHttpHeaders {
5 | const outputHeaders: OutgoingHttpHeaders = {}
6 | if (headers) {
7 | const _headers = new Headers(headers).entries()
8 | for (const [name, value] of _headers) {
9 | outputHeaders[name] =
10 | name === 'set-cookie' ? headers.getSetCookie() : value
11 | }
12 | }
13 | return outputHeaders
14 | }
15 |
16 | export function mergeIntoServerResponse(
17 | headers: OutgoingHttpHeaders,
18 | serverResponse: ServerResponse,
19 | ) {
20 | for (const [name, value] of Object.entries(headers)) {
21 | if (value !== undefined) {
22 | serverResponse.setHeader(name, value)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/node-utils/src/edge-to-node/index.ts:
--------------------------------------------------------------------------------
1 | export * from './handler'
2 | export * from './headers'
3 | export * from './stream'
4 |
--------------------------------------------------------------------------------
/packages/node-utils/src/edge-to-node/stream.ts:
--------------------------------------------------------------------------------
1 | export const toToReadable = require('stream').Readable.fromWeb
2 |
--------------------------------------------------------------------------------
/packages/node-utils/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './edge-to-node/index'
2 | export * from './node-to-edge/index'
3 | export * from './types'
4 |
--------------------------------------------------------------------------------
/packages/node-utils/src/node-to-edge/fetch-event.ts:
--------------------------------------------------------------------------------
1 | import type { Request } from '@edge-runtime/primitives'
2 | import { BuildDependencies } from '../types'
3 |
4 | export function buildToFetchEvent(dependencies: BuildDependencies) {
5 | return function toFetchEvent(request: Request) {
6 | return new dependencies.FetchEvent(request)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/node-utils/src/node-to-edge/headers.ts:
--------------------------------------------------------------------------------
1 | import type { Headers } from '@edge-runtime/primitives'
2 | import type { IncomingHttpHeaders } from 'http'
3 |
4 | interface Dependencies {
5 | Headers: typeof Headers
6 | }
7 |
8 | export function buildToHeaders({ Headers }: Dependencies) {
9 | return function toHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
10 | const headers = new Headers()
11 | for (let [key, value] of Object.entries(nodeHeaders)) {
12 | const values = Array.isArray(value) ? value : [value]
13 | for (let v of values) {
14 | if (v !== undefined) {
15 | headers.append(key, v)
16 | }
17 | }
18 | }
19 | return headers
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/node-utils/src/node-to-edge/index.ts:
--------------------------------------------------------------------------------
1 | export * from './fetch-event'
2 | export * from './headers'
3 | export * from './request'
4 | export * from './stream'
5 |
--------------------------------------------------------------------------------
/packages/node-utils/src/node-to-edge/request.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage } from 'node:http'
2 | import type { Request } from '@edge-runtime/primitives'
3 | import { buildToHeaders } from './headers'
4 | import { buildToReadableStream } from './stream'
5 | import { BuildDependencies, RequestOptions } from '../types'
6 |
7 | export function buildToRequest(dependencies: BuildDependencies) {
8 | const toHeaders = buildToHeaders(dependencies)
9 | const toReadableStream = buildToReadableStream(dependencies)
10 | const { Request } = dependencies
11 | return function toRequest(
12 | request: IncomingMessage,
13 | options: RequestOptions,
14 | ): Request {
15 | const base = computeOrigin(request, options.defaultOrigin)
16 | return new Request(
17 | String(
18 | request.url?.startsWith('//')
19 | ? new URL(base + request.url)
20 | : new URL(request.url || '/', base),
21 | ),
22 | {
23 | method: request.method,
24 | headers: toHeaders(request.headers),
25 | body: !['HEAD', 'GET'].includes(request.method ?? '')
26 | ? toReadableStream(request)
27 | : null,
28 | },
29 | )
30 | }
31 | }
32 |
33 | function computeOrigin({ headers }: IncomingMessage, defaultOrigin: string) {
34 | const authority = headers.host
35 | if (!authority) {
36 | return defaultOrigin
37 | }
38 | const [, port] = authority.split(':')
39 | return `${port === '443' ? 'https' : 'http'}://${authority}`
40 | }
41 |
--------------------------------------------------------------------------------
/packages/node-utils/src/node-to-edge/stream.ts:
--------------------------------------------------------------------------------
1 | import type { Readable } from 'node:stream'
2 |
3 | interface Dependencies {
4 | ReadableStream: typeof ReadableStream
5 | Uint8Array: typeof Uint8Array
6 | }
7 |
8 | export function buildToReadableStream(dependencies: Dependencies) {
9 | const { ReadableStream, Uint8Array } = dependencies
10 | return function toReadableStream(stream: Readable): ReadableStream {
11 | return new ReadableStream({
12 | start(controller) {
13 | stream.on('data', (chunk) => {
14 | controller.enqueue(new Uint8Array([...new Uint8Array(chunk)]))
15 | })
16 | stream.on('end', () => {
17 | controller.close()
18 | })
19 | stream.on('error', (err) => {
20 | controller.error(err)
21 | })
22 | },
23 | })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/node-utils/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage, ServerResponse } from 'http'
2 | import type {
3 | Request,
4 | Response,
5 | Headers,
6 | ReadableStream,
7 | FetchEvent,
8 | } from '@edge-runtime/primitives'
9 | export interface BuildDependencies {
10 | Headers: typeof Headers
11 | ReadableStream: typeof ReadableStream
12 | Request: typeof Request
13 | Uint8Array: typeof Uint8Array
14 | FetchEvent: typeof FetchEvent
15 | }
16 |
17 | export interface RequestOptions {
18 | defaultOrigin: string
19 | }
20 |
21 | export type NodeHandler = (
22 | req: IncomingMessage,
23 | res: ServerResponse,
24 | ) => Promise | void
25 |
26 | export type WebHandler = (
27 | req: Request,
28 | event: FetchEvent,
29 | ) => Promise | Response | null | undefined
30 |
--------------------------------------------------------------------------------
/packages/node-utils/test/edge-to-node/headers.test.ts:
--------------------------------------------------------------------------------
1 | import { Headers } from '@edge-runtime/primitives'
2 | import { toOutgoingHeaders } from '../../src'
3 |
4 | it('handles simple header values', () => {
5 | expect(
6 | toOutgoingHeaders(
7 | new Headers({
8 | 'Content-Type': 'image/jpeg',
9 | 'X-My-Custom-Header': 'Zeke are cool',
10 | }),
11 | ),
12 | ).toEqual({
13 | 'content-type': 'image/jpeg',
14 | 'x-my-custom-header': 'Zeke are cool',
15 | })
16 | })
17 |
18 | it('splits set-cookie with getSetCookie()', () => {
19 | const headers = new Headers({ 'set-cookie': 'value1' })
20 | headers.append('set-cookie', 'value2')
21 | headers.append('set-cookie', 'value3')
22 | expect(toOutgoingHeaders(headers)).toEqual({
23 | 'set-cookie': ['value1', 'value2', 'value3'],
24 | })
25 | })
26 |
27 | it('handles multiple values as single string', () => {
28 | const headers = new Headers({ 'x-multiple': 'value1' })
29 | headers.append('x-multiple', 'value2')
30 | headers.append('x-multiple', 'value3')
31 | expect(toOutgoingHeaders(headers)).toEqual({
32 | 'x-multiple': 'value1, value2, value3',
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/packages/node-utils/test/edge-to-node/stream.test.ts:
--------------------------------------------------------------------------------
1 | import { ReadableStream } from '@edge-runtime/primitives'
2 | import { Readable } from 'node:stream'
3 | import { toToReadable } from '../../src'
4 |
5 | it('handles a web ReadableStream', async () => {
6 | const readableStream = new ReadableStream({
7 | async start(controller) {
8 | const encoder = new TextEncoder()
9 | controller.enqueue(encoder.encode('hello'))
10 | await new Promise((resolve) => setTimeout(resolve, 200))
11 | controller.enqueue(encoder.encode(' world'))
12 | controller.close()
13 | },
14 | })
15 |
16 | const readable = toToReadable(readableStream)
17 | expect((await transformToBuffer(readable)).toString()).toEqual('hello world')
18 | })
19 |
20 | async function transformToBuffer(stream: Readable) {
21 | const buffers = []
22 | for await (const data of stream) {
23 | buffers.push(data)
24 | }
25 | return Buffer.concat(buffers)
26 | }
27 |
--------------------------------------------------------------------------------
/packages/node-utils/test/node-to-edge/fetch-event.test.ts:
--------------------------------------------------------------------------------
1 | import * as EdgeRuntime from '@edge-runtime/primitives'
2 | import { buildToFetchEvent } from '../../src'
3 |
4 | const toFetchEvent = buildToFetchEvent({
5 | Headers: EdgeRuntime.Headers,
6 | ReadableStream: EdgeRuntime.ReadableStream,
7 | Request: EdgeRuntime.Request,
8 | Uint8Array: Uint8Array,
9 | FetchEvent: EdgeRuntime.FetchEvent,
10 | })
11 |
12 | it('returns a fetch event with a request', () => {
13 | const request = new EdgeRuntime.Request('https://vercel.com')
14 | const event = toFetchEvent(request)
15 | expect(event).toBeInstanceOf(EdgeRuntime.FetchEvent)
16 | expect(event.request).toBe(request)
17 | })
18 |
19 | it('interacts with waitUntil', async () => {
20 | const request = new EdgeRuntime.Request('https://vercel.com')
21 | const event = toFetchEvent(request)
22 | let duration = Date.now()
23 | event.waitUntil(new Promise((resolve) => setTimeout(resolve, 1000)))
24 | await Promise.all(event.awaiting)
25 | duration = Date.now() - duration
26 | expect(duration).toBeGreaterThanOrEqual(1000)
27 | })
28 |
--------------------------------------------------------------------------------
/packages/node-utils/test/test-utils/get-kill-server.ts:
--------------------------------------------------------------------------------
1 | import type { Server } from 'http'
2 | import type { Socket } from 'net'
3 |
4 | /**
5 | * It takes some time to close the server when it has been invoked with a
6 | * TransformStream readable side so this function will help closing the
7 | * server immediately.
8 | */
9 | export function getKillServer(server: Server) {
10 | let sockets: Socket[] = []
11 |
12 | server.on('connection', (socket) => {
13 | sockets.push(socket)
14 | socket.once('close', () => {
15 | sockets.splice(sockets.indexOf(socket), 1)
16 | })
17 | })
18 |
19 | return () => {
20 | return new Promise((resolve, reject) => {
21 | server.close((err) => {
22 | if (err) {
23 | return reject(err)
24 | }
25 | resolve()
26 | })
27 |
28 | sockets.forEach(function (socket) {
29 | socket.destroy()
30 | })
31 |
32 | // Reset so the server can be restarted
33 | sockets = []
34 | })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/node-utils/test/test-utils/run-test-server.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage, ServerResponse } from 'http'
2 | import type * as ET from '@edge-runtime/primitives'
3 | import { getKillServer } from './get-kill-server'
4 | import { createServer } from 'http'
5 | import { fetch, URL } from '@edge-runtime/primitives'
6 | import listen from 'test-listen'
7 | interface ServerOptions {
8 | handler: (
9 | request: IncomingMessage,
10 | response: ServerResponse,
11 | ) => Promise | void
12 | port?: number
13 | }
14 |
15 | export interface TestServer {
16 | close: () => Promise
17 | fetch: (info: ET.RequestInfo, init?: ET.RequestInit) => Promise
18 | url: string
19 | }
20 |
21 | export async function runTestServer(
22 | options: ServerOptions,
23 | ): Promise {
24 | const server = createServer((req, res) => {
25 | try {
26 | const result = options.handler(req, res)
27 | if (result?.catch) {
28 | result.catch((error) => {
29 | res.statusCode = 500
30 | res.end(error.toString())
31 | })
32 | }
33 | } catch (error) {
34 | res.statusCode = 500
35 | res.end(error?.toString())
36 | }
37 | })
38 | const url = await listen(server)
39 | return {
40 | close: getKillServer(server),
41 | fetch: (info, init) => fetch(String(new URL(String(info), url)), init),
42 | url,
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/node-utils/test/test-utils/serialize-response.ts:
--------------------------------------------------------------------------------
1 | export async function serializeResponse(response: Response) {
2 | const text = await response.text()
3 | return {
4 | status: response.status,
5 | statusText: response.statusText,
6 | headers: Object.fromEntries(response.headers),
7 | json: toJSON(text),
8 | text,
9 | }
10 | }
11 |
12 | function toJSON(value: string) {
13 | try {
14 | return JSON.parse(value)
15 | } catch (error) {
16 | return {}
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/node-utils/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["src/**/*.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/node-utils/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig({
4 | dts: true,
5 | entry: ['./src/index.ts'],
6 | format: ['cjs', 'esm'],
7 | tsconfig: './tsconfig.prod.json',
8 | })
9 |
--------------------------------------------------------------------------------
/packages/ponyfill/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @edge-runtime/ponyfill
2 |
3 | ## 4.0.0
4 |
5 | ### Major Changes
6 |
7 | - drop node16 ([#1045](https://github.com/vercel/edge-runtime/pull/1045))
8 |
9 | ## 3.0.0
10 |
11 | ### Major Changes
12 |
13 | - use MIT license ([#909](https://github.com/vercel/edge-runtime/pull/909))
14 |
15 | ## 2.4.2
16 |
17 | ### Patch Changes
18 |
19 | - fix: expose `performance` constructor ([#772](https://github.com/vercel/edge-runtime/pull/772))
20 |
21 | ## 2.4.1
22 |
23 | ### Patch Changes
24 |
25 | - Don't return `NodeJS.Timer` from `setTimeout` and `setInterval` ([#622](https://github.com/vercel/edge-runtime/pull/622))
26 |
27 | ## 2.4.0
28 |
29 | ### Minor Changes
30 |
31 | - drop node14 support ([`7cc92cc`](https://github.com/vercel/edge-runtime/commit/7cc92ccd190c2d96483202d9f2e1a523778d1f48))
32 |
33 | ## 2.3.0
34 |
35 | ### Minor Changes
36 |
37 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
38 |
39 | ## 2.3.0-beta.0
40 |
41 | ### Minor Changes
42 |
43 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
44 |
45 | ## 2.2.0
46 |
47 | ### Minor Changes
48 |
49 | - Merge `EdgeRuntime` into `EdgeVM` ([#289](https://github.com/vercel/edge-runtime/pull/289))
50 |
51 | ## 2.1.2
52 |
53 | ### Patch Changes
54 |
55 | - Use valid SPDX license expression ([#276](https://github.com/vercel/edge-runtime/pull/276))
56 |
57 | ## 2.1.1
58 |
59 | ### Patch Changes
60 |
61 | - build: update dependencies ([#271](https://github.com/vercel/edge-runtime/pull/271))
62 |
63 | ## 2.1.0
64 |
65 | ### Minor Changes
66 |
67 | - feat(encoding): add `TextDecoderStream` and `TextEncoderStream` ([#267](https://github.com/vercel/edge-runtime/pull/267))
68 |
69 | ## 2.0.0
70 |
71 | ### Major Changes
72 |
73 | - BREAKING CHANGE: Drop Node.js 12 ([#191](https://github.com/vercel/edge-runtime/pull/191))
74 |
75 | ## 1.1.0
76 |
77 | ### Patch Changes
78 |
79 | - Allow to use URLPattern as a type with @edge-runtime/ponyfill ([#113](https://github.com/vercel/edge-runtime/pull/113))
80 |
81 | - Change release method to Changesets ([#110](https://github.com/vercel/edge-runtime/pull/110))
82 |
83 | - Add DOMException primitive ([#143](https://github.com/vercel/edge-runtime/pull/143))
84 |
85 | - update edge dependencies ([#160](https://github.com/vercel/edge-runtime/pull/160))
86 |
87 | - upgrading undici ([`3207fa2`](https://github.com/vercel/edge-runtime/commit/3207fa224783fecc70ac63aef4cd49a8404ecbc0))
88 |
89 | - Only export the specific values from the global scope as we define in our types ([#124](https://github.com/vercel/edge-runtime/pull/124))
90 |
91 | ## 1.1.0-beta.36
92 |
93 | ### Patch Changes
94 |
95 | - update edge dependencies ([#160](https://github.com/vercel/edge-runtime/pull/160))
96 |
97 | ## 1.1.0-beta.35
98 |
99 | ### Patch Changes
100 |
101 | - Add DOMException primitive ([#143](https://github.com/vercel/edge-runtime/pull/143))
102 |
103 | ## 1.1.0-beta.34
104 |
105 | ### Patch Changes
106 |
107 | - upgrading undici ([`3207fa2`](https://github.com/vercel/edge-runtime/commit/3207fa224783fecc70ac63aef4cd49a8404ecbc0))
108 |
109 | ## 1.1.0-beta.33
110 |
111 | ### Patch Changes
112 |
113 | - Allow to use URLPattern as a type with @edge-runtime/ponyfill ([#113](https://github.com/vercel/edge-runtime/pull/113))
114 |
115 | * Only export the specific values from the global scope as we define in our types ([#124](https://github.com/vercel/edge-runtime/pull/124))
116 |
117 | ## 1.1.0-beta.32
118 |
119 | ### Patch Changes
120 |
121 | - Change release method to Changesets ([#110](https://github.com/vercel/edge-runtime/pull/110))
122 |
--------------------------------------------------------------------------------
/packages/ponyfill/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/ponyfill: A ponyfill (doesn't overwrite the native methods) to use Edge Runtime APIs in any environment.
8 |
See @edge-runtime/ponyfill section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/ponyfill --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/ponyfill --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/ponyfill --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/ponyfill** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/ponyfill/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | export default config
6 |
--------------------------------------------------------------------------------
/packages/ponyfill/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/ponyfill",
3 | "description": "A ponyfill (doesn't overwrite the native methods) to use Edge Runtime APIs in any environment.",
4 | "homepage": "https://edge-runtime.vercel.app/packages/ponyfill",
5 | "version": "4.0.0",
6 | "main": "src/index.js",
7 | "module": "dist/index.mjs",
8 | "repository": {
9 | "directory": "packages/ponyfill",
10 | "type": "git",
11 | "url": "git+https://github.com/vercel/edge-runtime.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/vercel/edge-runtime/issues"
15 | },
16 | "keywords": [
17 | "apis",
18 | "edge",
19 | "edge-runtime",
20 | "functions",
21 | "polyfill",
22 | "ponyfill",
23 | "primitives",
24 | "runtime",
25 | "shim",
26 | "standard",
27 | "web"
28 | ],
29 | "devDependencies": {
30 | "@edge-runtime/jest-environment": "workspace:*",
31 | "@edge-runtime/primitives": "workspace:*",
32 | "@edge-runtime/vm": "workspace:*",
33 | "acorn": "8.14.0",
34 | "acorn-loose": "8.4.0",
35 | "acorn-walk": "8.3.4"
36 | },
37 | "engines": {
38 | "node": ">=18"
39 | },
40 | "files": [
41 | "src"
42 | ],
43 | "scripts": {
44 | "clean": "rm -rf node_modules",
45 | "test": "pnpm test:edge && pnpm test:node",
46 | "test:edge": "EDGE_RUNTIME_EXISTS=true jest --env=@edge-runtime/jest-environment --testPathIgnorePatterns='.node.test.ts$'",
47 | "test:node": "jest --env=node"
48 | },
49 | "license": "MIT",
50 | "publishConfig": {
51 | "access": "public"
52 | },
53 | "types": "src/index.d.ts"
54 | }
55 |
--------------------------------------------------------------------------------
/packages/ponyfill/src/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from '@edge-runtime/primitives'
2 |
--------------------------------------------------------------------------------
/packages/ponyfill/src/index.js:
--------------------------------------------------------------------------------
1 | module.exports =
2 | typeof EdgeRuntime === 'string' ? edge() : require('@edge-runtime/primitives')
3 |
4 | function edge() {
5 | return {
6 | AbortController,
7 | AbortSignal,
8 | atob,
9 | Blob,
10 | btoa,
11 | console,
12 | crypto,
13 | Crypto,
14 | CryptoKey,
15 | DOMException,
16 | Event,
17 | EventTarget,
18 | fetch,
19 | FetchEvent,
20 | File,
21 | FormData,
22 | Headers,
23 | performance,
24 | PromiseRejectionEvent,
25 | ReadableStream,
26 | ReadableStreamBYOBReader,
27 | ReadableStreamDefaultReader,
28 | Request,
29 | Response,
30 | setInterval,
31 | setTimeout,
32 | structuredClone,
33 | SubtleCrypto,
34 | TextDecoder,
35 | TextDecoderStream,
36 | TextEncoder,
37 | TextEncoderStream,
38 | TransformStream,
39 | URL,
40 | URLPattern,
41 | URLSearchParams,
42 | WebSocket,
43 | WritableStream,
44 | WritableStreamDefaultWriter,
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/ponyfill/test/EdgeRuntime.test.ts:
--------------------------------------------------------------------------------
1 | const shouldExist = Boolean(process.env.EDGE_RUNTIME_EXISTS)
2 |
3 | test(`EdgeRuntime is ${shouldExist ? 'not defined' : 'defined'}`, () => {
4 | expect(typeof EdgeRuntime).toBe(shouldExist ? 'string' : 'undefined')
5 | })
6 |
--------------------------------------------------------------------------------
/packages/ponyfill/test/acorn.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'acorn-loose' {
2 | const exported: typeof import('acorn')
3 | export = exported
4 | }
5 |
--------------------------------------------------------------------------------
/packages/ponyfill/test/compliance-with-primitives.node.test.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'acorn-loose'
2 | import { createRequire } from './create-require'
3 | import { promises as fs } from 'fs'
4 | import { simple } from 'acorn-walk'
5 | import { EdgeVM } from '@edge-runtime/vm'
6 |
7 | test('exports all primitives in Edge Runtime', async () => {
8 | const exportedNames = await getExportedNames()
9 |
10 | const anyObject = exportedNames.reduce(
11 | (acc: Record, name: string) => {
12 | acc[name] = expect.anything()
13 | return acc
14 | },
15 | {},
16 | )
17 |
18 | const runtime = new EdgeVM({
19 | codeGeneration: { strings: false, wasm: false },
20 | })
21 |
22 | const moduleRequire = createRequire(runtime.context, new Map())
23 | runtime.context.require = moduleRequire
24 |
25 | const result = moduleRequire(require.resolve('..'), require.resolve('..'))
26 |
27 | for (const key of LIMBO_STATE) {
28 | delete anyObject[key]
29 | delete result[key]
30 | }
31 |
32 | expect(result).toEqual(anyObject)
33 | })
34 |
35 | test('exports all primitives in Node.js', async () => {
36 | const exportedNames = await getExportedNames()
37 |
38 | const anyObject = exportedNames.reduce(
39 | (acc: Record, name: string) => {
40 | acc[name] = expect.anything()
41 | return acc
42 | },
43 | {},
44 | )
45 |
46 | const result = { ...require('..') }
47 |
48 | for (const key of LIMBO_STATE) {
49 | delete anyObject[key]
50 | delete result[key]
51 | }
52 |
53 | expect(result).toEqual(anyObject)
54 | })
55 |
56 | async function getExportedNames() {
57 | const typesPath = require.resolve('@edge-runtime/primitives/types/index.d.ts')
58 | const typesContents = await fs.readFile(typesPath, 'utf8')
59 | const ast = parse(typesContents, { ecmaVersion: 'latest' })
60 | const exportedNames: string[] = []
61 | simple(ast, {
62 | ExportNamedDeclaration(node: any) {
63 | for (const specifier of node.specifiers) {
64 | if (specifier.exported?.name) {
65 | exportedNames.push(specifier.exported.name)
66 | }
67 | }
68 | },
69 | })
70 | return exportedNames
71 | }
72 |
73 | export const LIMBO_STATE = [
74 | 'DOMException',
75 | 'setGlobalDispatcher',
76 | 'getGlobalDispatcher',
77 | 'RequestInfo',
78 | 'RequestInit',
79 | ]
80 |
--------------------------------------------------------------------------------
/packages/ponyfill/test/create-require.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'fs'
2 | import { dirname } from 'path'
3 | import { Context, runInContext } from 'vm'
4 |
5 | export function createRequire(
6 | context: Context,
7 | cache: Map,
8 | references?: Set,
9 | scopedContext: Record = {},
10 | ) {
11 | return function requireFn(referrer: string, specifier: string) {
12 | const resolved = require.resolve(specifier, {
13 | paths: [dirname(referrer)],
14 | })
15 |
16 | const cached = cache.get(specifier) || cache.get(resolved)
17 | if (cached !== undefined && cached !== null) {
18 | return cached.exports
19 | }
20 |
21 | const module = {
22 | exports: {},
23 | loaded: false,
24 | id: resolved,
25 | }
26 |
27 | cache.set(resolved, module)
28 | references?.add(resolved)
29 | const fn = runInContext(
30 | `(function(module,exports,require,__dirname,__filename,${Object.keys(
31 | scopedContext,
32 | ).join(',')}) {${readFileSync(resolved, 'utf-8')}\n})`,
33 | context,
34 | )
35 |
36 | try {
37 | fn(
38 | module,
39 | module.exports,
40 | requireFn.bind(null, resolved),
41 | dirname(resolved),
42 | resolved,
43 | ...Object.values(scopedContext),
44 | )
45 | } catch (error) {
46 | cache.delete(resolved)
47 | throw error
48 | }
49 | module.loaded = true
50 | return module.exports
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/primitives/.gitignore:
--------------------------------------------------------------------------------
1 | types
2 | load
3 |
--------------------------------------------------------------------------------
/packages/primitives/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/primitives: A set of primitives to build Vercel Edge Runtime.
8 |
See @edge-runtime/primitives section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/primitives --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/primitives --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/primitives --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/primitives** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/primitives/load/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "../dist/load.js",
3 | "types": "../types/load.d.ts"
4 | }
--------------------------------------------------------------------------------
/packages/primitives/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/primitives",
3 | "description": "A set of primitives to build Vercel Edge Runtime.",
4 | "homepage": "https://edge-runtime.vercel.app/packages/primitives",
5 | "version": "6.0.0",
6 | "main": "dist/index.js",
7 | "repository": {
8 | "directory": "packages/primitives",
9 | "type": "git",
10 | "url": "git+https://github.com/vercel/edge-runtime.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/vercel/edge-runtime/issues"
14 | },
15 | "keywords": [
16 | "apis",
17 | "edge",
18 | "edge-runtime",
19 | "functions",
20 | "primites",
21 | "runtime",
22 | "standard",
23 | "web"
24 | ],
25 | "devDependencies": {
26 | "@edge-runtime/format": "workspace:*",
27 | "esbuild": "0.24.0",
28 | "event-target-shim": "6.0.2",
29 | "tsup": "8",
30 | "undici": "6.21.0",
31 | "urlpattern-polyfill": "10.0.0"
32 | },
33 | "engines": {
34 | "node": ">=18"
35 | },
36 | "files": [
37 | "dist",
38 | "load",
39 | "types"
40 | ],
41 | "scripts": {
42 | "build": "ts-node scripts/build.ts",
43 | "clean:build": "rm -rf dist",
44 | "clean:node": "rm -rf node_modules",
45 | "prebuild": "pnpm run clean:build"
46 | },
47 | "license": "MIT",
48 | "publishConfig": {
49 | "access": "public"
50 | },
51 | "types": "types/index.d.ts"
52 | }
53 |
--------------------------------------------------------------------------------
/packages/primitives/src/injectSourceCode.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Injects the source code of a file into a string.
3 | * This relies on the build script to generate a file with the same name as the
4 | * file to be injected, but with a `.text.js` extension.
5 | */
6 | declare const injectSourceCode: (path: string) => string
7 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/abort-controller.js:
--------------------------------------------------------------------------------
1 | const kSignal = Symbol('kSignal')
2 | const kAborted = Symbol('kAborted')
3 | const kReason = Symbol('kReason')
4 | const kName = Symbol('kName')
5 | const kOnabort = Symbol('kOnabort')
6 |
7 | // this polyfill is heavily inspired from @flemist/abort-controller
8 | // @see https://github.com/NikolayMakhonin/abort-controller/tree/master/src/original
9 | export class DOMException extends Error {
10 | constructor(message, name) {
11 | super(message)
12 | this[kName] = name
13 | }
14 |
15 | get name() {
16 | return this[kName]
17 | }
18 | }
19 |
20 | function createAbortSignal() {
21 | const signal = new EventTarget()
22 | Object.setPrototypeOf(signal, AbortSignal.prototype)
23 | signal[kAborted] = false
24 | signal[kReason] = undefined
25 | signal[kOnabort] = undefined
26 | return signal
27 | }
28 |
29 | function abortSignalAbort(signal, reason) {
30 | if (typeof reason === 'undefined') {
31 | reason = new DOMException('This operation was aborted', 'AbortError')
32 | }
33 | if (signal.aborted) {
34 | return
35 | }
36 |
37 | signal[kReason] = reason
38 | signal[kAborted] = true
39 | signal.dispatchEvent(new Event('abort'))
40 | }
41 |
42 | export class AbortController {
43 | constructor() {
44 | this[kSignal] = createAbortSignal()
45 | }
46 |
47 | get signal() {
48 | return this[kSignal]
49 | }
50 |
51 | abort(reason) {
52 | abortSignalAbort(this.signal, reason)
53 | }
54 | }
55 |
56 | export class AbortSignal extends EventTarget {
57 | constructor() {
58 | throw new TypeError('Illegal constructor')
59 | }
60 |
61 | get aborted() {
62 | return this[kAborted]
63 | }
64 |
65 | get reason() {
66 | return this[kReason]
67 | }
68 |
69 | get onabort() {
70 | return this[kOnabort]
71 | }
72 |
73 | set onabort(value) {
74 | if (this[kOnabort]) {
75 | this.removeEventListener('abort', this[kOnabort])
76 | }
77 | if (value) {
78 | this[kOnabort] = value
79 | this.addEventListener('abort', this[kOnabort])
80 | }
81 | }
82 |
83 | throwIfAborted() {
84 | if (this[kAborted]) {
85 | throw this[kReason]
86 | }
87 | }
88 |
89 | static abort(reason) {
90 | const signal = createAbortSignal()
91 | abortSignalAbort(signal, reason)
92 | return signal
93 | }
94 |
95 | static timeout(milliseconds) {
96 | const signal = createAbortSignal()
97 | setTimeout(() => {
98 | abortSignalAbort(
99 | signal,
100 | new DOMException(
101 | 'The operation was aborted due to timeout',
102 | 'TimeoutError',
103 | ),
104 | )
105 | }, milliseconds)
106 | return signal
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/console.js:
--------------------------------------------------------------------------------
1 | import { createFormat } from '@edge-runtime/format'
2 |
3 | const format = createFormat()
4 | const bareError = console.error.bind(console)
5 | const bareLog = console.log.bind(console)
6 | const assert = console.assert.bind(console)
7 | const time = console.time.bind(console)
8 | const timeEnd = console.timeEnd.bind(console)
9 | const timeLog = console.timeLog.bind(console)
10 | const trace = console.trace.bind(console)
11 | const error = (...args) => bareError(format(...args))
12 | const log = (...args) => bareLog(format(...args))
13 |
14 | const konsole = {
15 | assert: (assertion, ...args) => assert(assertion, format(...args)),
16 | count: console.count.bind(console),
17 | debug: log,
18 | dir: console.dir.bind(console),
19 | error: error,
20 | info: log,
21 | log: log,
22 | time: (...args) => time(format(...args)),
23 | timeEnd: (...args) => timeEnd(format(...args)),
24 | timeLog,
25 | trace,
26 | warn: error,
27 | }
28 |
29 | export { konsole as console }
30 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/crypto.js:
--------------------------------------------------------------------------------
1 | import { webcrypto } from 'node:crypto'
2 |
3 | const { Crypto, CryptoKey } = webcrypto
4 |
5 | function SubtleCrypto() {
6 | if (!(this instanceof SubtleCrypto)) return new SubtleCrypto()
7 | throw TypeError('Illegal constructor')
8 | }
9 |
10 | export const crypto = new Crypto()
11 |
12 | export { Crypto }
13 | export { CryptoKey }
14 | export { SubtleCrypto }
15 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/events.js:
--------------------------------------------------------------------------------
1 | export class FetchEvent extends Event {
2 | constructor(request) {
3 | super('fetch')
4 | this.request = request
5 | this.response = null
6 | this.awaiting = new Set()
7 | }
8 |
9 | respondWith = (response) => {
10 | this.response = response
11 | }
12 |
13 | waitUntil = (promise) => {
14 | this.awaiting.add(promise)
15 | promise.finally(() => this.awaiting.delete(promise))
16 | }
17 | }
18 |
19 | export class PromiseRejectionEvent extends Event {
20 | constructor(type, init) {
21 | super(type, { cancelable: true })
22 | this.promise = init.promise
23 | this.reason = init.reason
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/fetch.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import { File } from 'node:buffer'
4 | import undici from 'undici'
5 |
6 | import {
7 | fromInnerResponse,
8 | makeNetworkError,
9 | } from 'undici/lib/web/fetch/response'
10 |
11 | /**
12 | * Add `duplex: 'half'` by default to all requests
13 | */
14 | function addDuplexToInit(options) {
15 | return typeof options === 'undefined' ||
16 | (typeof options === 'object' && options.duplex === undefined)
17 | ? { duplex: 'half', ...options }
18 | : options
19 | }
20 |
21 | /**
22 | * Add `duplex: 'half'` by default to all requests
23 | */
24 | class Request extends undici.Request {
25 | constructor(input, options) {
26 | super(input, addDuplexToInit(options))
27 | }
28 | }
29 |
30 | /**
31 | * Make the Response headers object mutable
32 | * Check https://github.com/nodejs/undici/blob/1cfe0949053aac6267f11b919cee9315a27f1fd6/lib/web/fetch/response.js#L41
33 | */
34 | const Response = undici.Response
35 | Response.error = function () {
36 | return fromInnerResponse(makeNetworkError(), '')
37 | }
38 |
39 | /**
40 | * Add `duplex: 'half'` by default to all requests
41 | * Recreate the Response object with the undici Response object to allow mutable headers
42 | */
43 | async function fetch(resource, options) {
44 | const res = await undici.fetch(resource, addDuplexToInit(options))
45 | const response = new Response(res.body, res)
46 | Object.defineProperty(response, 'url', { value: res.url })
47 | return response
48 | }
49 |
50 | const { Headers, FormData, WebSocket } = undici
51 | const { Blob } = globalThis
52 |
53 | export { fetch }
54 | export { Blob }
55 | export { Response }
56 | export { File }
57 | export { Request }
58 | export { FormData }
59 | export { Headers }
60 | export { WebSocket }
61 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/index.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { load } from './load'
4 |
5 | module.exports = load({ WeakRef: global.WeakRef })
6 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/stream.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | export {
4 | ReadableStream,
5 | ReadableStreamBYOBReader,
6 | ReadableStreamDefaultReader,
7 | TextDecoderStream,
8 | TextEncoderStream,
9 | TransformStream,
10 | WritableStream,
11 | WritableStreamDefaultWriter,
12 | } from 'node:stream/web'
13 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/timers.js:
--------------------------------------------------------------------------------
1 | const setTimeoutProxy = new Proxy(setTimeout, {
2 | apply: (target, thisArg, args) => {
3 | const timeout = Reflect.apply(target, thisArg, args)
4 | // Returns integer value of timeout ID
5 | return timeout[Symbol.toPrimitive]()
6 | },
7 | })
8 |
9 | const setIntervalProxy = new Proxy(setInterval, {
10 | apply: (target, thisArg, args) => {
11 | const timeout = Reflect.apply(target, thisArg, args)
12 | // Returns integer value of timeout ID
13 | return timeout[Symbol.toPrimitive]()
14 | },
15 | })
16 |
17 | export { setTimeoutProxy as setTimeout }
18 | export { setIntervalProxy as setInterval }
19 |
--------------------------------------------------------------------------------
/packages/primitives/src/primitives/url.js:
--------------------------------------------------------------------------------
1 | export { URLPattern } from 'urlpattern-polyfill'
2 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/abort-controller.d.ts:
--------------------------------------------------------------------------------
1 | declare const AbortControllerConstructor: typeof AbortController
2 |
3 | declare const DOMExceptionConstructor: typeof DOMException
4 |
5 | declare var AbortSignal: {
6 | prototype: typeof AbortSignal
7 | new (): typeof AbortSignal
8 | /** Returns an AbortSignal instance which will be aborted in milliseconds milliseconds. Its abort reason will be set to a "TimeoutError" DOMException. */
9 | timeout(milliseconds: number): AbortSignal
10 | /** Returns an AbortSignal instance whose abort reason is set to reason if not undefined; otherwise to an "AbortError" DOMException. */
11 | abort(reason?: string): AbortSignal
12 | }
13 |
14 | export {
15 | AbortControllerConstructor as AbortController,
16 | DOMExceptionConstructor as DOMException,
17 | AbortSignal,
18 | }
19 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/blob.d.ts:
--------------------------------------------------------------------------------
1 | declare const BlobConstructor: typeof Blob
2 | export { BlobConstructor as Blob }
3 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/console.d.ts:
--------------------------------------------------------------------------------
1 | interface IConsole {
2 | assert: Console['assert']
3 | count: Console['count']
4 | debug: Console['debug']
5 | dir: Console['dir']
6 | error: Console['error']
7 | info: Console['info']
8 | log: Console['log']
9 | time: Console['time']
10 | timeEnd: Console['timeEnd']
11 | timeLog: Console['timeLog']
12 | trace: Console['trace']
13 | warn: Console['warn']
14 | }
15 |
16 | export const console: IConsole
17 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/crypto.d.ts:
--------------------------------------------------------------------------------
1 | export const crypto: Crypto
2 |
3 | declare const CryptoConstructor: typeof Crypto
4 | declare const CryptoKeyConstructor: typeof CryptoKey
5 | declare const SubtleCryptoConstructor: typeof SubtleCrypto
6 |
7 | export { CryptoConstructor as Crypto }
8 | export { CryptoKeyConstructor as CryptoKey }
9 | export { SubtleCryptoConstructor as SubtleCrypto }
10 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/encoding.d.ts:
--------------------------------------------------------------------------------
1 | declare const TextEncoderConstructor: typeof TextEncoder
2 | declare const TextDecoderConstructor: typeof TextDecoder
3 |
4 | export { TextEncoderConstructor as TextEncoder }
5 | export { TextDecoderConstructor as TextDecoder }
6 |
7 | declare const _atob: typeof atob
8 | declare const _btoa: typeof btoa
9 |
10 | export { _atob as atob }
11 | export { _btoa as btoa }
12 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/events.d.ts:
--------------------------------------------------------------------------------
1 | import type { EventTarget } from 'event-target-shim'
2 |
3 | declare const EventTargetConstructor: typeof EventTarget
4 | declare const EventConstructor: typeof Event
5 |
6 | export declare class FetchEvent {
7 | request: Request
8 | response: Response | null
9 | awaiting: Set>
10 | constructor(request: Request)
11 | respondWith(response: Response | Promise): void
12 | waitUntil(promise: Promise): void
13 | }
14 |
15 | export {
16 | EventConstructor as Event,
17 | EventTargetConstructor as EventTarget,
18 | EventTarget as PromiseRejectionEvent,
19 | }
20 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/fetch.d.ts:
--------------------------------------------------------------------------------
1 | export class Request extends globalThis.Request {
2 | readonly headers: Headers
3 | readonly duplex: string
4 | }
5 |
6 | export class Response extends globalThis.Response {
7 | readonly headers: Headers
8 | static json(data: any, init?: ResponseInit): Response
9 | }
10 |
11 | export type RequestInfo = string | Request | globalThis.Request
12 | export type RequestInit = globalThis.RequestInit
13 | declare const fetchImplementation: (
14 | info: RequestInfo,
15 | init?: RequestInit,
16 | ) => Promise
17 |
18 | declare const FileConstructor: typeof File
19 | declare const FormDataConstructor: typeof FormData
20 | declare const WebSocketConstructor: typeof WebSocket
21 | declare const HeadersConstructor: typeof Headers
22 |
23 | export { HeadersConstructor as Headers }
24 | export { fetchImplementation as fetch }
25 | export { FileConstructor as File }
26 | export { FormDataConstructor as FormData }
27 | export { WebSocketConstructor as WebSocket }
28 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './abort-controller'
2 | export * from './blob'
3 | export * from './console'
4 | export * from './crypto'
5 | export * from './encoding'
6 | export * from './events'
7 | export * from './fetch'
8 | export * from './streams'
9 | export * from './text-encoding-streams'
10 | export * from './structured-clone'
11 | export * from './url'
12 | export * from './timers'
13 | export * from './performance'
14 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/load.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Load all the modules in the correct order.
3 | * This is just like the entrypoint (`@edge-runtime/primitives`), only
4 | * lazy.
5 | *
6 | * @param scopedContext a record of values that will be available to
7 | * all modules. This is useful for providing a different implementation of
8 | * globals, like `Uint8Array`.
9 | *
10 | * @example
11 | * ```ts
12 | * import { load } from '@edge-runtime/primitives/load'
13 | *
14 | * const { crypto, fetch, Request, Headers } = load({
15 | * Uint8Array: MyCustomUint8Array,
16 | * Error: MyCustomError,
17 | * })
18 | * ```
19 | */
20 | export function load(
21 | scopedContext: Record,
22 | ): typeof import('./index')
23 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/performance.d.ts:
--------------------------------------------------------------------------------
1 | declare const performanceConstructor: typeof performance
2 | export { performanceConstructor as performance }
3 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/streams.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The type of `ReadableStreamBYOBReader` is not included in Typescript so we
3 | * are declaring it inline to not have to worry about bundling.
4 | */
5 | declare class ReadableStreamBYOBReader {
6 | constructor(stream: ReadableStream)
7 | get closed(): Promise
8 | cancel(reason?: any): Promise
9 | read(
10 | view: T,
11 | ): Promise<{ done: false; value: T } | { done: true; value: T | undefined }>
12 | releaseLock(): void
13 | }
14 |
15 | declare const ReadableStreamConstructor: typeof ReadableStream
16 | declare const ReadableStreamBYOBReaderConstructor: typeof ReadableStreamBYOBReader
17 | declare const ReadableStreamDefaultReaderConstructor: typeof ReadableStreamDefaultReader
18 | declare const TransformStreamConstructor: typeof TransformStream
19 | declare const WritableStreamConstructor: typeof WritableStream
20 | declare const WritableStreamDefaultWriterConstructor: typeof WritableStreamDefaultWriter
21 |
22 | export { ReadableStreamConstructor as ReadableStream }
23 | export { ReadableStreamBYOBReaderConstructor as ReadableStreamBYOBReader }
24 | export { ReadableStreamDefaultReaderConstructor as ReadableStreamDefaultReader }
25 | export { TransformStreamConstructor as TransformStream }
26 | export { WritableStreamConstructor as WritableStream }
27 | export { WritableStreamDefaultWriterConstructor as WritableStreamDefaultWriter }
28 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/structured-clone.d.ts:
--------------------------------------------------------------------------------
1 | declare const structuredCloneConstructor: typeof structuredClone
2 | export { structuredCloneConstructor as structuredClone }
3 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/text-encoding-streams.d.ts:
--------------------------------------------------------------------------------
1 | declare const TextDecoderStreamConstructor: typeof TextDecoderStream
2 | declare const TextEncoderStreamConstructor: typeof TextEncoderStream
3 |
4 | export { TextDecoderStreamConstructor as TextDecoderStream }
5 | export { TextEncoderStreamConstructor as TextEncoderStream }
6 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/timers.d.ts:
--------------------------------------------------------------------------------
1 | declare const _setTimeout: (callback: () => void, ms?: number) => number
2 | declare const _setInterval: (callback: () => void, ms?: number) => number
3 | export { _setTimeout as setTimeout, _setInterval as setInterval }
4 |
--------------------------------------------------------------------------------
/packages/primitives/type-definitions/url.d.ts:
--------------------------------------------------------------------------------
1 | import type { URLPattern } from 'urlpattern-polyfill/dist/types'
2 |
3 | declare const _URL: typeof URL
4 | declare const _URLSearchParams: typeof URLSearchParams
5 | declare class _URLPattern extends URLPattern {}
6 |
7 | export {
8 | _URL as URL,
9 | _URLPattern as URLPattern,
10 | _URLSearchParams as URLSearchParams,
11 | }
12 |
--------------------------------------------------------------------------------
/packages/runtime/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
edge-runtime: Run any Edge Function from CLI or Node.js module.
8 |
See edge-runtime section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install edge-runtime --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add edge-runtime --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install edge-runtime --save
30 | ```
31 |
32 | ## License
33 |
34 | **edge-runtime** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/runtime/examples/cache.js:
--------------------------------------------------------------------------------
1 | /* global addEventListener, Response */
2 |
3 | async function handleRequest(event) {
4 | const cache = await caches.open('default')
5 | const { searchParams } = new URL(event.request.url)
6 | const url = searchParams.get('url') || 'https://example.vercel.sh'
7 |
8 | const cacheKey = new URL(url).toString()
9 | const request = new Request(cacheKey)
10 |
11 | let response = await cache.match(request)
12 | const isHIT = !!response
13 |
14 | if (isHIT) {
15 | response.headers.set('x-cache-status', 'HIT')
16 | return response
17 | }
18 |
19 | response = await fetch(cacheKey)
20 | response.headers.set('x-cache-status', 'MISS')
21 |
22 | event.waitUntil(cache.put(cacheKey, response.clone()))
23 | return response
24 | }
25 |
26 | addEventListener('fetch', (event) => {
27 | try {
28 | return event.respondWith(handleRequest(event))
29 | } catch (e) {
30 | return event.respondWith(new Response('Error thrown ' + e.message))
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/packages/runtime/examples/crypto.js:
--------------------------------------------------------------------------------
1 | console.log(self.crypto.randomUUID())
2 |
--------------------------------------------------------------------------------
/packages/runtime/examples/empty.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/edge-runtime/77be36e84fbeb20ecd83ad065243ded353a3f803/packages/runtime/examples/empty.js
--------------------------------------------------------------------------------
/packages/runtime/examples/error.js:
--------------------------------------------------------------------------------
1 | addEventListener('fetch', () => Promise.reject('error'))
2 |
--------------------------------------------------------------------------------
/packages/runtime/examples/fetch.js:
--------------------------------------------------------------------------------
1 | /* global addEventListener, fetch, Response */
2 |
3 | let globalCounter = 0
4 |
5 | function createAppendMarquee() {
6 | let count = -1
7 |
8 | return function (text, bg) {
9 | ++count
10 |
11 | return `
12 | `.trim()
36 | }
37 | }
38 |
39 | async function fetchRequest(url) {
40 | const response = await fetch(url)
41 | const content = await response.text()
42 |
43 | const appendMarquee = createAppendMarquee()
44 |
45 | const banners = [
46 | appendMarquee(
47 | 'Served by Edge Runtime, check: https://github.com/vercel/edge-runtime.',
48 | '#111',
49 | ),
50 | appendMarquee(
51 | 'Breaking news Next.js is powered by Edge Runtime. fetch is here. Polyfills are the lesser evil. ',
52 | '#333',
53 | ),
54 | appendMarquee(
55 | `Congrats you are the winner #${++globalCounter}!!! YOU WON A TESLA!! Talk to @rauchg`,
56 | '#444',
57 | ),
58 | ]
59 | return new Response(`${banners.join()}\n${content}`, {
60 | headers: {
61 | 'content-type': 'text/html; charset=UTF-8',
62 | },
63 | })
64 | }
65 |
66 | addEventListener('fetch', (event) => {
67 | const { searchParams } = new URL(event.request.url)
68 | const url = searchParams.get('url') || 'https://example.vercel.s'
69 | return event.respondWith(fetchRequest(url))
70 | })
71 |
--------------------------------------------------------------------------------
/packages/runtime/examples/html.js:
--------------------------------------------------------------------------------
1 | /* global Response, addEventListener */
2 |
3 | const html = `
4 |
5 | Hello World
6 | This markup was generated by a Vercel Edge Runtime.
7 | `
8 |
9 | async function handleRequest(request) {
10 | return new Response(html, {
11 | headers: {
12 | 'content-type': 'text/html;charset=UTF-8',
13 | },
14 | })
15 | }
16 |
17 | addEventListener('fetch', (event) => {
18 | return event.respondWith(handleRequest(event.request))
19 | })
20 |
--------------------------------------------------------------------------------
/packages/runtime/examples/unhandledrejection.js:
--------------------------------------------------------------------------------
1 | /* global addEventListener, Response */
2 |
3 | addEventListener('fetch', (event) => {
4 | Promise.reject(new TypeError('captured unhandledrejection error.'))
5 | return event.respondWith(new Response('OK'))
6 | })
7 |
--------------------------------------------------------------------------------
/packages/runtime/examples/urlpattern.js:
--------------------------------------------------------------------------------
1 | /* global URL, URLPattern, Response */
2 |
3 | const ROUTES = [
4 | [
5 | '/db/:id',
6 | ({ query, params }) => `Lookup for ${params.id}?cache=${query.cache}`,
7 | ],
8 | ['/greetings/:name', ({ params }) => `Greetings, ${params.name}`],
9 | ['/ping', () => 'pong'],
10 | ]
11 |
12 | const ROUTER = ROUTES.map(([pathname, handler]) => [
13 | new URLPattern({ pathname }),
14 | handler,
15 | ])
16 |
17 | function getRoute(url) {
18 | let route
19 |
20 | for (const [pattern, handler] of ROUTER) {
21 | const result = pattern.exec(url) || {}
22 |
23 | if ('pathname' in result) {
24 | route = { params: result.pathname.groups, handler }
25 | break
26 | }
27 | }
28 |
29 | return route
30 | }
31 |
32 | /**
33 | * Examples:
34 | * - http://localhost:3000/
35 | * - http://localhost:3000/db/id?cache=all
36 | * - http://localhost:3000/greetings/kiko
37 | */
38 | addEventListener('fetch', (event) => {
39 | const { url } = event.request
40 |
41 | const route = getRoute(url)
42 |
43 | if (!route) {
44 | return event.respondWith(new Response('no route found', { status: 404 }))
45 | }
46 |
47 | const { params, handler } = route
48 | const query = Object.fromEntries(new URL(url).searchParams)
49 | const result = handler({ url, params, query })
50 |
51 | return event.respondWith(new Response(result))
52 | })
53 |
--------------------------------------------------------------------------------
/packages/runtime/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | export default config
6 |
--------------------------------------------------------------------------------
/packages/runtime/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "edge-runtime",
3 | "description": "Run any Edge Function from CLI or Node.js module.",
4 | "homepage": "https://edge-runtime.vercel.app/packages/runtime",
5 | "version": "4.0.1",
6 | "main": "dist/index.js",
7 | "bin": {
8 | "edge-runtime": "dist/cli/index.js"
9 | },
10 | "repository": {
11 | "directory": "packages/runtime",
12 | "type": "git",
13 | "url": "git+https://github.com/vercel/edge-runtime.git"
14 | },
15 | "bugs": {
16 | "url": "https://github.com/vercel/edge-runtime/issues"
17 | },
18 | "keywords": [
19 | "edge",
20 | "edge-runtime",
21 | "functions",
22 | "runtime",
23 | "standard",
24 | "web"
25 | ],
26 | "dependencies": {
27 | "@edge-runtime/format": "workspace:*",
28 | "@edge-runtime/ponyfill": "workspace:*",
29 | "@edge-runtime/vm": "workspace:*",
30 | "async-listen": "3.0.1",
31 | "mri": "1.2.0",
32 | "picocolors": "1.1.1",
33 | "pretty-ms": "7.0.1",
34 | "signal-exit": "4.0.2",
35 | "time-span": "4.0.0"
36 | },
37 | "engines": {
38 | "node": ">=18"
39 | },
40 | "files": [
41 | "dist"
42 | ],
43 | "scripts": {
44 | "build": "tsc --project tsconfig.prod.json",
45 | "clean": "pnpm run clean:node && pnpm run clean:build",
46 | "clean:build": "rm -rf dist",
47 | "clean:node": "rm -rf node_modules",
48 | "postversion": "pnpm run build",
49 | "prebuild": "pnpm run clean:build",
50 | "test": "jest"
51 | },
52 | "license": "MIT",
53 | "publishConfig": {
54 | "access": "public"
55 | },
56 | "types": "dist/index.d.ts"
57 | }
58 |
--------------------------------------------------------------------------------
/packages/runtime/src/cli/eval.ts:
--------------------------------------------------------------------------------
1 | import { EdgeRuntime } from '../edge-runtime'
2 |
3 | export const inlineEval = async (script: string) => {
4 | const runtime = new EdgeRuntime()
5 | const result = await runtime.evaluate(script)
6 | return result
7 | }
8 |
--------------------------------------------------------------------------------
/packages/runtime/src/cli/help.ts:
--------------------------------------------------------------------------------
1 | import { dim, white } from 'picocolors'
2 | interface HelpOptions extends Record {}
3 |
4 | const flags: HelpOptions = {
5 | eval: 'Evaluate an input script',
6 | help: 'Display this message.',
7 | listen: 'Run as HTTP server.',
8 | port: 'Specify a port to use.',
9 | repl: 'Start an interactive session.',
10 | }
11 |
12 | export const help = () => `
13 | edge-runtime ${dim('[] [input]')}
14 |
15 | ${dim('Flags:')}
16 |
17 | ${getSectionSummary(flags)}
18 | `
19 |
20 | function getPadLength(options: HelpOptions) {
21 | const lengths = Object.keys(options).map((key) => key.length)
22 | return Math.max.apply(null, lengths) + 1
23 | }
24 |
25 | function getSectionSummary(options: HelpOptions) {
26 | const summaryPadLength = getPadLength(options)
27 |
28 | const summary = Object.entries(options)
29 | .map(
30 | ([key, description]) =>
31 | ` --${key.padEnd(summaryPadLength)} ${dim(description)}`,
32 | )
33 | .join('\n')
34 |
35 | return `${summary}`
36 | }
37 |
--------------------------------------------------------------------------------
/packages/runtime/src/cli/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { EdgeRuntime } from '../edge-runtime'
4 | import { promisify } from 'util'
5 | import { readFileSync } from 'fs'
6 | import { runServer, type EdgeRuntimeServer } from '../server'
7 | import childProcess from 'child_process'
8 | import { onExit } from 'signal-exit'
9 | import mri from 'mri'
10 | import path from 'path'
11 |
12 | const { _: input, ...flags } = mri(process.argv.slice(2), {
13 | alias: {
14 | e: 'eval',
15 | h: 'host',
16 | l: 'listen',
17 | p: 'port',
18 | },
19 | default: {
20 | cwd: process.cwd(),
21 | eval: false,
22 | help: false,
23 | host: '127.0.0.1',
24 | listen: false,
25 | port: 3000,
26 | repl: false,
27 | },
28 | })
29 |
30 | async function main() {
31 | if (flags.help) {
32 | const { help } = await import('./help')
33 | console.log(help())
34 | return
35 | }
36 |
37 | if (flags.eval) {
38 | const { inlineEval } = await import('./eval')
39 | console.log(await inlineEval(input[0]))
40 | return
41 | }
42 |
43 | /**
44 | * If there is no script path to run a server, the CLI will start a REPL.
45 | */
46 | const [scriptPath] = input
47 |
48 | if (!scriptPath) {
49 | const replPath = path.resolve(__dirname, 'repl.js')
50 | return promisify(childProcess.spawn).call(null, 'node', [replPath], {
51 | stdio: 'inherit',
52 | })
53 | }
54 |
55 | const initialCode = readFileSync(
56 | path.resolve(process.cwd(), scriptPath),
57 | 'utf-8',
58 | )
59 |
60 | const runtime = new EdgeRuntime({ initialCode })
61 | if (!flags.listen) return runtime.evaluate('')
62 |
63 | const logger = await import('./logger').then(({ createLogger }) =>
64 | createLogger(),
65 | )
66 |
67 | logger.debug(
68 | `v${String(require('../../package.json').version)} at Node.js ${
69 | process.version
70 | }`,
71 | )
72 |
73 | /**
74 | * Start a server with the script provided in the file path.
75 | */
76 | let server: undefined | EdgeRuntimeServer
77 | let port = flags.port
78 | while (server === undefined) {
79 | try {
80 | server = await runServer({
81 | host: flags.host,
82 | logger: logger,
83 | port,
84 | runtime,
85 | })
86 | } catch (error: any) {
87 | if (error?.code === 'EADDRINUSE') {
88 | logger.warn(`Port \`${port}\` already in use`)
89 | ++port
90 | } else throw error
91 | }
92 | }
93 |
94 | onExit(() => server?.close())
95 | logger(`Waiting incoming requests at ${logger.quotes(server.url)}`)
96 | }
97 |
98 | main().catch((error: any) => {
99 | if (!(error instanceof Error)) error = new Error(error)
100 | process.exit(1)
101 | })
102 |
--------------------------------------------------------------------------------
/packages/runtime/src/cli/logger.ts:
--------------------------------------------------------------------------------
1 | import { createFormat } from '@edge-runtime/format'
2 | import type { Logger, LoggerOptions } from '../types'
3 | import type { Formatter } from 'picocolors/types'
4 | import pico from 'picocolors'
5 |
6 | const isEnabled =
7 | process.env.EDGE_RUNTIME_LOGGING !== undefined
8 | ? Boolean(process.env.EDGE_RUNTIME_LOGGING)
9 | : true
10 |
11 | export const format = createFormat()
12 |
13 | /**
14 | * Creates basic logger with colors that can be used from the CLI and the
15 | * server logs.
16 | */
17 | export function createLogger() {
18 | const logger = function (message: string, opts?: LoggerOptions) {
19 | print(message, opts)
20 | } as Logger
21 |
22 | logger.info = logger
23 | logger.error = (message, opts) => print(message, { color: 'red', ...opts })
24 | logger.debug = (message, opts) => print(message, { color: 'dim', ...opts })
25 | logger.warn = (message, opts) => print(message, { color: 'yellow', ...opts })
26 | logger.quotes = (str: string) => `\`${str}\``
27 | return logger
28 | }
29 |
30 | function print(
31 | message: string,
32 | {
33 | color = 'white',
34 | withHeader = true,
35 | withBreakline = false,
36 | }: LoggerOptions = {},
37 | ) {
38 | if (!isEnabled) return
39 | const colorize = pico[color] as Formatter
40 | const header = withHeader ? `${colorize('ƒ')} ` : ''
41 | const separator = withBreakline ? '\n' : ''
42 | console.log(`${header}${separator}${colorize(message)}`)
43 | }
44 |
--------------------------------------------------------------------------------
/packages/runtime/src/cli/repl.ts:
--------------------------------------------------------------------------------
1 | import { createFormat } from '@edge-runtime/format'
2 | import createRepl from 'repl'
3 | import { homedir } from 'os'
4 | import { join } from 'path'
5 |
6 | import { EdgeRuntime } from '../edge-runtime'
7 |
8 | const format = createFormat()
9 |
10 | const writer: createRepl.REPLWriter = (output) => {
11 | return typeof output === 'function' ? output.toString() : format(output)
12 | }
13 |
14 | const repl = createRepl.start({ prompt: 'ƒ => ', writer })
15 | repl.setupHistory(join(homedir(), '.edge_runtime_repl_history'), () => {})
16 |
17 | Object.getOwnPropertyNames(repl.context).forEach(
18 | (mod) => delete repl.context[mod],
19 | )
20 |
21 | const runtime = new EdgeRuntime()
22 |
23 | Object.getOwnPropertyNames(runtime.context)
24 | .filter((key) => !key.startsWith('__'))
25 | .forEach((key) =>
26 | Object.assign(repl.context, { [key]: runtime.context[key] }),
27 | )
28 |
29 | Object.defineProperty(repl.context, 'EdgeRuntime', {
30 | configurable: false,
31 | enumerable: false,
32 | writable: false,
33 | value: runtime.context.EdgeRuntime,
34 | })
35 |
36 | export { repl }
37 |
--------------------------------------------------------------------------------
/packages/runtime/src/edge-runtime.ts:
--------------------------------------------------------------------------------
1 | export { EdgeVM as EdgeRuntime } from '@edge-runtime/vm'
2 |
--------------------------------------------------------------------------------
/packages/runtime/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | consumeUint8ArrayReadableStream,
3 | pipeBodyStreamToResponse,
4 | createHandler,
5 | runServer,
6 | } from './server'
7 |
8 | export { EdgeRuntime } from './edge-runtime'
9 |
--------------------------------------------------------------------------------
/packages/runtime/src/server/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | consumeUint8ArrayReadableStream,
3 | pipeBodyStreamToResponse,
4 | } from './body-streams'
5 | export { createHandler } from './create-handler'
6 | export { runServer, EdgeRuntimeServer } from './run-server'
7 |
--------------------------------------------------------------------------------
/packages/runtime/src/server/run-server.ts:
--------------------------------------------------------------------------------
1 | import { createHandler, Options } from './create-handler'
2 | import type { EdgeContext } from '@edge-runtime/vm'
3 | import listen from 'async-listen'
4 | import http from 'http'
5 | import type { ListenOptions } from 'net'
6 | import { promisify } from 'util'
7 |
8 | interface ServerOptions extends Options {}
9 |
10 | export interface EdgeRuntimeServer {
11 | /**
12 | * The server URL.
13 | */
14 | url: string
15 | /**
16 | * Waits for all the current effects and closes the server.
17 | */
18 | close: () => Promise
19 | /**
20 | * Waits for all current effects returning their result.
21 | */
22 | waitUntil: () => Promise
23 | }
24 |
25 | /**
26 | * This helper will create a handler based on the given options and then
27 | * immediately run a server on the provided port. If there is no port, the
28 | * server will use a random one.
29 | */
30 | export async function runServer(
31 | options: ListenOptions & ServerOptions,
32 | ): Promise {
33 | if (options.port === undefined) options.port = 0
34 | const { handler, waitUntil } = createHandler(options)
35 | const server = http.createServer(handler)
36 | const url = await listen(server, options)
37 | const closeServer = promisify(server.close.bind(server))
38 | return {
39 | url: String(url),
40 | close: () => Promise.all([waitUntil(), closeServer()]).then(() => void 0),
41 | waitUntil,
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/runtime/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Colors } from 'picocolors/types'
2 |
3 | export interface LoggerOptions {
4 | color?: keyof Colors
5 | withHeader?: boolean
6 | withBreakline?: boolean
7 | }
8 |
9 | export interface Logger {
10 | (message: string, opts?: LoggerOptions): void
11 | warn(message: string, opts?: LoggerOptions): void
12 | debug(message: string, opts?: LoggerOptions): void
13 | error(message: string, opts?: LoggerOptions): void
14 | info(message: string, opts?: LoggerOptions): void
15 | quotes(str: string): string
16 | }
17 |
18 | export interface NodeHeaders {
19 | [header: string]: string | string[] | undefined
20 | }
21 |
--------------------------------------------------------------------------------
/packages/runtime/tests/body-stream.test.ts:
--------------------------------------------------------------------------------
1 | import { ReadableStream } from '@edge-runtime/ponyfill'
2 |
3 | import { consumeUint8ArrayReadableStream } from '../src/server/body-streams'
4 |
5 | describe('consumeUint8ArrayReadableStream', () => {
6 | test('closes body stream when iteration breaks', async () => {
7 | const pull = jest.fn((controller: ReadableStreamDefaultController) => {
8 | controller.enqueue(new Uint8Array(pull.mock.calls.length))
9 | })
10 | const cancel = jest.fn()
11 | const readable = new ReadableStream({
12 | pull,
13 | cancel,
14 | })
15 |
16 | const consumable = consumeUint8ArrayReadableStream(readable)
17 | for await (const chunk of consumable) {
18 | expect(chunk).toEqual(new Uint8Array([0]))
19 | break
20 | }
21 | expect(pull).toBeCalledTimes(2)
22 | expect(cancel).toBeCalledTimes(1)
23 | expect(cancel).toBeCalledWith(undefined)
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/packages/runtime/tests/fixtures/pull-error.ts:
--------------------------------------------------------------------------------
1 | import { EdgeRuntime, runServer } from '../../src'
2 | import assert from 'assert'
3 | import fetch from 'node-fetch'
4 |
5 | async function main() {
6 | const runtime = new EdgeRuntime()
7 | const deferred = new Promise((resolve) => {
8 | runtime.context.handleRejection = (event: PromiseRejectionEvent) => {
9 | resolve(event)
10 | }
11 | })
12 |
13 | runtime.evaluate(`
14 | addEventListener('fetch', event => {
15 | const stream = new ReadableStream({
16 | pull(controller) {
17 | throw new Error('expected pull error');
18 | }
19 | });
20 | return event.respondWith(
21 | new Response(stream, {
22 | status: 200,
23 | })
24 | )
25 | })
26 |
27 | addEventListener('unhandledrejection', (event) => {
28 | globalThis.handleRejection(event)
29 | })
30 | `)
31 |
32 | const server = await runServer({ runtime })
33 |
34 | try {
35 | const url = new URL(server.url)
36 | const response = await fetch(String(url))
37 | assert.strictEqual(response.status, 200)
38 | assert.strictEqual(await response.text(), '')
39 | const event = await deferred
40 | assert.strictEqual(event.reason.message, 'expected pull error')
41 | return 'TEST PASSED!'
42 | } finally {
43 | await server.close()
44 | }
45 | }
46 |
47 | main()
48 | .then(console.log)
49 | .catch((error) => {
50 | console.log('TEST FAILED!')
51 | console.log(error)
52 | })
53 |
--------------------------------------------------------------------------------
/packages/runtime/tests/fixtures/unhandled-rejection.ts:
--------------------------------------------------------------------------------
1 | import { EdgeRuntime, runServer } from '../../src'
2 | import assert from 'assert'
3 |
4 | async function main() {
5 | const runtime = new EdgeRuntime()
6 | function waitForReject() {
7 | return new Promise((resolve) => {
8 | runtime.context.handleRejection = (event: PromiseRejectionEvent) => {
9 | resolve(event)
10 | }
11 | })
12 | }
13 |
14 | runtime.evaluate(`
15 | addEventListener('fetch', event => {
16 | const url = new URL(event.request.url)
17 | const chunk = url.searchParams.get('chunk')
18 | const stream = new ReadableStream({
19 | start(controller) {
20 | controller.enqueue(new TextEncoder().encode('hi there'));
21 | controller.enqueue(JSON.parse(chunk));
22 | controller.close();
23 | }
24 | });
25 | return event.respondWith(
26 | new Response(stream, {
27 | status: 200,
28 | })
29 | )
30 | })
31 |
32 | addEventListener('unhandledrejection', (event) => {
33 | globalThis.handleRejection(event)
34 | })
35 | `)
36 |
37 | const server = await runServer({ runtime })
38 |
39 | const chunks = [1, 'String', true, { b: 1 }, [1], Buffer.from('Buffer')]
40 |
41 | try {
42 | for (const chunk of chunks) {
43 | const deferred = waitForReject()
44 | const url = new URL(`${server.url}?chunk=${JSON.stringify(chunk)}`)
45 | const response = await fetch(String(url))
46 | assert.strictEqual(response.status, 200)
47 | assert.strictEqual(await response.text(), 'hi there')
48 | const event = await deferred
49 | assert.strictEqual(
50 | event.reason.message,
51 | 'This ReadableStream did not return bytes.',
52 | )
53 | }
54 | return 'TEST PASSED!'
55 | } finally {
56 | await server.close()
57 | }
58 | }
59 |
60 | main()
61 | .then(console.log)
62 | .catch((error) => {
63 | console.log('TEST FAILED!')
64 | console.log(error)
65 | })
66 |
--------------------------------------------------------------------------------
/packages/runtime/tests/rejections-and-errors.test.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'child_process'
2 | import { promisify } from 'util'
3 | import { resolve } from 'path'
4 |
5 | jest.setTimeout(20000)
6 | const execAsync = promisify(exec)
7 |
8 | it('handles correctly unhandled rejections', async () => {
9 | const result = await execAsync(
10 | `ts-node --transpile-only ${resolve(
11 | __dirname,
12 | './fixtures/unhandled-rejection.ts',
13 | )}`,
14 | { encoding: 'utf8' },
15 | )
16 | expect(result).toMatchObject({
17 | stdout: expect.stringContaining('TEST PASSED!'),
18 | stderr: '',
19 | })
20 | })
21 |
22 | it('reports unhandled rejection for pull errors', async () => {
23 | const result = await execAsync(
24 | `NODE_NO_WARNINGS=1 ts-node --transpile-only ${resolve(
25 | __dirname,
26 | './fixtures/pull-error.ts',
27 | )}`,
28 | { encoding: 'utf8' },
29 | )
30 | expect(result).toMatchObject({
31 | stdout: expect.stringContaining('TEST PASSED!'),
32 | stderr: '',
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/packages/runtime/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "outDir": "./dist"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/types/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/types: TypeScript global types for using Edge Runtime.
8 |
See @edge-runtime/types section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/types --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/types --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/types --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/types** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/types",
3 | "description": "TypeScript global types for using Edge Runtime.",
4 | "homepage": "https://edge-runtime.vercel.app/packages/types",
5 | "version": "4.0.0",
6 | "repository": {
7 | "directory": "packages/types",
8 | "type": "git",
9 | "url": "git+https://github.com/vercel/edge-runtime.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/vercel/edge-runtime/issues"
13 | },
14 | "keywords": [
15 | "context",
16 | "edge",
17 | "edge-runtime",
18 | "functions",
19 | "runtime",
20 | "standard",
21 | "types",
22 | "typescript",
23 | "vm",
24 | "web"
25 | ],
26 | "dependencies": {
27 | "@edge-runtime/primitives": "workspace:*"
28 | },
29 | "engines": {
30 | "node": ">=18"
31 | },
32 | "files": [
33 | "src"
34 | ],
35 | "license": "MIT",
36 | "publishConfig": {
37 | "access": "public"
38 | },
39 | "types": "src/index.d.ts"
40 | }
41 |
--------------------------------------------------------------------------------
/packages/types/src/index.d.ts:
--------------------------------------------------------------------------------
1 | // Reference required types from the default lib
2 |
3 | ///
4 |
5 | import * as Edge from '@edge-runtime/primitives'
6 |
7 | declare global {
8 | function addEventListener(
9 | type: 'fetch',
10 | listener: (event: Edge.FetchEvent) => void,
11 | ): void
12 | const EdgeRuntime: Record
13 | const globalThis: typeof Edge
14 | const FetchEvent: typeof Edge.FetchEvent
15 | const URLPattern: typeof Edge.URLPattern
16 | }
17 |
18 | export {}
19 |
--------------------------------------------------------------------------------
/packages/user-agent/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @edge-runtime/user-agent
2 |
3 | ## 3.0.0
4 |
5 | ### Major Changes
6 |
7 | - drop node16 ([#1045](https://github.com/vercel/edge-runtime/pull/1045))
8 |
9 | ## 2.0.0
10 |
11 | ### Major Changes
12 |
13 | - use MIT license ([#909](https://github.com/vercel/edge-runtime/pull/909))
14 |
15 | ## 1.4.1
16 |
17 | ### Patch Changes
18 |
19 | - build: upgrade tsup ([#773](https://github.com/vercel/edge-runtime/pull/773))
20 |
21 | - fix: expose `performance` constructor ([#772](https://github.com/vercel/edge-runtime/pull/772))
22 |
23 | ## 1.4.0
24 |
25 | ### Minor Changes
26 |
27 | - drop node14 support ([`7cc92cc`](https://github.com/vercel/edge-runtime/commit/7cc92ccd190c2d96483202d9f2e1a523778d1f48))
28 |
29 | ## 1.3.1
30 |
31 | ### Patch Changes
32 |
33 | - chore(cookies): expose `.splitCookiesString` ([#473](https://github.com/vercel/edge-runtime/pull/473))
34 |
35 | ## 1.3.0
36 |
37 | ### Minor Changes
38 |
39 | - Add new `Google-InspectionTool` token to known bot UA list ([`cb04b3e`](https://github.com/vercel/edge-runtime/commit/cb04b3ec0933c6e16bf25efda08c772ddccc588f))
40 |
41 | ## 1.2.0
42 |
43 | ### Minor Changes
44 |
45 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
46 |
47 | ## 1.2.0-beta.0
48 |
49 | ### Minor Changes
50 |
51 | - Fix `instanceof` tests, upgrade undici and revamp how we import stuff into the VM ([#309](https://github.com/vercel/edge-runtime/pull/309))
52 |
53 | ## 1.1.2
54 |
55 | ### Patch Changes
56 |
57 | - Use valid SPDX license expression ([#276](https://github.com/vercel/edge-runtime/pull/276))
58 |
59 | ## 1.1.1
60 |
61 | ### Patch Changes
62 |
63 | - build: update dependencies ([#271](https://github.com/vercel/edge-runtime/pull/271))
64 |
65 | ## 1.1.0
66 |
67 | ### Patch Changes
68 |
69 | - Change release method to Changesets ([#110](https://github.com/vercel/edge-runtime/pull/110))
70 |
71 | - upgrading undici ([`3207fa2`](https://github.com/vercel/edge-runtime/commit/3207fa224783fecc70ac63aef4cd49a8404ecbc0))
72 |
73 | ## 1.1.0-beta.33
74 |
75 | ### Patch Changes
76 |
77 | - upgrading undici ([`3207fa2`](https://github.com/vercel/edge-runtime/commit/3207fa224783fecc70ac63aef4cd49a8404ecbc0))
78 |
79 | ## 1.1.0-beta.32
80 |
81 | ### Patch Changes
82 |
83 | - Change release method to Changesets ([#110](https://github.com/vercel/edge-runtime/pull/110))
84 |
--------------------------------------------------------------------------------
/packages/user-agent/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/user-agent: An Edge Runtime compatible user-agent parsing utility.
8 |
See @edge-runtime/user-agent section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/user-agent --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/user-agent --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/user-agent --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/user-agent** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/user-agent/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | config.testEnvironment = '@edge-runtime/jest-environment'
6 | export default config
7 |
--------------------------------------------------------------------------------
/packages/user-agent/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/user-agent",
3 | "description": "An Edge Runtime compatible user-agent parsing utility",
4 | "homepage": "https://edge-runtime.vercel.app/packages/user-agent",
5 | "version": "3.0.0",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "repository": {
9 | "directory": "packages/user-agent",
10 | "type": "git",
11 | "url": "git+https://github.com/vercel/edge-runtime.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/vercel/edge-runtime/issues"
15 | },
16 | "keywords": [
17 | "edge",
18 | "edge-runtime",
19 | "functions",
20 | "runtime",
21 | "standard",
22 | "ua-parser",
23 | "ua-parser-js",
24 | "user-agent",
25 | "web"
26 | ],
27 | "devDependencies": {
28 | "@edge-runtime/jest-environment": "workspace:*",
29 | "@types/ua-parser-js": "0.7.39",
30 | "tsup": "8",
31 | "ua-parser-js": "1.0.39"
32 | },
33 | "engines": {
34 | "node": ">=18"
35 | },
36 | "files": [
37 | "dist"
38 | ],
39 | "scripts": {
40 | "build": "tsup",
41 | "clean:build": "rm -rf dist",
42 | "clean:node": "rm -rf node_modules",
43 | "prebuild": "pnpm run clean:build",
44 | "test": "jest"
45 | },
46 | "license": "MIT",
47 | "publishConfig": {
48 | "access": "public"
49 | },
50 | "types": "dist/index.d.ts"
51 | }
52 |
--------------------------------------------------------------------------------
/packages/user-agent/src/index.ts:
--------------------------------------------------------------------------------
1 | import parseua from 'ua-parser-js'
2 |
3 | export interface UserAgent {
4 | isBot: boolean
5 | ua: string
6 | browser: {
7 | name?: string
8 | version?: string
9 | }
10 | device: {
11 | model?: string
12 | type?: string
13 | vendor?: string
14 | }
15 | engine: {
16 | name?: string
17 | version?: string
18 | }
19 | os: {
20 | name?: string
21 | version?: string
22 | }
23 | cpu: {
24 | architecture?: string
25 | }
26 | }
27 |
28 | export function isBot(input: string): boolean {
29 | return /Googlebot|Mediapartners-Google|AdsBot-Google|googleweblight|Storebot-Google|Google-PageRenderer|Google-InspectionTool|Bingbot|BingPreview|Slurp|DuckDuckBot|baiduspider|yandex|sogou|LinkedInBot|bitlybot|tumblr|vkShare|quora link preview|facebookexternalhit|facebookcatalog|Twitterbot|applebot|redditbot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|ia_archiver/i.test(
30 | input,
31 | )
32 | }
33 |
34 | export function userAgentFromString(input: string | undefined): UserAgent {
35 | return {
36 | ...parseua(input),
37 | isBot: input === undefined ? false : isBot(input),
38 | }
39 | }
40 |
41 | type HeaderLike = { get(key: string): string | null | undefined }
42 | export function userAgent(request?: { headers: HeaderLike }): UserAgent {
43 | return userAgentFromString(request?.headers?.get('user-agent') || undefined)
44 | }
45 |
--------------------------------------------------------------------------------
/packages/user-agent/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { userAgent, userAgentFromString } from '../src'
2 |
3 | const emptyParsedUA = {
4 | browser: {},
5 | cpu: {},
6 | device: {},
7 | engine: {},
8 | isBot: false,
9 | os: {},
10 | ua: '',
11 | }
12 |
13 | describe('userAgent()', () => {
14 | it('accepts a request', () => {
15 | const request = new Request('https://example.vercel.sh', {
16 | headers: {
17 | 'User-Agent':
18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
19 | },
20 | })
21 | const ua = userAgent(request)
22 | expect(ua.browser).toMatchObject({
23 | name: 'Chrome',
24 | version: '83.0.4103.116',
25 | })
26 | })
27 |
28 | it('handles no input', () => {
29 | expect(userAgent()).toEqual(emptyParsedUA)
30 | })
31 |
32 | it('handles no user-agent header', () => {
33 | expect(userAgent(new Request('https://example.vercel.sh'))).toEqual(
34 | emptyParsedUA,
35 | )
36 | })
37 | })
38 |
39 | describe('userAgentFromString()', () => {
40 | it('can receive a nil value', () => {
41 | expect(userAgentFromString(undefined)).toEqual(emptyParsedUA)
42 | })
43 |
44 | it('parses regular user-agent', () => {
45 | const source =
46 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
47 | expect(userAgentFromString(source)).toMatchObject({
48 | browser: {
49 | major: '83',
50 | name: 'Chrome',
51 | version: '83.0.4103.116',
52 | },
53 | cpu: { architecture: 'amd64' },
54 | engine: {
55 | name: 'Blink',
56 | version: '83.0.4103.116',
57 | },
58 | isBot: false,
59 | os: {
60 | name: 'Windows',
61 | version: '10',
62 | },
63 | ua: source,
64 | })
65 | })
66 |
67 | it('detects bots', () => {
68 | const source =
69 | 'Mozilla/5.0 (Linux; Android 5.0; SM-G920A) AppleWebKit (KHTML, like Gecko) Chrome Mobile Safari (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)'
70 | expect(userAgentFromString(source)).toMatchObject({
71 | device: {
72 | model: 'SM-G920A',
73 | type: 'mobile',
74 | vendor: 'Samsung',
75 | },
76 | isBot: true,
77 | os: {
78 | name: 'Android',
79 | version: '5.0',
80 | },
81 | ua: source,
82 | })
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/packages/user-agent/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["src/**/*.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/user-agent/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig({
4 | dts: true,
5 | entry: ['./src/index.ts'],
6 | format: ['cjs', 'esm'],
7 | tsconfig: './tsconfig.prod.json',
8 | })
9 |
--------------------------------------------------------------------------------
/packages/vm/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |

5 |
6 |
7 |
@edge-runtime/vm: Low level bindings for creating Web Standard contexts.
8 |
See @edge-runtime/vm section in our website for more information.
9 |
10 |
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```sh
17 | npm install @edge-runtime/vm --save
18 | ```
19 |
20 | or using yarn:
21 |
22 | ```sh
23 | yarn add @edge-runtime/vm --dev
24 | ```
25 |
26 | or using pnpm:
27 |
28 | ```sh
29 | pnpm install @edge-runtime/vm --save
30 | ```
31 |
32 | ## License
33 |
34 | **@edge-runtime/vm** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.
35 | Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).
36 |
37 | > [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
38 |
--------------------------------------------------------------------------------
/packages/vm/jest.config.ts:
--------------------------------------------------------------------------------
1 | import buildConfig from '../../jest.config'
2 | import type { Config } from '@jest/types'
3 |
4 | const config: Config.InitialOptions = buildConfig(__dirname)
5 | export default config
6 |
--------------------------------------------------------------------------------
/packages/vm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@edge-runtime/vm",
3 | "description": "Low level bindings for creating Web Standard contexts.",
4 | "homepage": "https://edge-runtime.vercel.app/packages/vm",
5 | "version": "5.0.0",
6 | "main": "dist/index.js",
7 | "repository": {
8 | "directory": "packages/vm",
9 | "type": "git",
10 | "url": "git+https://github.com/vercel/edge-runtime.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/vercel/edge-runtime/issues"
14 | },
15 | "keywords": [
16 | "context",
17 | "edge",
18 | "edge-runtime",
19 | "functions",
20 | "runtime",
21 | "standard",
22 | "vm",
23 | "web"
24 | ],
25 | "engines": {
26 | "node": ">=18"
27 | },
28 | "files": [
29 | "dist"
30 | ],
31 | "scripts": {
32 | "build": "tsc --project ./tsconfig.prod.json",
33 | "clean": "pnpm run clean:node && pnpm run clean:build",
34 | "clean:build": "rm -rf dist",
35 | "clean:node": "rm -rf node_modules",
36 | "test": "jest"
37 | },
38 | "license": "MIT",
39 | "publishConfig": {
40 | "access": "public"
41 | },
42 | "types": "dist/index.d.ts",
43 | "dependencies": {
44 | "@edge-runtime/primitives": "workspace:*"
45 | },
46 | "devDependencies": {
47 | "@types/test-listen": "1.1.2",
48 | "@types/ws": "8.5.13",
49 | "test-listen": "1.1.0",
50 | "ws": "8.18.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/vm/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { EdgeVMOptions, EdgeContext } from './edge-vm'
2 | export type { VMOptions, VMContext } from './vm'
3 | export { EdgeVM } from './edge-vm'
4 | export { VM } from './vm'
5 |
--------------------------------------------------------------------------------
/packages/vm/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface DispatchFetch {
2 | (
3 | input: string,
4 | init?: RequestInit,
5 | ): Promise<
6 | Response & {
7 | waitUntil: () => Promise
8 | }
9 | >
10 | }
11 |
12 | export interface RejectionHandler {
13 | (reason?: {} | null, promise?: Promise): void
14 | }
15 |
16 | export interface ErrorHandler {
17 | (error?: {} | null): void
18 | }
19 |
--------------------------------------------------------------------------------
/packages/vm/src/vm.ts:
--------------------------------------------------------------------------------
1 | import type { CreateContextOptions } from 'vm'
2 | import { createContext, runInContext } from 'vm'
3 |
4 | export interface VMOptions {
5 | /**
6 | * Provide code generation options to the Node.js VM.
7 | * If you don't provide any option, code generation will be disabled.
8 | */
9 | codeGeneration?: CreateContextOptions['codeGeneration']
10 | /**
11 | * Allows to extend the VMContext. Note that it must return a contextified
12 | * object so ideally it should return the same reference it receives.
13 | */
14 | extend?: (context: VMContext) => VMContext & T
15 | }
16 |
17 | /**
18 | * A raw VM with a context that can be extended on instantiation. Implements
19 | * a realm-like interface where one can evaluate code or require CommonJS
20 | * modules in multiple ways.
21 | */
22 | export class VM> {
23 | public readonly context: VMContext & T
24 |
25 | constructor(options: VMOptions = {}) {
26 | const context = createContext(
27 | {},
28 | {
29 | name: 'Edge Runtime',
30 | codeGeneration: options.codeGeneration ?? {
31 | strings: false,
32 | wasm: true,
33 | },
34 | },
35 | ) as VMContext
36 |
37 | this.context = options.extend?.(context) ?? (context as VMContext & T)
38 | }
39 |
40 | /**
41 | * Allows to run arbitrary code within the VM.
42 | */
43 | evaluate(code: string): T {
44 | return runInContext(code, this.context)
45 | }
46 | }
47 |
48 | export interface VMContext {
49 | Array: typeof Array
50 | ArrayBuffer: typeof ArrayBuffer
51 | Atomics: typeof Atomics
52 | BigInt: typeof BigInt
53 | BigInt64Array: typeof BigInt64Array
54 | BigUint64Array: typeof BigUint64Array
55 | Boolean: typeof Boolean
56 | DataView: typeof DataView
57 | Date: typeof Date
58 | decodeURI: typeof decodeURI
59 | decodeURIComponent: typeof decodeURIComponent
60 | encodeURI: typeof encodeURI
61 | encodeURIComponent: typeof encodeURIComponent
62 | Error: typeof Error
63 | EvalError: typeof EvalError
64 | Float32Array: typeof Float32Array
65 | Float64Array: typeof Float64Array
66 | Function: typeof Function
67 | Infinity: typeof Infinity
68 | Int8Array: typeof Int8Array
69 | Int16Array: typeof Int16Array
70 | Int32Array: typeof Int32Array
71 | Intl: typeof Intl
72 | isFinite: typeof isFinite
73 | isNaN: typeof isNaN
74 | JSON: typeof JSON
75 | Map: typeof Map
76 | Math: typeof Math
77 | Number: typeof Number
78 | Object: typeof Object
79 | parseFloat: typeof parseFloat
80 | parseInt: typeof parseInt
81 | Promise: typeof Promise
82 | Proxy: typeof Proxy
83 | RangeError: typeof RangeError
84 | ReferenceError: typeof ReferenceError
85 | Reflect: typeof Reflect
86 | RegExp: typeof RegExp
87 | Set: typeof Set
88 | SharedArrayBuffer: typeof SharedArrayBuffer
89 | String: typeof String
90 | Symbol: typeof Symbol
91 | SyntaxError: typeof SyntaxError
92 | TypeError: typeof TypeError
93 | Uint8Array: typeof Uint8Array
94 | Uint8ClampedArray: typeof Uint8ClampedArray
95 | Uint16Array: typeof Uint16Array
96 | Uint32Array: typeof Uint32Array
97 | URIError: typeof URIError
98 | WeakMap: typeof WeakMap
99 | WeakSet: typeof WeakSet
100 | WebAssembly: typeof WebAssembly
101 | [key: string | number]: any
102 | }
103 |
--------------------------------------------------------------------------------
/packages/vm/tests/fetch-within-vm.test.ts:
--------------------------------------------------------------------------------
1 | import { createServer } from 'http'
2 | import listen from 'test-listen'
3 | import { EdgeVM } from '../src'
4 |
5 | test('fetch within vm', async () => {
6 | const server = createServer((req, res) => {
7 | res.write(`Hello from ${req.url}`)
8 | res.end()
9 | })
10 | try {
11 | const url = await listen(server)
12 | const vm = new EdgeVM()
13 |
14 | const result = await vm.evaluate(`fetch("${url}/foo")`)
15 | expect(await result.text()).toBe(`Hello from /foo`)
16 | } finally {
17 | server.close()
18 | }
19 | })
20 |
21 | test('sends a Uint8Array', async () => {
22 | const server = createServer(async (req, res) => {
23 | const chunks = [] as Buffer[]
24 | for await (const chunk of req) {
25 | chunks.push(chunk)
26 | }
27 | const body = Buffer.concat(chunks).toString()
28 | res.write(`Hello from ${req.url} with body ${body}`)
29 | res.end()
30 | })
31 | try {
32 | const url = await listen(server)
33 | const vm = new EdgeVM()
34 |
35 | const result = await vm.evaluate(
36 | `fetch("${url}/foo", { method: "POST", body: new Uint8Array([104, 105, 33]) })`,
37 | )
38 | expect(await result.text()).toBe(`Hello from /foo with body hi!`)
39 | } finally {
40 | server.close()
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/packages/vm/tests/fixtures/cjs-module.js:
--------------------------------------------------------------------------------
1 | function MockURL(href) {
2 | if (!(this instanceof MockURL)) return new MockURL(href)
3 | this.href = href
4 | }
5 |
6 | module.exports.URL = MockURL
7 |
--------------------------------------------------------------------------------
/packages/vm/tests/fixtures/legit-uncaught-exception.ts:
--------------------------------------------------------------------------------
1 | import { EdgeVM } from '../../src'
2 |
3 | new EdgeVM()
4 | throw new Error('intentional break')
5 |
--------------------------------------------------------------------------------
/packages/vm/tests/fixtures/legit-unhandled-rejection.ts:
--------------------------------------------------------------------------------
1 | import { EdgeVM } from '../../src/edge-vm'
2 |
3 | new EdgeVM()
4 | Promise.reject(new Error('intentional break'))
5 |
--------------------------------------------------------------------------------
/packages/vm/tests/fixtures/uncaught-exception.ts:
--------------------------------------------------------------------------------
1 | import { EdgeVM } from '../../src/edge-vm'
2 | import assert from 'assert'
3 |
4 | function main() {
5 | const runtime = new EdgeVM()
6 | runtime.context.handleError = (error: Error) => {
7 | assert.strictEqual(error?.message, 'expected error')
8 | console.log('TEST PASSED!')
9 | }
10 |
11 | runtime.evaluate(`
12 | addEventListener('error', (error) => {
13 | globalThis.handleError(error)
14 | })
15 |
16 | throw new Error('expected error')
17 | `)
18 | }
19 |
20 | main()
21 |
--------------------------------------------------------------------------------
/packages/vm/tests/integration/crypto.test.ts:
--------------------------------------------------------------------------------
1 | import { EdgeVM } from '../../src'
2 |
3 | test('crypto.subtle.digest returns an ArrayBuffer', async () => {
4 | const vm = new EdgeVM()
5 |
6 | async function fn() {
7 | const digest = await crypto.subtle.digest(
8 | 'SHA-256',
9 | crypto.getRandomValues(new Uint8Array(32)),
10 | )
11 | return digest
12 | }
13 |
14 | const fromContext = vm.evaluate(`({ ArrayBuffer })`)
15 |
16 | const digest = await vm.evaluate(`(${fn})()`)
17 | expect(digest).toBeInstanceOf(fromContext.ArrayBuffer)
18 | })
19 |
20 | test('crypto.generateKey works with a Uint8Array from the VM', async () => {
21 | async function fn() {
22 | await crypto.subtle.generateKey(
23 | {
24 | name: 'RSA-PSS',
25 | hash: 'SHA-256',
26 | publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
27 | modulusLength: 2048,
28 | },
29 | false,
30 | ['sign', 'verify'],
31 | )
32 | }
33 |
34 | const vm = new EdgeVM()
35 | await vm.evaluate(`(${fn})()`)
36 | })
37 |
--------------------------------------------------------------------------------
/packages/vm/tests/integration/error.test.ts:
--------------------------------------------------------------------------------
1 | import { EdgeVM } from '../../src'
2 |
3 | test('Error maintains a stack trace', async () => {
4 | const log = jest.fn()
5 | console.log = log
6 |
7 | function fn() {
8 | console.log(new Error('hello, world!'))
9 | }
10 |
11 | const vm = new EdgeVM()
12 | vm.evaluate(`(${fn})()`)
13 |
14 | expect(log).toHaveBeenCalledTimes(1)
15 | expect(log.mock.lastCall[0]).toMatch(/^Error: hello, world!\s+at fn/m)
16 | })
17 |
18 | test('additional error properties', async () => {
19 | const log = jest.fn()
20 | console.log = log
21 |
22 | function fn() {
23 | class CustomError extends Error {
24 | name = 'CustomError'
25 | constructor(
26 | message: string,
27 | private digest: string,
28 | private cause?: Error,
29 | ) {
30 | super(message)
31 | }
32 | }
33 | console.log(new CustomError('without cause', 'digest1'))
34 | console.log(new CustomError('with cause', 'digest2', new Error('oh no')))
35 | }
36 |
37 | const vm = new EdgeVM()
38 | vm.evaluate(`(${fn})()`)
39 |
40 | expect(log).toHaveBeenCalledTimes(2)
41 | const [[withoutCause], [withCause]] = log.mock.calls
42 | expect(withoutCause).toMatch(
43 | /^CustomError: without cause\s+at fn.+\{.+digest: 'digest1',.+cause: undefined.+\}/ms,
44 | )
45 | expect(withCause).toMatch(
46 | /^CustomError: with cause\s+at fn.+\{.+digest: 'digest2',.+cause: Error: oh no.+.+\}/ms,
47 | )
48 | })
49 |
--------------------------------------------------------------------------------
/packages/vm/tests/rejections-and-errors.test.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'child_process'
2 | import { promisify } from 'util'
3 | import { resolve } from 'path'
4 |
5 | jest.setTimeout(20000)
6 | const execAsync = promisify(exec)
7 |
8 | it('handles correctly uncaught exceptions', async () => {
9 | const result = await execAsync(
10 | `ts-node --transpile-only ${resolve(
11 | __dirname,
12 | './fixtures/uncaught-exception.ts',
13 | )}`,
14 | { encoding: 'utf8' },
15 | )
16 | expect(result).toMatchObject({
17 | stdout: expect.stringContaining('TEST PASSED!'),
18 | stderr: '',
19 | })
20 | })
21 |
22 | it('does not swallow uncaught exceptions outside of evaluation', async () => {
23 | const execAsync = promisify(exec)
24 | await expect(
25 | execAsync(
26 | `ts-node --transpile-only ${resolve(
27 | __dirname,
28 | './fixtures/legit-uncaught-exception.ts',
29 | )}`,
30 | { encoding: 'utf8' },
31 | ),
32 | ).rejects.toThrow(/intentional break/)
33 | })
34 |
35 | it('does not swallow unhandled rejections outside of evaluation', async () => {
36 | const execAsync = promisify(exec)
37 | await expect(
38 | execAsync(
39 | `ts-node --transpile-only ${resolve(
40 | __dirname,
41 | './fixtures/legit-unhandled-rejection.ts',
42 | )}`,
43 | {
44 | encoding: 'utf8',
45 | },
46 | ),
47 | ).rejects.toThrow(/intentional break/)
48 | })
49 |
--------------------------------------------------------------------------------
/packages/vm/tests/vm.test.ts:
--------------------------------------------------------------------------------
1 | import { VM } from '../src/vm'
2 |
3 | it('creates a VM with empty context', () => {
4 | const vm = new VM()
5 | vm.evaluate('this.foo = "bar"')
6 | expect(vm.context).toStrictEqual({ foo: 'bar' })
7 | })
8 |
9 | it('allows to extend the context with environment variables', () => {
10 | const vm = new VM({
11 | extend: (context) =>
12 | Object.assign(context, {
13 | process: { env: { NODE_ENV: 'development' } },
14 | }),
15 | })
16 |
17 | expect(vm.context.process.env.NODE_ENV).toEqual('development')
18 | const env = vm.evaluate('process.env.NODE_ENV')
19 | expect(env).toEqual('development')
20 | })
21 |
22 | it('allows to extend the context with APIs implementations', () => {
23 | class MockURL {
24 | href: string
25 | constructor(url: string) {
26 | this.href = url
27 | }
28 | }
29 |
30 | const vm = new VM({
31 | extend: (context) => {
32 | context.URL = MockURL
33 | return context
34 | },
35 | })
36 |
37 | vm.evaluate('this.hasURL = !!URL')
38 | vm.evaluate('this.url = new URL("https://edge-ping.vercel.app")')
39 |
40 | expect(vm.context.hasURL).toBeTruthy()
41 | expect(vm.context.url.href).toEqual('https://edge-ping.vercel.app')
42 | expect(vm.context.URL.name).toEqual('MockURL')
43 | })
44 |
45 | it('allows to extend the context with code evaluation', () => {
46 | const script = `
47 | function MockURL (href) {
48 | if (!(this instanceof MockURL)) return new MockURL(href)
49 | this.href = href
50 | }
51 | this.URL = MockURL`
52 |
53 | const vm = new VM()
54 |
55 | vm.evaluate(script)
56 | vm.evaluate('this.hasURL = !!URL')
57 | vm.evaluate('this.url = new URL("https://edge-ping.vercel.app")')
58 |
59 | expect(vm.context.hasURL).toBeTruthy()
60 | expect(vm.context.url.href).toEqual('https://edge-ping.vercel.app')
61 | expect(vm.context.URL.name).toEqual('MockURL')
62 | })
63 |
64 | it('does not allow to run `new Function`', () => {
65 | const vm = new VM()
66 | expect(() => {
67 | vm.evaluate('new Function(1)')
68 | }).toThrow({
69 | name: 'EvalError',
70 | message: 'Code generation from strings disallowed for this context',
71 | })
72 | })
73 |
74 | it('does not allow `eval`', () => {
75 | const vm = new VM()
76 | expect(() => {
77 | vm.evaluate('eval("1 + 1")')
78 | }).toThrow({
79 | name: 'EvalError',
80 | message: 'Code generation from strings disallowed for this context',
81 | })
82 | })
83 |
84 | it('does not define `require`', () => {
85 | const vm = new VM()
86 | expect(() => {
87 | vm.evaluate("const Blob = require('buffer').Blob; this.blob = new Blob()")
88 | }).toThrow({
89 | name: 'ReferenceError',
90 | message: 'require is not defined',
91 | })
92 | })
93 |
--------------------------------------------------------------------------------
/packages/vm/tests/websocket.test.ts:
--------------------------------------------------------------------------------
1 | import http from 'http'
2 | import { EdgeVM } from '../src'
3 | import { WebSocketServer, type WebSocket as Socket } from 'ws'
4 | import { promisify } from 'util'
5 |
6 | test(`makes a WebSocket connection`, async () => {
7 | const vm = new EdgeVM()
8 |
9 | const websocketServer = createWebsocketServer((socket) => {
10 | socket.on('message', (data) => {
11 | socket.send(`pong: ${data.toString()}`)
12 | })
13 | })
14 |
15 | async function userCode(url: string) {
16 | const client = new WebSocket(url)
17 | return await new Promise((resolve, reject) => {
18 | client.onopen = () => {
19 | client.send('ping')
20 | }
21 | client.addEventListener('message', (msg) => {
22 | resolve(String(msg.data))
23 | })
24 | client.onerror = (err) => reject(err)
25 | }).finally(() => client.close())
26 | }
27 |
28 | try {
29 | const v: Awaited> = await vm.evaluate(
30 | `(${userCode})(${JSON.stringify(websocketServer.url)})`,
31 | )
32 | expect(v).toBe(`pong: ping`)
33 | } finally {
34 | await websocketServer.close()
35 | }
36 | })
37 |
38 | function createWebsocketServer(callback: (ws: Socket) => void): {
39 | url: string
40 | close(): Promise
41 | } {
42 | const server = http.createServer()
43 | const websocketServer = new WebSocketServer({ server })
44 |
45 | websocketServer.on('connection', (socket) => {
46 | return callback(socket)
47 | })
48 |
49 | server.listen(0)
50 |
51 | const port = (server.address() as { port: number }).port
52 | const url = `ws://localhost:${port}`
53 |
54 | const closeServer = promisify(server.close.bind(server))
55 | const closeWebsocketServer = promisify(
56 | websocketServer.close.bind(websocketServer),
57 | )
58 |
59 | return {
60 | url,
61 | async close() {
62 | await Promise.all([closeServer(), closeWebsocketServer()])
63 | },
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/packages/vm/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig.json",
3 | "extends": "../../tsconfig.json",
4 | "include": ["src"],
5 | "compilerOptions": {
6 | "outDir": "./dist",
7 | "sourceMap": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 | - docs
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "declaration": true,
5 | "esModuleInterop": true,
6 | "inlineSources": true,
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "outDir": "./dist",
10 | "skipLibCheck": true,
11 | "sourceMap": true,
12 | "strict": true,
13 | "target": "ES2019",
14 | "types": ["node", "jest"],
15 | "stripInternal": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "globalEnv": ["RUNNER_OS", "NODE_VERSION"],
4 | "tasks": {
5 | "build": {
6 | "outputs": ["dist/**"],
7 | "dependsOn": ["^build"]
8 | },
9 | "edge-runtime#build": {
10 | "cache": false,
11 | "dependsOn": ["^build"],
12 | "outputs": ["src/version.ts"]
13 | },
14 | "@edge-runtime/primitives#build": {
15 | "dependsOn": ["^build"],
16 | "outputs": [
17 | "dist/**"
18 | ]
19 | },
20 | "test": {
21 | "cache": false,
22 | "dependsOn": ["build"],
23 | "outputs": []
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------