├── .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 |
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 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | Edge Runtime logo 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 |
2 |
3 | 4 | 5 |
6 |
7 |

@edge-runtime/format: A util.inspect implementation to serialize any value.

8 |

See @edge-runtime/format section in our website for more information.

9 |
10 |
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 | --------------------------------------------------------------------------------