├── .changeset ├── README.md └── config.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── benchmarks.yml │ ├── pr.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .node-version ├── .npmignore ├── .npmrc ├── .prettierignore ├── .vscode └── settings.json ├── .yarnrc ├── LICENSE ├── README.md ├── babel.config.js ├── benchmarks ├── node-fetch │ ├── k6.js │ ├── package.json │ ├── scenarios.ts │ └── server.ts └── server │ ├── CHANGELOG.md │ ├── k6.js │ ├── package.json │ ├── server.ts │ └── tsconfig.json ├── deno-jest.ts ├── deno.json ├── e2e ├── aws-lambda │ ├── CHANGELOG.md │ ├── package.json │ ├── scripts │ │ ├── bundle.js │ │ ├── createAwsLambdaDeployment.ts │ │ └── e2e.ts │ ├── src │ │ ├── awslambda.d.ts │ │ └── index.ts │ └── tsconfig.json ├── azure-function │ ├── .funcignore │ ├── .vscode │ │ └── extensions.json │ ├── host.json │ ├── package.json │ ├── scripts │ │ ├── bundle.js │ │ ├── createAzureFunctionDeployment.ts │ │ └── e2e.ts │ ├── src │ │ └── functions │ │ │ └── index.ts │ └── tsconfig.json ├── cloudflare-modules │ ├── package.json │ ├── scripts │ │ └── e2e.ts │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml ├── cloudflare-workers │ ├── package.json │ ├── scripts │ │ ├── createCfDeployment.ts │ │ └── e2e.ts │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml ├── shared-scripts │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── runTests.ts │ │ ├── types.ts │ │ └── utils.ts ├── shared-server │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ └── index.ts └── vercel │ ├── .gitignore │ ├── CHANGELOG.md │ ├── next-env.d.ts │ ├── package.json │ ├── scripts │ ├── bundle.js │ ├── createVercelDeployment.ts │ └── e2e.ts │ ├── src │ └── index.ts │ └── tsconfig.json ├── eslint.config.mjs ├── jest.config.js ├── package.json ├── packages ├── cookie-store │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── CookieChangeEvent.ts │ │ ├── CookieStore.ts │ │ ├── getCookieString.ts │ │ ├── index.ts │ │ ├── parse.ts │ │ └── types.ts │ └── test │ │ └── getCookieString.spec.ts ├── disposablestack │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── AsyncDisposableStack.ts │ │ ├── DisposableStack.ts │ │ ├── SupressedError.ts │ │ ├── index.ts │ │ ├── symbols.ts │ │ └── utils.ts │ └── tests │ │ └── disposablestack.spec.ts ├── events │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tests │ │ └── events.spec.ts ├── fetch │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── dist │ │ ├── create-node-ponyfill.js │ │ ├── esm-ponyfill.js │ │ ├── global-ponyfill.js │ │ ├── index.d.ts │ │ ├── node-ponyfill.js │ │ └── shouldSkipPonyfill.js │ └── package.json ├── fetchache │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── node-fetch │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── AbortError.ts │ │ ├── Blob.ts │ │ ├── Body.ts │ │ ├── CompressionStream.ts │ │ ├── DecompressionStream.ts │ │ ├── File.ts │ │ ├── FormData.ts │ │ ├── Headers.ts │ │ ├── IteratorObject.ts │ │ ├── ReadableStream.ts │ │ ├── Request.ts │ │ ├── Response.ts │ │ ├── TextEncoderDecoder.ts │ │ ├── TextEncoderDecoderStream.ts │ │ ├── TransformStream.ts │ │ ├── URL.ts │ │ ├── URLSearchParams.ts │ │ ├── WritableStream.ts │ │ ├── declarations.d.ts │ │ ├── fetch.ts │ │ ├── fetchCurl.ts │ │ ├── fetchNodeHttp.ts │ │ ├── index.ts │ │ └── utils.ts │ └── tests │ │ ├── Blob.spec.ts │ │ ├── Body.spec.ts │ │ ├── FormData.spec.ts │ │ ├── Headers.spec.ts │ │ ├── ReadableStream.spec.ts │ │ ├── Request.spec.ts │ │ ├── TextEncoderDecoderStream.spec.ts │ │ ├── btoa.spec.ts │ │ ├── cleanup-resources.spec.ts │ │ ├── fetch.spec.ts │ │ ├── fixtures │ │ └── test.json │ │ ├── http2.spec.ts │ │ ├── non-http-fetch.spec.ts │ │ └── redirect.spec.ts ├── promise-helpers │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tests │ │ ├── fakePromise.spec.ts │ │ ├── handleMaybePromise.spec.ts │ │ └── mapAsyncIterator.spec.ts ├── server-plugin-cookies │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── useCookies.ts │ └── test │ │ └── useCookies.spec.ts └── server │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── createServerAdapter.ts │ ├── index.ts │ ├── plugins │ │ ├── types.ts │ │ ├── useContentEncoding.ts │ │ ├── useCors.ts │ │ └── useErrorHandling.ts │ ├── types.ts │ ├── utils.ts │ └── uwebsockets.ts │ └── test │ ├── CustomAbortControllerSignal.spec.ts │ ├── abort.spec.ts │ ├── adapter.fetch.spec.ts │ ├── compression.spec.ts │ ├── fetch-event-listener.spec.ts │ ├── formdata.spec.ts │ ├── http2.spec.ts │ ├── instrumentation.spec.ts │ ├── node.spec.ts │ ├── plugins.spec.ts │ ├── proxy.spec.ts │ ├── reproductions.spec.ts │ ├── request-container.spec.ts │ ├── request-listener.spec.ts │ ├── server-context.spec.ts │ ├── server.bench.ts │ ├── test-fetch.ts │ ├── test-server.ts │ ├── typings-test.ts │ ├── useCors.spec.ts │ ├── useErrorHandling.spec.ts │ └── utils.spec.ts ├── patches ├── @eslint+eslintrc+3.3.1.patch └── jest-leak-detector+29.7.0.patch ├── prettier.config.mjs ├── renovate.json ├── test.mjs ├── tsconfig.build.json ├── tsconfig.json ├── uwsUtils.d.ts ├── uwsUtils.js ├── vitest.config.ts ├── vitest.projects.ts └── yarn.lock /.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/b 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "ardatan/whatwg-node" }], 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [], 10 | "snapshot": { 11 | "useCalculatedVersion": true, 12 | "prereleaseTemplate": "{tag}-{datetime}-{commit}" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ardatan] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | 8 | 9 | 10 | **To Reproduce** Steps to reproduce the behavior: 11 | 12 | 13 | 14 | **Expected behavior** 15 | 16 | 17 | 18 | **Environment:** 19 | 20 | - OS: 21 | - `package-name...`: 22 | - NodeJS: 23 | 24 | **Additional context** 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Have a question? 4 | url: https://github.com/ardatan/whatwg-node/discussions/new 5 | about: 6 | Not sure about something? need help from the community? have a question to our team? please 7 | ask and answer questions here. 8 | - name: Any issue with `npm audit` 9 | url: https://overreacted.io/npm-audit-broken-by-design/ 10 | about: 11 | Please do not create issues about `npm audit` and you can contact with us directly for more 12 | questions. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for the core of this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | 8 | 9 | 10 | **Describe the solution you'd like** 11 | 12 | 13 | 14 | **Describe alternatives you've considered** 15 | 16 | 17 | 18 | **Additional context** 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | 🚨 **IMPORTANT: Please do not create a Pull Request without creating an issue first.** 9 | 10 | _Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of 11 | the pull request._ 12 | 13 | ## Description 14 | 15 | Please include a summary of the change and which issue is fixed. Please also include relevant 16 | motivation and context. List any dependencies that are required for this change. 17 | 18 | Related # (issue) 19 | 20 | 23 | 24 | ## Type of change 25 | 26 | Please delete options that are not relevant. 27 | 28 | - [ ] Bug fix (non-breaking change which fixes an issue) 29 | - [ ] New feature (non-breaking change which adds functionality) 30 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as 31 | expected) 32 | - [ ] This change requires a documentation update 33 | 34 | ## Screenshots/Sandbox (if appropriate/relevant): 35 | 36 | Adding links to sandbox or providing screenshots can help us understand more about this PR and take 37 | action on it as appropriate 38 | 39 | ## How Has This Been Tested? 40 | 41 | Please describe the tests that you ran to verify your changes. Provide instructions so we can 42 | reproduce. Please also list any relevant details for your test configuration 43 | 44 | - [ ] Test A 45 | - [ ] Test B 46 | 47 | **Test Environment**: 48 | 49 | - OS: 50 | - `package-name`: 51 | - NodeJS: 52 | 53 | ## Checklist: 54 | 55 | - [ ] I have followed the 56 | [CONTRIBUTING](https://github.com/the-guild-org/Stack/blob/master/CONTRIBUTING.md) doc and the 57 | style guidelines of this project 58 | - [ ] I have performed a self-review of my own code 59 | - [ ] I have commented my code, particularly in hard-to-understand areas 60 | - [ ] I have made corresponding changes to the documentation 61 | - [ ] My changes generate no new warnings 62 | - [ ] I have added tests that prove my fix is effective or that my feature works 63 | - [ ] New and existing unit tests and linter rules pass locally with my changes 64 | - [ ] Any dependent changes have been merged and published in downstream modules 65 | 66 | ## Further comments 67 | 68 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose 69 | the solution you did and what alternatives you considered, etc... 70 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'docker' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'daily' 12 | groups: 13 | actions-deps: 14 | patterns: 15 | - '*' 16 | - package-ecosystem: 'github-actions' # See documentation for possible values 17 | directory: '/' # Location of package manifests 18 | schedule: 19 | interval: 'daily' 20 | groups: 21 | actions-deps: 22 | patterns: 23 | - '*' 24 | - package-ecosystem: 'npm' # See documentation for possible values 25 | directory: '/' # Location of package manifests 26 | schedule: 27 | interval: 'daily' 28 | groups: 29 | actions-deps: 30 | patterns: 31 | - '*' 32 | exclude-patterns: 33 | - '@changesets/*' 34 | - 'typescript' 35 | - '^@theguild/' 36 | - 'next' 37 | - 'tailwindcss' 38 | - 'husky' 39 | - '@pulumi/*' 40 | update-types: 41 | - 'minor' 42 | - 'patch' 43 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: benchmarks 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | paths-ignore: 9 | - 'website/**' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | server: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | scenario: 22 | - native 23 | - ponyfill 24 | - undici 25 | steps: 26 | - name: Checkout Repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 28 | - name: Setup env 29 | uses: the-guild-org/shared-config/setup@main 30 | with: 31 | nodeVersion: 24 32 | packageManager: yarn 33 | - name: Build Packages 34 | run: yarn build 35 | - name: Setup K6 36 | run: | 37 | wget https://github.com/grafana/k6/releases/download/v0.37.0/k6-v0.37.0-linux-amd64.deb 38 | sudo apt-get update 39 | sudo apt-get install ./k6-v0.37.0-linux-amd64.deb 40 | - name: Start Benchmark 41 | working-directory: ./benchmarks/server 42 | run: | 43 | yarn test 44 | env: 45 | SCENARIO: ${{ matrix.scenario }} 46 | NODE_NO_WARNINGS: true 47 | NODE_ENV: production 48 | GITHUB_PR: ${{ github.event.number }} 49 | GITHUB_SHA: ${{ github.sha }} 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | node-fetch: 53 | runs-on: ubuntu-latest 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | scenario: 58 | - noConsumeBody 59 | - consumeBody 60 | services: 61 | httpbin: 62 | image: mccutchen/go-httpbin 63 | env: 64 | PORT: 50000 65 | ports: 66 | - 50000:50000 67 | steps: 68 | - name: Checkout Repository 69 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 70 | - name: Setup env 71 | uses: the-guild-org/shared-config/setup@main 72 | with: 73 | nodeVersion: 24 74 | packageManager: yarn 75 | - name: Setup K6 76 | run: | 77 | wget https://github.com/grafana/k6/releases/download/v0.37.0/k6-v0.37.0-linux-amd64.deb 78 | sudo apt-get update 79 | sudo apt-get install ./k6-v0.37.0-linux-amd64.deb 80 | - name: Start server 81 | run: yarn workspace @benchmarks/node-fetch run start:server & 82 | - name: Wait for server 83 | run: curl --retry 5 --retry-delay 1 --retry-connrefused http://localhost:50001 84 | - name: Benchmark 85 | env: 86 | SCENARIO: ${{ matrix.scenario }} 87 | GITHUB_PR: ${{ github.event.number }} 88 | GITHUB_SHA: ${{ github.sha }} 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | run: k6 run ./benchmarks/node-fetch/k6.js 91 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | paths-ignore: 9 | - 'website/**' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | dependencies: 17 | uses: the-guild-org/shared-config/.github/workflows/changesets-dependencies.yaml@main 18 | if: ${{ github.event.pull_request.title != 'Upcoming Release Changes' }} 19 | secrets: 20 | githubToken: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | alpha: 23 | permissions: 24 | contents: read 25 | id-token: write 26 | pull-requests: write 27 | if: 28 | ${{ github.event.pull_request.head.repo.fork != true && github.event.pull_request.title != 29 | 'Upcoming Release Changes' }} 30 | uses: the-guild-org/shared-config/.github/workflows/release-snapshot.yml@main 31 | with: 32 | npmTag: alpha 33 | buildScript: build 34 | nodeVersion: 24 35 | secrets: 36 | githubToken: ${{ secrets.GITHUB_TOKEN }} 37 | npmToken: ${{ secrets.NODE_AUTH_TOKEN }} 38 | 39 | release-candidate: 40 | permissions: 41 | contents: read 42 | id-token: write 43 | pull-requests: write 44 | if: 45 | ${{ github.event.pull_request.head.repo.full_name == github.repository && 46 | github.event.pull_request.title == 'Upcoming Release Changes' }} 47 | uses: the-guild-org/shared-config/.github/workflows/release-snapshot.yml@main 48 | with: 49 | npmTag: rc 50 | buildScript: build 51 | nodeVersion: 24 52 | restoreDeletedChangesets: true 53 | secrets: 54 | githubToken: ${{ secrets.GITHUB_TOKEN }} 55 | npmToken: ${{ secrets.NODE_AUTH_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | stable: 14 | permissions: 15 | contents: write 16 | id-token: write 17 | pull-requests: write 18 | uses: the-guild-org/shared-config/.github/workflows/release-stable.yml@main 19 | with: 20 | releaseScript: release 21 | nodeVersion: 24 22 | secrets: 23 | githubToken: ${{ secrets.GITHUB_TOKEN }} 24 | npmToken: ${{ secrets.NODE_AUTH_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist 64 | build 65 | temp 66 | .idea 67 | .bob 68 | .cache 69 | .DS_Store 70 | 71 | test-results/ 72 | junit.xml 73 | 74 | *.tgz 75 | 76 | package-lock.json 77 | 78 | eslint_report.json 79 | 80 | deno.lock 81 | .helix/config.toml 82 | .helix/languages.toml 83 | .mise.toml 84 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v24 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | tests 4 | !dist 5 | .circleci 6 | .prettierrc 7 | bump.js 8 | jest.config.js 9 | tsconfig.json 10 | yarn.lock 11 | yarn-error.log 12 | bundle-test 13 | *.tgz 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | provenance=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | CHANGELOG.md 3 | .next 4 | [...slug].js 5 | .changeset 6 | .husky 7 | .bob 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-engines true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Arda TANRIKULU 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 | # WhatWG Node 2 | 3 | This repository contains a set of environment(Browser, Node.js, Deno, Cloudflare Workers, Bun, etc.) 4 | agnostic packages for the [WhatWG](https://whatwg.org) standards. 5 | 6 | Node.js currently is the only exception that doesn't fully support those standards so `whatwg-node` 7 | packages ponyfill the missing parts without modifying or monkey-patching the native global APIs like 8 | polyfills. 9 | 10 | > Polyfill patches the native parts of an environment while ponyfill just exports the “patched” 11 | > stuff without touching the environment’s internals. We prefer pony filling because it prevents us 12 | > from breaking other libraries and environmental functionalities. In case a ponyfill is imported in 13 | > an environment that already has that API built in like newer Node.js, Cloudflare Workers, Bun, 14 | > Deno or Browser, no ponyfills are added to your built application bundle. So you have a generic 15 | > package that works in all environments. 16 | 17 | ## Packages 18 | 19 | ### [@whatwg-node/fetch](./packages/fetch) 20 | 21 | A ponyfill package for the [Fetch Standard](https://fetch.spec.whatwg.org/). 22 | 23 | ### [@whatwg-node/events](./packages/events) 24 | 25 | A ponyfill package for the [DOM Events Standard](https://dom.spec.whatwg.org/#events). 26 | 27 | ### [@whatwg-node/server](./packages/server) 28 | 29 | A platform-independent JavaScript HTTP server adapter implementation that uses the 30 | [Fetch Standard](https://fetch.spec.whatwg.org/) to handle requests. The HTTP server implemented 31 | with this library can be used in any JS environment like Node.js, Deno, Cloudflare Workers, Bun, 32 | etc. For Node.js, it transpiles Node.js specific APIs to the standard ones, and for other 33 | environments, it uses the standard APIs directly. Even if your environment doesn't use Fetch API for 34 | the server implementation, you can still use `fetch` method to handle requests. 35 | 36 | ### [fetchache](./packages/fetchache) 37 | 38 | A fetch wrapper that allows you to respect HTTP caching strategies on non-browser environments with 39 | a key-value cache implementation. It follows the [HTTP Caching](https://tools.ietf.org/html/rfc7234) 40 | and [Conditional Requests](https://tools.ietf.org/html/rfc7232) standards. 41 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | '@babel/plugin-proposal-class-properties', 8 | '@babel/plugin-proposal-explicit-resource-management', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /benchmarks/node-fetch/k6.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // @ts-expect-error - TS doesn't know this import 4 | import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js'; 5 | // @ts-expect-error - TS doesn't know this import 6 | import { githubComment } from 'https://raw.githubusercontent.com/dotansimha/k6-github-pr-comment/master/lib.js'; 7 | import http from 'k6/http'; 8 | import { Trend } from 'k6/metrics'; 9 | 10 | const scenario = __ENV.SCENARIO; 11 | if (!scenario) { 12 | throw new Error('SCENARIO env var not defined, see scenarios.ts for available scenarios'); 13 | } 14 | 15 | /** @type{import('k6/options').Options} */ 16 | export const options = { 17 | thresholds: { 18 | active_handles: ['max<250'], // active handles must be below 250 19 | }, 20 | scenarios: { 21 | [scenario]: { 22 | executor: 'constant-vus', 23 | vus: 100, 24 | duration: '30s', 25 | gracefulStop: '0s', 26 | }, 27 | }, 28 | }; 29 | 30 | const activeHandles = new Trend('active_handles'); 31 | 32 | export default function () { 33 | http.get(`http://localhost:50001/scenarios/${scenario}`); 34 | 35 | const res = http.get('http://localhost:50001/activeHandles'); 36 | activeHandles.add(parseInt(String(res.body))); 37 | } 38 | 39 | export function handleSummary(data) { 40 | if (__ENV.GITHUB_TOKEN) { 41 | githubComment(data, { 42 | token: __ENV.GITHUB_TOKEN, 43 | commit: __ENV.GITHUB_SHA, 44 | pr: __ENV.GITHUB_PR, 45 | org: 'ardatan', 46 | repo: 'whatwg-node', 47 | commentKey: `@benchmarks/node-fetch+${scenario}`, 48 | renderTitle({ passes }) { 49 | return passes 50 | ? `✅ \`@benchmarks/node-fetch\` results (${scenario})` 51 | : `❌ \`@benchmarks/node-fetch\` failed (${scenario})`; 52 | }, 53 | renderMessage({ passes, checks, thresholds }) { 54 | const result = []; 55 | 56 | if (thresholds.failures) { 57 | result.push(`**Performance regression detected**`); 58 | } 59 | 60 | if (checks.failures) { 61 | result.push('**Failed assertions detected**'); 62 | } 63 | 64 | if (!passes) { 65 | result.push( 66 | `> If the performance regression is expected, please increase the failing threshold.`, 67 | ); 68 | } 69 | 70 | return result.join('\n'); 71 | }, 72 | }); 73 | } 74 | return { 75 | stdout: textSummary(data, { indent: ' ', enableColors: true }), 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /benchmarks/node-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmarks/node-fetch", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "benchmark": "k6 run k6.js", 7 | "start:httpbin": "docker run --rm -p 50000:50000 -e PORT=50000 mccutchen/go-httpbin", 8 | "start:server": "tsx server.ts" 9 | }, 10 | "devDependencies": { 11 | "@types/k6": "^1.0.0", 12 | "tsx": "^4.7.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /benchmarks/node-fetch/scenarios.ts: -------------------------------------------------------------------------------- 1 | import { fetchPonyfill as fetch } from '../../packages/node-fetch/src/fetch'; 2 | 3 | export const scenarios = { 4 | async noConsumeBody(url: string) { 5 | await fetch(url, { 6 | method: 'POST', 7 | body: '{ "hello": "world" }', 8 | }); 9 | }, 10 | async consumeBody(url: string) { 11 | const res = await fetch(url, { 12 | method: 'POST', 13 | body: '{ "hello": "world" }', 14 | }); 15 | await res.json(); 16 | }, 17 | } as const; 18 | 19 | export function isScenario(str: unknown): str is keyof typeof scenarios { 20 | return typeof str === 'string' && str in scenarios; 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/node-fetch/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { isScenario, scenarios } from './scenarios'; 3 | 4 | const port = 50001; 5 | const httpbinUrl = 'http://localhost:50000/anything'; 6 | 7 | const server = createServer(async (req, res) => { 8 | try { 9 | if (req.url?.endsWith('activeHandles')) { 10 | return res.writeHead(200, { 'content-type': 'text/plain' }).end( 11 | String( 12 | process 13 | // @ts-expect-error _getActiveHandles does exist 14 | ._getActiveHandles().length, 15 | ), 16 | ); 17 | } 18 | 19 | if (req.url?.includes('/scenarios/')) { 20 | const scenario = req.url.split('/').pop(); 21 | if (isScenario(scenario)) { 22 | await scenarios[scenario](httpbinUrl); 23 | return res.writeHead(200).end(); 24 | } 25 | } 26 | 27 | return res.writeHead(404).end(); 28 | } catch (err) { 29 | console.error(err); 30 | return res.writeHead(500).end(); 31 | } 32 | }); 33 | 34 | server.listen(port); 35 | 36 | console.log(`Server listening at http://localhost:${port}`); 37 | console.debug(`Available scenarios: ${Object.keys(scenarios).join(', ')}`); 38 | -------------------------------------------------------------------------------- /benchmarks/server/k6.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-check 3 | // @ts-expect-error - TS doesn't know this import 4 | import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js'; 5 | // @ts-expect-error - TS doesn't know this import 6 | import { githubComment } from 'https://raw.githubusercontent.com/dotansimha/k6-github-pr-comment/master/lib.js'; 7 | import { check } from 'k6'; 8 | import http from 'k6/http'; 9 | 10 | export const options = { 11 | vus: 1, 12 | duration: '30s', 13 | thresholds: { 14 | checks: ['rate>0.98'], 15 | }, 16 | }; 17 | 18 | export function handleSummary(data) { 19 | if (__ENV.GITHUB_TOKEN) { 20 | githubComment(data, { 21 | token: __ENV.GITHUB_TOKEN, 22 | commit: __ENV.GITHUB_SHA, 23 | pr: __ENV.GITHUB_PR, 24 | org: 'ardatan', 25 | repo: 'whatwg-node', 26 | commentKey: `@benchmarks/server+${__ENV.SCENARIO}`, 27 | renderTitle({ passes }) { 28 | return passes 29 | ? `✅ \`@benchmarks/server\` results (${__ENV.SCENARIO})` 30 | : `❌ \`@benchmarks/server\` failed (${__ENV.SCENARIO})`; 31 | }, 32 | renderMessage({ passes, checks, thresholds }) { 33 | const result = []; 34 | 35 | if (thresholds.failures) { 36 | result.push(`**Performance regression detected**`); 37 | } 38 | 39 | if (checks.failures) { 40 | result.push('**Failed assertions detected**'); 41 | } 42 | 43 | if (!passes) { 44 | result.push( 45 | `> If the performance regression is expected, please increase the failing threshold.`, 46 | ); 47 | } 48 | 49 | return result.join('\n'); 50 | }, 51 | }); 52 | } 53 | return { 54 | stdout: textSummary(data, { indent: ' ', enableColors: true }), 55 | }; 56 | } 57 | 58 | export default function run() { 59 | const res = http.get(`http://127.0.0.1:4000`); 60 | 61 | check(res, { 62 | 'no-errors': resp => resp.status === 200, 63 | 'expected-result': resp => { 64 | const json = resp.json(); 65 | return ( 66 | !!json && 67 | typeof json === 'object' && 68 | 'message' in json && 69 | typeof json.message === 'string' && 70 | json.message === 'Hello, World!' 71 | ); 72 | }, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /benchmarks/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benchmarks/server", 3 | "version": "0.0.57", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "build": "tsc", 8 | "check": "exit 0", 9 | "debug": "node --inspect-brk dist/server.js", 10 | "loadtest": "k6 -e GITHUB_PR=$GITHUB_PR -e GITHUB_SHA=$GITHUB_SHA -e GITHUB_TOKEN=$GITHUB_TOKEN run k6.js", 11 | "pretest": "npm run build", 12 | "start": "node dist/server.js", 13 | "test": "start-server-and-test start http://127.0.0.1:4000/ping loadtest" 14 | }, 15 | "dependencies": { 16 | "@whatwg-node/server": "0.10.10" 17 | }, 18 | "devDependencies": { 19 | "start-server-and-test": "2.0.12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/server/server.ts: -------------------------------------------------------------------------------- 1 | import { Blob as BufferBlob } from 'node:buffer'; 2 | import { createServer, type RequestListener } from 'node:http'; 3 | import * as undici from 'undici'; 4 | import { createServerAdapter, Response } from '@whatwg-node/server'; 5 | 6 | let serverAdapter: RequestListener; 7 | if (process.env.SCENARIO === 'native') { 8 | serverAdapter = createServerAdapter( 9 | () => globalThis.Response.json({ message: `Hello, World!` }), 10 | { 11 | fetchAPI: { 12 | fetch: globalThis.fetch, 13 | Request: globalThis.Request, 14 | Response: globalThis.Response, 15 | Headers: globalThis.Headers, 16 | FormData: globalThis.FormData, 17 | ReadableStream: globalThis.ReadableStream, 18 | WritableStream: globalThis.WritableStream, 19 | CompressionStream: globalThis.CompressionStream, 20 | TransformStream: globalThis.TransformStream, 21 | Blob: globalThis.Blob, 22 | File: globalThis.File, 23 | crypto: globalThis.crypto, 24 | btoa: globalThis.btoa, 25 | TextDecoder: globalThis.TextDecoder, 26 | TextEncoder: globalThis.TextEncoder, 27 | URL: globalThis.URL, 28 | URLSearchParams: globalThis.URLSearchParams, 29 | }, 30 | }, 31 | ); 32 | } else if (process.env.SCENARIO === 'undici') { 33 | serverAdapter = (createServerAdapter as any)( 34 | () => undici.Response.json({ message: `Hello, World!` }), 35 | { 36 | fetchAPI: { 37 | fetch: undici.fetch, 38 | Request: undici.Request, 39 | Response: undici.Response, 40 | Headers: undici.Headers, 41 | FormData: undici.FormData, 42 | ReadableStream: globalThis.ReadableStream, 43 | WritableStream: globalThis.WritableStream, 44 | CompressionStream: globalThis.CompressionStream, 45 | TransformStream: globalThis.TransformStream, 46 | Blob: BufferBlob, 47 | File: undici.File, 48 | crypto: globalThis.crypto, 49 | btoa: globalThis.btoa, 50 | TextDecoder: globalThis.TextDecoder, 51 | TextEncoder: globalThis.TextEncoder, 52 | URL: globalThis.URL, 53 | URLSearchParams: globalThis.URLSearchParams, 54 | }, 55 | }, 56 | ); 57 | } else { 58 | serverAdapter = createServerAdapter(() => Response.json({ message: `Hello, World!` })); 59 | } 60 | 61 | createServer(serverAdapter).listen(4000, () => { 62 | console.log('listening on 0.0.0.0:4000'); 63 | }); 64 | -------------------------------------------------------------------------------- /benchmarks/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "dist", 6 | "module": "Node16", 7 | "moduleResolution": "node16", 8 | "skipLibCheck": true 9 | }, 10 | "include": ["server.ts"], 11 | "exclude": [] 12 | } 13 | -------------------------------------------------------------------------------- /deno-jest.ts: -------------------------------------------------------------------------------- 1 | import { fn } from 'jsr:@std/expect'; 2 | import { it } from 'jsr:@std/testing/bdd'; 3 | 4 | it.each = 5 | (cases: object[]): typeof it => 6 | (name, runner) => { 7 | for (const c of cases) { 8 | let testName = name; 9 | Object.entries(c).forEach(([k, v]) => { 10 | testName = testName.replaceAll(k, v); 11 | }); 12 | it(testName, () => runner(c)); 13 | } 14 | }; 15 | export { it }; 16 | 17 | export { describe, test, beforeEach, afterEach, beforeAll, afterAll } from 'jsr:@std/testing/bdd'; 18 | export { expect } from 'jsr:@std/expect'; 19 | 20 | export const jest = { 21 | fn, 22 | spyOn(target: any, method: string) { 23 | Object.defineProperty(target, method, { 24 | value: fn(target[method]), 25 | writable: true, 26 | }); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@jest/globals": "./deno-jest.ts", 4 | "@whatwg-node/fetch": "./packages/fetch/dist/esm-ponyfill.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /e2e/aws-lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/aws-lambda", 3 | "version": "0.0.42", 4 | "private": true, 5 | "scripts": { 6 | "build": "node scripts/bundle.js", 7 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts" 8 | }, 9 | "dependencies": { 10 | "@e2e/shared-scripts": "0.0.0", 11 | "@whatwg-node/fetch": "0.10.8", 12 | "aws-lambda": "1.0.7" 13 | }, 14 | "devDependencies": { 15 | "@pulumi/aws": "6.81.0", 16 | "@pulumi/aws-native": "1.28.0", 17 | "@pulumi/pulumi": "3.173.0", 18 | "@types/aws-lambda": "8.10.149", 19 | "@types/node": "22.15.27", 20 | "esbuild": "0.25.5", 21 | "ts-node": "10.9.2", 22 | "tsconfig-paths": "4.2.0", 23 | "typescript": "5.8.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /e2e/aws-lambda/scripts/bundle.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | 3 | async function main() { 4 | await build({ 5 | entryPoints: ['./src/index.ts'], 6 | outfile: 'dist/index.js', 7 | format: 'cjs', 8 | minify: false, 9 | bundle: true, 10 | platform: 'node', 11 | target: 'es2020', 12 | }); 13 | 14 | console.info(`AWS Lambda build done!`); 15 | } 16 | 17 | main().catch(e => { 18 | console.error(e); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /e2e/aws-lambda/scripts/createAwsLambdaDeployment.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { 3 | assertDeployedEndpoint, 4 | DeploymentConfiguration, 5 | env, 6 | execPromise, 7 | } from '@e2e/shared-scripts'; 8 | import * as aws from '@pulumi/aws'; 9 | import * as awsNative from '@pulumi/aws-native'; 10 | import { version } from '@pulumi/aws/package.json'; 11 | import * as pulumi from '@pulumi/pulumi'; 12 | import { Stack } from '@pulumi/pulumi/automation'; 13 | 14 | export function createAwsLambdaDeployment(): DeploymentConfiguration<{ 15 | functionUrl: string; 16 | }> { 17 | return { 18 | name: 'aws-lambda', 19 | prerequisites: async (stack: Stack) => { 20 | console.info('\t\tℹ️ Installing AWS plugin...'); 21 | // Intall Pulumi AWS Plugin 22 | await stack.workspace.installPlugin('aws', version, 'resource'); 23 | 24 | // Build and bundle the worker 25 | console.info('\t\tℹ️ Build the AWS Lambda Function....'); 26 | await execPromise('yarn build', { 27 | cwd: join(__dirname, '..'), 28 | }); 29 | }, 30 | config: async (stack: Stack) => { 31 | // Configure the Pulumi environment with the AWS credentials 32 | // This will allow Pulummi program to just run without caring about secrets/configs. 33 | // See: https://www.pulumi.com/registry/packages/aws/installation-configuration/ 34 | await stack.setConfig('aws:accessKey', { 35 | value: env('AWS_ACCESS_KEY'), 36 | }); 37 | await stack.setConfig('aws:secretKey', { 38 | value: env('AWS_SECRET_KEY'), 39 | }); 40 | await stack.setConfig('aws:region', { 41 | value: env('AWS_REGION'), 42 | }); 43 | await stack.setConfig('aws:allowedAccountIds', { 44 | value: `[${env('AWS_ACCOUNT_ID')}]`, 45 | }); 46 | }, 47 | program: async () => { 48 | const lambdaRole = new aws.iam.Role('lambda-role', { 49 | assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ 50 | Service: 'lambda.amazonaws.com', 51 | }), 52 | }); 53 | 54 | const lambdaRolePolicy = new aws.iam.RolePolicy('role-policy', { 55 | role: lambdaRole.id, 56 | policy: { 57 | Version: '2012-10-17', 58 | Statement: [ 59 | { 60 | Effect: 'Allow', 61 | Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], 62 | Resource: 'arn:aws:logs:*:*:*', 63 | }, 64 | ], 65 | }, 66 | }); 67 | 68 | const func = new aws.lambda.Function( 69 | 'func', 70 | { 71 | role: lambdaRole.arn, 72 | runtime: 'nodejs20.x', 73 | handler: 'index.handler', 74 | code: new pulumi.asset.AssetArchive({ 75 | 'index.js': new pulumi.asset.FileAsset(join(__dirname, '../dist/index.js')), 76 | }), 77 | }, 78 | { dependsOn: lambdaRolePolicy }, 79 | ); 80 | 81 | new aws.lambda.Permission('streaming-permission', { 82 | action: 'lambda:InvokeFunctionUrl', 83 | function: func.arn, 84 | principal: '*', 85 | functionUrlAuthType: 'NONE', 86 | }); 87 | 88 | const lambdaGw = new awsNative.lambda.Url('streaming-url', { 89 | authType: 'NONE', 90 | targetFunctionArn: func.arn, 91 | invokeMode: 'RESPONSE_STREAM', 92 | }); 93 | 94 | return { 95 | functionUrl: lambdaGw.functionUrl, 96 | }; 97 | }, 98 | test: async ({ functionUrl }) => { 99 | console.log(`ℹ️ AWS Lambda Function deployed to URL: ${functionUrl.value}`); 100 | await assertDeployedEndpoint(functionUrl.value); 101 | }, 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /e2e/aws-lambda/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createAwsLambdaDeployment } from './createAwsLambdaDeployment'; 3 | 4 | runTests(createAwsLambdaDeployment()) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/aws-lambda/src/awslambda.d.ts: -------------------------------------------------------------------------------- 1 | import type { Writable } from 'node:stream'; 2 | import type { Context, Handler } from 'aws-lambda'; 3 | 4 | declare global { 5 | namespace awslambda { 6 | export namespace HttpResponseStream { 7 | function from( 8 | responseStream: ResponseStream, 9 | metadata: { 10 | statusCode?: number; 11 | headers?: Record; 12 | }, 13 | ): ResponseStream; 14 | } 15 | 16 | export type ResponseStream = Writable & { 17 | setContentType(type: string): void; 18 | }; 19 | 20 | export type StreamifyHandler = ( 21 | event: Event, 22 | responseStream: ResponseStream, 23 | context: Context, 24 | ) => Promise; 25 | 26 | export function streamifyResponse(handler: StreamifyHandler): Handler; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /e2e/aws-lambda/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { pipeline } from 'node:stream/promises'; 3 | import type { Context, LambdaFunctionURLEvent } from 'aws-lambda'; 4 | import { createTestServerAdapter } from '@e2e/shared-server'; 5 | 6 | const app = createTestServerAdapter(); 7 | 8 | interface ServerContext { 9 | event: LambdaFunctionURLEvent; 10 | lambdaContext: Context; 11 | res: awslambda.ResponseStream; 12 | } 13 | 14 | export const handler = awslambda.streamifyResponse(async function handler( 15 | event: LambdaFunctionURLEvent, 16 | res, 17 | lambdaContext, 18 | ) { 19 | const response = await app.fetch( 20 | // Construct the URL 21 | `https://${event.requestContext.domainName}${event.requestContext.http.path}?${event.rawQueryString}`, 22 | { 23 | method: event.requestContext.http.method, 24 | headers: event.headers as HeadersInit, 25 | // Parse the body if needed 26 | body: 27 | event.body && event.isBase64Encoded 28 | ? Buffer.from(event.body, 'base64') 29 | : event.body || null, 30 | }, 31 | { 32 | event, 33 | res, 34 | lambdaContext, 35 | }, 36 | ); 37 | 38 | // Attach the metadata to the response stream 39 | res = awslambda.HttpResponseStream.from(res, { 40 | statusCode: response.status, 41 | headers: Object.fromEntries(response.headers.entries()), 42 | }); 43 | 44 | if (response.body) { 45 | // @ts-expect-error - Pipe the response body to the response stream 46 | await pipeline(response.body, res); 47 | } 48 | 49 | // End the response stream 50 | res.end(); 51 | }); 52 | -------------------------------------------------------------------------------- /e2e/aws-lambda/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "files": ["src/index.ts", "src/awslambda.d.ts", "scripts/e2e.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/azure-function/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | getting_started.md 8 | node_modules/@types/ 9 | node_modules/azure-functions-core-tools/ 10 | node_modules/typescript/ -------------------------------------------------------------------------------- /e2e/azure-function/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["ms-azuretools.vscode-azurefunctions"] 3 | } 4 | -------------------------------------------------------------------------------- /e2e/azure-function/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /e2e/azure-function/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/azure-function", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "dist/functions/index.js", 6 | "scripts": { 7 | "build": "rm -rf dist/ && node scripts/bundle.js", 8 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts", 9 | "prestart": "npm run build", 10 | "start": "func start" 11 | }, 12 | "dependencies": { 13 | "@azure/functions": "^4.6.1", 14 | "@e2e/shared-scripts": "0.0.0", 15 | "tslib": "^2.6.3" 16 | }, 17 | "devDependencies": { 18 | "@pulumi/azure-native": "3.5.0", 19 | "@pulumi/pulumi": "3.173.0", 20 | "esbuild": "0.25.5", 21 | "ts-node": "10.9.2", 22 | "tsconfig-paths": "4.2.0", 23 | "typescript": "5.8.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /e2e/azure-function/scripts/bundle.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | const { writeFileSync } = require('fs'); 3 | const { join } = require('path'); 4 | const packageJson = require('../package.json'); 5 | 6 | const projectRoot = join(__dirname, '..'); 7 | 8 | async function main() { 9 | await build({ 10 | entryPoints: [join(projectRoot, './src/functions/index.ts')], 11 | outfile: join(projectRoot, 'dist/functions/index.js'), 12 | format: 'cjs', 13 | minify: false, 14 | bundle: true, 15 | platform: 'node', 16 | target: 'node20', 17 | external: ['@azure/functions-core'], 18 | }); 19 | 20 | console.info(`Azure Function build done!`); 21 | } 22 | 23 | main().catch(e => { 24 | console.error(e); 25 | process.exit(1); 26 | }); 27 | -------------------------------------------------------------------------------- /e2e/azure-function/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createAzureFunctionDeployment } from './createAzureFunctionDeployment'; 3 | 4 | runTests(createAzureFunctionDeployment()) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/azure-function/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | import { app, InvocationContext } from '@azure/functions'; 2 | import { createTestServerAdapter } from '@e2e/shared-server'; 3 | 4 | const handler = createTestServerAdapter(); 5 | 6 | declare global { 7 | interface ReadableStream { 8 | [Symbol.asyncIterator](): AsyncIterableIterator; 9 | } 10 | } 11 | 12 | app.http('whatwgnode', { 13 | methods: ['GET', 'POST'], 14 | authLevel: 'anonymous', 15 | handler, 16 | }); 17 | -------------------------------------------------------------------------------- /e2e/azure-function/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom", "esnext"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "files": ["src/functions/index.ts", "scripts/e2e.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/cloudflare-modules", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "wrangler deploy --dry-run --outdir=dist", 7 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts", 8 | "start": "wrangler dev" 9 | }, 10 | "dependencies": { 11 | "@e2e/shared-scripts": "0.0.0" 12 | }, 13 | "devDependencies": { 14 | "@pulumi/cloudflare": "4.16.0", 15 | "@pulumi/pulumi": "3.173.0", 16 | "ts-node": "10.9.2", 17 | "tsconfig-paths": "4.2.0", 18 | "typescript": "5.8.3", 19 | "wrangler": "4.18.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createCfDeployment } from '../../cloudflare-workers/scripts/createCfDeployment'; 3 | 4 | runTests(createCfDeployment('cloudflare-modules', true)) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createTestServerAdapter } from '@e2e/shared-server'; 2 | 3 | const app = createTestServerAdapter(); 4 | 5 | export default { 6 | fetch: app, 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "files": ["src/index.ts", "scripts/e2e.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/wrangler.toml: -------------------------------------------------------------------------------- 1 | compatibility_date = "2022-02-21" 2 | name = "whatwg-node" 3 | account_id = "" 4 | workers_dev = true 5 | main = "src/index.ts" 6 | compatibility_flags = ["streams_enable_constructors"] 7 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/cloudflare-workers", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "wrangler deploy --dry-run --outdir=dist", 7 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts", 8 | "start": "wrangler dev" 9 | }, 10 | "dependencies": { 11 | "@e2e/shared-scripts": "0.0.0" 12 | }, 13 | "devDependencies": { 14 | "@pulumi/cloudflare": "4.16.0", 15 | "@pulumi/pulumi": "3.173.0", 16 | "ts-node": "10.9.2", 17 | "tsconfig-paths": "4.2.0", 18 | "typescript": "5.8.3", 19 | "wrangler": "4.18.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/scripts/createCfDeployment.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { 3 | assertDeployedEndpoint, 4 | DeploymentConfiguration, 5 | env, 6 | execPromise, 7 | fsPromises, 8 | } from '@e2e/shared-scripts'; 9 | import * as cf from '@pulumi/cloudflare'; 10 | import { version } from '@pulumi/cloudflare/package.json'; 11 | import * as pulumi from '@pulumi/pulumi'; 12 | import { Stack } from '@pulumi/pulumi/automation'; 13 | 14 | export function createCfDeployment( 15 | projectName: string, 16 | isModule = false, 17 | ): DeploymentConfiguration<{ 18 | workerUrl: string; 19 | }> { 20 | return { 21 | name: projectName, 22 | prerequisites: async (stack: Stack) => { 23 | console.info('\t\tℹ️ Installing Pulumi CF plugin...'); 24 | // Intall Pulumi CF Plugin 25 | await stack.workspace.installPlugin('cloudflare', version, 'resource'); 26 | 27 | // Build and bundle the worker 28 | console.info('\t\tℹ️ Bundling the CF Worker....'); 29 | await execPromise('yarn build', { 30 | cwd: join(__dirname, '..', '..', projectName), 31 | }); 32 | }, 33 | config: async (stack: Stack) => { 34 | // Configure the Pulumi environment with the CloudFlare credentials 35 | // This will allow Pulummi program to just run without caring about secrets/configs. 36 | // See: https://www.pulumi.com/registry/packages/cloudflare/installation-configuration/ 37 | await stack.setConfig('cloudflare:apiToken', { 38 | value: env('CLOUDFLARE_API_TOKEN'), 39 | }); 40 | await stack.setConfig('cloudflare:accountId', { 41 | value: env('CLOUDFLARE_ACCOUNT_ID'), 42 | }); 43 | }, 44 | program: async () => { 45 | const stackName = pulumi.getStack(); 46 | const workerUrl = `e2e.graphql-yoga.com/${stackName}`; 47 | 48 | // Deploy CF script as Worker 49 | const workerScript = new cf.WorkerScript('worker', { 50 | content: await fsPromises.readFile( 51 | join(__dirname, '..', '..', projectName, 'dist', 'index.js'), 52 | 'utf-8', 53 | ), 54 | module: isModule, 55 | name: stackName, 56 | }); 57 | 58 | // Create a nice route for easy testing 59 | new cf.WorkerRoute('worker-route', { 60 | scriptName: workerScript.name, 61 | pattern: workerUrl + '*', 62 | zoneId: env('CLOUDFLARE_ZONE_ID'), 63 | }); 64 | 65 | return { 66 | workerUrl: `https://${workerUrl}`, 67 | }; 68 | }, 69 | test: async ({ workerUrl }) => { 70 | console.log(`ℹ️ CloudFlare Worker deployed to URL: ${workerUrl.value}`); 71 | await assertDeployedEndpoint(workerUrl.value); 72 | }, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createCfDeployment } from './createCfDeployment'; 3 | 4 | runTests(createCfDeployment('cloudflare-workers')) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServerAdapter, type FetchEvent } from '@whatwg-node/server'; 2 | 3 | const app = createServerAdapter(async (request, ctx) => 4 | Response.json({ 5 | url: request.url, 6 | method: request.method, 7 | headers: Object.fromEntries(request.headers.entries()), 8 | reqText: request.method === 'POST' ? await request.text() : '', 9 | reqExistsInCtx: ctx.request === request, 10 | }), 11 | ); 12 | 13 | self.addEventListener('fetch', app); 14 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "files": ["src/index.ts", "scripts/e2e.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/wrangler.toml: -------------------------------------------------------------------------------- 1 | compatibility_date = "2022-02-21" 2 | name = "whatwg-node" 3 | account_id = "" 4 | workers_dev = true 5 | main = "src/index.ts" 6 | compatibility_flags = ["streams_enable_constructors"] 7 | -------------------------------------------------------------------------------- /e2e/shared-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/shared-scripts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@pulumi/pulumi": "3.173.0", 7 | "@types/node": "22.15.27" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/shared-scripts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './runTests'; 2 | export * from './types'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /e2e/shared-scripts/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '@pulumi/pulumi'; 2 | import { OutputValue, Stack } from '@pulumi/pulumi/automation'; 3 | 4 | export type DeploymentConfiguration = { 5 | name: string; 6 | prerequisites?: (stack: Stack) => Promise; 7 | config?: (stack: Stack) => Promise; 8 | program: () => Promise<{ 9 | [K in keyof TProgramOutput]: Output | TProgramOutput[K]; 10 | }>; 11 | test: (output: { 12 | [K in keyof TProgramOutput]: Pick & { 13 | value: TProgramOutput[K]; 14 | }; 15 | }) => Promise; 16 | }; 17 | -------------------------------------------------------------------------------- /e2e/shared-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/shared-server", 3 | "version": "0.0.146", 4 | "private": true, 5 | "dependencies": { 6 | "@whatwg-node/fetch": "0.10.8", 7 | "@whatwg-node/server": "0.10.10" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/shared-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '@whatwg-node/fetch'; 2 | import { createServerAdapter } from '@whatwg-node/server'; 3 | 4 | export const createTestServerAdapter = () => 5 | createServerAdapter(async req => 6 | Response.json({ 7 | url: req.url, 8 | method: req.method, 9 | headers: Object.fromEntries(req.headers.entries()), 10 | reqText: req.method === 'POST' ? await req.text() : '', 11 | }), 12 | ); 13 | -------------------------------------------------------------------------------- /e2e/vercel/.gitignore: -------------------------------------------------------------------------------- 1 | pages 2 | -------------------------------------------------------------------------------- /e2e/vercel/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 | -------------------------------------------------------------------------------- /e2e/vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/vercel", 3 | "version": "0.0.146", 4 | "private": true, 5 | "scripts": { 6 | "build": "node scripts/bundle.js", 7 | "check": "tsc --pretty --noEmit", 8 | "dev": "next dev", 9 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts", 10 | "lint": "next lint", 11 | "start": "next start" 12 | }, 13 | "dependencies": { 14 | "@e2e/shared-scripts": "0.0.0", 15 | "@e2e/shared-server": "0.0.146", 16 | "encoding": "0.1.13", 17 | "next": "15.3.3", 18 | "react": "19.1.0", 19 | "react-dom": "19.1.0" 20 | }, 21 | "devDependencies": { 22 | "@pulumi/pulumi": "3.173.0", 23 | "@types/react": "19.1.6", 24 | "esbuild": "0.25.5", 25 | "eslint": "9.27.0", 26 | "eslint-config-next": "15.3.3", 27 | "ts-node": "10.9.2", 28 | "tsconfig-paths": "4.2.0", 29 | "typescript": "5.8.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /e2e/vercel/scripts/bundle.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | 3 | async function main() { 4 | await build({ 5 | entryPoints: ['./src/index.ts'], 6 | outfile: 'pages/api/whatwgnode.js', 7 | format: 'cjs', 8 | minify: false, 9 | bundle: true, 10 | platform: 'node', 11 | target: 'es2020', 12 | }); 13 | 14 | console.info(`Vercel Function build done!`); 15 | } 16 | 17 | main().catch(e => { 18 | console.error(e); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /e2e/vercel/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createVercelDeployment } from './createVercelDeployment'; 3 | 4 | runTests(createVercelDeployment()) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/vercel/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createTestServerAdapter } from '@e2e/shared-server'; 2 | 3 | export const config = { 4 | api: { 5 | // Disable body parsing (required for file uploads) 6 | bodyParser: false, 7 | }, 8 | }; 9 | 10 | export default createTestServerAdapter(); 11 | -------------------------------------------------------------------------------- /e2e/vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "incremental": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": ["next-env.d.ts", "./**/*.ts"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import globals from 'globals'; 4 | import { FlatCompat } from '@eslint/eslintrc'; 5 | import js from '@eslint/js'; 6 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 7 | import tsParser from '@typescript-eslint/parser'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | export default [ 17 | { 18 | ignores: [ 19 | '**/dist', 20 | '**/node_modules', 21 | 'packages/load/tests/loaders/schema', 22 | '**/website', 23 | '**/scripts', 24 | '**/e2e', 25 | '**/benchmarks', 26 | 'deno-jest.ts', 27 | '.bob', 28 | '*.mjs', 29 | '*.cjs', 30 | '*.js', 31 | ], 32 | }, 33 | ...compat.extends( 34 | 'eslint:recommended', 35 | 'standard', 36 | 'prettier', 37 | 'plugin:@typescript-eslint/recommended', 38 | ), 39 | { 40 | plugins: { 41 | '@typescript-eslint': typescriptEslint, 42 | }, 43 | 44 | languageOptions: { 45 | globals: { 46 | ...globals.node, 47 | BigInt: true, 48 | }, 49 | 50 | parser: tsParser, 51 | ecmaVersion: 5, 52 | sourceType: 'commonjs', 53 | 54 | parserOptions: { 55 | project: './tsconfig.json', 56 | }, 57 | }, 58 | 59 | rules: { 60 | 'no-empty': 'off', 61 | 'no-console': 'off', 62 | 'no-prototype-builtins': 'off', 63 | 'no-useless-constructor': 'off', 64 | 'no-useless-escape': 'off', 65 | 'no-undef': 'off', 66 | 'no-dupe-class-members': 'off', 67 | 'dot-notation': 'off', 68 | 'no-use-before-define': 'off', 69 | '@typescript-eslint/no-unused-vars': 'off', 70 | '@typescript-eslint/no-use-before-define': 'off', 71 | '@typescript-eslint/no-namespace': 'off', 72 | '@typescript-eslint/no-empty-interface': 'off', 73 | '@typescript-eslint/no-empty-function': 'off', 74 | '@typescript-eslint/no-var-requires': 'off', 75 | '@typescript-eslint/no-explicit-any': 'off', 76 | '@typescript-eslint/no-non-null-assertion': 'off', 77 | '@typescript-eslint/explicit-function-return-type': 'off', 78 | '@typescript-eslint/ban-ts-ignore': 'off', 79 | '@typescript-eslint/return-await': 'error', 80 | '@typescript-eslint/naming-convention': 'off', 81 | '@typescript-eslint/interface-name-prefix': 'off', 82 | '@typescript-eslint/explicit-module-boundary-types': 'off', 83 | '@typescript-eslint/no-empty-object-type': 'off', 84 | 'default-param-last': 'off', 85 | '@typescript-eslint/ban-types': 'off', 86 | 87 | 'import/no-extraneous-dependencies': [ 88 | 'error', 89 | { 90 | devDependencies: [ 91 | '**/*.test.ts', 92 | '**/*.spec.ts', 93 | '**/vitest.config.ts', 94 | '**/vitest.projects.ts', 95 | ], 96 | }, 97 | ], 98 | }, 99 | }, 100 | { 101 | files: ['**/{test,tests,testing}/**/*.{ts,js}', '**/*.{spec,test}.{ts,js}'], 102 | 103 | languageOptions: { 104 | globals: { 105 | ...globals.jest, 106 | }, 107 | }, 108 | 109 | rules: { 110 | '@typescript-eslint/no-unused-vars': 'off', 111 | 'import/no-extraneous-dependencies': 'off', 112 | }, 113 | }, 114 | ]; 115 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const { pathsToModuleNameMapper } = require('ts-jest'); 3 | const CI = !!process.env.CI; 4 | 5 | const ROOT_DIR = __dirname; 6 | const TSCONFIG = resolve(ROOT_DIR, 'tsconfig.json'); 7 | const tsconfig = require(TSCONFIG); 8 | const ESM_PACKAGES = []; 9 | 10 | let globals = {}; 11 | 12 | try { 13 | global.createUWS = require('./uwsUtils').createUWS; 14 | } catch (err) { 15 | console.warn(`Failed to load uWebSockets.js. Skipping tests that require it.`, err); 16 | } 17 | 18 | try { 19 | globals.libcurl = require('node-libcurl'); 20 | } catch (err) { 21 | console.warn('Failed to load node-libcurl. Skipping tests that require it.', err); 22 | } 23 | 24 | module.exports = { 25 | displayName: process.env.LEAK_TEST ? 'Leak Tests' : 'Unit Tests', 26 | testEnvironment: 'node', 27 | rootDir: ROOT_DIR, 28 | restoreMocks: true, 29 | reporters: ['default'], 30 | modulePathIgnorePatterns: ['dist', 'test-assets', 'test-files', 'fixtures', 'bun'], 31 | moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { 32 | prefix: `${ROOT_DIR}/`, 33 | }), 34 | transformIgnorePatterns: [`node_modules/(?!(${ESM_PACKAGES.join('|')})/)`], 35 | transform: { 36 | '^.+\\.mjs?$': 'babel-jest', 37 | '^.+\\.ts?$': 'babel-jest', 38 | '^.+\\.js$': 'babel-jest', 39 | }, 40 | collectCoverage: false, 41 | globals, 42 | cacheDirectory: resolve(ROOT_DIR, `${CI ? '' : 'node_modules/'}.cache/jest`), 43 | resolver: 'bob-the-bundler/jest-resolver', 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatwg-node-monorepo", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*", 6 | "e2e/*", 7 | "benchmarks/*" 8 | ], 9 | "packageManager": "yarn@1.22.22", 10 | "scripts": { 11 | "bench": "vitest bench --project bench", 12 | "build": "bob build", 13 | "ci:lint": "eslint . --output-file eslint_report.json --format json", 14 | "clean-dist": "rimraf \"dist\" && rimraf \".bob\"", 15 | "esm:check": "bob check", 16 | "jest-with-gc": "node --expose-gc ./node_modules/.bin/jest", 17 | "lint": "eslint .", 18 | "postinstall": "patch-package && husky", 19 | "prebuild": "yarn clean-dist", 20 | "prerelease": "yarn build", 21 | "prerelease-canary": "yarn build", 22 | "pretest:deno": "yarn build", 23 | "prettier": "prettier --ignore-path .gitignore --ignore-path .prettierignore --write --list-different .", 24 | "prettier:check": "prettier --ignore-path .gitignore --ignore-path .prettierignore --check .", 25 | "release": "changeset publish", 26 | "test": "jest --runInBand --forceExit", 27 | "test:bun": "bun test --bail", 28 | "test:deno": "deno test ./packages/**/*.spec.ts --allow-all --fail-fast --no-check --unstable-sloppy-imports --trace-leaks", 29 | "test:leaks": "LEAK_TEST=1 jest --detectOpenHandles --detectLeaks --runInBand --forceExit", 30 | "ts:check": "tsc --noEmit --skipLibCheck" 31 | }, 32 | "optionalDependencies": { 33 | "node-libcurl": "4.1.0", 34 | "uWebSockets.js": "uNetworking/uWebSockets.js#v20.52.0" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "7.27.3", 38 | "@babel/plugin-proposal-class-properties": "7.18.6", 39 | "@babel/plugin-proposal-explicit-resource-management": "7.27.3", 40 | "@babel/preset-env": "7.27.2", 41 | "@babel/preset-typescript": "7.27.1", 42 | "@changesets/changelog-github": "0.5.1", 43 | "@changesets/cli": "2.29.4", 44 | "@eslint/eslintrc": "3.3.1", 45 | "@eslint/js": "9.27.0", 46 | "@jest/globals": "29.7.0", 47 | "@theguild/prettier-config": "3.0.1", 48 | "@types/deno": "2.3.0", 49 | "@types/node": "22.15.27", 50 | "@types/react": "19.1.6", 51 | "@types/react-dom": "19.1.5", 52 | "@typescript-eslint/eslint-plugin": "8.33.0", 53 | "@typescript-eslint/parser": "8.33.0", 54 | "babel-jest": "29.7.0", 55 | "bob-the-bundler": "7.0.1", 56 | "bun": "1.2.15", 57 | "deno": "2.3.4", 58 | "eslint": "9.27.0", 59 | "eslint-config-prettier": "10.1.5", 60 | "eslint-config-standard": "17.1.0", 61 | "eslint-plugin-import": "2.31.0", 62 | "eslint-plugin-n": "17.18.0", 63 | "eslint-plugin-promise": "7.2.1", 64 | "eslint-plugin-standard": "5.0.0", 65 | "globals": "16.2.0", 66 | "husky": "9.1.7", 67 | "jest": "29.7.0", 68 | "lint-staged": "16.1.0", 69 | "patch-package": "8.0.0", 70 | "prettier": "3.5.3", 71 | "rimraf": "6.0.1", 72 | "ts-jest": "29.3.4", 73 | "typescript": "5.8.3", 74 | "vite-tsconfig-paths": "5.1.4", 75 | "vitest": "3.1.4" 76 | }, 77 | "resolutions": { 78 | "@pulumi/pulumi": "3.173.0", 79 | "cookie": "1.0.2", 80 | "esbuild": "0.25.5" 81 | }, 82 | "lint-staged": { 83 | "packages/**/*.{ts,tsx}": [ 84 | "eslint --fix" 85 | ], 86 | "**/*.{ts,tsx,graphql,yml,md,mdx,js,mjs,cjs,json}": [ 87 | "prettier --write" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/cookie-store/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @whatwg-node/cookie-store 2 | 3 | ## 0.2.3 4 | 5 | ### Patch Changes 6 | 7 | - [#2102](https://github.com/ardatan/whatwg-node/pull/2102) 8 | [`5cf6b2d`](https://github.com/ardatan/whatwg-node/commit/5cf6b2dbc589f4330c5efdee96356f48e438ae9e) 9 | Thanks [@ardatan](https://github.com/ardatan)! - dependencies updates: 10 | - Added dependency 11 | [`@whatwg-node/promise-helpers@^0.0.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/0.0.0) 12 | (to `dependencies`) 13 | - Updated dependencies 14 | [[`5cf6b2d`](https://github.com/ardatan/whatwg-node/commit/5cf6b2dbc589f4330c5efdee96356f48e438ae9e)]: 15 | - @whatwg-node/promise-helpers@1.0.0 16 | 17 | ## 0.2.2 18 | 19 | ### Patch Changes 20 | 21 | - [#727](https://github.com/ardatan/whatwg-node/pull/727) 22 | [`265aab1`](https://github.com/ardatan/whatwg-node/commit/265aab1ac3a1726d8e655060e6cbd22b8ff7d76d) 23 | Thanks [@GauBen](https://github.com/GauBen)! - Added missing .js extension to import (#726) 24 | 25 | ## 0.2.1 26 | 27 | ### Patch Changes 28 | 29 | - [#671](https://github.com/ardatan/whatwg-node/pull/671) 30 | [`08a9ae5`](https://github.com/ardatan/whatwg-node/commit/08a9ae5f675c7860b6a38ef02ea41390a4c75608) 31 | Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - add HttpOnly attribute 32 | 33 | ## 0.2.0 34 | 35 | ### Minor Changes 36 | 37 | - [`025613a`](https://github.com/ardatan/whatwg-node/commit/025613af57695c2158189156479129a461d758ce) 38 | Thanks [@ardatan](https://github.com/ardatan)! - Fix cookie handling 39 | 40 | ## 0.1.0 41 | 42 | ### Minor Changes 43 | 44 | - [#535](https://github.com/ardatan/whatwg-node/pull/535) 45 | [`01051f8`](https://github.com/ardatan/whatwg-node/commit/01051f8b3408ac26612b8d8ea2702a3f7e6667af) 46 | Thanks [@ardatan](https://github.com/ardatan)! - Drop Node 14 support 47 | 48 | ### Patch Changes 49 | 50 | - [#535](https://github.com/ardatan/whatwg-node/pull/535) 51 | [`01051f8`](https://github.com/ardatan/whatwg-node/commit/01051f8b3408ac26612b8d8ea2702a3f7e6667af) 52 | Thanks [@ardatan](https://github.com/ardatan)! - dependencies updates: 53 | - Removed dependency 54 | [`@whatwg-node/events@^0.0.3` ↗︎](https://www.npmjs.com/package/@whatwg-node/events/v/0.0.3) 55 | (from `dependencies`) 56 | 57 | ## 0.0.1 58 | 59 | ### Patch Changes 60 | 61 | - [#500](https://github.com/ardatan/whatwg-node/pull/500) 62 | [`2896da0`](https://github.com/ardatan/whatwg-node/commit/2896da0d524e1e42e16272f64c055fb868c2e41c) 63 | Thanks [@ardatan](https://github.com/ardatan)! - New CookieStore package 64 | -------------------------------------------------------------------------------- /packages/cookie-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/cookie-store", 3 | "version": "0.2.3", 4 | "type": "module", 5 | "description": "Cookie Store", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/cookie-store" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "@whatwg-node/promise-helpers": "^1.0.0", 38 | "tslib": "^2.6.3" 39 | }, 40 | "publishConfig": { 41 | "directory": "dist", 42 | "access": "public" 43 | }, 44 | "sideEffects": false, 45 | "buildOptions": { 46 | "input": "./src/index.ts" 47 | }, 48 | "typescript": { 49 | "definition": "dist/typings/index.d.ts" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/cookie-store/src/CookieChangeEvent.ts: -------------------------------------------------------------------------------- 1 | import { CookieChangeEventInit, CookieList } from './types.js'; 2 | 3 | export class CookieChangeEvent extends Event { 4 | changed: CookieList; 5 | deleted: CookieList; 6 | 7 | constructor(type: string, eventInitDict: CookieChangeEventInit = { changed: [], deleted: [] }) { 8 | super(type, eventInitDict); 9 | this.changed = eventInitDict.changed || []; 10 | this.deleted = eventInitDict.deleted || []; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/cookie-store/src/getCookieString.ts: -------------------------------------------------------------------------------- 1 | import type { Cookie, CookieListItem } from './types.js'; 2 | 3 | export function getCookieString(item: CookieListItem | Cookie) { 4 | let cookieString = `${item.name}=${encodeURIComponent(item.value!)}`; 5 | 6 | if (item.domain) { 7 | cookieString += '; Domain=' + item.domain; 8 | } 9 | 10 | if (item.path) { 11 | cookieString += '; Path=' + item.path; 12 | } 13 | 14 | if (typeof item.expires === 'number') { 15 | cookieString += '; Expires=' + new Date(item.expires).toUTCString(); 16 | } else if (item.expires) { 17 | cookieString += '; Expires=' + item.expires.toUTCString(); 18 | } 19 | 20 | if ((item.name && item.name.startsWith('__Secure')) || item.secure) { 21 | item.sameSite = item.sameSite || 'lax'; 22 | cookieString += '; Secure'; 23 | } 24 | 25 | switch (item.sameSite) { 26 | case 'lax': 27 | cookieString += '; SameSite=Lax'; 28 | break; 29 | case 'strict': 30 | cookieString += '; SameSite=Strict'; 31 | break; 32 | case 'none': 33 | cookieString += '; SameSite=None'; 34 | break; 35 | } 36 | 37 | if (item.httpOnly) { 38 | cookieString += '; HttpOnly'; 39 | } 40 | 41 | return cookieString; 42 | } 43 | -------------------------------------------------------------------------------- /packages/cookie-store/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CookieChangeEvent.js'; 2 | export * from './CookieStore.js'; 3 | export * from './parse.js'; 4 | export * from './types.js'; 5 | export * from './getCookieString.js'; 6 | -------------------------------------------------------------------------------- /packages/cookie-store/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { Cookie } from './types.js'; 2 | 3 | export interface ParseOptions { 4 | decode?: boolean; 5 | } 6 | 7 | const decode = decodeURIComponent; 8 | const pairSplitRegExp = /; */; 9 | 10 | // Try decoding a string using a decoding function. 11 | function tryDecode( 12 | str: string, 13 | decode: ((encodedURIComponent: string) => string) | boolean, 14 | ): string { 15 | try { 16 | return typeof decode === 'boolean' ? decodeURIComponent(str) : decode(str); 17 | } catch (e) { 18 | return str; 19 | } 20 | } 21 | 22 | /** 23 | * Parse a cookie header. 24 | * 25 | * Parse the given cookie header string into an object 26 | * The object has the various cookies as keys(names) => values 27 | */ 28 | export function parse(str: string, options: ParseOptions = {}): Map { 29 | if (typeof str !== 'string') { 30 | throw new TypeError('argument str must be a string'); 31 | } 32 | 33 | const map = new Map(); 34 | const opt = options || {}; 35 | const pairs = str.split(pairSplitRegExp); 36 | const dec = opt.decode || decode; 37 | 38 | for (let i = 0; i < pairs.length; i++) { 39 | const pair = pairs[i]; 40 | let eqIdx = pair.indexOf('='); 41 | 42 | // skip things that don't look like key=value 43 | if (eqIdx < 0) { 44 | continue; 45 | } 46 | 47 | const key = pair.substr(0, eqIdx).trim(); 48 | let val = pair.substr(++eqIdx, pair.length).trim(); 49 | 50 | // quoted values 51 | if (val[0] === '"') { 52 | val = val.slice(1, -1); 53 | } 54 | 55 | const cookiesPerKey = map.get(key); 56 | if (!cookiesPerKey) { 57 | map.set(key, { name: key, value: tryDecode(val, dec) }); 58 | } 59 | } 60 | 61 | return map; 62 | } 63 | -------------------------------------------------------------------------------- /packages/cookie-store/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Cookie { 2 | domain?: string; 3 | expires?: number; 4 | name: string; 5 | path?: string; 6 | secure?: boolean; 7 | sameSite?: CookieSameSite; 8 | value: string; 9 | httpOnly?: boolean; 10 | } 11 | 12 | export interface CookieStoreDeleteOptions { 13 | name: string; 14 | domain?: string; 15 | path?: string; 16 | } 17 | 18 | export interface CookieStoreGetOptions { 19 | name?: string | undefined; 20 | url?: string; 21 | } 22 | 23 | export type CookieSameSite = 'strict' | 'lax' | 'none'; 24 | 25 | export interface CookieListItem { 26 | name?: string | undefined; 27 | value?: string | undefined; 28 | domain: string | null; 29 | path?: string | undefined; 30 | expires: Date | number | null; 31 | secure?: boolean | undefined; 32 | sameSite?: CookieSameSite | undefined; 33 | httpOnly?: boolean | undefined; 34 | } 35 | 36 | export type CookieList = CookieListItem[]; 37 | 38 | export interface CookieChangeEventInit extends EventInit { 39 | changed: CookieList; 40 | deleted: CookieList; 41 | } 42 | -------------------------------------------------------------------------------- /packages/cookie-store/test/getCookieString.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { getCookieString } from '@whatwg-node/cookie-store'; 3 | 4 | describe('getCookieString helper', () => { 5 | const baseOptions = { 6 | name: 'foo', 7 | value: 'bar', 8 | }; 9 | 10 | it('should work with just name and value', () => { 11 | expect(getCookieString(baseOptions)).toBe(`foo=bar`); 12 | }); 13 | 14 | it('should work with a domain', () => { 15 | expect( 16 | getCookieString({ 17 | ...baseOptions, 18 | domain: 'example.com', 19 | }), 20 | ).toBe(`foo=bar; Domain=example.com`); 21 | }); 22 | 23 | it('should work with a path', () => { 24 | expect( 25 | getCookieString({ 26 | ...baseOptions, 27 | path: '/', 28 | }), 29 | ).toBe(`foo=bar; Path=/`); 30 | }); 31 | 32 | it('should work with a expires number', () => { 33 | expect( 34 | getCookieString({ 35 | ...baseOptions, 36 | expires: 1687492800 * 1000, 37 | }), 38 | ).toBe(`foo=bar; Expires=Fri, 23 Jun 2023 04:00:00 GMT`); 39 | }); 40 | 41 | it('should work with a expires Date', () => { 42 | expect( 43 | getCookieString({ 44 | ...baseOptions, 45 | domain: null, 46 | expires: new Date(1687492800 * 1000), 47 | }), 48 | ).toBe(`foo=bar; Expires=Fri, 23 Jun 2023 04:00:00 GMT`); 49 | }); 50 | 51 | it('should work with Secure', () => { 52 | expect( 53 | getCookieString({ 54 | ...baseOptions, 55 | secure: true, 56 | }), 57 | ).toBe(`foo=bar; Secure; SameSite=Lax`); 58 | }); 59 | 60 | it('Should preserve samesite when secure = true', () => { 61 | expect( 62 | getCookieString({ 63 | ...baseOptions, 64 | secure: true, 65 | sameSite: 'none', 66 | }), 67 | ).toBe(`foo=bar; Secure; SameSite=None`); 68 | }); 69 | 70 | it('Should preserve samesite when secure = false', () => { 71 | expect( 72 | getCookieString({ 73 | ...baseOptions, 74 | sameSite: 'none', 75 | }), 76 | ).toBe(`foo=bar; SameSite=None`); 77 | }); 78 | 79 | it('should work with HttpOnly', () => { 80 | expect( 81 | getCookieString({ 82 | ...baseOptions, 83 | httpOnly: true, 84 | }), 85 | ).toBe(`foo=bar; HttpOnly`); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/disposablestack/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @whatwg-node/disposablestack 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - [#2102](https://github.com/ardatan/whatwg-node/pull/2102) 8 | [`5cf6b2d`](https://github.com/ardatan/whatwg-node/commit/5cf6b2dbc589f4330c5efdee96356f48e438ae9e) 9 | Thanks [@ardatan](https://github.com/ardatan)! - dependencies updates: 10 | - Added dependency 11 | [`@whatwg-node/promise-helpers@^0.0.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/0.0.0) 12 | (to `dependencies`) 13 | - Updated dependencies 14 | [[`5cf6b2d`](https://github.com/ardatan/whatwg-node/commit/5cf6b2dbc589f4330c5efdee96356f48e438ae9e)]: 15 | - @whatwg-node/promise-helpers@1.0.0 16 | 17 | ## 0.0.5 18 | 19 | ### Patch Changes 20 | 21 | - [`effd12f`](https://github.com/ardatan/whatwg-node/commit/effd12f88e918a5b93ea88b1fc74f6ee05696c58) 22 | Thanks [@ardatan](https://github.com/ardatan)! - Fix SuppressedError reference 23 | 24 | ## 0.0.4 25 | 26 | ### Patch Changes 27 | 28 | - [`860bfde`](https://github.com/ardatan/whatwg-node/commit/860bfde7d7b6cf1b090e0b91c48bcb3cac69cb89) 29 | Thanks [@ardatan](https://github.com/ardatan)! - Ponyfill SuppressedError correctly inside 30 | DisposableStack ponyfills 31 | 32 | ## 0.0.3 33 | 34 | ### Patch Changes 35 | 36 | - [`0a49705`](https://github.com/ardatan/whatwg-node/commit/0a4970574738c918913d503223968c68a04186e7) 37 | Thanks [@ardatan](https://github.com/ardatan)! - Throw SupressedError in DisposableStack 38 | 39 | ## 0.0.2 40 | 41 | ### Patch Changes 42 | 43 | - [`8ab228c`](https://github.com/ardatan/whatwg-node/commit/8ab228cb348ec7e16250c7f530956186311e16d9) 44 | Thanks [@ardatan](https://github.com/ardatan)! - Improve `disposed` flag and cleanup callbacks on 45 | AsyncDisposable on disposeAsync call 46 | 47 | ## 0.0.1 48 | 49 | ### Patch Changes 50 | 51 | - [#1514](https://github.com/ardatan/whatwg-node/pull/1514) 52 | [`61a0480`](https://github.com/ardatan/whatwg-node/commit/61a0480f1f024b0455598c0c0bd213a74cd72394) 53 | Thanks [@ardatan](https://github.com/ardatan)! - New ponyfill 54 | -------------------------------------------------------------------------------- /packages/disposablestack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/disposablestack", 3 | "version": "0.0.6", 4 | "type": "module", 5 | "description": "Cross Platform Smart DisposableStack API Ponyfill", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/disposablestack" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "@whatwg-node/promise-helpers": "^1.0.0", 38 | "tslib": "^2.6.3" 39 | }, 40 | "publishConfig": { 41 | "directory": "dist", 42 | "access": "public" 43 | }, 44 | "sideEffects": false, 45 | "buildOptions": { 46 | "input": "./src/index.ts" 47 | }, 48 | "typescript": { 49 | "definition": "dist/typings/index.d.ts" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/disposablestack/src/AsyncDisposableStack.ts: -------------------------------------------------------------------------------- 1 | import { handleMaybePromise, type MaybePromiseLike } from '@whatwg-node/promise-helpers'; 2 | import { PonyfillSuppressedError } from './SupressedError.js'; 3 | import { DisposableSymbols } from './symbols.js'; 4 | import { isAsyncDisposable, isSyncDisposable } from './utils.js'; 5 | 6 | const SuppressedError = globalThis.SuppressedError || PonyfillSuppressedError; 7 | 8 | export class PonyfillAsyncDisposableStack implements AsyncDisposableStack { 9 | private callbacks: (() => MaybePromiseLike)[] = []; 10 | get disposed(): boolean { 11 | return this.callbacks.length === 0; 12 | } 13 | 14 | use(value: T): T { 15 | if (isAsyncDisposable(value)) { 16 | this.callbacks.push(() => value[DisposableSymbols.asyncDispose]()); 17 | } else if (isSyncDisposable(value)) { 18 | this.callbacks.push(() => value[DisposableSymbols.dispose]()); 19 | } 20 | return value; 21 | } 22 | 23 | adopt(value: T, onDisposeAsync: (value: T) => MaybePromiseLike): T { 24 | if (onDisposeAsync) { 25 | this.callbacks.push(() => onDisposeAsync(value)); 26 | } 27 | return value; 28 | } 29 | 30 | defer(onDisposeAsync: () => MaybePromiseLike): void { 31 | if (onDisposeAsync) { 32 | this.callbacks.push(onDisposeAsync); 33 | } 34 | } 35 | 36 | move(): AsyncDisposableStack { 37 | const stack = new PonyfillAsyncDisposableStack(); 38 | stack.callbacks = this.callbacks; 39 | this.callbacks = []; 40 | return stack; 41 | } 42 | 43 | disposeAsync(): Promise { 44 | return this[DisposableSymbols.asyncDispose](); 45 | } 46 | 47 | private _error?: Error | undefined; 48 | 49 | private _iterateCallbacks(): MaybePromiseLike { 50 | const cb = this.callbacks.pop(); 51 | if (cb) { 52 | return handleMaybePromise( 53 | cb, 54 | () => this._iterateCallbacks(), 55 | error => { 56 | this._error = this._error ? new SuppressedError(error, this._error) : error; 57 | return this._iterateCallbacks(); 58 | }, 59 | ); 60 | } 61 | } 62 | 63 | [DisposableSymbols.asyncDispose](): Promise { 64 | const res$ = this._iterateCallbacks(); 65 | if (res$?.then) { 66 | return res$.then(() => { 67 | if (this._error) { 68 | const error = this._error; 69 | this._error = undefined; 70 | throw error; 71 | } 72 | }) as Promise; 73 | } 74 | if (this._error) { 75 | const error = this._error; 76 | this._error = undefined; 77 | throw error; 78 | } 79 | return undefined as any as Promise; 80 | } 81 | 82 | readonly [Symbol.toStringTag]: string = 'AsyncDisposableStack'; 83 | } 84 | -------------------------------------------------------------------------------- /packages/disposablestack/src/DisposableStack.ts: -------------------------------------------------------------------------------- 1 | import { PonyfillSuppressedError } from './SupressedError.js'; 2 | import { DisposableSymbols } from './symbols.js'; 3 | import { isSyncDisposable } from './utils.js'; 4 | 5 | const SuppressedError = globalThis.SuppressedError || PonyfillSuppressedError; 6 | 7 | export class PonyfillDisposableStack implements DisposableStack { 8 | private callbacks: (() => void)[] = []; 9 | get disposed(): boolean { 10 | return this.callbacks.length === 0; 11 | } 12 | 13 | use(value: T): T { 14 | if (isSyncDisposable(value)) { 15 | this.callbacks.push(() => value[DisposableSymbols.dispose]()); 16 | } 17 | return value; 18 | } 19 | 20 | adopt(value: T, onDispose: (value: T) => void): T { 21 | if (onDispose) { 22 | this.callbacks.push(() => onDispose(value)); 23 | } 24 | return value; 25 | } 26 | 27 | defer(onDispose: () => void): void { 28 | if (onDispose) { 29 | this.callbacks.push(onDispose); 30 | } 31 | } 32 | 33 | move(): DisposableStack { 34 | const stack = new PonyfillDisposableStack(); 35 | stack.callbacks = this.callbacks; 36 | this.callbacks = []; 37 | return stack; 38 | } 39 | 40 | dispose(): void { 41 | return this[DisposableSymbols.dispose](); 42 | } 43 | 44 | private _error?: Error | undefined; 45 | 46 | private _iterateCallbacks(): void { 47 | const cb = this.callbacks.pop(); 48 | if (cb) { 49 | try { 50 | cb(); 51 | } catch (error: any) { 52 | this._error = this._error ? new SuppressedError(error, this._error) : error; 53 | } 54 | return this._iterateCallbacks(); 55 | } 56 | } 57 | 58 | [DisposableSymbols.dispose](): void { 59 | this._iterateCallbacks(); 60 | if (this._error) { 61 | const error = this._error; 62 | this._error = undefined; 63 | throw error; 64 | } 65 | } 66 | 67 | readonly [Symbol.toStringTag]: string = 'DisposableStack'; 68 | } 69 | -------------------------------------------------------------------------------- /packages/disposablestack/src/SupressedError.ts: -------------------------------------------------------------------------------- 1 | export class PonyfillSuppressedError extends Error implements SuppressedError { 2 | // eslint-disable-next-line n/handle-callback-err 3 | constructor( 4 | public error: any, 5 | public suppressed: any, 6 | message?: string, 7 | ) { 8 | super(message); 9 | this.name = 'SuppressedError'; 10 | Error.captureStackTrace(this, this.constructor); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/disposablestack/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PonyfillAsyncDisposableStack } from './AsyncDisposableStack.js'; 2 | import { PonyfillDisposableStack } from './DisposableStack.js'; 3 | import { PonyfillSuppressedError } from './SupressedError.js'; 4 | 5 | export const DisposableStack = globalThis.DisposableStack || PonyfillDisposableStack; 6 | export const AsyncDisposableStack = globalThis.AsyncDisposableStack || PonyfillAsyncDisposableStack; 7 | export const SuppressedError = globalThis.SuppressedError || PonyfillSuppressedError; 8 | export * from './symbols.js'; 9 | -------------------------------------------------------------------------------- /packages/disposablestack/src/symbols.ts: -------------------------------------------------------------------------------- 1 | export const DisposableSymbols = { 2 | get dispose(): typeof Symbol.dispose { 3 | return Symbol.dispose || Symbol.for('dispose'); 4 | }, 5 | get asyncDispose(): typeof Symbol.asyncDispose { 6 | return Symbol.asyncDispose || Symbol.for('asyncDispose'); 7 | }, 8 | }; 9 | 10 | export function patchSymbols() { 11 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 12 | // @ts-ignore - we ponyfill these symbols 13 | Symbol.dispose ||= Symbol.for('dispose'); 14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 15 | // @ts-ignore - we ponyfill these symbols 16 | Symbol.asyncDispose ||= Symbol.for('asyncDispose'); 17 | } 18 | -------------------------------------------------------------------------------- /packages/disposablestack/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { DisposableSymbols } from './symbols.js'; 2 | 3 | export function isSyncDisposable(obj: any): obj is Disposable { 4 | return obj?.[DisposableSymbols.dispose] != null; 5 | } 6 | 7 | export function isAsyncDisposable(obj: any): obj is AsyncDisposable { 8 | return obj?.[DisposableSymbols.asyncDispose] != null; 9 | } 10 | -------------------------------------------------------------------------------- /packages/events/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @whatwg-node/events 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - [#1514](https://github.com/ardatan/whatwg-node/pull/1514) 8 | [`61a0480`](https://github.com/ardatan/whatwg-node/commit/61a0480f1f024b0455598c0c0bd213a74cd72394) 9 | Thanks [@ardatan](https://github.com/ardatan)! - dependencies updates: 10 | - Added dependency [`tslib@^2.6.3` ↗︎](https://www.npmjs.com/package/tslib/v/2.6.3) (to 11 | `dependencies`) 12 | 13 | ## 0.1.1 14 | 15 | ### Patch Changes 16 | 17 | - [#554](https://github.com/ardatan/whatwg-node/pull/554) 18 | [`dc29e24`](https://github.com/ardatan/whatwg-node/commit/dc29e24a27921a39a8a3009f9fe32f5c8e6b3b50) 19 | Thanks [@n1ru4l](https://github.com/n1ru4l)! - Follow the spec and set `detail` to null by default 20 | 21 | ## 0.1.0 22 | 23 | ### Minor Changes 24 | 25 | - [#535](https://github.com/ardatan/whatwg-node/pull/535) 26 | [`01051f8`](https://github.com/ardatan/whatwg-node/commit/01051f8b3408ac26612b8d8ea2702a3f7e6667af) 27 | Thanks [@ardatan](https://github.com/ardatan)! - Drop Node 14 support 28 | 29 | ## 0.0.3 30 | 31 | ### Patch Changes 32 | 33 | - [#427](https://github.com/ardatan/whatwg-node/pull/427) 34 | [`e8bda7c`](https://github.com/ardatan/whatwg-node/commit/e8bda7cdf440a7f4bb617ee1b5df8ee1becb4ad6) 35 | Thanks [@Rugvip](https://github.com/Rugvip)! - Restructure type declarations to avoid polluting 36 | global namespace. 37 | 38 | ## 0.0.2 39 | 40 | ### Patch Changes 41 | 42 | - [`c0d5c43`](https://github.com/ardatan/whatwg-node/commit/c0d5c43a1c4d3d9fcdf542472fabdebd5118fe23) 43 | Thanks [@ardatan](https://github.com/ardatan)! - Fix dispatchEvent on Node 14 44 | 45 | ## 0.0.1 46 | 47 | ### Patch Changes 48 | 49 | - [`9502102`](https://github.com/ardatan/whatwg-node/commit/9502102b265945b37ee38b276ec1533fae0f308f) 50 | Thanks [@ardatan](https://github.com/ardatan)! - New Event API ponyfill 51 | -------------------------------------------------------------------------------- /packages/events/README.md: -------------------------------------------------------------------------------- 1 | # `@whatwg-node/events` 2 | 3 | A ponyfill package for JavaScript [DOM Events Standard](https://dom.spec.whatwg.org/#events). If 4 | your JavaScript environment doesn't implement this standard natively, this package automatically 5 | ponyfills the missing parts, and export them as a module. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | yarn add @whatwg-node/events 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```ts 16 | import { Event, EventTarget } from '@whatwg-node/events' 17 | 18 | const target = new EventTarget() 19 | target.addEventListener('foo', (event: Event) => { 20 | console.log(event.type) // foo 21 | }) 22 | 23 | target.dispatchEvent(new Event('foo')) 24 | ``` 25 | 26 | > If your environment already implements these natively, this package will export the native ones 27 | > automatically. 28 | 29 | ## Custom Events 30 | 31 | ```ts 32 | import { CustomEvent, EventTarget } from '@whatwg-node/events' 33 | 34 | const target = new EventTarget() 35 | target.addEventListener('foo', (event: CustomEvent) => { 36 | console.assert(event.detail.foo, 'bar') 37 | }) 38 | 39 | // `detail` can take any value 40 | target.dispatchEvent(new CustomEvent('foo', { detail: { foo: 'bar' } })) 41 | ``` 42 | 43 | ## API 44 | 45 | The following classes are exported by this package: 46 | 47 | - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) 48 | - [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) 49 | - [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) 50 | -------------------------------------------------------------------------------- /packages/events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/events", 3 | "version": "0.1.2", 4 | "type": "module", 5 | "description": "Cross Platform Smart Event API Ponyfill", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/events" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "tslib": "^2.6.3" 38 | }, 39 | "publishConfig": { 40 | "directory": "dist", 41 | "access": "public" 42 | }, 43 | "sideEffects": false, 44 | "buildOptions": { 45 | "input": "./src/index.ts" 46 | }, 47 | "typescript": { 48 | "definition": "dist/typings/index.d.ts" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/events/src/index.ts: -------------------------------------------------------------------------------- 1 | export const CustomEvent = 2 | globalThis.CustomEvent || 3 | class PonyfillCustomEvent extends Event implements CustomEvent { 4 | detail: T = null as T; 5 | constructor(type: string, eventInitDict?: CustomEventInit) { 6 | super(type, eventInitDict); 7 | if (eventInitDict?.detail != null) { 8 | this.detail = eventInitDict.detail; 9 | } 10 | } 11 | 12 | initCustomEvent( 13 | type: string, 14 | bubbles?: boolean, 15 | cancelable?: boolean, 16 | detail?: T | undefined, 17 | ): void { 18 | this.initEvent(type, bubbles, cancelable); 19 | if (detail != null) { 20 | this.detail = detail; 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/events/tests/events.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, jest } from '@jest/globals'; 2 | import { CustomEvent } from '@whatwg-node/events'; 3 | 4 | describe('CustomEvent', () => { 5 | it('detail should be set', () => { 6 | const target = new EventTarget(); 7 | let receivedEvent: CustomEvent | null = null; 8 | const listener = jest.fn(e => { 9 | receivedEvent = e as CustomEvent; 10 | }); 11 | target.addEventListener('test', listener); 12 | target.dispatchEvent(new CustomEvent('test', { detail: 123 })); 13 | expect(receivedEvent).toBeInstanceOf(CustomEvent); 14 | expect(receivedEvent!.detail).toBe(123); 15 | }); 16 | it('detail should be null by default', () => { 17 | const target = new EventTarget(); 18 | let receivedEvent: CustomEvent | null = null; 19 | const listener = jest.fn(e => { 20 | receivedEvent = e as CustomEvent; 21 | }); 22 | target.addEventListener('test', listener); 23 | target.dispatchEvent(new CustomEvent('test')); 24 | expect(receivedEvent).toBeInstanceOf(CustomEvent); 25 | expect(receivedEvent!.detail).toBeFalsy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/fetch/.gitignore: -------------------------------------------------------------------------------- 1 | !dist 2 | -------------------------------------------------------------------------------- /packages/fetch/dist/create-node-ponyfill.js: -------------------------------------------------------------------------------- 1 | const shouldSkipPonyfill = require('./shouldSkipPonyfill'); 2 | let newNodeFetch; 3 | 4 | module.exports = function createNodePonyfill(opts = {}) { 5 | const ponyfills = {}; 6 | 7 | ponyfills.URLPattern = globalThis.URLPattern; 8 | 9 | // We call this previously to patch `Bun` 10 | if (!ponyfills.URLPattern) { 11 | const urlPatternModule = require('urlpattern-polyfill'); 12 | ponyfills.URLPattern = urlPatternModule.URLPattern; 13 | } 14 | 15 | if (opts.skipPonyfill || shouldSkipPonyfill()) { 16 | return { 17 | fetch: globalThis.fetch, 18 | Headers: globalThis.Headers, 19 | Request: globalThis.Request, 20 | Response: globalThis.Response, 21 | FormData: globalThis.FormData, 22 | ReadableStream: globalThis.ReadableStream, 23 | WritableStream: globalThis.WritableStream, 24 | TransformStream: globalThis.TransformStream, 25 | CompressionStream: globalThis.CompressionStream, 26 | DecompressionStream: globalThis.DecompressionStream, 27 | TextDecoderStream: globalThis.TextDecoderStream, 28 | TextEncoderStream: globalThis.TextEncoderStream, 29 | Blob: globalThis.Blob, 30 | File: globalThis.File, 31 | crypto: globalThis.crypto, 32 | btoa: globalThis.btoa, 33 | TextEncoder: globalThis.TextEncoder, 34 | TextDecoder: globalThis.TextDecoder, 35 | URLPattern: ponyfills.URLPattern, 36 | URL: globalThis.URL, 37 | URLSearchParams: globalThis.URLSearchParams 38 | }; 39 | } 40 | 41 | newNodeFetch ||= require('@whatwg-node/node-fetch'); 42 | 43 | ponyfills.fetch = newNodeFetch.fetch; 44 | ponyfills.Request = newNodeFetch.Request; 45 | ponyfills.Response = newNodeFetch.Response; 46 | ponyfills.Headers = newNodeFetch.Headers; 47 | ponyfills.FormData = newNodeFetch.FormData; 48 | ponyfills.ReadableStream = newNodeFetch.ReadableStream; 49 | 50 | ponyfills.URL = newNodeFetch.URL; 51 | ponyfills.URLSearchParams = newNodeFetch.URLSearchParams; 52 | 53 | ponyfills.WritableStream = newNodeFetch.WritableStream; 54 | ponyfills.TransformStream = newNodeFetch.TransformStream; 55 | ponyfills.CompressionStream = newNodeFetch.CompressionStream; 56 | ponyfills.DecompressionStream = newNodeFetch.DecompressionStream; 57 | ponyfills.TextDecoderStream = newNodeFetch.TextDecoderStream; 58 | ponyfills.TextEncoderStream = newNodeFetch.TextEncoderStream; 59 | 60 | ponyfills.Blob = newNodeFetch.Blob; 61 | ponyfills.File = newNodeFetch.File; 62 | ponyfills.crypto = globalThis.crypto; 63 | ponyfills.btoa = newNodeFetch.btoa; 64 | ponyfills.TextEncoder = newNodeFetch.TextEncoder; 65 | ponyfills.TextDecoder = newNodeFetch.TextDecoder; 66 | 67 | if (opts.formDataLimits) { 68 | ponyfills.Body = class Body extends newNodeFetch.Body { 69 | constructor(body, userOpts) { 70 | super(body, { 71 | formDataLimits: opts.formDataLimits, 72 | ...userOpts, 73 | }); 74 | } 75 | } 76 | ponyfills.Request = class Request extends newNodeFetch.Request { 77 | constructor(input, userOpts) { 78 | super(input, { 79 | formDataLimits: opts.formDataLimits, 80 | ...userOpts, 81 | }); 82 | } 83 | } 84 | ponyfills.Response = class Response extends newNodeFetch.Response { 85 | constructor(body, userOpts) { 86 | super(body, { 87 | formDataLimits: opts.formDataLimits, 88 | ...userOpts, 89 | }); 90 | } 91 | } 92 | } 93 | 94 | if (!ponyfills.crypto) { 95 | const cryptoModule = require("crypto"); 96 | ponyfills.crypto = cryptoModule.webcrypto; 97 | } 98 | 99 | return ponyfills; 100 | } 101 | -------------------------------------------------------------------------------- /packages/fetch/dist/esm-ponyfill.js: -------------------------------------------------------------------------------- 1 | const fetch = globalThis.fetch; 2 | const Headers = globalThis.Headers; 3 | const Request = globalThis.Request; 4 | const Response = globalThis.Response; 5 | const FormData = globalThis.FormData; 6 | const ReadableStream = globalThis.ReadableStream; 7 | const WritableStream = globalThis.WritableStream; 8 | const TransformStream = globalThis.TransformStream; 9 | const CompressionStream = globalThis.CompressionStream; 10 | const DecompressionStream = globalThis.DecompressionStream; 11 | const TextDecoderStream = globalThis.TextDecoderStream; 12 | const TextEncoderStream = globalThis.TextEncoderStream; 13 | const Blob = globalThis.Blob; 14 | const File = globalThis.File; 15 | const crypto = globalThis.crypto; 16 | const btoa = globalThis.btoa; 17 | const TextEncoder = globalThis.TextEncoder; 18 | const TextDecoder = globalThis.TextDecoder; 19 | const URLPattern = globalThis.URLPattern; 20 | const URL = globalThis.URL; 21 | const URLSearchParams = globalThis.URLSearchParams; 22 | 23 | export { 24 | fetch, 25 | Headers, 26 | Request, 27 | Response, 28 | FormData, 29 | ReadableStream, 30 | WritableStream, 31 | TransformStream, 32 | CompressionStream, 33 | DecompressionStream, 34 | TextDecoderStream, 35 | TextEncoderStream, 36 | Blob, 37 | File, 38 | crypto, 39 | btoa, 40 | TextEncoder, 41 | TextDecoder, 42 | URLPattern, 43 | URL, 44 | URLSearchParams 45 | } 46 | 47 | export function createFetch() { 48 | return { 49 | fetch, 50 | Headers, 51 | Request, 52 | Response, 53 | FormData, 54 | ReadableStream, 55 | WritableStream, 56 | TransformStream, 57 | CompressionStream, 58 | DecompressionStream, 59 | TextDecoderStream, 60 | TextEncoderStream, 61 | Blob, 62 | File, 63 | crypto, 64 | btoa, 65 | TextEncoder, 66 | TextDecoder, 67 | URLPattern, 68 | URL, 69 | URLSearchParams 70 | }; 71 | } -------------------------------------------------------------------------------- /packages/fetch/dist/global-ponyfill.js: -------------------------------------------------------------------------------- 1 | module.exports.fetch = globalThis.fetch; 2 | module.exports.Headers = globalThis.Headers; 3 | module.exports.Request = globalThis.Request; 4 | module.exports.Response = globalThis.Response; 5 | module.exports.FormData = globalThis.FormData; 6 | module.exports.ReadableStream = globalThis.ReadableStream; 7 | module.exports.WritableStream = globalThis.WritableStream; 8 | module.exports.TransformStream = globalThis.TransformStream; 9 | module.exports.CompressionStream = globalThis.CompressionStream; 10 | module.exports.DecompressionStream = globalThis.DecompressionStream; 11 | module.exports.TextDecoderStream = globalThis.TextDecoderStream; 12 | module.exports.TextEncoderStream = globalThis.TextEncoderStream; 13 | module.exports.Blob = globalThis.Blob; 14 | module.exports.File = globalThis.File; 15 | module.exports.crypto = globalThis.crypto; 16 | module.exports.btoa = globalThis.btoa; 17 | module.exports.TextEncoder = globalThis.TextEncoder; 18 | module.exports.TextDecoder = globalThis.TextDecoder; 19 | module.exports.URLPattern = globalThis.URLPattern; 20 | module.exports.URL = globalThis.URL; 21 | module.exports.URLSearchParams = globalThis.URLSearchParams; 22 | module.exports.createFetch = () => globalThis; 23 | -------------------------------------------------------------------------------- /packages/fetch/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare type _URLPattern = typeof URLPattern 6 | 7 | declare module '@whatwg-node/fetch' { 8 | export const fetch: typeof globalThis.fetch; 9 | export const Request: typeof globalThis.Request; 10 | export const Response: typeof globalThis.Response & { 11 | json(data: any, init?: ResponseInit): globalThis.Response; 12 | }; 13 | export const Headers: typeof globalThis.Headers; 14 | export const FormData: typeof globalThis.FormData; 15 | export const ReadableStream: typeof globalThis.ReadableStream; 16 | export const WritableStream: typeof globalThis.WritableStream; 17 | export const TransformStream: typeof globalThis.TransformStream; 18 | export const CompressionStream: typeof globalThis.CompressionStream; 19 | export const DecompressionStream: typeof globalThis.DecompressionStream; 20 | export const TextDecoderStream: typeof globalThis.TextDecoderStream; 21 | export const TextEncoderStream: typeof globalThis.TextEncoderStream; 22 | export const Blob: typeof globalThis.Blob; 23 | export const File: typeof globalThis.File; 24 | export const crypto: typeof globalThis.crypto; 25 | export const btoa: typeof globalThis.btoa; 26 | export const TextDecoder: typeof globalThis.TextDecoder; 27 | export const TextEncoder: typeof globalThis.TextEncoder; 28 | export const URL: typeof globalThis.URL; 29 | export const URLSearchParams: typeof globalThis.URLSearchParams; 30 | export const URLPattern: _URLPattern; 31 | export interface FormDataLimits { 32 | /* Max field name size (in bytes). Default: 100. */ 33 | fieldNameSize?: number; 34 | /* Max field value size (in bytes). Default: 1MB. */ 35 | fieldSize?: number; 36 | /* Max number of fields. Default: Infinity. */ 37 | fields?: number; 38 | /* For multipart forms, the max file size (in bytes). Default: Infinity. */ 39 | fileSize?: number; 40 | /* For multipart forms, the max number of file fields. Default: Infinity. */ 41 | files?: number; 42 | /* For multipart forms, the max number of parts (fields + files). Default: Infinity. */ 43 | parts?: number; 44 | /* For multipart forms, the max number of header key-value pairs to parse. Default: 2000. */ 45 | headerSize?: number; 46 | } 47 | export const createFetch: (opts?: { 48 | useNodeFetch?: boolean; 49 | formDataLimits?: FormDataLimits; 50 | skipPonyfill?: boolean; 51 | }) => { 52 | fetch: typeof fetch; 53 | Request: typeof Request; 54 | Response: typeof Response; 55 | Headers: typeof Headers; 56 | FormData: typeof FormData; 57 | ReadableStream: typeof ReadableStream; 58 | WritableStream: typeof WritableStream; 59 | TransformStream: typeof TransformStream; 60 | CompressionStream: typeof CompressionStream; 61 | DecompressionStream: typeof DecompressionStream; 62 | TextDecoderStream: typeof TextDecoderStream; 63 | TextEncoderStream: typeof TextEncoderStream; 64 | Blob: typeof Blob; 65 | File: typeof File; 66 | crypto: typeof crypto; 67 | btoa: typeof btoa; 68 | TextEncoder: typeof TextEncoder; 69 | TextDecoder: typeof TextDecoder; 70 | URLPattern: typeof URLPattern; 71 | URL: typeof URL; 72 | URLSearchParams: typeof URLSearchParams; 73 | }; 74 | } -------------------------------------------------------------------------------- /packages/fetch/dist/node-ponyfill.js: -------------------------------------------------------------------------------- 1 | 2 | const createNodePonyfill = require('./create-node-ponyfill'); 3 | const shouldSkipPonyfill = require('./shouldSkipPonyfill'); 4 | const ponyfills = createNodePonyfill(); 5 | 6 | if (!shouldSkipPonyfill()) { 7 | try { 8 | const nodelibcurlName = 'node-libcurl' 9 | globalThis.libcurl = globalThis.libcurl || require(nodelibcurlName); 10 | } catch (e) { } 11 | } 12 | 13 | module.exports.fetch = ponyfills.fetch; 14 | module.exports.Headers = ponyfills.Headers; 15 | module.exports.Request = ponyfills.Request; 16 | module.exports.Response = ponyfills.Response; 17 | module.exports.FormData = ponyfills.FormData; 18 | module.exports.ReadableStream = ponyfills.ReadableStream; 19 | module.exports.WritableStream = ponyfills.WritableStream; 20 | module.exports.TransformStream = ponyfills.TransformStream; 21 | module.exports.CompressionStream = ponyfills.CompressionStream; 22 | module.exports.DecompressionStream = ponyfills.DecompressionStream; 23 | module.exports.TextDecoderStream = ponyfills.TextDecoderStream; 24 | module.exports.TextEncoderStream = ponyfills.TextEncoderStream; 25 | module.exports.Blob = ponyfills.Blob; 26 | module.exports.File = ponyfills.File; 27 | module.exports.crypto = ponyfills.crypto; 28 | module.exports.btoa = ponyfills.btoa; 29 | module.exports.TextEncoder = ponyfills.TextEncoder; 30 | module.exports.TextDecoder = ponyfills.TextDecoder; 31 | module.exports.URLPattern = ponyfills.URLPattern; 32 | module.exports.URL = ponyfills.URL; 33 | module.exports.URLSearchParams = ponyfills.URLSearchParams; 34 | 35 | exports.createFetch = createNodePonyfill; 36 | -------------------------------------------------------------------------------- /packages/fetch/dist/shouldSkipPonyfill.js: -------------------------------------------------------------------------------- 1 | 2 | function isNextJs() { 3 | return Object.keys(globalThis).some(key => key.startsWith('__NEXT')) 4 | } 5 | 6 | module.exports = function shouldSkipPonyfill() { 7 | if (globalThis.Deno) { 8 | return true 9 | } 10 | if (globalThis.Bun) { 11 | return true 12 | } 13 | if (isNextJs()) { 14 | return true 15 | } 16 | return false 17 | } -------------------------------------------------------------------------------- /packages/fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/fetch", 3 | "version": "0.10.8", 4 | "description": "Cross Platform Smart Fetch Ponyfill", 5 | "repository": { 6 | "type": "git", 7 | "url": "ardatan/whatwg-node", 8 | "directory": "packages/fetch" 9 | }, 10 | "author": "Arda TANRIKULU ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">=18.0.0" 14 | }, 15 | "main": "dist/node-ponyfill.js", 16 | "browser": "dist/global-ponyfill.js", 17 | "types": "dist/index.d.ts", 18 | "dependencies": { 19 | "@whatwg-node/node-fetch": "^0.7.21", 20 | "urlpattern-polyfill": "^10.0.0" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "sideEffects": false, 26 | "bob": false, 27 | "denoify": { 28 | "index": "dist/esm-ponyfill.js" 29 | }, 30 | "react-native": "dist/global-ponyfill.js" 31 | } 32 | -------------------------------------------------------------------------------- /packages/fetchache/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # fetchache 2 | 3 | ## 0.1.6 4 | 5 | ### Patch Changes 6 | 7 | - [`145e46e`](https://github.com/ardatan/whatwg-node/commit/145e46e8d11ddfddb3fbb5335a1a959cc63c0eba) 8 | Thanks [@ardatan](https://github.com/ardatan)! - Implement `.bytes` method for `Blob` and `Body`, 9 | now `Uint8Array` is available with `bytes` format 10 | 11 | ## 0.1.5 12 | 13 | ### Patch Changes 14 | 15 | - [#434](https://github.com/ardatan/whatwg-node/pull/434) 16 | [`9f242f8`](https://github.com/ardatan/whatwg-node/commit/9f242f8268748345899ea4b6f05dac3c6dcecbeb) 17 | Thanks [@ardatan](https://github.com/ardatan)! - Update bob 18 | 19 | ## 0.1.4 20 | 21 | ### Patch Changes 22 | 23 | - [`f1db96f`](https://github.com/ardatan/whatwg-node/commit/f1db96fdd4988a1384ddefa2b7d148b128ee8f97) 24 | Thanks [@ardatan](https://github.com/ardatan)! - Respect additional parameters to fetch 25 | 26 | ## 0.1.3 27 | 28 | ### Patch Changes 29 | 30 | - [#104](https://github.com/ardatan/whatwg-node/pull/104) 31 | [`7093734`](https://github.com/ardatan/whatwg-node/commit/70937343d07bbfbbd56fdf44b8f143c9bcbc5c03) 32 | Thanks [@ardatan](https://github.com/ardatan)! - Avoid using Request constructor 33 | 34 | ## 0.1.2 35 | 36 | ### Patch Changes 37 | 38 | - 1e8d9d5: fix(fetchache): support binary responses 39 | -------------------------------------------------------------------------------- /packages/fetchache/README.md: -------------------------------------------------------------------------------- 1 | # Fetchache 2 | 3 | A fetch wrapper that allows you to respect HTTP caching strategies on non-browser environments with 4 | a key-value cache implementation. It follows the [HTTP Caching](https://tools.ietf.org/html/rfc7234) 5 | and [Conditional Requests](https://tools.ietf.org/html/rfc7232) standards. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | yarn add fetchache 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```ts 16 | import { fetchFactory } from 'fetchache' 17 | import { fetch, Response } from 'some-fetch-impl' 18 | 19 | // We recommend using `@whatwg-node/fetch` 20 | 21 | const someCacheImpl = { 22 | get: async key => { 23 | // Get the cached value from your cache implementation 24 | }, 25 | set: async (key, value) => { 26 | // Set the cached value to your cache implementation 27 | } 28 | } 29 | 30 | const fetchWithCache = fetchFactory({ 31 | fetch, 32 | Response, 33 | cache 34 | }) 35 | 36 | // Then you can use it like a normal fetch 37 | const response = await fetchWithCache('https://example.com') 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/fetchache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetchache", 3 | "version": "0.1.6", 4 | "type": "module", 5 | "description": "Cross Platform Fetch Wrapper with Key Value Cache support", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/fetchache" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "http-cache-semantics": "^4.1.0", 38 | "tslib": "^2.6.3" 39 | }, 40 | "devDependencies": { 41 | "@types/http-cache-semantics": "4.0.4" 42 | }, 43 | "publishConfig": { 44 | "directory": "dist", 45 | "access": "public" 46 | }, 47 | "sideEffects": false, 48 | "buildOptions": { 49 | "input": "./src/index.ts" 50 | }, 51 | "typescript": { 52 | "definition": "dist/typings/index.d.ts" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/node-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/node-fetch", 3 | "version": "0.7.21", 4 | "type": "module", 5 | "description": "Fetch API implementation for Node", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/node-fetch" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "@fastify/busboy": "^3.1.1", 38 | "@whatwg-node/disposablestack": "^0.0.6", 39 | "@whatwg-node/promise-helpers": "^1.3.2", 40 | "tslib": "^2.6.3" 41 | }, 42 | "devDependencies": { 43 | "@types/pem": "^1.14.0", 44 | "pem": "^1.14.8" 45 | }, 46 | "publishConfig": { 47 | "directory": "dist", 48 | "access": "public" 49 | }, 50 | "sideEffects": false, 51 | "buildOptions": { 52 | "input": "./src/index.ts" 53 | }, 54 | "typescript": { 55 | "definition": "dist/typings/index.d.ts" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/node-fetch/src/AbortError.ts: -------------------------------------------------------------------------------- 1 | export class PonyfillAbortError extends Error { 2 | constructor(reason?: any) { 3 | let message = 'The operation was aborted'; 4 | if (reason) { 5 | message += ` reason: ${reason}`; 6 | } 7 | super(message, { 8 | cause: reason, 9 | }); 10 | this.name = 'AbortError'; 11 | } 12 | 13 | get reason() { 14 | return this.cause; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/node-fetch/src/CompressionStream.ts: -------------------------------------------------------------------------------- 1 | import { createBrotliCompress, createDeflate, createDeflateRaw, createGzip } from 'node:zlib'; 2 | import { PonyfillTransformStream } from './TransformStream.js'; 3 | 4 | export type PonyfillCompressionFormat = 5 | | 'x-gzip' 6 | | 'gzip' 7 | | 'x-deflate' 8 | | 'deflate' 9 | | 'deflate-raw' 10 | | 'br'; 11 | 12 | export class PonyfillCompressionStream 13 | extends PonyfillTransformStream 14 | implements CompressionStream 15 | { 16 | static supportedFormats: PonyfillCompressionFormat[] = globalThis.process?.version?.startsWith( 17 | 'v2', 18 | ) 19 | ? ['gzip', 'deflate', 'br'] 20 | : ['gzip', 'deflate', 'deflate-raw', 'br']; 21 | 22 | constructor(compressionFormat: PonyfillCompressionFormat) { 23 | switch (compressionFormat) { 24 | case 'x-gzip': 25 | case 'gzip': 26 | super(createGzip()); 27 | break; 28 | case 'x-deflate': 29 | case 'deflate': 30 | super(createDeflate()); 31 | break; 32 | case 'deflate-raw': 33 | super(createDeflateRaw()); 34 | break; 35 | case 'br': 36 | super(createBrotliCompress()); 37 | break; 38 | default: 39 | throw new Error(`Unsupported compression format: ${compressionFormat}`); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/node-fetch/src/DecompressionStream.ts: -------------------------------------------------------------------------------- 1 | import { createBrotliDecompress, createGunzip, createInflate, createInflateRaw } from 'node:zlib'; 2 | import { PonyfillCompressionFormat } from './CompressionStream.js'; 3 | import { PonyfillTransformStream } from './TransformStream.js'; 4 | 5 | export class PonyfillDecompressionStream 6 | extends PonyfillTransformStream 7 | implements DecompressionStream 8 | { 9 | static supportedFormats: PonyfillCompressionFormat[] = globalThis.process?.version?.startsWith( 10 | 'v2', 11 | ) 12 | ? ['gzip', 'deflate', 'br'] 13 | : ['gzip', 'deflate', 'deflate-raw', 'br']; 14 | 15 | constructor(compressionFormat: PonyfillCompressionFormat) { 16 | switch (compressionFormat) { 17 | case 'x-gzip': 18 | case 'gzip': 19 | super(createGunzip()); 20 | break; 21 | case 'x-deflate': 22 | case 'deflate': 23 | super(createInflate()); 24 | break; 25 | case 'deflate-raw': 26 | super(createInflateRaw()); 27 | break; 28 | case 'br': 29 | super(createBrotliDecompress()); 30 | break; 31 | default: 32 | throw new TypeError(`Unsupported compression format: '${compressionFormat}'`); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/node-fetch/src/File.ts: -------------------------------------------------------------------------------- 1 | import { PonyfillBlob } from './Blob.js'; 2 | 3 | export class PonyfillFile extends PonyfillBlob implements File { 4 | public lastModified: number; 5 | constructor( 6 | fileBits: BlobPart[], 7 | public name: string, 8 | options?: FilePropertyBag, 9 | ) { 10 | super(fileBits, options); 11 | this.lastModified = options?.lastModified || Date.now(); 12 | } 13 | 14 | public webkitRelativePath = ''; 15 | } 16 | -------------------------------------------------------------------------------- /packages/node-fetch/src/TextEncoderDecoder.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { isArrayBufferView } from './utils.js'; 3 | 4 | export class PonyfillTextEncoder implements TextEncoder { 5 | constructor(public encoding: BufferEncoding = 'utf-8') {} 6 | 7 | encode(input: string): Buffer { 8 | return Buffer.from(input, this.encoding); 9 | } 10 | 11 | encodeInto(source: string, destination: Uint8Array): TextEncoderEncodeIntoResult { 12 | const buffer = this.encode(source); 13 | const copied = buffer.copy(destination); 14 | return { 15 | read: copied, 16 | written: copied, 17 | }; 18 | } 19 | } 20 | 21 | export class PonyfillTextDecoder implements TextDecoder { 22 | fatal = false; 23 | ignoreBOM = false; 24 | constructor( 25 | public encoding: BufferEncoding = 'utf-8', 26 | options?: TextDecoderOptions, 27 | ) { 28 | if (options) { 29 | this.fatal = options.fatal || false; 30 | this.ignoreBOM = options.ignoreBOM || false; 31 | } 32 | } 33 | 34 | decode(input: BufferSource): string { 35 | if (Buffer.isBuffer(input)) { 36 | return input.toString(this.encoding); 37 | } 38 | if (isArrayBufferView(input)) { 39 | return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString(this.encoding); 40 | } 41 | return Buffer.from(input).toString(this.encoding); 42 | } 43 | } 44 | 45 | export function PonyfillBtoa(input: string): string { 46 | return Buffer.from(input, 'binary').toString('base64'); 47 | } 48 | -------------------------------------------------------------------------------- /packages/node-fetch/src/TextEncoderDecoderStream.ts: -------------------------------------------------------------------------------- 1 | import { PonyfillTextDecoder, PonyfillTextEncoder } from './TextEncoderDecoder.js'; 2 | import { PonyfillTransformStream } from './TransformStream.js'; 3 | 4 | export class PonyfillTextDecoderStream 5 | extends PonyfillTransformStream 6 | implements TextDecoderStream 7 | { 8 | private textDecoder: TextDecoder; 9 | constructor(encoding?: BufferEncoding, options?: TextDecoderOptions) { 10 | super({ 11 | transform: (chunk, controller) => 12 | controller.enqueue(this.textDecoder.decode(chunk, { stream: true })), 13 | }); 14 | this.textDecoder = new PonyfillTextDecoder(encoding, options); 15 | } 16 | 17 | get encoding(): string { 18 | return this.textDecoder.encoding; 19 | } 20 | 21 | get fatal(): boolean { 22 | return this.textDecoder.fatal; 23 | } 24 | 25 | get ignoreBOM(): boolean { 26 | return this.textDecoder.ignoreBOM; 27 | } 28 | } 29 | 30 | export class PonyfillTextEncoderStream 31 | extends PonyfillTransformStream 32 | implements TextEncoderStream 33 | { 34 | private textEncoder: TextEncoder; 35 | constructor(encoding?: BufferEncoding) { 36 | super({ 37 | transform: (chunk, controller) => controller.enqueue(this.textEncoder.encode(chunk)), 38 | }); 39 | this.textEncoder = new PonyfillTextEncoder(encoding); 40 | } 41 | 42 | get encoding(): string { 43 | return this.textEncoder.encoding; 44 | } 45 | 46 | encode(input: string): Uint8Array { 47 | return this.textEncoder.encode(input); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/node-fetch/src/TransformStream.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'node:stream'; 2 | import { PonyfillReadableStream } from './ReadableStream.js'; 3 | import { endStream } from './utils.js'; 4 | import { PonyfillWritableStream } from './WritableStream.js'; 5 | 6 | export class PonyfillTransformStream implements TransformStream { 7 | transform: Transform; 8 | writable: PonyfillWritableStream; 9 | readable: PonyfillReadableStream; 10 | 11 | constructor(transformer?: Transformer | Transform) { 12 | if (transformer instanceof Transform) { 13 | this.transform = transformer; 14 | } else if (transformer) { 15 | const controller: TransformStreamDefaultController = { 16 | enqueue(chunk: O) { 17 | transform.push(chunk); 18 | }, 19 | error(reason: any) { 20 | transform.destroy(reason); 21 | }, 22 | terminate() { 23 | endStream(transform); 24 | }, 25 | get desiredSize() { 26 | return transform.writableLength; 27 | }, 28 | }; 29 | const transform = new Transform({ 30 | read() {}, 31 | write(chunk: I, _encoding: BufferEncoding, callback: (error?: Error | null) => void) { 32 | try { 33 | const result = transformer.transform?.(chunk, controller); 34 | if (result instanceof Promise) { 35 | result.then( 36 | () => { 37 | callback(); 38 | }, 39 | err => { 40 | callback(err); 41 | }, 42 | ); 43 | } else { 44 | callback(); 45 | } 46 | } catch (err) { 47 | callback(err as Error); 48 | } 49 | }, 50 | final(callback: (error?: Error | null) => void) { 51 | try { 52 | const result = transformer.flush?.(controller); 53 | if (result instanceof Promise) { 54 | result.then( 55 | () => { 56 | callback(); 57 | }, 58 | err => { 59 | callback(err); 60 | }, 61 | ); 62 | } else { 63 | callback(); 64 | } 65 | } catch (err) { 66 | callback(err as Error); 67 | } 68 | }, 69 | }); 70 | this.transform = transform; 71 | } else { 72 | this.transform = new Transform(); 73 | } 74 | this.writable = new PonyfillWritableStream(this.transform); 75 | this.readable = new PonyfillReadableStream(this.transform); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/node-fetch/src/URL.ts: -------------------------------------------------------------------------------- 1 | import NodeBuffer from 'node:buffer'; 2 | import { randomUUID } from 'node:crypto'; 3 | import { PonyfillBlob } from './Blob.js'; 4 | 5 | const NativeURL = globalThis.URL; 6 | 7 | class URL extends NativeURL { 8 | // This part is only needed to handle `PonyfillBlob` objects 9 | static blobRegistry = new Map(); 10 | static createObjectURL(blob: Blob): string { 11 | const blobUrl = `blob:whatwgnode:${randomUUID()}`; 12 | this.blobRegistry.set(blobUrl, blob); 13 | return blobUrl; 14 | } 15 | 16 | static revokeObjectURL(url: string): void { 17 | if (!this.blobRegistry.has(url)) { 18 | NativeURL.revokeObjectURL(url); 19 | } else { 20 | this.blobRegistry.delete(url); 21 | } 22 | } 23 | 24 | static getBlobFromURL(url: string): Blob | PonyfillBlob | undefined { 25 | return (this.blobRegistry.get(url) || NodeBuffer?.resolveObjectURL?.(url)) as 26 | | Blob 27 | | PonyfillBlob 28 | | undefined; 29 | } 30 | } 31 | 32 | export { URL as PonyfillURL }; 33 | -------------------------------------------------------------------------------- /packages/node-fetch/src/URLSearchParams.ts: -------------------------------------------------------------------------------- 1 | export const PonyfillURLSearchParams = globalThis.URLSearchParams; 2 | -------------------------------------------------------------------------------- /packages/node-fetch/src/WritableStream.ts: -------------------------------------------------------------------------------- 1 | import { once } from 'node:events'; 2 | import { Writable } from 'node:stream'; 3 | import { fakeRejectPromise } from '@whatwg-node/promise-helpers'; 4 | import { endStream, fakePromise, safeWrite } from './utils.js'; 5 | 6 | export class PonyfillWritableStream implements WritableStream { 7 | writable: Writable; 8 | constructor(underlyingSink?: UnderlyingSink | Writable) { 9 | if (underlyingSink instanceof Writable) { 10 | this.writable = underlyingSink; 11 | } else if (underlyingSink) { 12 | const writable = new Writable({ 13 | write(chunk: W, _encoding: BufferEncoding, callback: (error?: Error | null) => void) { 14 | try { 15 | const result = underlyingSink.write?.(chunk, controller); 16 | if (result instanceof Promise) { 17 | result.then( 18 | () => { 19 | callback(); 20 | }, 21 | err => { 22 | callback(err); 23 | }, 24 | ); 25 | } else { 26 | callback(); 27 | } 28 | } catch (err) { 29 | callback(err as Error); 30 | } 31 | }, 32 | final(callback: (error?: Error | null) => void) { 33 | const result = underlyingSink.close?.(); 34 | if (result instanceof Promise) { 35 | result.then( 36 | () => { 37 | callback(); 38 | }, 39 | err => { 40 | callback(err); 41 | }, 42 | ); 43 | } else { 44 | callback(); 45 | } 46 | }, 47 | }); 48 | this.writable = writable; 49 | const abortCtrl = new AbortController(); 50 | const controller: WritableStreamDefaultController = { 51 | signal: abortCtrl.signal, 52 | error(e) { 53 | writable.destroy(e); 54 | }, 55 | }; 56 | writable.once('error', err => abortCtrl.abort(err)); 57 | writable.once('close', () => abortCtrl.abort()); 58 | } else { 59 | this.writable = new Writable(); 60 | } 61 | } 62 | 63 | getWriter(): WritableStreamDefaultWriter { 64 | const writable = this.writable; 65 | return { 66 | get closed() { 67 | return once(writable, 'close') as Promise; 68 | }, 69 | get desiredSize() { 70 | return writable.writableLength; 71 | }, 72 | get ready() { 73 | return once(writable, 'drain') as Promise; 74 | }, 75 | releaseLock() { 76 | // no-op 77 | }, 78 | write(chunk: W) { 79 | const promise = fakePromise(); 80 | if (chunk == null) { 81 | return promise; 82 | } 83 | return promise.then(() => safeWrite(chunk, writable)) as Promise; 84 | }, 85 | close() { 86 | if (!writable.errored && writable.closed) { 87 | return fakePromise(); 88 | } 89 | if (writable.errored) { 90 | return fakeRejectPromise(writable.errored); 91 | } 92 | return fakePromise().then(() => endStream(writable)); 93 | }, 94 | abort(reason) { 95 | writable.destroy(reason); 96 | return once(writable, 'close') as Promise; 97 | }, 98 | }; 99 | } 100 | 101 | close(): Promise { 102 | if (!this.writable.errored && this.writable.closed) { 103 | return fakePromise(); 104 | } 105 | if (this.writable.errored) { 106 | return fakeRejectPromise(this.writable.errored); 107 | } 108 | return fakePromise().then(() => endStream(this.writable)); 109 | } 110 | 111 | abort(reason: any): Promise { 112 | this.writable.destroy(reason); 113 | return once(this.writable, 'close') as Promise; 114 | } 115 | 116 | locked = false; 117 | } 118 | -------------------------------------------------------------------------------- /packages/node-fetch/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '@kamilkisiela/fast-url-parser' { 3 | class Url { 4 | static queryString: { 5 | parse(value: string): any; 6 | stringify(value: any): string; 7 | }; 8 | 9 | parse(urlString: string): void; 10 | parse( 11 | urlString: string, 12 | parseQueryString: false | undefined, 13 | slashesDenoteHost?: boolean, 14 | ): void; 15 | parse(urlString: string, parseQueryString: true, slashesDenoteHost?: boolean): void; 16 | parse(urlString: string, parseQueryString: boolean, slashesDenoteHost?: boolean): void; 17 | format(): string; 18 | auth: string; 19 | hash: string; 20 | host: string; 21 | hostname: string; 22 | href: string; 23 | path: string; 24 | pathname: string; 25 | protocol: string; 26 | search: string; 27 | slashes: boolean; 28 | port: string; 29 | query: string | any; 30 | } 31 | export = Url; 32 | } 33 | 34 | // TODO 35 | declare var libcurl: any; 36 | declare module 'scheduler/tracing' { 37 | export type Interaction = any; 38 | } 39 | -------------------------------------------------------------------------------- /packages/node-fetch/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { createReadStream, promises as fsPromises } from 'node:fs'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { fetchCurl } from './fetchCurl.js'; 5 | import { fetchNodeHttp } from './fetchNodeHttp.js'; 6 | import { PonyfillRequest, RequestPonyfillInit } from './Request.js'; 7 | import { PonyfillResponse } from './Response.js'; 8 | import { PonyfillURL } from './URL.js'; 9 | import { fakePromise } from './utils.js'; 10 | 11 | const BASE64_SUFFIX = ';base64'; 12 | 13 | async function getResponseForFile(url: string) { 14 | const path = fileURLToPath(url); 15 | try { 16 | await fsPromises.access(path, fsPromises.constants.R_OK); 17 | const stats = await fsPromises.stat(path, { 18 | bigint: true, 19 | }); 20 | const readable = createReadStream(path); 21 | return new PonyfillResponse(readable, { 22 | status: 200, 23 | statusText: 'OK', 24 | headers: { 25 | 'content-type': 'application/octet-stream', 26 | 'last-modified': stats.mtime.toUTCString(), 27 | }, 28 | }); 29 | } catch (err: any) { 30 | if (err.code === 'ENOENT') { 31 | return new PonyfillResponse(null, { 32 | status: 404, 33 | statusText: 'Not Found', 34 | }); 35 | } else if (err.code === 'EACCES') { 36 | return new PonyfillResponse(null, { 37 | status: 403, 38 | statusText: 'Forbidden', 39 | }); 40 | } 41 | throw err; 42 | } 43 | } 44 | 45 | function getResponseForDataUri(url: string) { 46 | const [mimeType = 'text/plain', ...datas] = url.substring(5).split(','); 47 | const data = decodeURIComponent(datas.join(',')); 48 | if (mimeType.endsWith(BASE64_SUFFIX)) { 49 | const buffer = Buffer.from(data, 'base64url'); 50 | const realMimeType = mimeType.slice(0, -BASE64_SUFFIX.length); 51 | return new PonyfillResponse(buffer, { 52 | status: 200, 53 | statusText: 'OK', 54 | headers: { 55 | 'content-type': realMimeType, 56 | }, 57 | }); 58 | } 59 | return new PonyfillResponse(data, { 60 | status: 200, 61 | statusText: 'OK', 62 | headers: { 63 | 'content-type': mimeType, 64 | }, 65 | }); 66 | } 67 | 68 | function getResponseForBlob(url: string) { 69 | const blob = PonyfillURL.getBlobFromURL(url); 70 | if (!blob) { 71 | throw new TypeError('Invalid Blob URL'); 72 | } 73 | return new PonyfillResponse(blob, { 74 | status: 200, 75 | headers: { 76 | 'content-type': blob.type, 77 | 'content-length': blob.size.toString(), 78 | }, 79 | }); 80 | } 81 | 82 | function isURL(obj: any): obj is URL { 83 | return obj != null && obj.href != null; 84 | } 85 | 86 | export function fetchPonyfill( 87 | info: string | PonyfillRequest | URL, 88 | init?: RequestPonyfillInit, 89 | ): Promise> { 90 | if (typeof info === 'string' || isURL(info)) { 91 | const ponyfillRequest = new PonyfillRequest(info, init); 92 | return fetchPonyfill(ponyfillRequest); 93 | } 94 | const fetchRequest = info; 95 | if (fetchRequest.url.startsWith('data:')) { 96 | const response = getResponseForDataUri(fetchRequest.url); 97 | return fakePromise(response); 98 | } 99 | 100 | if (fetchRequest.url.startsWith('file:')) { 101 | const response = getResponseForFile(fetchRequest.url); 102 | return response; 103 | } 104 | if (fetchRequest.url.startsWith('blob:')) { 105 | const response = getResponseForBlob(fetchRequest.url); 106 | return fakePromise(response); 107 | } 108 | if (globalThis.libcurl && !fetchRequest.agent) { 109 | return fetchCurl(fetchRequest); 110 | } 111 | return fetchNodeHttp(fetchRequest); 112 | } 113 | -------------------------------------------------------------------------------- /packages/node-fetch/src/index.ts: -------------------------------------------------------------------------------- 1 | export { fetchPonyfill as fetch } from './fetch.js'; 2 | export { PonyfillHeaders as Headers } from './Headers.js'; 3 | export { PonyfillBody as Body } from './Body.js'; 4 | export { PonyfillRequest as Request, type RequestPonyfillInit as RequestInit } from './Request.js'; 5 | export { 6 | PonyfillResponse as Response, 7 | type ResponsePonyfilInit as ResponseInit, 8 | } from './Response.js'; 9 | export { PonyfillReadableStream as ReadableStream } from './ReadableStream.js'; 10 | export { PonyfillFile as File } from './File.js'; 11 | export { PonyfillFormData as FormData } from './FormData.js'; 12 | export { PonyfillBlob as Blob } from './Blob.js'; 13 | export { 14 | PonyfillTextEncoder as TextEncoder, 15 | PonyfillTextDecoder as TextDecoder, 16 | PonyfillBtoa as btoa, 17 | } from './TextEncoderDecoder.js'; 18 | export { PonyfillURL as URL } from './URL.js'; 19 | export { PonyfillURLSearchParams as URLSearchParams } from './URLSearchParams.js'; 20 | export { PonyfillWritableStream as WritableStream } from './WritableStream.js'; 21 | export { PonyfillTransformStream as TransformStream } from './TransformStream.js'; 22 | export { PonyfillCompressionStream as CompressionStream } from './CompressionStream.js'; 23 | export { PonyfillDecompressionStream as DecompressionStream } from './DecompressionStream.js'; 24 | export { PonyfillIteratorObject as IteratorObject } from './IteratorObject.js'; 25 | export { 26 | PonyfillTextDecoderStream as TextDecoderStream, 27 | PonyfillTextEncoderStream as TextEncoderStream, 28 | } from './TextEncoderDecoderStream.js'; 29 | -------------------------------------------------------------------------------- /packages/node-fetch/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { once } from 'node:events'; 2 | import { IncomingMessage } from 'node:http'; 3 | import { PassThrough, Readable, Writable } from 'node:stream'; 4 | import { pipeline } from 'node:stream/promises'; 5 | 6 | function isHeadersInstance(obj: any): obj is Headers { 7 | return obj?.forEach != null; 8 | } 9 | 10 | export function getHeadersObj(headers: Headers): Record { 11 | if (headers == null || !isHeadersInstance(headers)) { 12 | return headers as any; 13 | } 14 | // @ts-expect-error - `headersInit` is not a public property 15 | if (headers.headersInit && !headers._map && !isHeadersInstance(headers.headersInit)) { 16 | // @ts-expect-error - `headersInit` is not a public property 17 | return headers.headersInit; 18 | } 19 | return Object.fromEntries(headers.entries()); 20 | } 21 | 22 | export function defaultHeadersSerializer( 23 | headers: Headers, 24 | onContentLength?: (value: string) => void, 25 | ): string[] { 26 | const headerArray: string[] = []; 27 | headers.forEach((value, key) => { 28 | if (onContentLength && key === 'content-length') { 29 | onContentLength(value); 30 | } 31 | headerArray.push(`${key}: ${value}`); 32 | }); 33 | return headerArray; 34 | } 35 | 36 | export { fakePromise } from '@whatwg-node/promise-helpers'; 37 | 38 | export function isArrayBufferView(obj: any): obj is ArrayBufferView { 39 | return obj != null && obj.buffer != null && obj.byteLength != null && obj.byteOffset != null; 40 | } 41 | 42 | export function isNodeReadable(obj: any): obj is Readable { 43 | return obj != null && obj.pipe != null; 44 | } 45 | 46 | export function isIterable(value: any): value is Iterable { 47 | return value?.[Symbol.iterator] != null; 48 | } 49 | 50 | export function shouldRedirect(status?: number): boolean { 51 | return status === 301 || status === 302 || status === 303 || status === 307 || status === 308; 52 | } 53 | 54 | export function wrapIncomingMessageWithPassthrough({ 55 | incomingMessage, 56 | signal, 57 | passThrough = new PassThrough(), 58 | onError = (e: Error) => { 59 | passThrough.destroy(e); 60 | }, 61 | }: { 62 | incomingMessage: IncomingMessage; 63 | passThrough?: PassThrough | undefined; 64 | signal?: AbortSignal | undefined; 65 | onError?: (e: Error) => void; 66 | }) { 67 | pipeline(incomingMessage, passThrough, { 68 | signal, 69 | end: true, 70 | }) 71 | .then(() => { 72 | if (!incomingMessage.destroyed) { 73 | incomingMessage.resume(); 74 | } 75 | }) 76 | .catch(onError); 77 | return passThrough; 78 | } 79 | 80 | export function endStream(stream: { end: () => void }) { 81 | // @ts-expect-error Avoid arguments adaptor trampoline https://v8.dev/blog/adaptor-frame 82 | return stream.end(null, null, null); 83 | } 84 | 85 | export function safeWrite(chunk: any, stream: Writable) { 86 | const result = stream.write(chunk); 87 | if (!result) { 88 | return once(stream, 'drain'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/Blob.spec.ts: -------------------------------------------------------------------------------- 1 | import { Buffer, Blob as NodeBlob } from 'node:buffer'; 2 | import { describe, expect, it } from '@jest/globals'; 3 | import { isArrayBuffer, PonyfillBlob } from '../src/Blob'; 4 | 5 | describe('Blob', () => { 6 | const blobParts: Record = { 7 | string: 'string', 8 | globalBlob: new Blob(['globalBlob']), 9 | nodeBlob: new NodeBlob(['nodeBlob']) as Blob, 10 | arrayBuffer: Buffer.from('arrayBuffer'), 11 | }; 12 | for (const [name, blobPart] of Object.entries(blobParts)) { 13 | describe(name, () => { 14 | describe('arrayBuffer', () => { 15 | it('content', async () => { 16 | const blob = new PonyfillBlob([blobPart]); 17 | const buffer = await blob.arrayBuffer(); 18 | expect(isArrayBuffer(buffer)).toBe(true); 19 | expect(Buffer.from(buffer, undefined, buffer.byteLength).toString('utf-8')).toBe(name); 20 | }); 21 | it('size', async () => { 22 | const blob = new PonyfillBlob([blobPart]); 23 | const buffer = await blob.arrayBuffer(); 24 | expect(blob.size).toBe(buffer.byteLength); 25 | }); 26 | }); 27 | describe('text', () => { 28 | it('content', async () => { 29 | const blob = new PonyfillBlob([blobPart]); 30 | const text = await blob.text(); 31 | expect(typeof text).toBe('string'); 32 | expect(text).toBe(name); 33 | }); 34 | it('size', async () => { 35 | const blob = new PonyfillBlob([blobPart]); 36 | const text = await blob.text(); 37 | expect(blob.size).toBe(Buffer.byteLength(text)); 38 | }); 39 | }); 40 | describe('stream', () => { 41 | it('content', async () => { 42 | const blob = new PonyfillBlob([blobPart]); 43 | const stream = blob.stream(); 44 | expect(typeof stream[Symbol.asyncIterator]).toBe('function'); 45 | const chunks: Buffer[] = []; 46 | for await (const chunk of stream) { 47 | chunks.push(chunk); 48 | } 49 | expect(Buffer.concat(chunks).toString('utf-8')).toBe(name); 50 | }); 51 | it('size', async () => { 52 | const blob = new PonyfillBlob([blobPart]); 53 | const stream = blob.stream(); 54 | let size = 0; 55 | for await (const chunk of stream) { 56 | size += chunk.length; 57 | } 58 | expect(blob.size).toBe(size); 59 | }); 60 | }); 61 | }); 62 | } 63 | it('together', async () => { 64 | const blob = new PonyfillBlob([ 65 | blobParts.string, 66 | blobParts.globalBlob, 67 | blobParts.nodeBlob, 68 | blobParts.arrayBuffer, 69 | ]); 70 | const text = await blob.text(); 71 | expect(text).toBe('stringglobalBlobnodeBlobarrayBuffer'); 72 | const buffer = await blob.arrayBuffer(); 73 | expect(Buffer.from(buffer, undefined, buffer.byteLength).toString('utf-8')).toBe( 74 | 'stringglobalBlobnodeBlobarrayBuffer', 75 | ); 76 | const stream = blob.stream(); 77 | const chunks: Buffer[] = []; 78 | for await (const chunk of stream) { 79 | chunks.push(chunk); 80 | } 81 | expect(Buffer.concat(chunks).toString('utf-8')).toBe('stringglobalBlobnodeBlobarrayBuffer'); 82 | }); 83 | it('empty', async () => { 84 | const blob = new PonyfillBlob(); 85 | expect(blob.size).toBe(0); 86 | expect(await blob.text()).toBe(''); 87 | expect(Buffer.from(await blob.arrayBuffer()).toString()).toBe(''); 88 | const stream = blob.stream(); 89 | const chunks: Buffer[] = []; 90 | for await (const chunk of stream) { 91 | chunks.push(chunk); 92 | } 93 | expect(Buffer.concat(chunks).toString()); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/Body.spec.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { Readable } from 'node:stream'; 3 | import { describe, expect, it, jest } from '@jest/globals'; 4 | import { PonyfillBlob } from '../src/Blob.js'; 5 | import { PonyfillBody } from '../src/Body.js'; 6 | import { PonyfillTextDecoder } from '../src/TextEncoderDecoder.js'; 7 | 8 | const exampleData = { 9 | data: { 10 | hello: 'world', 11 | }, 12 | }; 13 | const examples = { 14 | Blob: new PonyfillBlob([JSON.stringify(exampleData)], { type: 'application/json' }), 15 | Buffer: Buffer.from(JSON.stringify(exampleData)), 16 | ArrayBuffer: new Uint8Array(Buffer.from(JSON.stringify(exampleData))).buffer, 17 | String: JSON.stringify(exampleData), 18 | Uint8Array: new Uint8Array(Buffer.from(JSON.stringify(exampleData))), 19 | }; 20 | 21 | function runExamples(fn: (body: PonyfillBody) => void | Promise) { 22 | const exampleTypes = Object.keys(examples) as (keyof typeof examples)[]; 23 | exampleTypes.forEach(exampleName => { 24 | const example = examples[exampleName]; 25 | exampleTypes.forEach(toType => { 26 | it(`from ${exampleName} to ${toType}`, (): any => { 27 | const body = new PonyfillBody(example); 28 | return fn(body); 29 | }); 30 | }); 31 | }); 32 | } 33 | 34 | describe('Body', () => { 35 | describe('should parse correctly', () => { 36 | runExamples(async body => { 37 | const result = await body.json(); 38 | expect(result).toMatchObject(exampleData); 39 | }); 40 | }); 41 | describe('performance optimizations', () => { 42 | describe('should not generate a body stream', () => { 43 | runExamples(async body => { 44 | expect(body['_generatedBody']).toBe(null); 45 | }); 46 | }); 47 | it('should not create a Blob for a basic text body', async () => { 48 | const readable = Readable.from(Buffer.from('hello world')); 49 | const body = new PonyfillBody(readable); 50 | jest.spyOn(PonyfillBody.prototype, 'blob'); 51 | const result = await body.text(); 52 | expect(result).toBe('hello world'); 53 | expect(body.blob).not.toHaveBeenCalled(); 54 | }); 55 | }); 56 | it('works with empty responses', async () => { 57 | const body = new PonyfillBody(null); 58 | const result = await body.text(); 59 | expect(result).toBe(''); 60 | }); 61 | it('works with custom decoding', async () => { 62 | const body = new PonyfillBody('hello world'); 63 | const buf = await body.bytes(); 64 | const decoder = new PonyfillTextDecoder('utf-8'); 65 | const result = decoder.decode(buf); 66 | expect(result).toBe('hello world'); 67 | }); 68 | 69 | it('throws an error if the body is unable to parse as FormData', async () => { 70 | const formStr = 71 | '--Boundary_with_capital_letters\r\n' + 72 | 'Content-Type: application/json\r\n' + 73 | 'Content-Disposition: form-data; name="does_this_work"\r\n' + 74 | '\r\n' + 75 | 'YES\r\n' + 76 | '--Boundary_with_capital_letters-Random junk'; 77 | 78 | const body = new PonyfillBody( 79 | new PonyfillBlob([formStr], { 80 | type: 'multipart/form-data; boundary=Boundary_with_capital_letters', 81 | }), 82 | ); 83 | 84 | let err; 85 | try { 86 | await body.formData(); 87 | } catch (e) { 88 | err = e; 89 | } 90 | expect(String(err)).toContain('Unexpected end of multipart data'); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/Headers.spec.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { describe, expect, it } from '@jest/globals'; 3 | import { fetchPonyfill } from '../src/fetch.js'; 4 | import { PonyfillHeaders } from '../src/Headers.js'; 5 | 6 | describe('Headers', () => { 7 | const baseUrl = process.env.CI ? 'http://localhost:8888' : 'https://httpbin.org'; 8 | it('be case-insensitive', () => { 9 | const headers = new PonyfillHeaders(); 10 | headers.set('X-Header', 'foo'); 11 | expect(headers.get('x-header')).toBe('foo'); 12 | headers.append('x-HEADER', 'bar'); 13 | expect(headers.get('X-HEADER')).toBe('foo, bar'); 14 | }); 15 | describe('performance optimizations', () => { 16 | it('should not create a map if the input is an object and only getter is used', () => { 17 | const headersInit = { 18 | 'X-Header': 'foo', 19 | }; 20 | const headers = new PonyfillHeaders(headersInit); 21 | headersInit['X-Header'] = 'bar'; 22 | expect(headers.get('x-header')).toBe('bar'); 23 | }); 24 | }); 25 | // TODO 26 | it.skip('should respect custom header serializer', async () => { 27 | const res = await fetchPonyfill(`${baseUrl}/headers`, { 28 | headersSerializer() { 29 | return ['X-Test: test', 'Accept: application/json']; 30 | }, 31 | }); 32 | expect(res.status).toBe(200); 33 | const body = await res.json(); 34 | expect(body).toMatchObject({ 35 | headers: { 36 | 'X-Test': 'test', 37 | Accept: 'application/json', 38 | }, 39 | }); 40 | }); 41 | it('should work with node.util.inspect', () => { 42 | const headers = new PonyfillHeaders(); 43 | headers.set('X-Header', 'foo'); 44 | expect(inspect(headers)).toBe("Headers { 'x-header': 'foo' }"); 45 | }); 46 | it('should iterate each set-cookie individually', () => { 47 | const headers = new PonyfillHeaders(); 48 | headers.append('set-cookie', 'foo'); 49 | headers.append('set-cookie', 'bar'); 50 | const headerEntries: [string, string][] = []; 51 | headers.forEach((value, key) => { 52 | headerEntries.push([key, value]); 53 | }); 54 | expect(headerEntries).toEqual([ 55 | ['set-cookie', 'foo'], 56 | ['set-cookie', 'bar'], 57 | ]); 58 | }); 59 | it('inspect correctly with null header values', () => { 60 | const headers = new PonyfillHeaders(); 61 | headers.set('X-Header', null!); 62 | expect(inspect(headers)).toBe("Headers { 'x-header': null }"); 63 | }); 64 | describe('Set-Cookie', () => { 65 | it('handles values in the given map for get method', () => { 66 | const headers = new PonyfillHeaders([ 67 | ['set-cookie', 'a=b'], 68 | ['set-cookie', 'c=d'], 69 | ]); 70 | expect(headers.get('Set-Cookie')).toBe('a=b, c=d'); 71 | }); 72 | it('handles values in the given map for getSetCookie method', () => { 73 | const headers = new PonyfillHeaders([ 74 | ['set-cookie', 'a=b'], 75 | ['set-cookie', 'c=d'], 76 | ]); 77 | expect(headers.getSetCookie()).toEqual(['a=b', 'c=d']); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/Request.spec.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from 'node:http'; 2 | import { describe, expect, it } from '@jest/globals'; 3 | import { PonyfillRequest } from '../src/Request.js'; 4 | 5 | const skipIf = (condition: boolean) => (condition ? it.skip : it); 6 | 7 | describe('Request', () => { 8 | it('should normalize the method name', () => { 9 | const req = new PonyfillRequest('http://a', { method: 'get' }); 10 | expect(req.method).toBe('GET'); 11 | }); 12 | 13 | skipIf(!!globalThis.Deno)( 14 | 'should instatitate PonyfillRequest from a Request correctly', 15 | async () => { 16 | const req = new Request('http://a', { 17 | method: 'put', 18 | headers: { 'x-test': '1' }, 19 | body: 'test', 20 | }); 21 | 22 | const ponyReq = new PonyfillRequest(req); 23 | expect(ponyReq.method).toBe('PUT'); 24 | expect(ponyReq.headers.get('x-test')).toBe('1'); 25 | expect(await ponyReq.text()).toBe('test'); 26 | }, 27 | ); 28 | 29 | it('should instatitate PonyfillRequest from another PonyfillRequest correctly', async () => { 30 | const firstPony = new PonyfillRequest('http://a', { 31 | method: 'put', 32 | headers: { 'x-test': '1' }, 33 | body: 'test', 34 | }); 35 | 36 | const secondPony = new PonyfillRequest(firstPony); 37 | expect(secondPony.method).toBe('PUT'); 38 | expect(secondPony.headers.get('x-test')).toBe('1'); 39 | expect(await secondPony.text()).toBe('test'); 40 | }); 41 | 42 | it('should allow agent as RequestInfo and RequestInit', async () => { 43 | const agent = new Agent(); 44 | const firstPony = new PonyfillRequest('http://a', { 45 | agent, 46 | }); 47 | const secondPony = new PonyfillRequest(firstPony); 48 | expect(firstPony.agent).toBe(agent); 49 | expect(secondPony.agent).toBe(agent); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/TextEncoderDecoderStream.spec.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { describe, expect, it } from '@jest/globals'; 3 | import { runTestsForEachFetchImpl } from '../../server/test/test-fetch'; 4 | 5 | describe('TextEncoderDecoderStream', () => { 6 | runTestsForEachFetchImpl( 7 | (_, { fetchAPI }) => { 8 | it('TextEncoderStream', async () => { 9 | const readableStream = new fetchAPI.ReadableStream({ 10 | start(controller) { 11 | controller.enqueue(Buffer.from('Hello, ')); 12 | controller.enqueue(Buffer.from('world!')); 13 | controller.close(); 14 | }, 15 | }); 16 | const pipedStream = readableStream.pipeThrough(new fetchAPI.TextEncoderStream()); 17 | const reader = pipedStream.getReader(); 18 | const chunks: Uint8Array[] = []; 19 | while (true) { 20 | const { done, value } = await reader.read(); 21 | if (done) { 22 | break; 23 | } 24 | chunks.push(value); 25 | } 26 | const encoded = Buffer.concat(chunks); 27 | expect(encoded.toString('utf-8')).toBe('Hello, world!'); 28 | }); 29 | it('TextDecoderStream', async () => { 30 | const textEncoder = new fetchAPI.TextEncoder(); 31 | const decodedHello = textEncoder.encode('Hello, '); 32 | const decodedWorld = textEncoder.encode('world!'); 33 | const readableStream = new fetchAPI.ReadableStream({ 34 | start(controller) { 35 | controller.enqueue(decodedHello); 36 | controller.enqueue(decodedWorld); 37 | controller.close(); 38 | }, 39 | }); 40 | const chunks: string[] = []; 41 | const pipedStream = readableStream.pipeThrough(new fetchAPI.TextDecoderStream()); 42 | const reader = pipedStream.getReader(); 43 | while (true) { 44 | const { done, value } = await reader.read(); 45 | if (done) { 46 | break; 47 | } 48 | chunks.push(value); 49 | } 50 | expect(chunks.join('')).toBe('Hello, world!'); 51 | }); 52 | it('piped cancellation works', async () => { 53 | const expectedError = new Error('test error'); 54 | const thrownError = await new Promise(resolve => 55 | new fetchAPI.ReadableStream({ 56 | cancel: resolve, 57 | }) 58 | .pipeThrough(new fetchAPI.TextEncoderStream()) 59 | .cancel(expectedError) 60 | .then( 61 | () => {}, 62 | () => {}, 63 | ), 64 | ); 65 | expect(thrownError).toBe(expectedError); 66 | }); 67 | }, 68 | { noLibCurl: true }, 69 | ); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/btoa.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@jest/globals'; 2 | import { PonyfillBtoa } from '../src/TextEncoderDecoder.js'; 3 | 4 | it('should work as expected', () => { 5 | expect(PonyfillBtoa('Hello, world!')).toBe('SGVsbG8sIHdvcmxkIQ=='); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/cleanup-resources.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from '@jest/globals'; 2 | import { runTestsForEachFetchImpl } from '../../server/test/test-fetch'; 3 | import { runTestsForEachServerImpl } from '../../server/test/test-server'; 4 | 5 | const describeIf = (condition: boolean) => (condition ? describe : describe.skip); 6 | 7 | describeIf(!globalThis.Deno)('Cleanup Resources', () => { 8 | runTestsForEachFetchImpl((_, { createServerAdapter, fetchAPI: { Response, fetch } }) => { 9 | describe('internal calls', () => { 10 | runTestsForEachServerImpl(testServer => { 11 | beforeEach(async () => { 12 | await testServer.addOnceHandler( 13 | createServerAdapter(() => Response.json({ test: 'test' })), 14 | ); 15 | }); 16 | it('should free resources when body is not consumed', async () => { 17 | const response = await fetch(testServer.url); 18 | expect(response.ok).toBe(true); 19 | }); 20 | }); 21 | }); 22 | describe('external calls', () => { 23 | it('http - should free resources when body is not consumed', async () => { 24 | const response = await fetch('http://google.com'); 25 | expect(response.ok).toBe(true); 26 | }); 27 | it('https - should free resources when body is not consumed', async () => { 28 | const response = await fetch('https://google.com'); 29 | expect(response.ok).toBe(true); 30 | }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/fixtures/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/http2.spec.ts: -------------------------------------------------------------------------------- 1 | import { unlink, writeFile } from 'node:fs/promises'; 2 | import { createSecureServer, ServerHttp2Session, type Http2SecureServer } from 'node:http2'; 3 | import { AddressInfo } from 'node:net'; 4 | import { tmpdir } from 'node:os'; 5 | import { join } from 'node:path'; 6 | import type { CertificateCreationResult } from 'pem'; 7 | import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; 8 | import { fetchPonyfill } from '../src/fetch'; 9 | 10 | const describeIf = (condition: boolean) => (condition ? describe : describe.skip); 11 | describeIf(globalThis.libcurl && !process.env.LEAK_TEST && !globalThis.Deno)('http2', () => { 12 | let server: Http2SecureServer; 13 | let pemPath: string; 14 | const oldEnvVar = process.env.NODE_EXTRA_CA_CERTS; 15 | const sessions = new Set(); 16 | beforeAll(async () => { 17 | const { createCertificate } = await import('pem'); 18 | const keys = await new Promise((resolve, reject) => { 19 | createCertificate( 20 | { 21 | selfSigned: true, 22 | days: 1, 23 | }, 24 | (err, result) => { 25 | if (err) { 26 | reject(err); 27 | } 28 | resolve(result); 29 | }, 30 | ); 31 | }); 32 | pemPath = join(tmpdir(), 'test.pem'); 33 | process.env.NODE_EXTRA_CA_CERTS = pemPath; 34 | await writeFile(pemPath, keys.certificate); 35 | // Create a secure HTTP/2 server 36 | server = createSecureServer( 37 | { 38 | allowHTTP1: false, 39 | key: keys.serviceKey, 40 | cert: keys.certificate, 41 | }, 42 | (request, response) => { 43 | response.writeHead(200, { 44 | 'Content-Type': 'application/json', 45 | }); 46 | response.end(JSON.stringify(request.headers)); 47 | }, 48 | ); 49 | 50 | server.on('session', session => { 51 | sessions.add(session); 52 | session.once('close', () => { 53 | sessions.delete(session); 54 | }); 55 | }); 56 | 57 | await new Promise(resolve => server.listen(0, resolve)); 58 | }); 59 | afterAll(async () => { 60 | await unlink(pemPath); 61 | process.env.NODE_EXTRA_CA_CERTS = oldEnvVar; 62 | for (const session of sessions) { 63 | session.destroy(); 64 | } 65 | await new Promise(resolve => server.close(resolve)); 66 | }); 67 | it('works', async () => { 68 | const res = await fetchPonyfill(`https://localhost:${(server.address() as AddressInfo).port}`, { 69 | headers: { 70 | 'x-foo': 'bar', 71 | }, 72 | }); 73 | const resJson = await res.json(); 74 | expect(resJson['x-foo']).toBe('bar'); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/non-http-fetch.spec.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { join } from 'node:path'; 3 | import { pathToFileURL } from 'node:url'; 4 | import { describe, expect, it } from '@jest/globals'; 5 | import { fetchPonyfill } from '../src/fetch.js'; 6 | 7 | describe('File protocol', () => { 8 | it('reads', async () => { 9 | const response = await fetchPonyfill( 10 | pathToFileURL(join(process.cwd(), './packages/node-fetch/tests/fixtures/test.json')), 11 | ); 12 | expect(response.status).toBe(200); 13 | const body = await response.json(); 14 | expect(body.foo).toBe('bar'); 15 | }); 16 | it('returns 404 if file does not exist', async () => { 17 | const response = await fetchPonyfill( 18 | pathToFileURL(join(process.cwd(), './packages/node-fetch/tests/fixtures/missing.json')), 19 | ); 20 | expect(response.status).toBe(404); 21 | }); 22 | it('returns 403 if file is not accessible', async () => { 23 | // this can yield a false negative if the test is run with sufficient privileges 24 | // TODO: consistent behavior across platforms 25 | const response = await fetchPonyfill(pathToFileURL('/root/private_data.txt')); 26 | expect(response.status).toBe(403); 27 | }); 28 | }); 29 | 30 | describe('data uris', () => { 31 | it('should accept base64-encoded gif data uri', async () => { 32 | const mimeType = 'image/gif'; 33 | const base64Part = 'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; 34 | const length = 35; 35 | const b64 = `data:${mimeType};base64,${base64Part}`; 36 | const res = await fetchPonyfill(b64); 37 | expect(res.status).toBe(200); 38 | expect(res.headers.get('Content-Type')).toBe(mimeType); 39 | expect(res.headers.get('Content-Length')).toBe(length.toString()); 40 | const buf = await res.bytes(); 41 | expect(Buffer.from(buf).toString('base64')).toBe(base64Part); 42 | }); 43 | it('should accept data uri with specified charset', async () => { 44 | const r = await fetchPonyfill('data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678'); 45 | expect(r.status).toBe(200); 46 | expect(r.headers.get('Content-Type')).toBe('text/plain;charset=UTF-8;page=21'); 47 | 48 | const b = await r.text(); 49 | expect(b).toBe('the data:1234,5678'); 50 | }); 51 | 52 | it('should accept data uri of plain text', async () => { 53 | const r = await fetchPonyfill('data:,Hello%20World!'); 54 | expect(r.status).toBe(200); 55 | const text = await r.text(); 56 | expect(text).toBe('Hello World!'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/node-fetch/tests/redirect.spec.ts: -------------------------------------------------------------------------------- 1 | import { createServer, IncomingMessage, RequestListener, Server, ServerResponse } from 'node:http'; 2 | import { AddressInfo } from 'node:net'; 3 | import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; 4 | import { runTestsForEachFetchImpl } from '../../server/test/test-fetch'; 5 | 6 | describe('Redirections', () => { 7 | runTestsForEachFetchImpl((_, { fetchAPI }) => { 8 | const redirectionStatusCodes = [301, 302, 303, 307, 308]; 9 | const nonRedirectionLocationStatusCodes = [200, 201, 204]; 10 | let requestListener: RequestListener; 11 | let server: Server; 12 | let addressInfo: AddressInfo; 13 | beforeEach(() => { 14 | requestListener = jest.fn((req: IncomingMessage, res: ServerResponse) => { 15 | if (req.url?.startsWith('/status-')) { 16 | const [_, statusCode] = req.url.split('-'); 17 | res.writeHead(Number(statusCode), { 18 | Location: '/redirected', 19 | }); 20 | res.end(); 21 | } else if (req.url === '/redirected') { 22 | res.writeHead(200); 23 | res.end('redirected'); 24 | } 25 | }); 26 | return new Promise(resolve => { 27 | server = createServer(requestListener).listen(0, () => { 28 | addressInfo = server.address() as AddressInfo; 29 | resolve(); 30 | }); 31 | }); 32 | }); 33 | afterEach(done => { 34 | server.close(done); 35 | }); 36 | for (const statusCode of redirectionStatusCodes) { 37 | it(`should follow ${statusCode} redirection`, async () => { 38 | const res = await fetchAPI.fetch( 39 | `http://localhost:${addressInfo.port}/status-${statusCode}`, 40 | ); 41 | expect(res.status).toBe(200); 42 | expect(await res.text()).toBe('redirected'); 43 | expect(requestListener).toHaveBeenCalledTimes(2); 44 | }); 45 | } 46 | for (const statusCode of nonRedirectionLocationStatusCodes) { 47 | it(`should not follow ${statusCode} redirection with Location header`, async () => { 48 | const res = await fetchAPI.fetch( 49 | `http://localhost:${addressInfo.port}/status-${statusCode}`, 50 | ); 51 | expect(res.status).toBe(statusCode); 52 | expect(res.headers.get('Location')).toBe('/redirected'); 53 | expect(await res.text()).toBe(''); 54 | expect(requestListener).toHaveBeenCalledTimes(1); 55 | }); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/promise-helpers/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @whatwg-node/promise-helpers 2 | 3 | ## 1.3.2 4 | 5 | ### Patch Changes 6 | 7 | - [#2408](https://github.com/ardatan/whatwg-node/pull/2408) 8 | [`d86b4f3`](https://github.com/ardatan/whatwg-node/commit/d86b4f3df884709145023bf32bb1022c4a8bb9cb) 9 | Thanks [@slagiewka](https://github.com/slagiewka)! - Reuse fake promise Symbol 10 | 11 | ## 1.3.1 12 | 13 | ### Patch Changes 14 | 15 | - [#2276](https://github.com/ardatan/whatwg-node/pull/2276) 16 | [`6bf6aa0`](https://github.com/ardatan/whatwg-node/commit/6bf6aa0b6d4e0c7524aec55fb666147d0862c9b9) 17 | Thanks [@andreialecu](https://github.com/andreialecu)! - Fix types by replacing `VoidFunction` 18 | type to `() => void` 19 | 20 | ## 1.3.0 21 | 22 | ### Minor Changes 23 | 24 | - [#2152](https://github.com/ardatan/whatwg-node/pull/2152) 25 | [`54a26bb`](https://github.com/ardatan/whatwg-node/commit/54a26bb5c568fdd43945c0050889c1413ebf9391) 26 | Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - Allow to pass a finally callback to 27 | `handleMaybePromise` 28 | 29 | ## 1.2.5 30 | 31 | ### Patch Changes 32 | 33 | - [#2182](https://github.com/ardatan/whatwg-node/pull/2182) 34 | [`a45e929`](https://github.com/ardatan/whatwg-node/commit/a45e9290cdc110392d9175d2780c96ad4fd31727) 35 | Thanks [@ardatan](https://github.com/ardatan)! - - Name functions in `iterateAsync` for more 36 | readable traces 37 | - `fakePromise` accepts `MaybePromise` as an input 38 | 39 | ## 1.2.4 40 | 41 | ### Patch Changes 42 | 43 | - [`a448fd1`](https://github.com/ardatan/whatwg-node/commit/a448fd130ace70f5c65e8ad5a28846a7af8d9777) 44 | Thanks [@ardatan](https://github.com/ardatan)! - Do not consider fake promises as real promises 45 | 46 | ## 1.2.3 47 | 48 | ### Patch Changes 49 | 50 | - [#2068](https://github.com/ardatan/whatwg-node/pull/2068) 51 | [`516bf60`](https://github.com/ardatan/whatwg-node/commit/516bf60b55babd57e1721d404a01c526ec218acf) 52 | Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - Fix return type of the callback of 53 | `iterateAsync`. The callback can actually return `null` or `undefined`, the implementation is 54 | already handling this case. 55 | 56 | ## 1.2.2 57 | 58 | ### Patch Changes 59 | 60 | - [#2123](https://github.com/ardatan/whatwg-node/pull/2123) 61 | [`2ca563a`](https://github.com/ardatan/whatwg-node/commit/2ca563a205d12fa6f0bfe2fec39c838b757f7319) 62 | Thanks [@ardatan](https://github.com/ardatan)! - Use Node 16 at least to prevent breaking change 63 | on dependent Tools packages 64 | 65 | ## 1.2.1 66 | 67 | ### Patch Changes 68 | 69 | - [`a587b3d`](https://github.com/ardatan/whatwg-node/commit/a587b3dd1e8a5791ee01ce90d96d3527e0091f99) 70 | Thanks [@ardatan](https://github.com/ardatan)! - Fix the termination of the loop in `iterateAsync` 71 | 72 | ## 1.2.0 73 | 74 | ### Minor Changes 75 | 76 | - [`156f85f`](https://github.com/ardatan/whatwg-node/commit/156f85f0de1c43ee62f745132f315f3dc5b9a42b) 77 | Thanks [@ardatan](https://github.com/ardatan)! - Pass `index` to `iterateAsync` 78 | 79 | ## 1.1.0 80 | 81 | ### Minor Changes 82 | 83 | - [`fae5127`](https://github.com/ardatan/whatwg-node/commit/fae5127a1de3aa76c8b1ff21cba9ce7901d47584) 84 | Thanks [@ardatan](https://github.com/ardatan)! - Add `iterateAsync` 85 | 86 | ## 1.0.0 87 | 88 | ### Major Changes 89 | 90 | - [#2102](https://github.com/ardatan/whatwg-node/pull/2102) 91 | [`5cf6b2d`](https://github.com/ardatan/whatwg-node/commit/5cf6b2dbc589f4330c5efdee96356f48e438ae9e) 92 | Thanks [@ardatan](https://github.com/ardatan)! - New promise helpers 93 | -------------------------------------------------------------------------------- /packages/promise-helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/promise-helpers", 3 | "version": "1.3.2", 4 | "type": "module", 5 | "description": "Promise helpers", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/promise-helpers" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=16.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "tslib": "^2.6.3" 38 | }, 39 | "publishConfig": { 40 | "directory": "dist", 41 | "access": "public" 42 | }, 43 | "sideEffects": false, 44 | "buildOptions": { 45 | "input": "./src/index.ts" 46 | }, 47 | "typescript": { 48 | "definition": "dist/typings/index.d.ts" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/promise-helpers/tests/fakePromise.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, jest } from '@jest/globals'; 2 | import { fakePromise, handleMaybePromise } from '@whatwg-node/promise-helpers'; 3 | 4 | it('should not consider fakePromises as Promises', () => { 5 | const fake$ = fakePromise('value'); 6 | const thenSpy = jest.fn(val => val); 7 | const returnVal = handleMaybePromise(() => fake$, thenSpy); 8 | expect(thenSpy).toHaveBeenCalledWith('value'); 9 | expect(returnVal).toBe('value'); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/promise-helpers/tests/mapAsyncIterator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, jest } from '@jest/globals'; 2 | import { mapAsyncIterator } from '../src/index'; 3 | 4 | describe('mapAsyncIterator', () => { 5 | it('should invoke onNext callback, for each value, replace results and invoke onEnd only once regardless of how many times return was called', async () => { 6 | const onNext = jest.fn(() => 'replacer'); 7 | const onEnd = jest.fn(); 8 | const iter = mapAsyncIterator( 9 | (async function* () { 10 | yield 1; 11 | yield 2; 12 | yield 3; 13 | })(), 14 | onNext, 15 | () => { 16 | // noop onError 17 | }, 18 | // @ts-expect-error - noop 19 | onEnd, 20 | ); 21 | const onNextResults: string[] = []; 22 | for await (const result of iter) { 23 | onNextResults.push(result); 24 | } 25 | await Promise.all([iter.return?.(), iter.return?.(), iter.return?.()]); 26 | expect(onNext).toHaveBeenCalledTimes(3); 27 | expect(onNextResults).toEqual(['replacer', 'replacer', 'replacer']); 28 | expect(onEnd).toHaveBeenCalledTimes(1); 29 | }); 30 | 31 | it('should invoke onError only once regardless of how many times throw was called', async () => { 32 | const err = new Error('Woopsie!'); 33 | const onNext = jest.fn(); 34 | const onError = jest.fn(); 35 | const iter = mapAsyncIterator( 36 | (async function* () { 37 | yield 1; 38 | yield 2; 39 | yield 3; 40 | })(), 41 | onNext, 42 | onError, 43 | ); 44 | for await (const _ of iter) { 45 | // noop 46 | } 47 | await Promise.all([iter.throw?.(err), iter.throw?.(err), iter.throw?.(err)]); 48 | expect(onNext).toHaveBeenCalledTimes(3); 49 | expect(onError).toHaveBeenCalledWith(err); 50 | expect(onError).toHaveBeenCalledTimes(1); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/server-plugin-cookies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/server-plugin-cookies", 3 | "version": "1.0.5", 4 | "type": "module", 5 | "description": "Cookies Plugin", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/server-plugin-cookies" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "@whatwg-node/cookie-store": "^0.2.2", 38 | "@whatwg-node/server": "^0.10.0", 39 | "tslib": "^2.6.3" 40 | }, 41 | "publishConfig": { 42 | "directory": "dist", 43 | "access": "public" 44 | }, 45 | "sideEffects": false, 46 | "buildOptions": { 47 | "input": "./src/index.ts" 48 | }, 49 | "typescript": { 50 | "definition": "dist/typings/index.d.ts" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/server-plugin-cookies/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCookies.js'; 2 | -------------------------------------------------------------------------------- /packages/server-plugin-cookies/src/useCookies.ts: -------------------------------------------------------------------------------- 1 | import { CookieStore, getCookieString } from '@whatwg-node/cookie-store'; 2 | import { ServerAdapterPlugin } from '@whatwg-node/server'; 3 | 4 | declare global { 5 | interface Request { 6 | cookieStore?: CookieStore; 7 | } 8 | } 9 | 10 | export function useCookies(): ServerAdapterPlugin { 11 | const cookieStringsByRequest = new WeakMap(); 12 | return { 13 | onRequest({ request }) { 14 | const cookieStrings: string[] = []; 15 | request.cookieStore = new CookieStore(request.headers.get('cookie') ?? ''); 16 | request.cookieStore.onchange = function ({ changed, deleted }) { 17 | changed.forEach(cookie => { 18 | cookieStrings.push(getCookieString(cookie)); 19 | }); 20 | deleted.forEach(cookie => { 21 | cookieStrings.push(getCookieString({ ...cookie, value: undefined })); 22 | }); 23 | }; 24 | cookieStringsByRequest.set(request, cookieStrings); 25 | }, 26 | onResponse({ request, response }) { 27 | const cookieStrings = cookieStringsByRequest.get(request); 28 | cookieStrings?.forEach(cookieString => { 29 | response.headers.append('Set-Cookie', cookieString); 30 | }); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@whatwg-node/server", 3 | "version": "0.10.10", 4 | "type": "module", 5 | "description": "Fetch API compliant HTTP Server adapter", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/whatwg-node", 9 | "directory": "packages/server" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "@envelop/instrumentation": "^1.0.0", 38 | "@whatwg-node/disposablestack": "^0.0.6", 39 | "@whatwg-node/fetch": "^0.10.8", 40 | "@whatwg-node/promise-helpers": "^1.3.2", 41 | "tslib": "^2.6.3" 42 | }, 43 | "devDependencies": { 44 | "@hapi/hapi": "21.4.0", 45 | "@types/compression": "1.8.0", 46 | "@types/express": "5.0.2", 47 | "@types/koa": "2.15.0", 48 | "@types/node": "22.15.27", 49 | "compression": "1.8.0", 50 | "express": "5.1.0", 51 | "fastify": "5.3.3", 52 | "form-data": "4.0.2", 53 | "koa": "3.0.0", 54 | "react": "19.1.0", 55 | "react-dom": "19.1.0" 56 | }, 57 | "publishConfig": { 58 | "directory": "dist", 59 | "access": "public" 60 | }, 61 | "sideEffects": false, 62 | "buildOptions": { 63 | "input": "./src/index.ts" 64 | }, 65 | "typescript": { 66 | "definition": "dist/typings/index.d.ts" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createServerAdapter.js'; 2 | export * from './types.js'; 3 | export * from './utils.js'; 4 | export * from './plugins/types.js'; 5 | export * from './plugins/useCors.js'; 6 | export * from './plugins/useErrorHandling.js'; 7 | export * from './plugins/useContentEncoding.js'; 8 | export * from './uwebsockets.js'; 9 | export { Response } from '@whatwg-node/fetch'; 10 | export { DisposableSymbols } from '@whatwg-node/disposablestack'; 11 | export * from '@envelop/instrumentation'; 12 | -------------------------------------------------------------------------------- /packages/server/src/plugins/useContentEncoding.ts: -------------------------------------------------------------------------------- 1 | import { decompressedResponseMap, getSupportedEncodings } from '../utils.js'; 2 | import type { ServerAdapterPlugin } from './types.js'; 3 | 4 | const emptyEncodings = ['none', 'identity']; 5 | 6 | export function useContentEncoding(): ServerAdapterPlugin { 7 | return { 8 | onRequest({ request, setRequest, fetchAPI, endResponse }) { 9 | const contentEncodingHeader = request.headers.get('content-encoding'); 10 | if ( 11 | contentEncodingHeader && 12 | contentEncodingHeader !== 'none' && 13 | contentEncodingHeader !== 'identity' && 14 | request.body 15 | ) { 16 | const contentEncodings = contentEncodingHeader 17 | .split(',') 18 | .filter(encoding => !emptyEncodings.includes(encoding)) as CompressionFormat[]; 19 | if (contentEncodings.length) { 20 | if ( 21 | !contentEncodings.every(encoding => getSupportedEncodings(fetchAPI).includes(encoding)) 22 | ) { 23 | endResponse( 24 | new fetchAPI.Response(`Unsupported 'Content-Encoding': ${contentEncodingHeader}`, { 25 | status: 415, 26 | statusText: 'Unsupported Media Type', 27 | }), 28 | ); 29 | return; 30 | } 31 | let newBody = request.body; 32 | for (const contentEncoding of contentEncodings) { 33 | newBody = request.body.pipeThrough(new fetchAPI.DecompressionStream(contentEncoding)); 34 | } 35 | setRequest( 36 | new fetchAPI.Request(request.url, { 37 | body: newBody, 38 | cache: request.cache, 39 | credentials: request.credentials, 40 | headers: request.headers, 41 | integrity: request.integrity, 42 | keepalive: request.keepalive, 43 | method: request.method, 44 | mode: request.mode, 45 | redirect: request.redirect, 46 | referrer: request.referrer, 47 | referrerPolicy: request.referrerPolicy, 48 | signal: request.signal, 49 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 50 | // @ts-ignore - not in the TS types yet 51 | duplex: 'half', 52 | }), 53 | ); 54 | } 55 | } 56 | }, 57 | onResponse({ request, response, setResponse, fetchAPI }) { 58 | const acceptEncoding = request.headers.get('accept-encoding'); 59 | if (acceptEncoding) { 60 | const encodings = acceptEncoding.split(',') as CompressionFormat[]; 61 | if (encodings.length && response.body) { 62 | const supportedEncoding = encodings.find(encoding => 63 | getSupportedEncodings(fetchAPI).includes(encoding), 64 | ); 65 | if (supportedEncoding) { 66 | const compressionStream = new fetchAPI.CompressionStream( 67 | supportedEncoding as CompressionFormat, 68 | ); 69 | const newHeaders = new fetchAPI.Headers(response.headers); 70 | newHeaders.set('content-encoding', supportedEncoding); 71 | newHeaders.delete('content-length'); 72 | const compressedBody = response.body.pipeThrough(compressionStream); 73 | const compressedResponse = new fetchAPI.Response(compressedBody, { 74 | status: response.status, 75 | statusText: response.statusText, 76 | headers: newHeaders, 77 | }); 78 | decompressedResponseMap.set(compressedResponse, response); 79 | setResponse(compressedResponse); 80 | } 81 | } 82 | } 83 | }, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /packages/server/src/plugins/useErrorHandling.ts: -------------------------------------------------------------------------------- 1 | import { Response as DefaultResponseCtor } from '@whatwg-node/fetch'; 2 | import { handleMaybePromise, MaybePromise } from '@whatwg-node/promise-helpers'; 3 | import type { ServerAdapterPlugin } from './types.js'; 4 | 5 | export function createDefaultErrorHandler( 6 | ResponseCtor: typeof Response = DefaultResponseCtor, 7 | ): ErrorHandler { 8 | return function defaultErrorHandler(e: any): Response { 9 | if (e.details || e.status || e.headers || e.name === 'HTTPError') { 10 | return new ResponseCtor( 11 | typeof e.details === 'object' ? JSON.stringify(e.details) : e.message, 12 | { 13 | status: e.status, 14 | headers: e.headers || {}, 15 | }, 16 | ); 17 | } 18 | console.error(e); 19 | return createDefaultErrorResponse(ResponseCtor); 20 | }; 21 | } 22 | 23 | function createDefaultErrorResponse(ResponseCtor: typeof Response) { 24 | if (ResponseCtor.error) { 25 | return ResponseCtor.error(); 26 | } 27 | return new ResponseCtor(null, { status: 500 }); 28 | } 29 | 30 | export class HTTPError extends Error { 31 | name = 'HTTPError'; 32 | constructor( 33 | public status: number = 500, 34 | public message: string, 35 | public headers: HeadersInit = {}, 36 | public details?: any, 37 | ) { 38 | super(message); 39 | Error.captureStackTrace(this, HTTPError); 40 | } 41 | } 42 | 43 | export type ErrorHandler = ( 44 | e: any, 45 | request: Request, 46 | ctx: TServerContext, 47 | ) => MaybePromise | void; 48 | 49 | export function useErrorHandling( 50 | onError?: ErrorHandler, 51 | ): ServerAdapterPlugin { 52 | return { 53 | onRequest({ requestHandler, setRequestHandler, fetchAPI }) { 54 | const errorHandler = onError || createDefaultErrorHandler(fetchAPI.Response); 55 | setRequestHandler(function handlerWithErrorHandling(request, serverContext) { 56 | return handleMaybePromise( 57 | () => requestHandler(request, serverContext), 58 | response => response, 59 | e => 60 | errorHandler(e, request, serverContext) || 61 | createDefaultErrorResponse(fetchAPI.Response), 62 | ); 63 | }); 64 | }, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /packages/server/test/CustomAbortControllerSignal.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { createCustomAbortControllerSignal } from '@whatwg-node/server'; 3 | 4 | describe('CustomAbortControllerSignal', () => { 5 | it('supports AbortSignal.any', async () => { 6 | const customCtrl = createCustomAbortControllerSignal(); 7 | const ctrl2 = new AbortController(); 8 | const signal2 = ctrl2.signal; 9 | const anySignal = AbortSignal.any([customCtrl.signal, signal2]); 10 | const reason = new Error('my reason'); 11 | customCtrl.abort(reason); 12 | expect(anySignal.aborted).toBe(true); 13 | expect(anySignal.reason).toBe(reason); 14 | expect(signal2.aborted).toBe(false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/server/test/abort.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from '@jest/globals'; 2 | import { runTestsForEachFetchImpl } from './test-fetch'; 3 | import { runTestsForEachServerImpl } from './test-server'; 4 | 5 | const skipIf = (condition: boolean) => (condition ? it.skip : it); 6 | 7 | describe('Request Abort', () => { 8 | runTestsForEachServerImpl((server, serverImplName) => { 9 | runTestsForEachFetchImpl((_, { fetchAPI, createServerAdapter }) => { 10 | skipIf( 11 | (globalThis.Bun && serverImplName !== 'Bun') || 12 | (globalThis.Deno && serverImplName !== 'Deno'), 13 | )( 14 | 'calls body.cancel on request abort', 15 | () => 16 | new Promise(resolve => { 17 | const adapter = createServerAdapter( 18 | () => 19 | new fetchAPI.Response( 20 | new fetchAPI.ReadableStream({ 21 | cancel() { 22 | resolve(); 23 | }, 24 | }), 25 | ), 26 | ); 27 | server.addOnceHandler(adapter); 28 | const abortCtrl = new AbortController(); 29 | fetchAPI.fetch(server.url, { signal: abortCtrl.signal }).then( 30 | () => {}, 31 | () => {}, 32 | ); 33 | setTimeout(() => { 34 | abortCtrl.abort(); 35 | }, 300); 36 | }), 37 | 1000, 38 | ); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/server/test/fetch-event-listener.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, jest } from '@jest/globals'; 2 | import { CustomEvent } from '@whatwg-node/events'; 3 | import { fakePromise, FetchEvent } from '@whatwg-node/server'; 4 | import { runTestsForEachFetchImpl } from './test-fetch.js'; 5 | 6 | class PonyfillFetchEvent extends CustomEvent<{}> implements FetchEvent { 7 | constructor( 8 | public request: Request, 9 | public respondWith: FetchEvent['respondWith'], 10 | public waitUntil: FetchEvent['waitUntil'], 11 | ) { 12 | super('fetch'); 13 | } 14 | } 15 | 16 | describe('FetchEvent listener', () => { 17 | runTestsForEachFetchImpl( 18 | (_, { createServerAdapter, fetchAPI: { Request, Response } }) => { 19 | it('should not return a promise to event listener', async () => { 20 | const response = new Response(); 21 | const response$ = fakePromise(response); 22 | const adapter = createServerAdapter(() => response$); 23 | let returnedResponse$: Response | Promise | undefined; 24 | const respondWith = jest.fn((response$: Response | Promise) => { 25 | returnedResponse$ = response$; 26 | }); 27 | const waitUntil = jest.fn(); 28 | const fetchEvent = new PonyfillFetchEvent( 29 | new Request('http://localhost:8080'), 30 | respondWith, 31 | waitUntil, 32 | ); 33 | const returnValue = adapter(fetchEvent); 34 | expect(returnValue).toBeUndefined(); 35 | expect(await returnedResponse$).toBe(response); 36 | }); 37 | it('should expose FetchEvent as server context', async () => { 38 | let calledRequest: Request | undefined; 39 | let calledContext: any; 40 | const handleRequest = jest.fn((_req: Request, _ctx: any) => { 41 | calledRequest = _req; 42 | calledContext = _ctx; 43 | return Response.json({}); 44 | }); 45 | const adapter = createServerAdapter(handleRequest); 46 | const respondWith = jest.fn(); 47 | const waitUntil = jest.fn(); 48 | const fetchEvent = new PonyfillFetchEvent( 49 | new Request('http://localhost:8080'), 50 | respondWith, 51 | waitUntil, 52 | ); 53 | adapter(fetchEvent); 54 | expect(calledRequest).toBe(fetchEvent.request); 55 | expect(calledContext.request).toBe(fetchEvent.request); 56 | expect(calledContext.respondWith).toBe(fetchEvent.respondWith); 57 | expect(calledContext.waitUntil).toBe(fetchEvent.waitUntil); 58 | }); 59 | it('should accept additional parameters as server context', async () => { 60 | const handleRequest = jest.fn((_req: Request, _ctx: any) => Response.json({})); 61 | const adapter = createServerAdapter<{ 62 | foo: string; 63 | }>(handleRequest); 64 | const respondWith = jest.fn(); 65 | const waitUntil = jest.fn(); 66 | const fetchEvent = new PonyfillFetchEvent( 67 | new Request('http://localhost:8080'), 68 | respondWith, 69 | waitUntil, 70 | ); 71 | const additionalCtx = { foo: 'bar' }; 72 | adapter(fetchEvent, additionalCtx); 73 | expect(handleRequest).toHaveBeenCalledWith( 74 | fetchEvent.request, 75 | expect.objectContaining(additionalCtx), 76 | ); 77 | }); 78 | }, 79 | { noLibCurl: true }, 80 | ); 81 | }); 82 | -------------------------------------------------------------------------------- /packages/server/test/http2.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClientHttp2Session, 3 | connect as connectHttp2, 4 | constants as constantsHttp2, 5 | createServer, 6 | Http2Server, 7 | } from 'node:http2'; 8 | import { AddressInfo } from 'node:net'; 9 | import { afterEach, describe, expect, it, jest } from '@jest/globals'; 10 | import { runTestsForEachFetchImpl } from './test-fetch'; 11 | 12 | const describeIf = (condition: boolean) => (condition ? describe : describe.skip); 13 | 14 | // HTTP2 is not supported fully on Bun and Deno 15 | describeIf(!globalThis.Bun && !globalThis.Deno)('http2', () => { 16 | let server: Http2Server; 17 | let client: ClientHttp2Session; 18 | 19 | afterEach(async () => { 20 | if (client) { 21 | await new Promise(resolve => client.close(resolve)); 22 | } 23 | if (server) { 24 | await new Promise(resolve => server.close(resolve)); 25 | } 26 | }); 27 | 28 | runTestsForEachFetchImpl((_, { createServerAdapter }) => { 29 | it('should support http2 and respond as expected', async () => { 30 | const handleRequest = jest.fn(async (request: Request) => { 31 | return Response.json( 32 | { 33 | body: await request.json(), 34 | headers: Object.fromEntries(request.headers), 35 | method: request.method, 36 | url: request.url, 37 | }, 38 | { 39 | headers: { 40 | 'x-is-this-http2': 'yes', 41 | 'content-type': 'text/plain;charset=UTF-8', 42 | }, 43 | status: 418, 44 | }, 45 | ); 46 | }); 47 | const adapter = createServerAdapter(handleRequest); 48 | 49 | server = createServer(adapter); 50 | await new Promise(resolve => server.listen(0, resolve)); 51 | 52 | const port = (server.address() as AddressInfo).port; 53 | 54 | // Node's fetch API does not support HTTP/2, we use the http2 module directly instead 55 | 56 | client = connectHttp2(`http://localhost:${port}`); 57 | 58 | const req = client.request({ 59 | [constantsHttp2.HTTP2_HEADER_METHOD]: 'POST', 60 | [constantsHttp2.HTTP2_HEADER_PATH]: '/hi', 61 | [constantsHttp2.HTTP2_HEADER_CONTENT_TYPE]: 'application/json', 62 | }); 63 | 64 | req.write(JSON.stringify({ hello: 'world' })); 65 | req.end(); 66 | 67 | const receivedNodeRequest = await new Promise<{ 68 | headers: Record; 69 | data: string; 70 | status?: number | undefined; 71 | }>((resolve, reject) => { 72 | req.once( 73 | 'response', 74 | ({ 75 | date, // omit date from snapshot 76 | ...headers 77 | }) => { 78 | let data = ''; 79 | req.on('data', chunk => { 80 | data += chunk; 81 | }); 82 | req.on('end', () => { 83 | resolve({ 84 | headers, 85 | data: JSON.parse(data), 86 | status: headers[':status'], 87 | }); 88 | }); 89 | }, 90 | ); 91 | req.once('error', reject); 92 | }); 93 | 94 | expect(receivedNodeRequest).toMatchObject({ 95 | data: { 96 | body: { 97 | hello: 'world', 98 | }, 99 | method: 'POST', 100 | url: expect.stringMatching(/^http:\/\/localhost:\d+\/hi$/), 101 | headers: { 102 | 'content-type': 'application/json', 103 | }, 104 | }, 105 | headers: { 106 | ':status': 418, 107 | 'content-type': 'text/plain;charset=UTF-8', 108 | 'x-is-this-http2': 'yes', 109 | }, 110 | status: 418, 111 | }); 112 | 113 | await new Promise(resolve => req.end(resolve)); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /packages/server/test/instrumentation.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { createServerAdapter, ServerAdapterPlugin } from '@whatwg-node/server'; 3 | 4 | describe('instrumentation', () => { 5 | it('should wrap request handler with instrumentation and automatically compose them', async () => { 6 | const results: string[] = []; 7 | 8 | function make(name: string): ServerAdapterPlugin { 9 | return { 10 | instrumentation: { 11 | request: async (_, wrapped) => { 12 | results.push(`pre-${name}`); 13 | await wrapped(); 14 | results.push(`post-${name}`); 15 | }, 16 | }, 17 | }; 18 | } 19 | 20 | const adapter = createServerAdapter<{}>( 21 | () => { 22 | results.push('request'); 23 | return Response.json({ message: 'Hello, World!' }); 24 | }, 25 | { 26 | plugins: [make('1'), make('2'), make('3')], 27 | }, 28 | ); 29 | 30 | await adapter.fetch('http://whatwg-node/graphql'); 31 | expect(results).toEqual(['pre-1', 'pre-2', 'pre-3', 'request', 'post-3', 'post-2', 'post-1']); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/server/test/plugins.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { Request, Response } from '@whatwg-node/fetch'; 3 | import { createServerAdapter } from '@whatwg-node/server'; 4 | 5 | describe('Plugins', () => { 6 | it('should reflect updated response in subsequent plugins', async () => { 7 | let firstRes: Response | undefined; 8 | let secondRes: Response | undefined; 9 | const adapter = createServerAdapter<{}>(() => Response.json({ message: 'Hello, World!' }), { 10 | plugins: [ 11 | { 12 | onResponse({ response, setResponse }) { 13 | firstRes = response; 14 | setResponse(Response.json({ message: 'Good bye!' }, { status: 418 })); 15 | }, 16 | }, 17 | { 18 | onResponse({ response }) { 19 | secondRes = response; 20 | }, 21 | }, 22 | ], 23 | }); 24 | const request = new Request('http://localhost'); 25 | const response = await adapter.fetch(request); 26 | expect(response.status).toBe(418); 27 | expect(await response.json()).toEqual({ message: 'Good bye!' }); 28 | expect(firstRes?.status).toBe(200); 29 | expect(secondRes?.status).toBe(418); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/server/test/request-container.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, jest } from '@jest/globals'; 2 | import { runTestsForEachFetchImpl } from './test-fetch.js'; 3 | 4 | describe('Request Container', () => { 5 | runTestsForEachFetchImpl( 6 | (_, { createServerAdapter, fetchAPI: { Request } }) => { 7 | it('should receive correct request and container as a context', async () => { 8 | const handleRequest = jest.fn((_req: Request, _ctx: any) => Response.json({})); 9 | const adapter = createServerAdapter(handleRequest); 10 | const requestContainer = { 11 | request: new Request('http://localhost:8080'), 12 | }; 13 | await adapter(requestContainer); 14 | expect(handleRequest).toHaveBeenCalledWith( 15 | requestContainer.request, 16 | expect.objectContaining(requestContainer), 17 | ); 18 | }); 19 | it('should accept additional parameters as server context', async () => { 20 | const handleRequest = jest.fn((_req: Request, _ctx: any) => Response.json({})); 21 | const adapter = createServerAdapter<{ 22 | foo: string; 23 | }>(handleRequest); 24 | const requestContainer = { 25 | request: new Request('http://localhost:8080'), 26 | foo: 'bar', 27 | }; 28 | await adapter(requestContainer); 29 | expect(handleRequest).toHaveBeenCalledWith( 30 | requestContainer.request, 31 | expect.objectContaining(requestContainer), 32 | ); 33 | }); 34 | }, 35 | { noLibCurl: true }, 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/server/test/server-context.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { runTestsForEachFetchImpl } from './test-fetch'; 3 | 4 | describe('Server Context', () => { 5 | runTestsForEachFetchImpl( 6 | (_, { createServerAdapter, fetchAPI: { Request, Response } }) => { 7 | it('should be passed to the handler', async () => { 8 | const exampleStaticCtx = { foo: 'bar' }; 9 | const seenCtx = new Set(); 10 | const adapter = createServerAdapter(function handler(_req, ctx) { 11 | seenCtx.add(ctx); 12 | ctx.foo = 'baz'; 13 | return new Response('ok'); 14 | }); 15 | const res = await adapter(new Request('https://example.com'), exampleStaticCtx); 16 | expect(res.status).toBe(200); 17 | expect(seenCtx.size).toBe(1); 18 | expect(seenCtx.has(exampleStaticCtx)).toBe(false); 19 | expect(exampleStaticCtx.foo).toBe('bar'); 20 | const res2 = await adapter(new Request('https://example.com'), exampleStaticCtx); 21 | expect(res2.status).toBe(200); 22 | expect(seenCtx.size).toBe(2); 23 | expect(seenCtx.has(exampleStaticCtx)).toBe(false); 24 | expect(exampleStaticCtx.foo).toBe('bar'); 25 | }); 26 | it('filters empty ctx', async () => { 27 | const adapter = createServerAdapter(function handler(_req, ctx) { 28 | return Response.json(ctx); 29 | }); 30 | const ctxParts: any[] = [undefined, undefined, { foo: 'bar' }, undefined, { bar: 'baz' }]; 31 | const res = await adapter(new Request('https://example.com'), ...ctxParts); 32 | expect(res.status).toBe(200); 33 | expect(await res.json()).toEqual({ foo: 'bar', bar: 'baz' }); 34 | }); 35 | it('retains the prototype in case of `Object.create`', async () => { 36 | class MyContext {} 37 | await using serverAdapter = createServerAdapter((_req, context0: MyContext) => { 38 | return Response.json({ 39 | isMyContext: context0 instanceof MyContext, 40 | }); 41 | }); 42 | const res = await serverAdapter.fetch('http://localhost', new MyContext()); 43 | const resJson = await res.json(); 44 | expect(resJson).toEqual({ 45 | isMyContext: true, 46 | }); 47 | }); 48 | it('Do not pollute the original object in case of `Object.create`', async () => { 49 | await using serverAdapter = createServerAdapter((_req, context0: any) => { 50 | context0.i = 0; 51 | const context1 = Object.create(context0); 52 | context1.i = 1; 53 | const context2 = Object.create(context0); 54 | context2.i = 2; 55 | return Response.json({ 56 | i0: context0.i, 57 | i1: context1.i, 58 | i2: context2.i, 59 | }); 60 | }); 61 | const res = await serverAdapter.fetch('http://localhost'); 62 | const resJson = await res.json(); 63 | expect(resJson).toEqual({ 64 | i0: 0, 65 | i1: 1, 66 | i2: 2, 67 | }); 68 | }); 69 | it('Do not pollute the original object in case of `Object.create` and `Object.defineProperty`', async () => { 70 | await using serverAdapter = createServerAdapter((_req, context0: any) => { 71 | const context1 = Object.create(context0); 72 | Object.defineProperty(context1, 'i', { value: 1, configurable: true }); 73 | const context2 = Object.create(context0); 74 | Object.defineProperty(context2, 'i', { value: 2, configurable: true }); 75 | return Response.json({ 76 | i1: context1.i, 77 | i2: context2.i, 78 | }); 79 | }); 80 | const res = await serverAdapter.fetch('http://localhost'); 81 | const resJson = await res.json(); 82 | expect(resJson).toEqual({ 83 | i1: 1, 84 | i2: 2, 85 | }); 86 | }); 87 | }, 88 | { noLibCurl: true }, 89 | ); 90 | }); 91 | -------------------------------------------------------------------------------- /packages/server/test/server.bench.ts: -------------------------------------------------------------------------------- 1 | import { createServer, RequestListener } from 'node:http'; 2 | import { AddressInfo } from 'node:net'; 3 | import { bench, BenchOptions, describe } from 'vitest'; 4 | import { fetch } from '@whatwg-node/fetch'; 5 | import { createServerAdapter, Response } from '@whatwg-node/server'; 6 | 7 | const isCI = !!process.env['CI']; 8 | 9 | function useNumberEnv(envName: string, defaultValue: number): number { 10 | const value = process.env[envName]; 11 | if (!value) { 12 | return defaultValue; 13 | } 14 | return parseInt(value, 10); 15 | } 16 | 17 | const duration = useNumberEnv('BENCH_DURATION', isCI ? 60000 : 15000); 18 | const warmupTime = useNumberEnv('BENCH_WARMUP_TIME', isCI ? 10000 : 5000); 19 | const warmupIterations = useNumberEnv('BENCH_WARMUP_ITERATIONS', isCI ? 30 : 10); 20 | const benchConfig: BenchOptions = { 21 | time: duration, 22 | warmupTime, 23 | warmupIterations, 24 | throws: true, 25 | }; 26 | 27 | function benchForAdapter(name: string, adapter: RequestListener) { 28 | const server = (createServer(adapter).listen(0).address() as AddressInfo).port; 29 | 30 | bench(name, () => fetch(`http://localhost:${server}`).then(res => res.json()), benchConfig); 31 | } 32 | 33 | const adapters = { 34 | 'without custom abort ctrl and without single write head': createServerAdapter( 35 | () => Response.json({ hello: 'world' }), 36 | { 37 | __useCustomAbortCtrl: false, 38 | __useSingleWriteHead: false, 39 | }, 40 | ), 41 | 'with custom abort ctrl and without single write head': createServerAdapter( 42 | () => Response.json({ hello: 'world' }), 43 | { 44 | __useCustomAbortCtrl: true, 45 | __useSingleWriteHead: false, 46 | }, 47 | ), 48 | 'with custom abort ctrl and with single write head': createServerAdapter( 49 | () => Response.json({ hello: 'world' }), 50 | { 51 | __useCustomAbortCtrl: true, 52 | __useSingleWriteHead: true, 53 | }, 54 | ), 55 | 'without custom abort ctrl and with single write head': createServerAdapter( 56 | () => Response.json({ hello: 'world' }), 57 | { 58 | __useCustomAbortCtrl: false, 59 | __useSingleWriteHead: true, 60 | }, 61 | ), 62 | }; 63 | 64 | /* Randomize array in-place using Durstenfeld shuffle algorithm */ 65 | function shuffleArray(array: T[]): T[] { 66 | for (let i = array.length - 1; i > 0; i--) { 67 | const j = Math.floor(Math.random() * (i + 1)); 68 | const temp = array[i]; 69 | array[i] = array[j]; 70 | array[j] = temp; 71 | } 72 | return array; 73 | } 74 | 75 | describe('Simple JSON Response', () => { 76 | const adapterEntries = shuffleArray([...Object.entries(adapters)]); 77 | for (const [benchName, adapter] of adapterEntries) { 78 | benchForAdapter(benchName, adapter); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /packages/server/test/test-fetch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/no-callback-literal */ 2 | import { globalAgent as httpGlobalAgent } from 'node:http'; 3 | import { globalAgent as httpsGlobalAgent } from 'node:https'; 4 | import { setTimeout } from 'node:timers/promises'; 5 | import type { Dispatcher } from 'undici'; 6 | import { afterAll, afterEach, beforeAll, describe } from '@jest/globals'; 7 | import { patchSymbols } from '@whatwg-node/disposablestack'; 8 | import { createFetch } from '@whatwg-node/fetch'; 9 | import { createServerAdapter } from '../src/createServerAdapter'; 10 | import { FetchAPI } from '../src/types'; 11 | 12 | patchSymbols(); 13 | const describeIf = (condition: boolean) => (condition ? describe : describe.skip); 14 | const libcurl = globalThis.libcurl; 15 | export function runTestsForEachFetchImpl( 16 | callback: ( 17 | implementationName: string, 18 | api: { 19 | fetchAPI: FetchAPI; 20 | createServerAdapter: typeof createServerAdapter; 21 | }, 22 | ) => void, 23 | opts: { noLibCurl?: boolean; noNativeFetch?: boolean } = {}, 24 | ) { 25 | describeIf(!globalThis.Deno)('Ponyfill', () => { 26 | if (opts.noLibCurl) { 27 | const fetchAPI = createFetch({ skipPonyfill: false }); 28 | callback('ponyfill', { 29 | fetchAPI, 30 | createServerAdapter: (baseObj: any, opts?: any) => 31 | createServerAdapter(baseObj, { 32 | fetchAPI, 33 | ...opts, 34 | }), 35 | }); 36 | return; 37 | } 38 | describeIf(libcurl)('libcurl', () => { 39 | const fetchAPI = createFetch({ skipPonyfill: false }); 40 | callback('libcurl', { 41 | fetchAPI, 42 | createServerAdapter: (baseObj: any, opts?: any) => 43 | createServerAdapter(baseObj, { 44 | fetchAPI, 45 | ...opts, 46 | }), 47 | }); 48 | afterAll(() => { 49 | libcurl.Curl.globalCleanup(); 50 | }); 51 | }); 52 | describe('node-http', () => { 53 | beforeAll(() => { 54 | (globalThis.libcurl as any) = null; 55 | }); 56 | afterAll(() => { 57 | httpGlobalAgent.destroy(); 58 | httpsGlobalAgent.destroy(); 59 | globalThis.libcurl = libcurl; 60 | }); 61 | const fetchAPI = createFetch({ skipPonyfill: false }); 62 | callback('node-http', { 63 | fetchAPI, 64 | createServerAdapter: (baseObj: any, opts?: any) => 65 | createServerAdapter(baseObj, { 66 | fetchAPI, 67 | ...opts, 68 | }), 69 | }); 70 | }); 71 | }); 72 | let noNative = opts.noNativeFetch; 73 | if ( 74 | process.env.LEAK_TEST && 75 | // @ts-expect-error - Only if global dispatcher is available 76 | !globalThis[Symbol.for('undici.globalDispatcher.1')] 77 | ) { 78 | noNative = true; 79 | } 80 | describeIf(!noNative || globalThis.Bun || globalThis.Deno)('Native', () => { 81 | const fetchAPI = createFetch({ skipPonyfill: true }); 82 | callback('native', { 83 | fetchAPI, 84 | createServerAdapter: (baseObj: any, opts?: any) => 85 | createServerAdapter(baseObj, { 86 | fetchAPI, 87 | ...opts, 88 | }), 89 | }); 90 | afterEach(async () => { 91 | const undiciGlobalDispatcher: Dispatcher = 92 | // @ts-expect-error TS types are not available yet but documented [here](https://github.com/nodejs/undici/discussions/2167#discussioncomment-6239992) 93 | globalThis[Symbol.for('undici.globalDispatcher.1')]; 94 | await undiciGlobalDispatcher?.close(); 95 | await undiciGlobalDispatcher?.destroy(); 96 | return setTimeout(300); 97 | }); 98 | }); 99 | afterEach(() => { 100 | globalThis?.gc?.(); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /packages/server/test/typings-test.ts: -------------------------------------------------------------------------------- 1 | import { createServer as createHttpServer, IncomingMessage, ServerResponse } from 'node:http'; 2 | import { 3 | createServer as createHttp2Server, 4 | Http2ServerRequest, 5 | Http2ServerResponse, 6 | } from 'node:http2'; 7 | import { App } from 'uWebSockets.js'; 8 | import { createServerAdapter } from '../src/createServerAdapter.js'; 9 | 10 | const adapter = createServerAdapter(() => { 11 | return null as any; 12 | }); 13 | 14 | const http2Req = null as unknown as Http2ServerRequest; 15 | const http2Res = null as unknown as Http2ServerResponse; 16 | 17 | adapter.handleNodeRequest(http2Req); 18 | adapter.handleNodeRequestAndResponse(http2Req, http2Res); 19 | adapter.handle(http2Req, http2Res); 20 | adapter(http2Req, http2Res); 21 | const http2Server = createHttp2Server(adapter); 22 | http2Server.on('request', adapter); 23 | 24 | const httpReq = null as unknown as IncomingMessage; 25 | const httpRes = null as unknown as ServerResponse; 26 | 27 | adapter.handleNodeRequest(httpReq); 28 | adapter.handleNodeRequestAndResponse(httpReq, httpRes); 29 | adapter.handle(httpReq, httpRes); 30 | adapter(httpReq, httpRes); 31 | 32 | const httpServer = createHttpServer(adapter); 33 | httpServer.on('request', adapter); 34 | 35 | App().any('/*', adapter); 36 | -------------------------------------------------------------------------------- /packages/server/test/useErrorHandling.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, jest } from '@jest/globals'; 2 | import { useErrorHandling } from '../src/plugins/useErrorHandling.js'; 3 | import { runTestsForEachFetchImpl } from './test-fetch.js'; 4 | 5 | describe('useErrorHandling', () => { 6 | runTestsForEachFetchImpl( 7 | (_, { createServerAdapter, fetchAPI }) => { 8 | it('should return error response when error is thrown', async () => { 9 | const errorHandler = jest.fn(() => {}); 10 | let request: Request | undefined; 11 | const router = createServerAdapter( 12 | req => { 13 | request = req; 14 | throw new Error('Unexpected error'); 15 | }, 16 | { 17 | plugins: [useErrorHandling(errorHandler)], 18 | fetchAPI, 19 | }, 20 | ); 21 | const response = await router.fetch('http://localhost/greetings/John'); 22 | const errRes = fetchAPI.Response.error(); 23 | expect(response.status).toBe(errRes.status); 24 | expect(response.statusText).toBe(errRes.statusText); 25 | const text = await response.text(); 26 | expect(text).toHaveLength(0); 27 | expect(errorHandler).toHaveBeenCalledWith(new Error('Unexpected error'), request, { 28 | waitUntil: expect.any(Function), 29 | }); 30 | }); 31 | }, 32 | { noLibCurl: true }, 33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/server/test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals'; 2 | import { isolateObject } from '../src/utils'; 3 | 4 | describe('isolateObject', () => { 5 | describe('Object.create', () => { 6 | it('property assignments', () => { 7 | const origin = isolateObject({}); 8 | const a = Object.create(origin); 9 | const b = Object.create(origin); 10 | a.a = 1; 11 | expect(b.a).toEqual(undefined); 12 | }); 13 | it('property assignments with defineProperty', () => { 14 | const origin = isolateObject({}); 15 | const a = Object.create(origin); 16 | const b = Object.create(origin); 17 | Object.defineProperty(a, 'a', { value: 1 }); 18 | expect(b.a).toEqual(undefined); 19 | }); 20 | it('property deletions', () => { 21 | const origin = isolateObject({}); 22 | const a = Object.create(origin); 23 | const b = Object.create(origin); 24 | b.a = 2; 25 | a.a = 1; 26 | delete a.a; 27 | expect(b.a).toEqual(2); 28 | }); 29 | it('ownKeys', () => { 30 | const origin = isolateObject({}); 31 | const a = Object.create(origin); 32 | const b = Object.create(origin); 33 | a.a = 1; 34 | expect(Object.keys(a)).toEqual(['a']); 35 | expect(Object.keys(b)).toEqual([]); 36 | }); 37 | it('hasOwnProperty', () => { 38 | const origin = isolateObject({}); 39 | const a = Object.create(origin); 40 | const b = Object.create(origin); 41 | a.a = 1; 42 | expect(a.hasOwnProperty('a')).toEqual(true); 43 | expect(b.hasOwnProperty('a')).toEqual(false); 44 | }); 45 | it('getOwnPropertyDescriptor', () => { 46 | const origin = isolateObject({}); 47 | const a = Object.create(origin); 48 | const b = Object.create(origin); 49 | a.a = 1; 50 | const desc = Object.getOwnPropertyDescriptor(a, 'a'); 51 | expect(desc?.value).toEqual(1); 52 | expect(Object.getOwnPropertyDescriptor(b, 'a')).toEqual(undefined); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /patches/@eslint+eslintrc+3.3.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@eslint/eslintrc/lib/shared/config-validator.js b/node_modules/@eslint/eslintrc/lib/shared/config-validator.js 2 | index 6857e7a..3f8635c 100644 3 | --- a/node_modules/@eslint/eslintrc/lib/shared/config-validator.js 4 | +++ b/node_modules/@eslint/eslintrc/lib/shared/config-validator.js 5 | @@ -324,12 +324,6 @@ export default class ConfigValidator { 6 | * @throws {Error} If the config is invalid. 7 | */ 8 | validateConfigSchema(config, source = null) { 9 | - validateSchema = validateSchema || ajv.compile(configSchema); 10 | - 11 | - if (!validateSchema(config)) { 12 | - throw new Error(`ESLint configuration in ${source} is invalid:\n${this.formatErrors(validateSchema.errors)}`); 13 | - } 14 | - 15 | if (Object.hasOwn(config, "ecmaFeatures")) { 16 | emitDeprecationWarning(source, "ESLINT_LEGACY_ECMAFEATURES"); 17 | } 18 | -------------------------------------------------------------------------------- /patches/jest-leak-detector+29.7.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/jest-leak-detector/build/index.js b/node_modules/jest-leak-detector/build/index.js 2 | index a8ccb1e..70699fd 100644 3 | --- a/node_modules/jest-leak-detector/build/index.js 4 | +++ b/node_modules/jest-leak-detector/build/index.js 5 | @@ -74,26 +74,14 @@ class LeakDetector { 6 | value = null; 7 | } 8 | async isLeaking() { 9 | - this._runGarbageCollector(); 10 | + (0, _v().setFlagsFromString)('--allow-natives-syntax'); 11 | 12 | // wait some ticks to allow GC to run properly, see https://github.com/nodejs/node/issues/34636#issuecomment-669366235 13 | for (let i = 0; i < 10; i++) { 14 | + eval('%CollectGarbage(true)'); 15 | await tick(); 16 | } 17 | return this._isReferenceBeingHeld; 18 | } 19 | - _runGarbageCollector() { 20 | - // @ts-expect-error: not a function on `globalThis` 21 | - const isGarbageCollectorHidden = globalThis.gc == null; 22 | - 23 | - // GC is usually hidden, so we have to expose it before running. 24 | - (0, _v().setFlagsFromString)('--expose-gc'); 25 | - (0, _vm().runInNewContext)('gc')(); 26 | - 27 | - // The GC was not initially exposed, so let's hide it again. 28 | - if (isGarbageCollectorHidden) { 29 | - (0, _v().setFlagsFromString)('--no-expose-gc'); 30 | - } 31 | - } 32 | } 33 | exports.default = LeakDetector; 34 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import guildConfig from '@theguild/prettier-config'; 2 | 3 | export default { 4 | ...guildConfig, 5 | importOrderParserPlugins: ['explicitResourceManagement', ...guildConfig.importOrderParserPlugins], 6 | }; 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>the-guild-org/shared-config:renovate"], 4 | "automerge": true, 5 | "major": { 6 | "automerge": false 7 | }, 8 | "lockFileMaintenance": { 9 | "enabled": true, 10 | "automerge": true 11 | }, 12 | "packageRules": [ 13 | { 14 | "excludePackagePatterns": [ 15 | "@changesets/*", 16 | "typescript", 17 | "typedoc*", 18 | "^@theguild/", 19 | "@graphql-inspector/core", 20 | "next", 21 | "react", 22 | "react-dom", 23 | "@pulumi/*", 24 | "husky" 25 | ], 26 | "matchPackagePatterns": ["*"], 27 | "matchUpdateTypes": ["minor", "patch"], 28 | "groupName": "all non-major dependencies", 29 | "groupSlug": "all-minor-patch" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test.mjs: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { Response } from '@whatwg-node/fetch'; 3 | import { createServerAdapter } from '@whatwg-node/server'; 4 | 5 | createServer(createServerAdapter(() => Response.json({ hello: 'world' }))).listen(3000, () => { 6 | console.log('Server is running at http://localhost:3000'); 7 | console.log('Press Ctrl+C to stop the server.'); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "inlineSourceMap": false 6 | }, 7 | "exclude": [ 8 | "**/test/*.ts", 9 | "*.spec.ts", 10 | "**/tests", 11 | "**/test-assets", 12 | "**/test-files", 13 | "packages/testing", 14 | "e2e", 15 | "examples", 16 | "**/dist", 17 | "vitest.config.ts", 18 | "vitest.projects.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "baseUrl": ".", 5 | 6 | "target": "es2022", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "lib": ["esnext"], 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "importHelpers": true, 13 | "resolveJsonModule": true, 14 | "sourceMap": true, 15 | "declaration": true, 16 | "downlevelIteration": true, 17 | "incremental": true, 18 | "exactOptionalPropertyTypes": true, 19 | 20 | "skipLibCheck": false, 21 | 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noPropertyAccessFromIndexSignature": false, 27 | "paths": { 28 | "@whatwg-node/cookie-store": ["packages/cookie-store/src/index.ts"], 29 | "@whatwg-node/server": ["packages/server/src/index.ts"], 30 | "@whatwg-node/node-fetch": ["packages/node-fetch/src/index.ts"], 31 | "@whatwg-node/events": ["packages/events/src/index.ts"], 32 | "@whatwg-node/disposablestack": ["packages/disposablestack/src/index.ts"], 33 | "@whatwg-node/promise-helpers": ["packages/promise-helpers/src/index.ts"], 34 | "@e2e/*": ["e2e/*/src/index.ts"], 35 | "fetchache": ["packages/fetchache/src/index.ts"] 36 | } 37 | }, 38 | "include": [ 39 | "packages", 40 | "examples", 41 | "uwsUtils.d.ts", 42 | "scheduler-tracing.d.ts", 43 | "vitest.config.ts", 44 | "vitest.projects.ts" 45 | ], 46 | "exclude": ["**/node_modules", "**/test-files", "**/dist", "**/e2e", "**/benchmark"] 47 | } 48 | -------------------------------------------------------------------------------- /uwsUtils.d.ts: -------------------------------------------------------------------------------- 1 | import type uws from 'uWebSockets.js'; 2 | 3 | declare global { 4 | function createUWS(): { 5 | start(): Promise; 6 | stop(): void; 7 | addOnceHandler(handler: Parameters[1], ...ctxParts: any[]): void; 8 | port?: number; 9 | }; 10 | } 11 | 12 | declare global { 13 | // eslint-disable-next-line no-var 14 | var Bun: any; 15 | } 16 | -------------------------------------------------------------------------------- /uwsUtils.js: -------------------------------------------------------------------------------- 1 | const uws = require('uWebSockets.js'); 2 | 3 | module.exports = { 4 | createUWS() { 5 | let handler; 6 | let uwsApp = uws.App().any('/*', (...args) => { 7 | const res = handler(...args); 8 | handler = undefined; 9 | return res; 10 | }); 11 | let listenSocket; 12 | return { 13 | getApp() { 14 | return uwsApp; 15 | }, 16 | start() { 17 | return new Promise(function (resolve, reject) { 18 | uwsApp.listen(0, function (newListenSocket) { 19 | if (newListenSocket) { 20 | listenSocket = newListenSocket; 21 | resolve(uws.us_socket_local_port(listenSocket)); 22 | } else { 23 | reject(new Error('uWS App cannot start')); 24 | } 25 | }); 26 | }); 27 | }, 28 | stop() { 29 | if (listenSocket) { 30 | uws.us_listen_socket_close(listenSocket); 31 | uwsApp.close(); 32 | } 33 | }, 34 | get port() { 35 | if (listenSocket) { 36 | return uws.us_socket_local_port(listenSocket); 37 | } 38 | }, 39 | addOnceHandler(newHandler) { 40 | handler = newHandler; 41 | }, 42 | }; 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | }); 7 | -------------------------------------------------------------------------------- /vitest.projects.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config'; 2 | 3 | export default defineWorkspace([ 4 | { 5 | extends: './vitest.config.ts', 6 | test: { 7 | name: 'bench', 8 | benchmark: { 9 | include: ['**/*.bench.ts'], 10 | reporters: ['verbose'], 11 | outputJson: 'bench/results.json', 12 | }, 13 | }, 14 | }, 15 | ]); 16 | --------------------------------------------------------------------------------