├── .changeset ├── README.md ├── config.json └── healthy-buckets-drum.md ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── validate.yml ├── .gitignore ├── .prettierignore ├── Readme.md ├── package.json ├── packages ├── blob │ ├── CHANGELOG.md │ ├── License.md │ ├── Readme.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── blob.js │ │ ├── blob.node.js │ │ ├── lib.js │ │ ├── lib.node.js │ │ └── package.js │ ├── test │ │ ├── all.spec.js │ │ ├── blob.spec.js │ │ ├── fetch.spec.js │ │ ├── slice.spec.js │ │ ├── test.js │ │ └── web.spec.js │ └── tsconfig.json ├── fetch │ ├── .editorconfig │ ├── .gitignore │ ├── .npmrc │ ├── CHANGELOG.md │ ├── CODE_OF_CONDUCT.md │ ├── LICENSE.md │ ├── Readme.md │ ├── docs │ │ ├── CHANGELOG.md │ │ ├── ERROR-HANDLING.md │ │ ├── media │ │ │ ├── Banner.svg │ │ │ ├── Logo.svg │ │ │ └── NodeFetch.sketch │ │ ├── v2-LIMITS.md │ │ ├── v2-UPGRADE-GUIDE.md │ │ ├── v3-LIMITS.md │ │ └── v3-UPGRADE-GUIDE.md │ ├── example.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── body.js │ │ ├── errors │ │ │ ├── abort-error.js │ │ │ ├── base.js │ │ │ └── fetch-error.js │ │ ├── fetch.js │ │ ├── headers.js │ │ ├── lib.js │ │ ├── lib.node.js │ │ ├── package.js │ │ ├── package.ts │ │ ├── request.js │ │ ├── response.js │ │ └── utils │ │ │ ├── form-data.js │ │ │ ├── get-search.js │ │ │ ├── is-redirect.js │ │ │ ├── is.js │ │ │ └── utf8.js │ ├── test │ │ ├── commonjs │ │ │ ├── package.json │ │ │ └── test-artifact.js │ │ ├── external-encoding.js │ │ ├── file.js │ │ ├── form-data.js │ │ ├── headers.js │ │ ├── main.js │ │ ├── request.js │ │ ├── response.js │ │ └── utils │ │ │ ├── chai-timeout.js │ │ │ ├── dummy.txt │ │ │ ├── read-stream.js │ │ │ └── server.js │ └── tsconfig.json ├── file │ ├── CHANGELOG.md │ ├── License.md │ ├── Readme.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── file.js │ │ ├── lib.js │ │ ├── lib.node.js │ │ └── package.js │ ├── test │ │ ├── all.spec.js │ │ ├── fetch.spec.js │ │ ├── file.spec.js │ │ ├── test.js │ │ └── web.spec.js │ └── tsconfig.json ├── form-data │ ├── CHANGELOG.md │ ├── License.md │ ├── Readme.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── form-data.js │ │ ├── lib.js │ │ └── lib.node.js │ ├── test │ │ ├── all.spec.js │ │ ├── fetch.spec.js │ │ ├── form-data.spec.js │ │ ├── test.js │ │ └── web.spec.js │ └── tsconfig.json └── stream │ ├── CHANGELOG.md │ ├── Readme.md │ ├── package.json │ ├── src │ ├── lib.d.ts │ ├── lib.js │ ├── lib.node.js │ └── stream.cjs │ ├── test │ ├── all.spec.js │ ├── lib.spec.cjs │ ├── lib.spec.js │ ├── node.spec.cjs │ ├── test.js │ └── web.spec.js │ └── tsconfig.json ├── tsconfig.json └── 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/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": [ 4 | "@changesets/cli/changelog", 5 | { "repo": "remix-run/web-std-io" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/healthy-buckets-drum.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@remix-run/web-fetch": patch 3 | --- 4 | 5 | If locationURL’s scheme is not an HTTP(S) scheme, then return a network error. https://fetch.spec.whatwg.org/#http-redirect-fetch 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🦋 Changesets Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: 🦋 Changesets Release 13 | 14 | if: ${{ github.repository == 'remix-run/web-std-io' }} 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: ⬇️ Checkout repo 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: ⎔ Setup node 25 | uses: actions/setup-node@v4 26 | with: 27 | cache: yarn 28 | node-version: 16 29 | 30 | - name: 📥 Install deps 31 | run: yarn --frozen-lockfile 32 | 33 | - name: 🔐 Setup npm auth 34 | run: | 35 | echo "registry=https://registry.npmjs.org" >> ~/.npmrc 36 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc 37 | 38 | # This action has two responsibilities. The first time the workflow runs 39 | # (initial push to `main`) it will create a new branch and then PR back 40 | # to `main` with the related changes for the new version. After the PR 41 | # is merged, the workflow will run again and this action will publish to 42 | # npm. 43 | - name: 🚀 PR / Publish 44 | id: changesets 45 | uses: changesets/action@v1 46 | with: 47 | version: yarn version-bump 48 | commit: "chore: Update version for release" 49 | title: "chore: Update version for release" 50 | publish: yarn release 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | typecheck: 11 | name: ʦ TypeScript (${{ matrix.project }}) 12 | 13 | strategy: 14 | fail-fast: false 15 | 16 | matrix: 17 | project: [blob, fetch, file, form-data, stream] 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: ⬇️ Checkout repo 23 | uses: actions/checkout@v4 24 | 25 | - name: ⎔ Setup node 26 | uses: actions/setup-node@v4 27 | with: 28 | cache: yarn 29 | node-version: 16 30 | 31 | - name: 📥 Install deps 32 | run: yarn --frozen-lockfile 33 | 34 | - name: 🔎 Type check 35 | run: yarn typecheck 36 | working-directory: packages/${{matrix.project}} 37 | 38 | test: 39 | name: 🧪 Test (${{ matrix.project }} / ${{ matrix.os }} / node@${{ matrix.node }}) 40 | 41 | strategy: 42 | fail-fast: false 43 | 44 | matrix: 45 | node: [16, 18, 20] 46 | os: [ubuntu-latest, windows-latest, macos-latest] 47 | project: [blob, fetch, file, form-data, stream] 48 | 49 | runs-on: ${{ matrix.os }} 50 | 51 | steps: 52 | - name: ⬇️ Checkout repo 53 | uses: actions/checkout@v4 54 | 55 | - name: ⎔ Setup node 56 | uses: actions/setup-node@v4 57 | with: 58 | cache: yarn 59 | node-version: ${{ matrix.node }} 60 | 61 | - name: 📥 Install deps 62 | run: yarn --frozen-lockfile 63 | 64 | - name: 🧪 Test (CJS) 65 | run: yarn test:cjs 66 | working-directory: packages/${{matrix.project}} 67 | 68 | - name: 🧪 Test (ES) 69 | run: yarn test:es 70 | working-directory: packages/${{matrix.project}} 71 | 72 | - name: 🧪 Test (Web) 73 | run: npm run test:web --if-present 74 | working-directory: packages/${{matrix.project}} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | fetch 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Web Standard IO 2 | 3 | Set of libraries that provide standard web IO APIs so they can be imported and used across web and node runtimes without any configurations. 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remix-run/web-std-io", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/blob", 6 | "packages/file", 7 | "packages/form-data", 8 | "packages/fetch", 9 | "packages/stream" 10 | ], 11 | "scripts": { 12 | "changeset": "changeset", 13 | "release": "changeset publish", 14 | "version-bump": "changeset version", 15 | "postinstall": "manypkg fix", 16 | "prepare": "yarn prepare:blob && yarn prepare:file && yarn prepare:form-data && yarn prepare:fetch", 17 | "prepare:blob": "yarn --cwd packages/blob prepare", 18 | "prepare:file": "yarn --cwd packages/file prepare", 19 | "prepare:form-data": "yarn --cwd packages/form-data prepare", 20 | "prepare:fetch": "yarn --cwd packages/fetch prepare", 21 | "test": "yarn test:stream && yarn test:blob && yarn test:file && yarn test:form-data && yarn test:fetch", 22 | "test:blob": "yarn --cwd packages/blob test", 23 | "test:file": "yarn --cwd packages/file test", 24 | "test:form-data": "yarn --cwd packages/form-data test", 25 | "test:fetch": "yarn --cwd packages/fetch test", 26 | "test:stream": "yarn --cwd packages/stream test" 27 | }, 28 | "dependencies": { 29 | "@changesets/changelog-github": "0.4.4", 30 | "@changesets/cli": "^2.22.0", 31 | "@manypkg/cli": "0.19.1", 32 | "@manypkg/get-packages": "^1.1.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/blob/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Export CJS version for browser ([807fc63](https://github.com/remix-run/web-std-io/commit/807fc63)) 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - `@remix-run/web-stream@1.1.0` 13 | 14 | ## 3.0.5 15 | 16 | ### Patch Changes 17 | 18 | - 43c6ce2: Move types conditional export to the top of the list to align with [the node guidance](https://nodejs.org/api/packages.html#community-conditions-definitions) 19 | - Updated dependencies [43c6ce2] 20 | - @remix-run/web-stream@1.0.4 21 | 22 | ## [3.0.4](https://www.github.com/web-std/io/compare/blob-v3.0.3...blob-v3.0.4) (2022-02-24) 23 | 24 | ### Changes 25 | 26 | - disable node native blob ([#49](https://www.github.com/web-std/io/issues/49)) ([18e426e](https://www.github.com/web-std/io/commit/18e426e0552eb855275faadceab35c41335582f2)) 27 | 28 | ## [3.0.3](https://www.github.com/web-std/io/compare/blob-v3.0.2...blob-v3.0.3) (2022-01-21) 29 | 30 | ### Changes 31 | 32 | - bump version ([91420e2](https://www.github.com/web-std/io/commit/91420e294b4188a6da9c961ce4ef4eeac93595a1)) 33 | 34 | ## [3.0.2](https://www.github.com/web-std/io/compare/blob-v3.0.1...blob-v3.0.2) (2022-01-19) 35 | 36 | ### Bug Fixes 37 | 38 | - ship less files to address TSC issues ([#35](https://www.github.com/web-std/io/issues/35)) ([0651e62](https://www.github.com/web-std/io/commit/0651e62ae42d17eae2db89858c9e44f3342c304c)) 39 | 40 | ## [3.0.1](https://www.github.com/web-std/io/compare/blob-v3.0.0...blob-v3.0.1) (2021-11-08) 41 | 42 | ### Changes 43 | 44 | - align package versions ([09c8676](https://www.github.com/web-std/io/commit/09c8676348619313d9df24d9597cea0eb82704d2)) 45 | 46 | ## 3.0.0 (2021-11-05) 47 | 48 | ### Features 49 | 50 | - Refactor streams into own subpackage ([#19](https://www.github.com/web-std/io/issues/19)) ([90624cf](https://www.github.com/web-std/io/commit/90624cfd2d4253c2cbc316d092f26e77b5169f47)) 51 | 52 | ### Changes 53 | 54 | - bump versions ([#27](https://www.github.com/web-std/io/issues/27)) ([0fe5224](https://www.github.com/web-std/io/commit/0fe5224124e318f560dcfbd8a234d05367c9fbcb)) 55 | -------------------------------------------------------------------------------- /packages/blob/License.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Irakli Gozalishvili. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /packages/blob/Readme.md: -------------------------------------------------------------------------------- 1 | # @remix-run/web-blob 2 | 3 | [![ci][ci.icon]][ci.url] 4 | [![package][version.icon] ![downloads][downloads.icon]][package.url] 5 | [![styled with prettier][prettier.icon]][prettier.url] 6 | 7 | Web API compatible [Blob][] for nodejs. 8 | 9 | ## Comparison to Alternatives 10 | 11 | #### [fetch-blob][] 12 | 13 | The reason this library exists is because [fetch-blob][] chooses to compromise 14 | Web API compatibility of [`blob.stream()`][w3c blob.stream] by using nodejs 15 | native [Readable][] stream. We found this to be problematic when sharing code 16 | across nodejs and browser runtimes. Instead this library stays true to the 17 | specification by using [ReadableStream][] implementation from [@remix-run/web-stream][] 18 | library even if that is less convenient in nodejs context. 19 | 20 | > Note: Both node [Readable][] streams and web [ReadableStream][] implement 21 | > `AsyncIterable` interface and in theory either could be used with [for await][] 22 | > loops. In practice however major browsers do not yet ship `AsyncIterable` 23 | > support for [ReadableStream][]s which in our experience makes choice made by 24 | > [node-fetch][] impractical. 25 | 26 | [fetch-blob][] is build around node [Buffer][]s. This implementation is built 27 | around standard [Uint8Array][]s. 28 | 29 | [fetch-blob] chooses to use [WeakMap][]s for encapsulating private state. This 30 | library chooses to use to use properties with names that start with `_`. While 31 | those properties aren't truly private they do have better performance profile 32 | and make it possible to interop with this library, which we found impossible 33 | to do with [node-fetch][]. 34 | 35 | ### Usage 36 | 37 | ```js 38 | import { Blob } from "@remix-run/web-blob" 39 | const blob = new Blob(["hello", new TextEncoder().encode("world")]) 40 | for await (const chunk of blob.stream()) { 41 | console.log(chunk) 42 | } 43 | ``` 44 | 45 | ### Usage from Typescript 46 | 47 | This library makes use of [typescript using JSDOC annotations][ts-jsdoc] and 48 | also generates type definitions along with typed definition maps. So you should 49 | be able to get all the type inference out of the box. 50 | 51 | ## Install 52 | 53 | npm install @remix-run/web-blob 54 | 55 | [ci.icon]: https://github.com/web-std/io/workflows/blob/badge.svg 56 | [ci.url]: https://github.com/web-std/io/actions/workflows/blob.yml 57 | [version.icon]: https://img.shields.io/npm/v/@remix-run/web-blob.svg 58 | [downloads.icon]: https://img.shields.io/npm/dm/@remix-run/web-blob.svg 59 | [package.url]: https://npmjs.org/package/@remix-run/web-blob 60 | [downloads.image]: https://img.shields.io/npm/dm/@remix-run/web-blob.svg 61 | [downloads.url]: https://npmjs.org/package/@remix-run/web-blob 62 | [prettier.icon]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg 63 | [prettier.url]: https://github.com/prettier/prettier 64 | [blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob 65 | [fetch-blob]: https://github.com/node-fetch/fetch-blob 66 | [readablestream]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream 67 | [readable]: https://nodejs.org/api/stream.html#stream_readable_streams 68 | [w3c blob.stream]: https://w3c.github.io/FileAPI/#dom-blob-stream 69 | [@remix-run/web-stream]: https://github.com/web-std/io/tree/main/stream 70 | [for await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of 71 | [buffer]: https://nodejs.org/api/buffer.html 72 | [weakmap]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap 73 | [ts-jsdoc]: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html 74 | [uint8array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array 75 | [node-fetch]: https://github.com/node-fetch/ 76 | -------------------------------------------------------------------------------- /packages/blob/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remix-run/web-blob", 3 | "version": "3.1.0", 4 | "description": "Web API compatible Blob implementation", 5 | "keywords": [ 6 | "blob", 7 | "typed" 8 | ], 9 | "files": [ 10 | "src", 11 | "dist/src", 12 | "License.md", 13 | "Readme.md" 14 | ], 15 | "type": "module", 16 | "module": "./src/lib.js", 17 | "main": "./dist/src/lib.node.cjs", 18 | "types": "./dist/src/lib.d.ts", 19 | "exports": { 20 | ".": { 21 | "types": "./dist/src/lib.d.ts", 22 | "browser": { 23 | "require": "./dist/src/lib.cjs", 24 | "import": "./src/lib.js" 25 | }, 26 | "require": "./dist/src/lib.node.cjs", 27 | "import": "./src/lib.node.js" 28 | } 29 | }, 30 | "dependencies": { 31 | "@remix-run/web-stream": "^1.1.0", 32 | "web-encoding": "1.1.5" 33 | }, 34 | "author": "Irakli Gozalishvili (https://gozala.io)", 35 | "repository": "https://github.com/remix-run/web-std-io", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@remix-run/web-fetch": "^4.4.2-pre.0", 39 | "@types/node": "15.0.2", 40 | "git-validate": "2.2.4", 41 | "husky": "^6.0.0", 42 | "lint-staged": "^11.0.0", 43 | "playwright-test": "^7.2.0", 44 | "prettier": "^2.3.0", 45 | "rimraf": "3.0.2", 46 | "rollup": "2.47.0", 47 | "rollup-plugin-multi-input": "1.2.0", 48 | "typescript": "^4.4.4", 49 | "uvu": "0.5.2" 50 | }, 51 | "scripts": { 52 | "typecheck": "tsc", 53 | "build": "npm run build:cjs && npm run build:types", 54 | "build:cjs": "rollup --config rollup.config.js", 55 | "build:types": "tsc --build", 56 | "prepare": "npm run build", 57 | "test:es": "uvu test all.spec.js", 58 | "test:web": "playwright-test -r uvu test/web.spec.js", 59 | "test:cjs": "rimraf dist && npm run build && node dist/test/all.spec.cjs", 60 | "test": "npm run test:es && npm run test:web && npm run test:cjs", 61 | "precommit": "lint-staged" 62 | }, 63 | "lint-staged": { 64 | "*.js": [ 65 | "prettier --no-semi --write", 66 | "git add" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/blob/rollup.config.js: -------------------------------------------------------------------------------- 1 | import multiInput from "rollup-plugin-multi-input" 2 | 3 | const config = [ 4 | ["test", "dist/test"], 5 | ["src", "dist/src"], 6 | ].map(([base, dest]) => ({ 7 | input: [`${base}/**/*.js`], 8 | output: { 9 | dir: dest, 10 | preserveModules: true, 11 | sourcemap: true, 12 | format: "cjs", 13 | entryFileNames: "[name].cjs", 14 | }, 15 | plugins: [multiInput({ relative: base })], 16 | })) 17 | export default config 18 | -------------------------------------------------------------------------------- /packages/blob/src/blob.js: -------------------------------------------------------------------------------- 1 | import { ReadableStream, TextEncoder, TextDecoder } from "./package.js" 2 | 3 | /** 4 | * @implements {globalThis.Blob} 5 | */ 6 | const WebBlob = class Blob { 7 | /** 8 | * @param {BlobPart[]} [init] 9 | * @param {BlobPropertyBag} [options] 10 | */ 11 | constructor(init = [], options = {}) { 12 | /** @type {Uint8Array[]} */ 13 | const parts = [] 14 | 15 | let size = 0 16 | for (const part of init) { 17 | if (typeof part === "string") { 18 | const bytes = new TextEncoder().encode(part) 19 | parts.push(bytes) 20 | size += bytes.byteLength 21 | } else if (part instanceof WebBlob) { 22 | size += part.size 23 | // @ts-ignore - `_parts` is marked private so TS will complain about 24 | // accessing it. 25 | parts.push(...part._parts) 26 | } else if (part instanceof ArrayBuffer) { 27 | parts.push(new Uint8Array(part)) 28 | size += part.byteLength 29 | } else if (part instanceof Uint8Array) { 30 | parts.push(part) 31 | size += part.byteLength 32 | } else if (ArrayBuffer.isView(part)) { 33 | const { buffer, byteOffset, byteLength } = part 34 | parts.push(new Uint8Array(buffer, byteOffset, byteLength)) 35 | size += byteLength 36 | } else { 37 | const bytes = new TextEncoder().encode(String(part)) 38 | parts.push(bytes) 39 | size += bytes.byteLength 40 | } 41 | } 42 | 43 | /** @private */ 44 | this._size = size 45 | /** @private */ 46 | this._type = readType(options.type) 47 | /** @private */ 48 | this._parts = parts 49 | 50 | Object.defineProperties(this, { 51 | _size: { enumerable: false }, 52 | _type: { enumerable: false }, 53 | _parts: { enumerable: false }, 54 | }) 55 | } 56 | 57 | /** 58 | * A string indicating the MIME type of the data contained in the Blob. 59 | * If the type is unknown, this string is empty. 60 | * @type {string} 61 | */ 62 | get type() { 63 | return this._type 64 | } 65 | /** 66 | * The size, in bytes, of the data contained in the Blob object. 67 | * @type {number} 68 | */ 69 | get size() { 70 | return this._size 71 | } 72 | 73 | /** 74 | * Returns a new Blob object containing the data in the specified range of 75 | * bytes of the blob on which it's called. 76 | * @param {number} [start=0] - An index into the Blob indicating the first 77 | * byte to include in the new Blob. If you specify a negative value, it's 78 | * treated as an offset from the end of the Blob toward the beginning. For 79 | * example, `-10` would be the 10th from last byte in the Blob. The default 80 | * value is `0`. If you specify a value for start that is larger than the 81 | * size of the source Blob, the returned Blob has size 0 and contains no 82 | * data. 83 | * @param {number} [end] - An index into the `Blob` indicating the first byte 84 | * that will *not* be included in the new `Blob` (i.e. the byte exactly at 85 | * this index is not included). If you specify a negative value, it's treated 86 | * as an offset from the end of the Blob toward the beginning. For example, 87 | * `-10` would be the 10th from last byte in the `Blob`. The default value is 88 | * size. 89 | * @param {string} [type] - The content type to assign to the new Blob; 90 | * this will be the value of its type property. The default value is an empty 91 | * string. 92 | * @returns {Blob} 93 | */ 94 | slice(start = 0, end = this.size, type = "") { 95 | const { size, _parts } = this 96 | let offset = start < 0 ? Math.max(size + start, 0) : Math.min(start, size) 97 | 98 | let limit = end < 0 ? Math.max(size + end, 0) : Math.min(end, size) 99 | const span = Math.max(limit - offset, 0) 100 | const blob = new Blob([], { type }) 101 | 102 | if (span === 0) { 103 | return blob 104 | } 105 | 106 | let blobSize = 0 107 | const blobParts = [] 108 | for (const part of _parts) { 109 | const { byteLength } = part 110 | if (offset > 0 && byteLength <= offset) { 111 | offset -= byteLength 112 | limit -= byteLength 113 | } else { 114 | const chunk = part.subarray(offset, Math.min(byteLength, limit)) 115 | blobParts.push(chunk) 116 | blobSize += chunk.byteLength 117 | // no longer need to take that into account 118 | offset = 0 119 | 120 | // don't add the overflow to new blobParts 121 | if (blobSize >= span) { 122 | break 123 | } 124 | } 125 | } 126 | 127 | blob._parts = blobParts 128 | blob._size = blobSize 129 | 130 | return blob 131 | } 132 | 133 | /** 134 | * Returns a promise that resolves with an ArrayBuffer containing the entire 135 | * contents of the Blob as binary data. 136 | * @returns {Promise} 137 | */ 138 | // eslint-disable-next-line require-await 139 | async arrayBuffer() { 140 | const buffer = new ArrayBuffer(this.size) 141 | const bytes = new Uint8Array(buffer) 142 | let offset = 0 143 | for (const part of this._parts) { 144 | bytes.set(part, offset) 145 | offset += part.byteLength 146 | } 147 | return buffer 148 | } 149 | 150 | /** 151 | * Returns a promise that resolves with a USVString containing the entire 152 | * contents of the Blob interpreted as UTF-8 text. 153 | * @returns {Promise} 154 | */ 155 | // eslint-disable-next-line require-await 156 | async text() { 157 | const decoder = new TextDecoder() 158 | let text = "" 159 | for (const part of this._parts) { 160 | text += decoder.decode(part) 161 | } 162 | return text 163 | } 164 | 165 | /** 166 | * @returns {BlobStream} 167 | */ 168 | stream() { 169 | return new BlobStream(this._parts) 170 | } 171 | 172 | /** 173 | * @returns {string} 174 | */ 175 | toString() { 176 | return "[object Blob]" 177 | } 178 | 179 | get [Symbol.toStringTag]() { 180 | return "Blob" 181 | } 182 | } 183 | 184 | // Marking export as a DOM File object instead of custom class. 185 | /** @type {typeof globalThis.Blob} */ 186 | const Blob = WebBlob 187 | 188 | /** 189 | * Blob stream is a `ReadableStream` extension optimized to have minimal 190 | * overhead when consumed as `AsyncIterable`. 191 | * @extends {ReadableStream} 192 | * @implements {AsyncIterable} 193 | */ 194 | class BlobStream extends ReadableStream { 195 | /** 196 | * @param {Uint8Array[]} chunks 197 | */ 198 | constructor(chunks) { 199 | // @ts-ignore 200 | super(new BlobStreamController(chunks.values()), { type: "bytes" }) 201 | /** @private */ 202 | this._chunks = chunks 203 | } 204 | 205 | /** 206 | * @param {Object} [_options] 207 | * @property {boolean} [_options.preventCancel] 208 | * @returns {AsyncIterator} 209 | */ 210 | async *[Symbol.asyncIterator](_options) { 211 | const reader = this.getReader() 212 | yield* this._chunks 213 | reader.releaseLock() 214 | } 215 | } 216 | 217 | class BlobStreamController { 218 | /** 219 | * @param {Iterator} chunks 220 | */ 221 | constructor(chunks) { 222 | this.chunks = chunks 223 | } 224 | 225 | /** 226 | * @param {ReadableStreamDefaultController} controller 227 | */ 228 | start(controller) { 229 | this.work(controller) 230 | this.isWorking = false 231 | this.isCancelled = false 232 | } 233 | /** 234 | * 235 | * @param {ReadableStreamDefaultController} controller 236 | */ 237 | async work(controller) { 238 | const { chunks } = this 239 | 240 | this.isWorking = true 241 | while (!this.isCancelled && (controller.desiredSize || 0) > 0) { 242 | let next = null 243 | try { 244 | next = chunks.next() 245 | } catch (error) { 246 | controller.error(error) 247 | break 248 | } 249 | 250 | if (next) { 251 | if (!next.done && !this.isCancelled) { 252 | controller.enqueue(next.value) 253 | } else { 254 | controller.close() 255 | } 256 | } 257 | } 258 | 259 | this.isWorking = false 260 | } 261 | 262 | /** 263 | * @param {ReadableStreamDefaultController} controller 264 | */ 265 | pull(controller) { 266 | if (!this.isWorking) { 267 | this.work(controller) 268 | } 269 | } 270 | cancel() { 271 | this.isCancelled = true 272 | } 273 | } 274 | 275 | /** 276 | * @param {string} [input] 277 | * @returns {string} 278 | */ 279 | const readType = (input = "") => { 280 | const type = String(input).toLowerCase() 281 | return /[^\u0020-\u007E]/.test(type) ? "" : type 282 | } 283 | 284 | export { Blob, ReadableStream, TextEncoder, TextDecoder } 285 | -------------------------------------------------------------------------------- /packages/blob/src/blob.node.js: -------------------------------------------------------------------------------- 1 | import * as builtin from "buffer" 2 | 3 | /** 4 | * @returns {typeof globalThis.Blob|null} 5 | */ 6 | const use = () => { 7 | try { 8 | // @ts-ignore 9 | const { Blob } = builtin 10 | const view = new Uint16Array(1) 11 | // Checks if critical issue with node implementation of Blob is fixed 12 | // @see https://github.com/nodejs/node/issues/40705 13 | const isBugFixed = new Blob([view]).size === view.byteLength 14 | return isBugFixed ? Blob : null 15 | } catch (error) { 16 | return null 17 | } 18 | } 19 | 20 | export const Blob = use() 21 | -------------------------------------------------------------------------------- /packages/blob/src/lib.js: -------------------------------------------------------------------------------- 1 | export { TextEncoder, TextDecoder, ReadableStream } from "./package.js" 2 | 3 | // On the web we just export native Blob implementation 4 | export const { Blob } = globalThis 5 | -------------------------------------------------------------------------------- /packages/blob/src/lib.node.js: -------------------------------------------------------------------------------- 1 | export { TextEncoder, TextDecoder, ReadableStream } from "./package.js" 2 | // import { Blob as NodeBlob } from "./blob.node.js" 3 | import { Blob as WebBlob } from "./blob.js" 4 | 5 | /** @type {typeof globalThis.Blob} */ 6 | // Our first choise is to use global `Blob` because it may be available e.g. in 7 | // electron renderrer process. If not available fall back to node native 8 | // implementation, if also not available use our implementation. 9 | export const Blob = 10 | globalThis.Blob || 11 | // Disable node native blob until impractical perf issue is fixed 12 | // @see https://github.com/nodejs/node/issues/42108 13 | // NodeBlob || 14 | WebBlob 15 | -------------------------------------------------------------------------------- /packages/blob/src/package.js: -------------------------------------------------------------------------------- 1 | export { TextEncoder, TextDecoder } from "web-encoding" 2 | export { ReadableStream } from "@remix-run/web-stream" 3 | -------------------------------------------------------------------------------- /packages/blob/test/all.spec.js: -------------------------------------------------------------------------------- 1 | import { test as blobTest } from "./blob.spec.js" 2 | import { test as sliceTest } from "./slice.spec.js" 3 | import { test as fetchTest } from "./fetch.spec.js" 4 | import { test } from "./test.js" 5 | 6 | blobTest(test) 7 | sliceTest(test) 8 | fetchTest(test) 9 | test.run() 10 | -------------------------------------------------------------------------------- /packages/blob/test/blob.spec.js: -------------------------------------------------------------------------------- 1 | import { Blob, TextDecoder } from "@remix-run/web-blob" 2 | import * as lib from "@remix-run/web-blob" 3 | import { assert } from "./test.js" 4 | 5 | /** 6 | * @param {import('./test').Test} test 7 | */ 8 | export const test = test => { 9 | test("test baisc", async () => { 10 | assert.isEqual(typeof lib.Blob, "function") 11 | assert.isEqual(typeof lib.TextDecoder, "function") 12 | assert.isEqual(typeof lib.TextEncoder, "function") 13 | assert.isEqual(typeof lib.ReadableStream, "function") 14 | }) 15 | 16 | if (globalThis.window === globalThis) { 17 | test("exports built-ins", () => { 18 | assert.equal(lib.Blob, globalThis.Blob) 19 | assert.equal(lib.TextDecoder, globalThis.TextDecoder) 20 | assert.equal(lib.TextEncoder, globalThis.TextEncoder) 21 | assert.equal(lib.ReadableStream, globalThis.ReadableStream) 22 | }) 23 | } 24 | 25 | test("test jsdom", async () => { 26 | const blob = new Blob(["TEST"]) 27 | assert.isEqual(blob.size, 4, "Initial blob should have a size of 4") 28 | }) 29 | 30 | test("should encode a blob with proper size when given two strings as arguments", async () => { 31 | const blob = new Blob(["hi", "hello"]) 32 | assert.isEqual(blob.size, 7) 33 | }) 34 | 35 | test("should encode arraybuffers with right content", async () => { 36 | const bytes = new Uint8Array(5) 37 | for (let i = 0; i < 5; i++) bytes[i] = i 38 | const blob = new Blob([bytes.buffer]) 39 | const buffer = await blob.arrayBuffer() 40 | const result = new Uint8Array(buffer) 41 | for (let i = 0; i < 5; i++) { 42 | assert.isEqual(result[i], i) 43 | } 44 | }) 45 | 46 | test("should encode typed arrays with right content", async () => { 47 | const bytes = new Uint8Array(5) 48 | for (let i = 0; i < 5; i++) bytes[i] = i 49 | const blob = new Blob([bytes]) 50 | 51 | const buffer = await blob.arrayBuffer() 52 | const result = new Uint8Array(buffer) 53 | 54 | for (let i = 0; i < 5; i++) { 55 | assert.isEqual(result[i], i) 56 | } 57 | }) 58 | 59 | test("should encode sliced typed arrays with right content", async () => { 60 | const bytes = new Uint8Array(5) 61 | for (let i = 0; i < 5; i++) bytes[i] = i 62 | const blob = new Blob([bytes.subarray(2)]) 63 | 64 | const buffer = await blob.arrayBuffer() 65 | const result = new Uint8Array(buffer) 66 | for (let i = 0; i < 3; i++) { 67 | assert.isEqual(result[i], i + 2) 68 | } 69 | }) 70 | 71 | test("should encode with blobs", async () => { 72 | const bytes = new Uint8Array(5) 73 | for (let i = 0; i < 5; i++) bytes[i] = i 74 | const blob = new Blob([new Blob([bytes.buffer])]) 75 | const buffer = await blob.arrayBuffer() 76 | const result = new Uint8Array(buffer) 77 | for (let i = 0; i < 5; i++) { 78 | assert.isEqual(result[i], i) 79 | } 80 | }) 81 | 82 | test("should enode mixed contents to right size", async () => { 83 | const bytes = new Uint8Array(5) 84 | for (let i = 0; i < 5; i++) { 85 | bytes[i] = i 86 | } 87 | const blob = new Blob([bytes.buffer, "hello"]) 88 | assert.isEqual(blob.size, 10) 89 | }) 90 | 91 | test("should accept mime type", async () => { 92 | const blob = new Blob(["hi", "hello"], { type: "text/html" }) 93 | assert.isEqual(blob.type, "text/html") 94 | }) 95 | 96 | test("should be an instance of constructor", async () => { 97 | const blob = new Blob(["hi"]) 98 | assert.equal(blob instanceof Blob, true) 99 | }) 100 | 101 | test("from text", async () => { 102 | const blob = new Blob(["hello"]) 103 | assert.isEqual(blob.size, 5, "is right size") 104 | assert.isEqual(blob.type, "", "type is empty") 105 | assert.isEqual(await blob.text(), "hello", "reads as text") 106 | assert.isEquivalent( 107 | new Uint8Array(await blob.arrayBuffer()), 108 | new Uint8Array("hello".split("").map(char => char.charCodeAt(0))) 109 | ) 110 | }) 111 | 112 | test("from text with type", async () => { 113 | const blob = new Blob(["hello"], { type: "text/markdown" }) 114 | assert.isEqual(blob.size, 5, "is right size") 115 | assert.isEqual(blob.type, "text/markdown", "type is set") 116 | assert.isEqual(await blob.text(), "hello", "reads as text") 117 | 118 | assert.isEquivalent( 119 | new Uint8Array(await blob.arrayBuffer()), 120 | new Uint8Array("hello".split("").map(char => char.charCodeAt(0))) 121 | ) 122 | }) 123 | 124 | test("empty blob", async () => { 125 | const blob = new Blob([]) 126 | assert.isEqual(blob.size, 0, "size is 0") 127 | assert.isEqual(blob.type, "", "type is empty") 128 | assert.isEqual(await blob.text(), "", "reads as text") 129 | assert.isEquivalent( 130 | await blob.arrayBuffer(), 131 | new ArrayBuffer(0), 132 | "returns empty buffer" 133 | ) 134 | }) 135 | 136 | test("no args", async () => { 137 | const blob = new Blob() 138 | assert.isEqual(blob.size, 0, "size is 0") 139 | assert.isEqual(blob.type, "", "type is empty") 140 | assert.isEqual(await blob.text(), "", "reads as text") 141 | assert.isEquivalent( 142 | await blob.arrayBuffer(), 143 | new ArrayBuffer(0), 144 | "returns empty buffer" 145 | ) 146 | }) 147 | 148 | test("all emtpy args", async () => { 149 | const blob = new Blob([ 150 | "", 151 | new Blob(), 152 | "", 153 | new Uint8Array(0), 154 | new ArrayBuffer(0), 155 | ]) 156 | assert.isEqual(blob.size, 0, "size is 0") 157 | assert.isEqual(blob.type, "", "type is empty") 158 | assert.isEqual(await blob.text(), "", "reads as text") 159 | assert.isEquivalent( 160 | await blob.arrayBuffer(), 161 | new ArrayBuffer(0), 162 | "returns empty buffer" 163 | ) 164 | }) 165 | 166 | test("combined blob", async () => { 167 | const uint8 = new Uint8Array([1, 2, 3]) 168 | const uint16 = new Uint16Array([8, 190]) 169 | const float32 = new Float32Array([5.4, 9, 1.5]) 170 | const string = "hello world" 171 | const blob = new Blob([uint8, uint16, float32, string]) 172 | 173 | const b8 = blob.slice(0, uint8.byteLength) 174 | const r8 = new Uint8Array(await b8.arrayBuffer()) 175 | assert.isEquivalent(uint8, r8) 176 | 177 | const b16 = blob.slice( 178 | uint8.byteLength, 179 | uint8.byteLength + uint16.byteLength 180 | ) 181 | const r16 = new Uint16Array(await b16.arrayBuffer()) 182 | assert.isEquivalent(uint16, r16) 183 | 184 | const b32 = blob.slice( 185 | uint8.byteLength + uint16.byteLength, 186 | uint8.byteLength + uint16.byteLength + float32.byteLength 187 | ) 188 | const r32 = new Float32Array(await b32.arrayBuffer()) 189 | assert.isEquivalent(float32, r32) 190 | 191 | const bs = blob.slice( 192 | uint8.byteLength + uint16.byteLength + float32.byteLength 193 | ) 194 | assert.isEqual(string, await bs.text()) 195 | 196 | assert.isEqual("wo", await bs.slice(6, 8).text()) 197 | assert.isEqual("world", await bs.slice(6).text()) 198 | assert.isEqual("world", await blob.slice(-5).text()) 199 | }) 200 | 201 | test("emoji", async () => { 202 | const emojis = `👍🤷🎉😤` 203 | const blob = new Blob([emojis]) 204 | const nestle = new Blob([new Blob([blob, blob])]) 205 | assert.isEqual(emojis + emojis, await nestle.text()) 206 | }) 207 | 208 | test("streams", async () => { 209 | const blob = new Blob(["hello", " ", "world"], { type: "text/plain" }) 210 | const stream = blob.stream() 211 | 212 | const reader = stream.getReader() 213 | const chunks = [] 214 | while (true) { 215 | const { done, value } = await reader.read() 216 | if (done) { 217 | break 218 | } 219 | 220 | if (value != null) { 221 | chunks.push(new TextDecoder().decode(value)) 222 | } 223 | } 224 | 225 | assert.deepEqual("hello world", chunks.join("")) 226 | }) 227 | } 228 | -------------------------------------------------------------------------------- /packages/blob/test/fetch.spec.js: -------------------------------------------------------------------------------- 1 | import { Response } from "@remix-run/web-fetch" 2 | import { Blob } from "@remix-run/web-blob" 3 | import { assert } from "./test.js" 4 | 5 | /** 6 | * @param {import('./test').Test} test 7 | */ 8 | export const test = test => { 9 | test("nodefetch recognizes blobs", async () => { 10 | const response = new Response(new Blob(["hello"])) 11 | 12 | assert.equal(await response.text(), "hello") 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /packages/blob/test/slice.spec.js: -------------------------------------------------------------------------------- 1 | import { Blob, TextEncoder } from "@remix-run/web-blob" 2 | import { assert } from "./test.js" 3 | 4 | /** 5 | * 6 | * @param {Blob} blob 7 | * @param {Object} expected 8 | * @param {number} expected.size 9 | * @param {string} [expected.type] 10 | * @param {Uint8Array[]} expected.content 11 | */ 12 | const assertBlob = async (blob, expected) => { 13 | assert.equal(blob instanceof Blob, true, "blob is instanceof Blob") 14 | assert.equal(String(blob), "[object Blob]", "String(blob) -> [object Blob]") 15 | assert.equal( 16 | blob.toString(), 17 | "[object Blob]", 18 | "blob.toString() -> [object Blob]" 19 | ) 20 | assert.equal(blob.size, expected.size, `blob.size == ${expected.size}`) 21 | assert.equal(blob.type, expected.type || "", "blob.type") 22 | 23 | const chunks = [] 24 | // @ts-ignore - https://github.com/microsoft/TypeScript/issues/29867 25 | const stream = blob.stream() 26 | const reader = stream.getReader() 27 | while (true) { 28 | const chunk = await reader.read() 29 | if (chunk.done) { 30 | reader.releaseLock() 31 | break 32 | } else { 33 | chunks.push(chunk.value) 34 | } 35 | } 36 | 37 | assert.deepEqual( 38 | concatUint8Array(chunks), 39 | concatUint8Array(expected.content), 40 | "blob.stream() matches expectation" 41 | ) 42 | 43 | let text = "" 44 | const encoder = new TextDecoder() 45 | for (const chunk of expected.content) { 46 | text += encoder.decode(chunk) 47 | } 48 | 49 | assert.deepEqual( 50 | await blob.text(), 51 | text, 52 | "blob.text() produces expected text" 53 | ) 54 | 55 | // Not all browsers implement this 56 | const bytes = concatUint8Array(expected.content) 57 | const buffer = await blob.arrayBuffer() 58 | assert.equal(buffer instanceof ArrayBuffer, true) 59 | assert.deepEqual(buffer, bytes.buffer) 60 | assert.deepEqual( 61 | new Uint8Array(buffer), 62 | bytes, 63 | "blob.arrayBuffer() produces expected buffer" 64 | ) 65 | } 66 | 67 | /** 68 | * @param {Uint8Array[]} chunks 69 | */ 70 | const concatUint8Array = chunks => { 71 | const bytes = [] 72 | for (const chunk of chunks) { 73 | bytes.push(...chunk) 74 | } 75 | return new Uint8Array(bytes) 76 | } 77 | 78 | /** 79 | * @param {*} input 80 | * @returns {Uint8Array} 81 | */ 82 | const toUint8Array = input => { 83 | if (typeof input === "string") { 84 | return new TextEncoder().encode(input) 85 | } else if (input instanceof ArrayBuffer) { 86 | return new Uint8Array(input) 87 | } else if (input instanceof Uint8Array) { 88 | return input 89 | } else if (ArrayBuffer.isView(input)) { 90 | return new Uint8Array(input.buffer, input.byteOffset, input.byteLength) 91 | } else { 92 | throw new TypeError(`Invalid input ${input}`) 93 | } 94 | } 95 | 96 | /** 97 | * @param {import('./test').Test} test 98 | */ 99 | export const test = test => { 100 | test("new Blob()", async () => { 101 | const blob = new Blob() 102 | 103 | await assertBlob(blob, { 104 | type: "", 105 | size: 0, 106 | content: [], 107 | }) 108 | }) 109 | 110 | test('new Blob("a=1")', async () => { 111 | const data = "a=1" 112 | const blob = new Blob([data]) 113 | 114 | await assertBlob(blob, { 115 | size: 3, 116 | type: "", 117 | content: [toUint8Array(data)], 118 | }) 119 | }) 120 | 121 | test("Blob with mixed parts", async () => { 122 | const parts = [ 123 | "a", 124 | new Uint8Array([98]), 125 | new Uint16Array([25699]), 126 | new Uint8Array([101]).buffer, 127 | new TextEncoder().encode("f"), 128 | new Blob(["g"]), 129 | ] 130 | 131 | await assertBlob(new Blob(parts), { 132 | size: 7, 133 | content: [...parts.slice(0, -1).map(toUint8Array), toUint8Array("g")], 134 | }) 135 | }) 136 | 137 | test("Blob slice", async () => { 138 | const parts = ["hello ", "world"] 139 | const blob = new Blob(parts) 140 | 141 | await assertBlob(blob, { 142 | size: 11, 143 | content: parts.map(toUint8Array), 144 | }) 145 | 146 | assertBlob(blob.slice(), { 147 | size: 11, 148 | content: parts.map(toUint8Array), 149 | }) 150 | 151 | assertBlob(blob.slice(2), { 152 | size: 9, 153 | content: [toUint8Array("llo "), toUint8Array("world")], 154 | }) 155 | 156 | assertBlob(blob.slice(5), { 157 | size: 6, 158 | content: [toUint8Array(" "), toUint8Array("world")], 159 | }) 160 | 161 | assertBlob(blob.slice(6), { 162 | size: 5, 163 | content: [toUint8Array("world")], 164 | }) 165 | 166 | assertBlob(blob.slice(5, 100), { 167 | size: 6, 168 | content: [toUint8Array(" "), toUint8Array("world")], 169 | }) 170 | 171 | assertBlob(blob.slice(-5), { 172 | size: 5, 173 | content: [toUint8Array("world")], 174 | }) 175 | 176 | assertBlob(blob.slice(-5, -10), { 177 | size: 0, 178 | content: [], 179 | }) 180 | 181 | assertBlob(blob.slice(-5, -2), { 182 | size: 3, 183 | content: [toUint8Array("wor")], 184 | }) 185 | 186 | assertBlob(blob.slice(-5, 11), { 187 | size: 5, 188 | content: [toUint8Array("world")], 189 | }) 190 | 191 | assertBlob(blob.slice(-5, 12), { 192 | size: 5, 193 | content: [toUint8Array("world")], 194 | }) 195 | 196 | assertBlob(blob.slice(-5, 10), { 197 | size: 4, 198 | content: [toUint8Array("worl")], 199 | }) 200 | }) 201 | 202 | test("Blob type", async () => { 203 | const type = "text/plain" 204 | const blob = new Blob([], { type }) 205 | await assertBlob(blob, { size: 0, type, content: [] }) 206 | }) 207 | 208 | test("Blob slice type", async () => { 209 | const type = "text/plain" 210 | const blob = new Blob().slice(0, 0, type) 211 | await assertBlob(blob, { size: 0, type, content: [] }) 212 | }) 213 | 214 | test("invalid Blob type", async () => { 215 | const blob = new Blob([], { type: "\u001Ftext/plain" }) 216 | await assertBlob(blob, { size: 0, type: "", content: [] }) 217 | }) 218 | 219 | test("invalid Blob slice type", async () => { 220 | const blob = new Blob().slice(0, 0, "\u001Ftext/plain") 221 | await assertBlob(blob, { size: 0, type: "", content: [] }) 222 | }) 223 | 224 | test("normalized Blob type", async () => { 225 | const blob = new Blob().slice(0, 0, "text/Plain") 226 | await assertBlob(blob, { size: 0, type: "text/plain", content: [] }) 227 | }) 228 | 229 | test("Blob slice(0, 1)", async () => { 230 | const data = "abcdefgh" 231 | const blob = new Blob([data]).slice(0, 1) 232 | await assertBlob(blob, { 233 | size: 1, 234 | content: [toUint8Array("a")], 235 | }) 236 | }) 237 | 238 | test("Blob slice(-1)", async () => { 239 | const data = "abcdefgh" 240 | const blob = new Blob([data]).slice(-1) 241 | await assertBlob(blob, { 242 | size: 1, 243 | content: [toUint8Array("h")], 244 | }) 245 | }) 246 | 247 | test("Blob slice(0, -1)", async () => { 248 | const data = "abcdefgh" 249 | const blob = new Blob([data]).slice(0, -1) 250 | await assertBlob(blob, { 251 | size: 7, 252 | content: [toUint8Array("abcdefg")], 253 | }) 254 | }) 255 | 256 | test("blob.slice(1, 2)", async () => { 257 | const blob = new Blob(["a", "b", "c"]).slice(1, 2) 258 | await assertBlob(blob, { 259 | size: 1, 260 | content: [toUint8Array("b")], 261 | }) 262 | }) 263 | } 264 | -------------------------------------------------------------------------------- /packages/blob/test/test.js: -------------------------------------------------------------------------------- 1 | import * as uvu from "uvu" 2 | import * as uvuassert from "uvu/assert" 3 | 4 | const deepEqual = uvuassert.equal 5 | const isEqual = uvuassert.equal 6 | const isEquivalent = uvuassert.equal 7 | export const assert = { ...uvuassert, deepEqual, isEqual, isEquivalent } 8 | export const test = uvu.test 9 | 10 | 11 | /** 12 | * @typedef {uvu.Test} Test 13 | */ 14 | -------------------------------------------------------------------------------- /packages/blob/test/web.spec.js: -------------------------------------------------------------------------------- 1 | import { test as blobTest } from "./blob.spec.js" 2 | import { test as sliceTest } from "./slice.spec.js" 3 | import { test } from "./test.js" 4 | 5 | blobTest(test) 6 | sliceTest(test) 7 | 8 | test.run() 9 | -------------------------------------------------------------------------------- /packages/blob/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "paths": { 6 | "@remix-run/web-blob": ["packages/blob/src/lib.js"] 7 | } 8 | }, 9 | "include": [ 10 | "src", 11 | "test" 12 | ], 13 | "references": [ 14 | { "path": "../fetch"} 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/fetch/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.yml] 16 | indent_style = space 17 | 18 | [package.json] 19 | indent_style = space 20 | indent_size = 2 21 | insert_final_newline = false 22 | -------------------------------------------------------------------------------- /packages/fetch/.gitignore: -------------------------------------------------------------------------------- 1 | # Sketch temporary file 2 | ~*.sketch 3 | 4 | # Generated files 5 | dist/ 6 | @types/ 7 | *.tsbuildinfo 8 | 9 | # Logs 10 | logs 11 | *.log 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like nyc and istanbul 22 | .nyc_output 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # Commenting this out is preferred by some people, see 33 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 34 | node_modules 35 | 36 | # Users Environment Variables 37 | .lock-wscript 38 | 39 | # OS files 40 | .DS_Store 41 | 42 | # Babel-compiled files 43 | lib 44 | 45 | # Ignore package manager lock files 46 | package-lock.json 47 | yarn.lock 48 | 49 | # Ignore IDE 50 | .idea 51 | -------------------------------------------------------------------------------- /packages/fetch/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact=false 3 | -------------------------------------------------------------------------------- /packages/fetch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.4.2 4 | 5 | ### Patch Changes 6 | 7 | - Support HTTP2 pseudo-headers like `:authority`, `:method`, etc. ([#55](https://github.com/remix-run/web-std-io/pull/55)) 8 | 9 | ## 4.4.1 10 | 11 | ### Patch Changes 12 | 13 | - 15868ef: Add missing `@remix-run/web-blob` dependency to `@remix-run/web-fetch` 14 | 15 | ## 4.4.0 16 | 17 | ### Minor Changes 18 | 19 | - Export CJS version for browser ([807fc63](https://github.com/remix-run/web-std-io/commit/807fc63)) 20 | 21 | ### Patch Changes 22 | 23 | - Fix `toFormData` imports ([d3a1ffd](https://github.com/remix-run/web-std-io/commit/d3a1ffd)) 24 | - Updated dependencies 25 | - `@remix-run/web-file@3.1.0` 26 | - `@remix-run/web-form-data@3.1.0` 27 | - `@remix-run/web-stream@1.1.0` 28 | 29 | ## 4.3.8 30 | 31 | ### Patch Changes 32 | 33 | - 8ea9e6c: fetch called on non Window object 34 | 35 | ## 4.3.7 36 | 37 | ### Patch Changes 38 | 39 | - Fix `headers.entries`/`values`/`forEach` iteration for `Set-Cookie` headers ([#39](https://github.com/remix-run/web-std-io/pull/39)) 40 | - Import `Buffer` from `"buffer"` instead of relying on a global `Buffer` ([#40](https://github.com/remix-run/web-std-io/pull/40)) 41 | 42 | ## 4.3.6 43 | 44 | ### Patch Changes 45 | 46 | - 5d1e12a: Remove socket listeners if request is aborted 47 | - 43c6ce2: Move types conditional export to the top of the list to align with [the node guidance](https://nodejs.org/api/packages.html#community-conditions-definitions) 48 | - Updated dependencies [43c6ce2] 49 | - @remix-run/web-blob@3.0.5 50 | - @remix-run/web-form-data@3.0.5 51 | - @remix-run/web-stream@1.0.4 52 | 53 | ## 4.3.5 54 | 55 | ### Patch Changes 56 | 57 | - cf9ee6f: Submitted empty file inputs are now correctly parsed out as empty `File` instances instead of being surfaced as an empty string via `request.formData()` 58 | 59 | ## 4.3.4 60 | 61 | ### Patch Changes 62 | 63 | - 7f91c87: fixes "ERR_INVALID_THIS" on Node 20 64 | 65 | ## 4.3.3 66 | 67 | ### Patch Changes 68 | 69 | - Align with [spec](https://fetch.spec.whatwg.org/#methods) for `new Request()` `method` normalization ([#30](https://github.com/remix-run/web-std-io/pull/30)) 70 | 71 | - Only `DELETE`, `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT` get automatically uppercased 72 | - Note that `method: "patch"` will no longer be automatically uppercased 73 | - Throw a `TypeError` for `CONNECT`, `TRACE`, and `TRACK` 74 | 75 | ## 4.3.2 76 | 77 | ### Patch Changes 78 | 79 | - 3b9b384: Memory leak caused by unregistered listeners. Solution was copied from a node-fetch pr. 80 | - a85373d: Add support for custom "credentials" value. Nothing is done with them at the moment but they pass through for the consumer of the request to access if needed. 81 | 82 | ## 4.3.1 83 | 84 | ### Patch Changes 85 | 86 | - eb54144: Make Request signal handling follow spec: https://fetch.spec.whatwg.org/#ref-for-map-exists%E2%91%A0%E2%91%A3 87 | 88 | ## 4.3.0 89 | 90 | ### Minor Changes 91 | 92 | - 6d9cd44: expose RequestExtraOptions to fetch & add HTTPs.agent to types 93 | 94 | ### Patch Changes 95 | 96 | - 908263e: allow clone of request and responses will null body 97 | 98 | ## 4.2.0 99 | 100 | ### Minor Changes 101 | 102 | - a34cb39: Fixes redirects failing when response is chunked but empty. This is backported from https://github.com/node-fetch/node-fetch/pull/1222 103 | 104 | ### Patch Changes 105 | 106 | - dcfcac4: Fix generated types to work with node ESM / NodeNext 107 | - Updated dependencies [6521895] 108 | - @remix-run/web-form-data@3.0.3 109 | 110 | ## [4.1.0](https://www.github.com/web-std/io/compare/fetch-v4.0.1...fetch-v4.1.0) (2022-04-20) 111 | 112 | ### Features 113 | 114 | - add support for application/x-www-form-urlencoded in request.formData() ([#60](https://www.github.com/web-std/io/issues/60)) ([c719b0d](https://www.github.com/web-std/io/commit/c719b0de442811eb588309b777ab6ab3d966cdf1)) 115 | 116 | ## [4.0.1](https://www.github.com/web-std/io/compare/fetch-v4.0.0...fetch-v4.0.1) (2022-04-13) 117 | 118 | ### Bug Fixes 119 | 120 | - **packages/fetch:** only export what's needed so TS doesn't mess up the imports in the output files ([30ad037](https://www.github.com/web-std/io/commit/30ad0377a88ebffc3a998616e3b774ce5bcc584a)) 121 | - **packages/stream:** no initializers in ambient contexts ([30ad037](https://www.github.com/web-std/io/commit/30ad0377a88ebffc3a998616e3b774ce5bcc584a)) 122 | - typescript types ([#56](https://www.github.com/web-std/io/issues/56)) ([30ad037](https://www.github.com/web-std/io/commit/30ad0377a88ebffc3a998616e3b774ce5bcc584a)) 123 | 124 | ## [4.0.0](https://www.github.com/web-std/io/compare/fetch-v3.0.3...fetch-v4.0.0) (2022-02-28) 125 | 126 | ### ⚠ BREAKING CHANGES 127 | 128 | - export native fetch on the web (#53) 129 | 130 | ### Features 131 | 132 | - export native fetch on the web ([#53](https://www.github.com/web-std/io/issues/53)) ([af03280](https://www.github.com/web-std/io/commit/af03280788286cd69185efb0572da162f16d48cc)) 133 | - implement file: protocol support for fetch ([#55](https://www.github.com/web-std/io/issues/55)) ([19d17c7](https://www.github.com/web-std/io/commit/19d17c76f995800c9e07d5d6a923f33b81ab1d22)) 134 | 135 | ## [3.0.3](https://www.github.com/web-std/io/compare/fetch-v3.0.2...fetch-v3.0.3) (2022-01-28) 136 | 137 | ### Bug Fixes 138 | 139 | - include dist/index.cjs in files ([#47](https://www.github.com/web-std/io/issues/47)) ([2a12474](https://www.github.com/web-std/io/commit/2a1247404650bf5b6662fa520248bf07ae457987)) 140 | 141 | ## [3.0.2](https://www.github.com/web-std/io/compare/fetch-v3.0.1...fetch-v3.0.2) (2022-01-21) 142 | 143 | ### Changes 144 | 145 | - bump fetch versions ([e8ae4e5](https://www.github.com/web-std/io/commit/e8ae4e5e61591f1bcbd45a0541c762468e134e4b)) 146 | 147 | ## [3.0.1](https://www.github.com/web-std/io/compare/fetch-v3.0.0...fetch-v3.0.1) (2022-01-19) 148 | 149 | ### Bug Fixes 150 | 151 | - ship less files to address TSC issues ([#35](https://www.github.com/web-std/io/issues/35)) ([0651e62](https://www.github.com/web-std/io/commit/0651e62ae42d17eae2db89858c9e44f3342c304c)) 152 | 153 | ## 3.0.0 (2021-11-08) 154 | 155 | ### Features 156 | 157 | - revamp the repo ([#19](https://www.github.com/web-std/io/issues/19)) ([90624cf](https://www.github.com/web-std/io/commit/90624cfd2d4253c2cbc316d092f26e77b5169f47)) 158 | 159 | ### Changes 160 | 161 | - align package versions ([09c8676](https://www.github.com/web-std/io/commit/09c8676348619313d9df24d9597cea0eb82704d2)) 162 | - bump versions ([#27](https://www.github.com/web-std/io/issues/27)) ([0fe5224](https://www.github.com/web-std/io/commit/0fe5224124e318f560dcfbd8a234d05367c9fbcb)) 163 | -------------------------------------------------------------------------------- /packages/fetch/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jimmy@warting.se. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /packages/fetch/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 - 2020 Node Fetch Team 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 | 23 | -------------------------------------------------------------------------------- /packages/fetch/Readme.md: -------------------------------------------------------------------------------- 1 | # @remix-run/web-fetch 2 | 3 | [![ci][ci.icon]][ci.url] 4 | [![package][version.icon] ![downloads][downloads.icon]][package.url] 5 | 6 | Web API compatible [fetch API][] for nodejs. 7 | 8 | ## Comparison to Alternatives 9 | 10 | #### [node-fetch][] 11 | 12 | The reason this fork exists is because [node-fetch][] chooses to compromise 13 | Web API compatibility and by using nodejs native [Readable][] stream. They way 14 | they put it is: 15 | 16 | > 17 | > - Make conscious trade-off when following [WHATWG fetch spec][whatwg-fetch] and [stream spec](https://streams.spec.whatwg.org/) implementation details, document known differences. 18 | > - Use native Node streams for body, on both request and response. 19 | > 20 | 21 | We found these incompatibility to be really problematic when sharing code 22 | across nodejs and browser rutimes. This library uses [@remix-run/web-stream][] instead. 23 | 24 | 25 | 26 | [ci.icon]: https://github.com/web-std/io/workflows/fetch/badge.svg 27 | [ci.url]: https://github.com/web-std/io/actions/workflows/fetch.yml 28 | [version.icon]: https://img.shields.io/npm/v/@remix-run/web-fetch.svg 29 | [downloads.icon]: https://img.shields.io/npm/dm/@remix-run/web-fetch.svg 30 | [package.url]: https://npmjs.org/package/@remix-run/web-fetch 31 | [downloads.image]: https://img.shields.io/npm/dm/@remix-run/web-fetch.svg 32 | [downloads.url]: https://npmjs.org/package/@remix-run/web-fetch 33 | [prettier.icon]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg 34 | [prettier.url]: https://github.com/prettier/prettier 35 | [blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob 36 | [fetch-blob]: https://github.com/node-fetch/fetch-blob 37 | [readablestream]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream 38 | [readable]: https://nodejs.org/api/stream.html#stream_readable_streams 39 | [w3c blob.stream]: https://w3c.github.io/FileAPI/#dom-blob-stream 40 | [@remix-run/web-stream]:https://github.com/web-std/io/tree/main/stream 41 | [Uint8Array]:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array 42 | [node-fetch]:https://github.com/node-fetch/ 43 | [fetch api]:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 44 | [readable]: https://nodejs.org/api/stream.html#stream_readable_streams 45 | -------------------------------------------------------------------------------- /packages/fetch/docs/ERROR-HANDLING.md: -------------------------------------------------------------------------------- 1 | 2 | Error handling with node-fetch 3 | ============================== 4 | 5 | Because `window.fetch` isn't designed to be transparent about the cause of request errors, we have to come up with our own solutions. 6 | 7 | The basics: 8 | 9 | - A cancelled request is rejected with an [`AbortError`](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-aborterror). You can check if the reason for rejection was that the request was aborted by checking the `Error`'s `name` is `AbortError`. 10 | 11 | ```js 12 | const fetch = require('node-fetch'); 13 | 14 | (async () => { 15 | try { 16 | await fetch(url, {signal}); 17 | } catch (error) { 18 | if (error.name === 'AbortError') { 19 | console.log('request was aborted'); 20 | } 21 | } 22 | })(); 23 | ``` 24 | 25 | - All [operational errors][joyent-guide] *other than aborted requests* are rejected with a [FetchError](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-fetcherror). You can handle them all through the `try/catch` block or promise `catch` clause. 26 | 27 | - All errors come with an `error.message` detailing the cause of errors. 28 | 29 | - All errors originating from `node-fetch` are marked with a custom `err.type`. 30 | 31 | - All errors originating from Node.js core are marked with `error.type = 'system'`, and in addition contain an `error.code` and an `error.errno` for error handling. These are aliases for error codes thrown by Node.js core. 32 | 33 | - [Programmer errors][joyent-guide] are either thrown as soon as possible, or rejected with default `Error` with `error.message` for ease of troubleshooting. 34 | 35 | List of error types: 36 | 37 | - Because we maintain 100% coverage, see [test/main.js](https://github.com/node-fetch/node-fetch/blob/master/test/main.js) for a full list of custom `FetchError` types, as well as some of the common errors from Node.js 38 | 39 | [joyent-guide]: https://www.joyent.com/node-js/production/design/errors#operational-errors-vs-programmer-errors 40 | -------------------------------------------------------------------------------- /packages/fetch/docs/media/Banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Lunacy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /packages/fetch/docs/media/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Lunacy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /packages/fetch/docs/media/NodeFetch.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/web-std-io/32e85f441ca99b77fa78ad8735bcc195e5f6d438/packages/fetch/docs/media/NodeFetch.sketch -------------------------------------------------------------------------------- /packages/fetch/docs/v2-LIMITS.md: -------------------------------------------------------------------------------- 1 | 2 | Known differences 3 | ================= 4 | 5 | *As of 2.x release* 6 | 7 | - Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. 8 | 9 | - URL input must be an absolute URL, using either `http` or `https` as scheme. 10 | 11 | - On the upside, there are no forbidden headers. 12 | 13 | - `res.url` contains the final url when following redirects. 14 | 15 | - For convenience, `res.body` is a Node.js [Readable stream][readable-stream], so decoding can be handled independently. 16 | 17 | - Similarly, `req.body` can either be `null`, a string, a buffer or a Readable stream. 18 | 19 | - Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. See [ERROR-HANDLING.md][] for more info. 20 | 21 | - Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()` 22 | 23 | - There is currently no built-in caching, as server-side caching varies by use-cases. 24 | 25 | - Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually. 26 | 27 | - If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). 28 | 29 | - Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. 30 | 31 | [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams 32 | [ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md 33 | -------------------------------------------------------------------------------- /packages/fetch/docs/v2-UPGRADE-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Upgrade to node-fetch v2.x 2 | 3 | node-fetch v2.x brings about many changes that increase the compliance of 4 | WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes mean 5 | that apps written for node-fetch v1.x needs to be updated to work with 6 | node-fetch v2.x and be conformant with the Fetch Standard. This document helps 7 | you make this transition. 8 | 9 | Note that this document is not an exhaustive list of all changes made in v2.x, 10 | but rather that of the most important breaking changes. See our [changelog] for 11 | other comparatively minor modifications. 12 | 13 | ## `.text()` no longer tries to detect encoding 14 | 15 | In v1.x, `response.text()` attempts to guess the text encoding of the input 16 | material and decode it for the user. However, it runs counter to the Fetch 17 | Standard which demands `.text()` to always use UTF-8. 18 | 19 | In "response" to that, we have changed `.text()` to use UTF-8. A new function 20 | **`response.textConverted()`** is created that maintains the behavior of 21 | `.text()` in v1.x. 22 | 23 | ## Internal methods hidden 24 | 25 | In v1.x, the user can access internal methods such as `_clone()`, `_decode()`, 26 | and `_convert()` on the `response` object. While these methods should never 27 | have been used, node-fetch v2.x makes these functions completely inaccessible. 28 | If your app makes use of these functions, it may break when upgrading to v2.x. 29 | 30 | If you have a use case that requires these methods to be available, feel free 31 | to file an issue and we will be happy to help you solve the problem. 32 | 33 | ## Headers 34 | 35 | The main goal we have for the `Headers` class in v2.x is to make it completely 36 | spec-compliant. These changes are done in conjunction with GitHub's 37 | [`whatwg-fetch`][gh-fetch] polyfill, [Chrome][chrome-headers], and 38 | [Firefox][firefox-headers]. 39 | 40 | ```js 41 | ////////////////////////////////////////////////////////////////////////////// 42 | // `get()` now returns **all** headers, joined by a comma, instead of only the 43 | // first one. Its original behavior can be emulated using 44 | // `get().split(',')[0]`. 45 | 46 | const headers = new Headers({ 47 | 'Abc': 'string', 48 | 'Multi': ['header1', 'header2'] 49 | }); 50 | 51 | // before after 52 | headers.get('Abc') => headers.get('Abc') => 53 | 'string' 'string' 54 | headers.get('Multi') => headers.get('Multi') => 55 | 'header1'; 'header1,header2'; 56 | headers.get('Multi').split(',')[0] => 57 | 'header1'; 58 | 59 | 60 | ////////////////////////////////////////////////////////////////////////////// 61 | // `getAll()` is removed. Its behavior in v1 can be emulated with 62 | // `get().split(',')`. 63 | 64 | const headers = new Headers({ 65 | 'Abc': 'string', 66 | 'Multi': ['header1', 'header2'] 67 | }); 68 | 69 | // before after 70 | headers.getAll('Multi') => headers.getAll('Multi') => 71 | [ 'header1', 'header2' ]; throws ReferenceError 72 | headers.get('Multi').split(',') => 73 | ['header1', 'header2']; 74 | 75 | 76 | ////////////////////////////////////////////////////////////////////////////// 77 | // All method parameters are now stringified. 78 | const headers = new Headers(); 79 | headers.set('null-header', null); 80 | headers.set('undefined', undefined); 81 | 82 | // before after 83 | headers.get('null-header') headers.get('null-header') 84 | => null => 'null' 85 | headers.get(undefined) headers.get(undefined) 86 | => throws => 'undefined' 87 | 88 | 89 | ////////////////////////////////////////////////////////////////////////////// 90 | // Invalid HTTP header names and values are now rejected outright. 91 | const headers = new Headers(); 92 | headers.set('Héy', 'ok'); // now throws 93 | headers.get('Héy'); // now throws 94 | new Headers({'Héy': 'ok'}); // now throws 95 | ``` 96 | 97 | ## Node.js v0.x support dropped 98 | 99 | If you are still using Node.js v0.10 or v0.12, upgrade ASAP. Not only has it 100 | become too much work for us to maintain, Node.js has also dropped support for 101 | those release branches in 2016. Check out Node.js' official [LTS plan] for more 102 | information on Node.js' support lifetime. 103 | 104 | [whatwg-fetch]: https://fetch.spec.whatwg.org/ 105 | [LTS plan]: https://github.com/nodejs/LTS#lts-plan 106 | [gh-fetch]: https://github.com/github/fetch 107 | [chrome-headers]: https://crbug.com/645492 108 | [firefox-headers]: https://bugzilla.mozilla.org/show_bug.cgi?id=1278275 109 | [changelog]: CHANGELOG.md 110 | -------------------------------------------------------------------------------- /packages/fetch/docs/v3-LIMITS.md: -------------------------------------------------------------------------------- 1 | Known differences 2 | ================= 3 | 4 | *As of 3.x release* 5 | 6 | - Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. 7 | 8 | - On the upside, there are no forbidden headers. 9 | 10 | - `res.url` contains the final url when following redirects. 11 | 12 | - For convenience, `res.body` is a Node.js [Readable stream][readable-stream], so decoding can be handled independently. 13 | 14 | - Similarly, `req.body` can either be `null`, a buffer or a Readable stream. 15 | 16 | - Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. See [ERROR-HANDLING.md][] for more info. 17 | 18 | - Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()` 19 | 20 | - There is currently no built-in caching, as server-side caching varies by use-cases. 21 | 22 | - Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually. 23 | 24 | - If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js has a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Learn [how to get around this][highwatermark-fix]. 25 | 26 | - Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. 27 | 28 | [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams 29 | [ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md 30 | [highwatermark-fix]: https://github.com/node-fetch/node-fetch/blob/master/README.md#custom-highwatermark 31 | -------------------------------------------------------------------------------- /packages/fetch/docs/v3-UPGRADE-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Upgrade to node-fetch v3.x 2 | 3 | node-fetch v3.x brings about many changes that increase the compliance of 4 | WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes mean 5 | that apps written for node-fetch v2.x needs to be updated to work with 6 | node-fetch v3.x and be conformant with the Fetch Standard. This document helps 7 | you make this transition. 8 | 9 | Note that this document is not an exhaustive list of all changes made in v3.x, 10 | but rather that of the most important breaking changes. See our [changelog] for 11 | other comparatively minor modifications. 12 | 13 | - [Breaking Changes](#breaking) 14 | - [Enhancements](#enhancements) 15 | 16 | --- 17 | 18 | 19 | 20 | # Breaking Changes 21 | 22 | ## Minimum supported Node.js version is now 10.16 23 | 24 | Since Node.js will deprecate version 8 at the end of 2019, we decided that node-fetch v3.x will not only drop support for Node.js 4 and 6 (which were supported in v2.x), but also for Node.js 8. We strongly encourage you to upgrade, if you still haven't done so. Check out Node.js' official [LTS plan] for more information on Node.js' support lifetime. 25 | 26 | ## The `timeout` option was removed. 27 | 28 | Since this was never part of the fetch specification, it was removed. AbortSignal offers a more finegrained control of request timeouts, and is standardized in the Fetch spec. For convenience, you can use [timeout-signal](https://github.com/Richienb/timeout-signal) as a workaround: 29 | 30 | ```js 31 | const timeoutSignal = require('timeout-signal'); 32 | const fetch = require('node-fetch'); 33 | 34 | const {AbortError} = fetch 35 | 36 | fetch('https://www.google.com', { signal: timeoutSignal(5000) }) 37 | .then(response => { 38 | // Handle response 39 | }) 40 | .catch(error => { 41 | if (error instanceof AbortError) { 42 | // Handle timeout 43 | } 44 | }) 45 | ``` 46 | 47 | ## `Response.statusText` no longer sets a default message derived from the HTTP status code 48 | 49 | If the server didn't respond with status text, node-fetch would set a default message derived from the HTTP status code. This behavior was not spec-compliant and now the `statusText` will remain blank instead. 50 | 51 | ## Dropped the `browser` field in package.json 52 | 53 | Prior to v3.x, we included a `browser` field in the package.json file. Since node-fetch is intended to be used on the server, we have removed this field. If you are using node-fetch client-side, consider switching to something like [cross-fetch]. 54 | 55 | ## Dropped the `res.textConverted()` function 56 | 57 | If you want charset encoding detection, please use the [fetch-charset-detection] package ([documentation][fetch-charset-detection-docs]). 58 | 59 | ```js 60 | const fetch = require('node-fetch'); 61 | const convertBody = require('fetch-charset-detection'); 62 | 63 | fetch('https://somewebsite.com').then(res => { 64 | const text = convertBody(res.buffer(), res.headers); 65 | }); 66 | ``` 67 | 68 | ## JSON parsing errors from `res.json()` are of type `SyntaxError` instead of `FetchError` 69 | 70 | When attempting to parse invalid json via `res.json()`, a `SyntaxError` will now be thrown instead of a `FetchError` to align better with the spec. 71 | 72 | ```js 73 | const fetch = require('node-fetch'); 74 | 75 | fetch('https://somewebsitereturninginvalidjson.com').then(res => res.json()) 76 | // Throws 'Uncaught SyntaxError: Unexpected end of JSON input' or similar. 77 | ``` 78 | 79 | ## A stream pipeline is now used to forward errors 80 | 81 | If you are listening for errors via `res.body.on('error', () => ...)`, replace it with `res.body.once('error', () => ...)` so that your callback is not [fired twice](https://github.com/node-fetch/node-fetch/issues/668#issuecomment-569386115) in NodeJS >=13.5. 82 | 83 | ## `req.body` can no longer be a string 84 | 85 | We are working towards changing body to become either null or a stream. 86 | 87 | ## Changed default user agent 88 | 89 | The default user agent has been changed from `node-fetch/1.0 (+https://github.com/node-fetch/node-fetch)` to `node-fetch (+https://github.com/node-fetch/node-fetch)`. 90 | 91 | ## Arbitrary URLs are no longer supported 92 | 93 | Since in 3.x we are using the WHATWG's `new URL()`, arbitrary URL parsing will fail due to lack of base. 94 | 95 | # Enhancements 96 | 97 | ## Data URI support 98 | 99 | Previously, node-fetch only supported http url scheme. However, the Fetch Standard recently introduced the `data:` URI support. Following the specification, we implemented this feature in v3.x. Read more about `data:` URLs [here][data-url]. 100 | 101 | ## New & exposed Blob implementation 102 | 103 | Blob implementation is now [fetch-blob] and hence is exposed, unlikely previously, where Blob type was only internal and not exported. 104 | 105 | ## Better UTF-8 URL handling 106 | 107 | We now use the new Node.js [WHATWG-compliant URL API][whatwg-nodejs-url], so UTF-8 URLs are handled properly. 108 | 109 | ## Request errors are now piped using `stream.pipeline` 110 | 111 | Since the v3.x requires at least Node.js 10, we can utilise the new API. 112 | 113 | ## Creating Request/Response objects with relative URLs is no longer supported 114 | 115 | We introduced Node.js `new URL()` API in 3.x, because it offers better UTF-8 support and is WHATWG URL compatible. The drawback is, given current limit of the API (nodejs/node#12682), it's not possible to support relative URL parsing without hacks. 116 | Due to the lack of a browsing context in Node.js, we opted to drop support for relative URLs on Request/Response object, and it will now throw errors if you do so. 117 | The main `fetch()` function will support absolute URLs and data url. 118 | 119 | ## Bundled TypeScript types 120 | 121 | Since v3.x you no longer need to install `@types/node-fetch` package in order to use `node-fetch` with TypeScript. 122 | 123 | [whatwg-fetch]: https://fetch.spec.whatwg.org/ 124 | [data-url]: https://fetch.spec.whatwg.org/#data-url-processor 125 | [LTS plan]: https://github.com/nodejs/LTS#lts-plan 126 | [cross-fetch]: https://github.com/lquixada/cross-fetch 127 | [fetch-charset-detection]: https://github.com/Richienb/fetch-charset-detection 128 | [fetch-charset-detection-docs]: https://richienb.github.io/fetch-charset-detection/globals.html#convertbody 129 | [fetch-blob]: https://github.com/node-fetch/fetch-blob#readme 130 | [whatwg-nodejs-url]: https://nodejs.org/api/url.html#url_the_whatwg_url_api 131 | [changelog]: CHANGELOG.md 132 | -------------------------------------------------------------------------------- /packages/fetch/example.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | // Plain text or HTML 4 | (async () => { 5 | const response = await fetch('https://github.com/'); 6 | const body = await response.text(); 7 | 8 | console.log(body); 9 | })(); 10 | 11 | // JSON 12 | (async () => { 13 | const response = await fetch('https://github.com/'); 14 | const json = await response.json(); 15 | 16 | console.log(json); 17 | })(); 18 | 19 | // Simple Post 20 | (async () => { 21 | const response = await fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}); 22 | const json = await response.json(); 23 | 24 | console.log(json); 25 | })(); 26 | 27 | // Post with JSON 28 | (async () => { 29 | const body = {a: 1}; 30 | 31 | const response = await fetch('https://httpbin.org/post', { 32 | method: 'post', 33 | body: JSON.stringify(body), 34 | headers: {'Content-Type': 'application/json'} 35 | }); 36 | const json = await response.json(); 37 | 38 | console.log(json); 39 | })(); 40 | -------------------------------------------------------------------------------- /packages/fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remix-run/web-fetch", 3 | "version": "4.4.2", 4 | "description": "Web API compatible fetch implementation", 5 | "main": "./dist/lib.node.cjs", 6 | "module": "./src/lib.node.js", 7 | "types": "./dist/src/lib.node.d.ts", 8 | "sideEffects": false, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/src/lib.node.d.ts", 13 | "browser": { 14 | "require": "./dist/lib.cjs", 15 | "import": "./src/lib.js" 16 | }, 17 | "require": "./dist/lib.node.cjs", 18 | "import": "./src/lib.node.js" 19 | }, 20 | "./package.json": "./package.json", 21 | "./body": { 22 | "types": "./dist/src/body.d.ts", 23 | "import": "./src/body.js" 24 | }, 25 | "./src/request.js": { 26 | "types": "./dist/src/request.d.ts", 27 | "import": "./src/request.js" 28 | }, 29 | "./src/response.js": { 30 | "types": "./dist/src/response.d.ts", 31 | "import": "./src/response.js" 32 | }, 33 | "./src/headers.js": { 34 | "types": "./dist/src/headers.d.ts", 35 | "import": "./src/headers.js" 36 | } 37 | }, 38 | "files": [ 39 | "src", 40 | "dist", 41 | "License.md", 42 | "Readme.md" 43 | ], 44 | "engines": { 45 | "node": "^10.17 || >=12.3" 46 | }, 47 | "scripts": { 48 | "build": "npm run build:cjs && npm run build:types", 49 | "test:es": "node --experimental-modules ../../node_modules/c8/bin/c8 --reporter=html --reporter=lcov --reporter=text --check-coverage node --experimental-modules ../../node_modules/mocha/bin/mocha", 50 | "test:cjs": "node ./test/commonjs/test-artifact.js", 51 | "test": "npm run test:es && npm run test:cjs", 52 | "coverage": "c8 report --reporter=text-lcov | coveralls", 53 | "typecheck": "tsc", 54 | "build:types": "tsc --build", 55 | "build:cjs": "rollup -c", 56 | "lint": "xo", 57 | "prepare": "npm run build" 58 | }, 59 | "repository": { 60 | "type": "git", 61 | "url": "https://github.com/remix-run/web-std-io.git" 62 | }, 63 | "keywords": [ 64 | "fetch", 65 | "http", 66 | "promise", 67 | "request", 68 | "curl", 69 | "wget", 70 | "xhr", 71 | "whatwg" 72 | ], 73 | "author": "David Frank", 74 | "license": "MIT", 75 | "bugs": { 76 | "url": "https://github.com/remix-run/web-std-io/issues" 77 | }, 78 | "homepage": "https://github.com/remix-run/web-std-io", 79 | "devDependencies": { 80 | "@types/chai": "^4.3.0", 81 | "@types/chai-as-promised": "^7.1.5", 82 | "@types/chai-string": "^1.4.2", 83 | "@types/mocha": "^9.1.0", 84 | "abortcontroller-polyfill": "^1.7.1", 85 | "busboy": "^0.3.1", 86 | "c8": "^7.3.0", 87 | "chai": "^4.2.0", 88 | "chai-as-promised": "^7.1.1", 89 | "chai-iterator": "^3.0.2", 90 | "chai-string": "^1.5.0", 91 | "coveralls": "^3.1.0", 92 | "delay": "^4.4.0", 93 | "form-data": "^3.0.0", 94 | "formdata-node": "^2.4.0", 95 | "mocha": "^8.1.3", 96 | "p-timeout": "^3.2.0", 97 | "rollup": "2.47.0", 98 | "tsd": "^0.13.1", 99 | "typescript": "^4.4.4", 100 | "xo": "^0.33.1" 101 | }, 102 | "dependencies": { 103 | "@remix-run/web-blob": "^3.1.0", 104 | "@remix-run/web-file": "^3.1.0", 105 | "@remix-run/web-form-data": "^3.1.0", 106 | "@remix-run/web-stream": "^1.1.0", 107 | "@web3-storage/multipart-parser": "^1.0.0", 108 | "abort-controller": "^3.0.0", 109 | "data-uri-to-buffer": "^3.0.1", 110 | "mrmime": "^1.0.0" 111 | }, 112 | "esm": { 113 | "sourceMap": true, 114 | "cjs": false 115 | }, 116 | "tsd": { 117 | "cwd": "@types", 118 | "compilerOptions": { 119 | "target": "esnext", 120 | "lib": [ 121 | "es2018", 122 | "DOM" 123 | ], 124 | "allowSyntheticDefaultImports": false, 125 | "esModuleInterop": false 126 | } 127 | }, 128 | "xo": { 129 | "envs": [ 130 | "node", 131 | "browser" 132 | ], 133 | "rules": { 134 | "complexity": 0, 135 | "import/extensions": 0, 136 | "import/no-useless-path-segments": 0, 137 | "import/no-anonymous-default-export": 0, 138 | "unicorn/import-index": 0, 139 | "unicorn/no-reduce": 0, 140 | "capitalized-comments": 0 141 | }, 142 | "ignores": [ 143 | "dist", 144 | "@types" 145 | ], 146 | "overrides": [ 147 | { 148 | "files": "test/**/*.js", 149 | "envs": [ 150 | "node", 151 | "mocha" 152 | ], 153 | "rules": { 154 | "max-nested-callbacks": 0, 155 | "no-unused-expressions": 0, 156 | "new-cap": 0, 157 | "guard-for-in": 0, 158 | "unicorn/prevent-abbreviations": 0, 159 | "promise/prefer-await-to-then": 0, 160 | "ava/no-import-test-files": 0 161 | } 162 | }, 163 | { 164 | "files": "example.js", 165 | "rules": { 166 | "import/no-extraneous-dependencies": 0 167 | } 168 | } 169 | ] 170 | }, 171 | "runkitExampleFilename": "example.js" 172 | } 173 | -------------------------------------------------------------------------------- /packages/fetch/rollup.config.js: -------------------------------------------------------------------------------- 1 | import {builtinModules} from 'module'; 2 | import {dependencies} from './package.json'; 3 | 4 | export default [ 5 | { 6 | input: 'src/lib.js', 7 | output: { 8 | file: 'dist/lib.cjs', 9 | format: 'cjs', 10 | esModule: false, 11 | interop: false, 12 | sourcemap: true, 13 | preferConst: true, 14 | exports: 'named', 15 | // https://github.com/rollup/rollup/issues/1961#issuecomment-534977678 16 | outro: 'exports = module.exports = Object.assign(fetch, exports);' 17 | }, 18 | external: [...builtinModules, ...Object.keys(dependencies)] 19 | }, 20 | { 21 | input: 'src/lib.node.js', 22 | output: { 23 | file: 'dist/lib.node.cjs', 24 | format: 'cjs', 25 | esModule: false, 26 | interop: false, 27 | sourcemap: true, 28 | preferConst: true, 29 | exports: 'named', 30 | // https://github.com/rollup/rollup/issues/1961#issuecomment-534977678 31 | outro: 'exports = module.exports = Object.assign(fetch, exports);' 32 | }, 33 | external: [...builtinModules, ...Object.keys(dependencies)] 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /packages/fetch/src/errors/abort-error.js: -------------------------------------------------------------------------------- 1 | import {FetchBaseError} from './base.js'; 2 | 3 | /** 4 | * AbortError interface for cancelled requests 5 | */ 6 | export class AbortError extends FetchBaseError { 7 | /** 8 | * @param {string} message 9 | * @param {string} [type] 10 | */ 11 | constructor(message, type = 'aborted') { 12 | super(message, type); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/fetch/src/errors/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export class FetchBaseError extends Error { 4 | /** 5 | * @param {string} message 6 | * @param {string} type 7 | */ 8 | constructor(message, type) { 9 | super(message); 10 | // Hide custom error implementation details from end-users 11 | Error.captureStackTrace(this, this.constructor); 12 | 13 | this.type = type; 14 | } 15 | 16 | get name() { 17 | return this.constructor.name; 18 | } 19 | 20 | get [Symbol.toStringTag]() { 21 | return this.constructor.name; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /packages/fetch/src/errors/fetch-error.js: -------------------------------------------------------------------------------- 1 | 2 | import {FetchBaseError} from './base.js'; 3 | 4 | /** 5 | * @typedef {{ 6 | * address?: string 7 | * code: string 8 | * dest?: string 9 | * errno: number 10 | * info?: object 11 | * message: string 12 | * path?: string 13 | * port?: number 14 | * syscall: string 15 | * }} SystemError 16 | */ 17 | 18 | /** 19 | * FetchError interface for operational errors 20 | */ 21 | export class FetchError extends FetchBaseError { 22 | /** 23 | * @param {string} message - Error message for human 24 | * @param {string} type - Error type for machine 25 | * @param {SystemError} [systemError] - For Node.js system error 26 | */ 27 | constructor(message, type, systemError) { 28 | super(message, type); 29 | // When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code 30 | if (systemError) { 31 | // eslint-disable-next-line no-multi-assign 32 | this.code = this.errno = systemError.code; 33 | this.erroredSysCall = systemError.syscall; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/fetch/src/headers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Headers.js 3 | * 4 | * Headers class offers convenient helpers 5 | */ 6 | 7 | import {types} from 'util'; 8 | import http from 'http'; 9 | import { isIterable } from './utils/is.js' 10 | 11 | /** @type {{validateHeaderValue?:(name:string, value:string) => any}} */ 12 | const validators = (http) 13 | 14 | /** 15 | * @param {string} name 16 | */ 17 | const validateHeaderName = name => { 18 | if (!/^[\^`\-\w!#$%&'*+.|~:]+$/.test(name)) { 19 | const err = new TypeError(`Header name must be a valid HTTP token [${name}]`); 20 | Object.defineProperty(err, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'}); 21 | throw err; 22 | } 23 | }; 24 | 25 | const validateHeaderValue = typeof validators.validateHeaderValue === 'function' ? 26 | validators.validateHeaderValue : 27 | /** 28 | * @param {string} name 29 | * @param {string} value 30 | */ 31 | (name, value) => { 32 | if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) { 33 | const err = new TypeError(`Invalid character in header content ["${name}"]`); 34 | Object.defineProperty(err, 'code', {value: 'ERR_INVALID_CHAR'}); 35 | throw err; 36 | } 37 | }; 38 | 39 | /** 40 | * @typedef {Headers | Record | Iterable | Iterable>} HeadersInit 41 | */ 42 | 43 | /** 44 | * This Fetch API interface allows you to perform various actions on HTTP request and response headers. 45 | * These actions include retrieving, setting, adding to, and removing. 46 | * A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs. 47 | * You can add to this using methods like append() (see Examples.) 48 | * In all methods of this interface, header names are matched by case-insensitive byte sequence. 49 | * 50 | * @implements {globalThis.Headers} 51 | */ 52 | export default class Headers extends URLSearchParams { 53 | /** 54 | * Headers class 55 | * 56 | * @constructor 57 | * @param {HeadersInit} [init] - Response headers 58 | */ 59 | constructor(init) { 60 | // Validate and normalize init object in [name, value(s)][] 61 | /** @type {string[][]} */ 62 | let result = []; 63 | if (init instanceof Headers) { 64 | const raw = init.raw(); 65 | for (const [name, values] of Object.entries(raw)) { 66 | result.push(...values.map(value => [name, value])); 67 | } 68 | } else if (init == null) { // eslint-disable-line no-eq-null, eqeqeq 69 | // No op 70 | } else if (isIterable(init)) { 71 | // Sequence> 72 | // Note: per spec we have to first exhaust the lists then process them 73 | result = [...init] 74 | .map(pair => { 75 | if ( 76 | typeof pair !== 'object' || types.isBoxedPrimitive(pair) 77 | ) { 78 | throw new TypeError('Each header pair must be an iterable object'); 79 | } 80 | 81 | return [...pair]; 82 | }).map(pair => { 83 | if (pair.length !== 2) { 84 | throw new TypeError('Each header pair must be a name/value tuple'); 85 | } 86 | 87 | return [...pair]; 88 | }); 89 | } else if (typeof init === "object" && init !== null) { 90 | // Record 91 | result.push(...Object.entries(init)); 92 | } else { 93 | throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence> or record)'); 94 | } 95 | 96 | // Validate and lowercase 97 | result = 98 | result.length > 0 ? 99 | result.map(([name, value]) => { 100 | validateHeaderName(name); 101 | validateHeaderValue(name, String(value)); 102 | return [String(name).toLowerCase(), String(value)]; 103 | }) : 104 | []; 105 | 106 | super(result); 107 | 108 | // Returning a Proxy that will lowercase key names, validate parameters and sort keys 109 | // eslint-disable-next-line no-constructor-return 110 | return new Proxy(this, { 111 | get(target, p, receiver) { 112 | switch (p) { 113 | case 'append': 114 | case 'set': 115 | /** 116 | * @param {string} name 117 | * @param {string} value 118 | */ 119 | return (name, value) => { 120 | validateHeaderName(name); 121 | validateHeaderValue(name, String(value)); 122 | return URLSearchParams.prototype[p].call( 123 | target, 124 | String(name).toLowerCase(), 125 | String(value) 126 | ); 127 | }; 128 | 129 | case 'delete': 130 | case 'has': 131 | case 'getAll': 132 | /** 133 | * @param {string} name 134 | */ 135 | return name => { 136 | validateHeaderName(name); 137 | // @ts-ignore 138 | return URLSearchParams.prototype[p].call( 139 | target, 140 | String(name).toLowerCase() 141 | ); 142 | }; 143 | 144 | case 'keys': 145 | return () => { 146 | target.sort(); 147 | return new Set(URLSearchParams.prototype.keys.call(target)).keys(); 148 | }; 149 | 150 | default: 151 | return Reflect.get(target, p, receiver); 152 | } 153 | } 154 | /* c8 ignore next */ 155 | }); 156 | } 157 | 158 | get [Symbol.toStringTag]() { 159 | return this.constructor.name; 160 | } 161 | 162 | toString() { 163 | return Object.prototype.toString.call(this); 164 | } 165 | 166 | /** 167 | * 168 | * @param {string} name 169 | */ 170 | get(name) { 171 | const values = this.getAll(name); 172 | if (values.length === 0) { 173 | return null; 174 | } 175 | 176 | let value = values.join(', '); 177 | if (/^content-encoding$/i.test(name)) { 178 | value = value.toLowerCase(); 179 | } 180 | 181 | return value; 182 | } 183 | 184 | /** 185 | * @param {(value: string, key: string, parent: this) => void} callback 186 | * @param {any} thisArg 187 | * @returns {void} 188 | */ 189 | forEach(callback, thisArg = undefined) { 190 | for (const name of this.keys()) { 191 | if (name.toLowerCase() === 'set-cookie') { 192 | let cookies = this.getAll(name); 193 | while (cookies.length > 0) { 194 | Reflect.apply(callback, thisArg, [cookies.shift(), name, this]) 195 | } 196 | } else { 197 | Reflect.apply(callback, thisArg, [this.get(name), name, this]); 198 | } 199 | } 200 | } 201 | 202 | /** 203 | * @returns {IterableIterator} 204 | */ 205 | * values() { 206 | for (const name of this.keys()) { 207 | if (name.toLowerCase() === 'set-cookie') { 208 | let cookies = this.getAll(name); 209 | while (cookies.length > 0) { 210 | yield /** @type {string} */(cookies.shift()); 211 | } 212 | } else { 213 | yield /** @type {string} */(this.get(name)); 214 | } 215 | } 216 | } 217 | 218 | /** 219 | * @returns {IterableIterator<[string, string]>} 220 | */ 221 | * entries() { 222 | for (const name of this.keys()) { 223 | if (name.toLowerCase() === 'set-cookie') { 224 | let cookies = this.getAll(name); 225 | while (cookies.length > 0) { 226 | yield [name, /** @type {string} */(cookies.shift())]; 227 | } 228 | } else { 229 | yield [name, /** @type {string} */(this.get(name))]; 230 | } 231 | } 232 | } 233 | 234 | [Symbol.iterator]() { 235 | return this.entries(); 236 | } 237 | 238 | /** 239 | * Node-fetch non-spec method 240 | * returning all headers and their values as array 241 | * @returns {Record} 242 | */ 243 | raw() { 244 | return [...this.keys()].reduce((result, key) => { 245 | result[key] = this.getAll(key); 246 | return result; 247 | }, /** @type {Record} */({})); 248 | } 249 | 250 | /** 251 | * For better console.log(headers) and also to convert Headers into Node.js Request compatible format 252 | */ 253 | [Symbol.for('nodejs.util.inspect.custom')]() { 254 | return [...this.keys()].reduce((result, key) => { 255 | const values = this.getAll(key); 256 | // Http.request() only supports string as Host header. 257 | // This hack makes specifying custom Host header possible. 258 | if (key === 'host') { 259 | result[key] = values[0]; 260 | } else { 261 | result[key] = values.length > 1 ? values : values[0]; 262 | } 263 | 264 | return result; 265 | }, /** @type {Record} */({})); 266 | } 267 | } 268 | 269 | /** 270 | * Re-shaping object for Web IDL tests 271 | * Only need to do it for overridden methods 272 | */ 273 | Object.defineProperties( 274 | Headers.prototype, 275 | ['get', 'entries', 'forEach', 'values'].reduce((result, property) => { 276 | result[property] = {enumerable: true}; 277 | return result; 278 | }, /** @type {Record} */ ({})) 279 | ); 280 | 281 | /** 282 | * Create a Headers object from an http.IncomingMessage.rawHeaders, ignoring those that do 283 | * not conform to HTTP grammar productions. 284 | * @param {import('http').IncomingMessage['rawHeaders']} headers 285 | */ 286 | export function fromRawHeaders(headers = []) { 287 | return new Headers( 288 | headers 289 | // Split into pairs 290 | .reduce((result, value, index, array) => { 291 | if (index % 2 === 0) { 292 | result.push(array.slice(index, index + 2)); 293 | } 294 | 295 | return result; 296 | }, /** @type {string[][]} */([])) 297 | .filter(([name, value]) => { 298 | try { 299 | validateHeaderName(name); 300 | validateHeaderValue(name, String(value)); 301 | return true; 302 | } catch { 303 | return false; 304 | } 305 | }) 306 | 307 | ); 308 | } 309 | -------------------------------------------------------------------------------- /packages/fetch/src/lib.js: -------------------------------------------------------------------------------- 1 | // On the web we just export native fetch implementation 2 | export { ReadableStream, Blob, FormData, File } from './package.js'; 3 | export const { Headers, Request, Response } = globalThis; 4 | export default globalThis.fetch.bind(globalThis) 5 | -------------------------------------------------------------------------------- /packages/fetch/src/lib.node.js: -------------------------------------------------------------------------------- 1 | export { default, fetch, Headers, Request, Response } from "./fetch.js"; 2 | 3 | export { ReadableStream, Blob, FormData, File } from './package.js'; 4 | // Node 18+ introduces fetch API globally and it doesn't support our use-cases yet. 5 | // For now we always use the polyfill. 6 | -------------------------------------------------------------------------------- /packages/fetch/src/package.js: -------------------------------------------------------------------------------- 1 | 2 | export { Blob, ReadableStream } from '@remix-run/web-blob' 3 | export { File } from '@remix-run/web-file' 4 | export { FormData } from '@remix-run/web-form-data' 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/fetch/src/package.ts: -------------------------------------------------------------------------------- 1 | export const { FormData, Blob, File } = globalThis 2 | export { ReadableStream } from "@remix-run/web-stream" 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/fetch/src/request.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Request.js 4 | * 5 | * Request class contains server only options 6 | * 7 | * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. 8 | */ 9 | 10 | import {format as formatUrl} from 'url'; 11 | import {AbortController as AbortControllerPolyfill} from 'abort-controller'; 12 | import Headers from './headers.js'; 13 | import Body, {clone, extractContentType, getTotalBytes} from './body.js'; 14 | import {isAbortSignal} from './utils/is.js'; 15 | import {getSearch} from './utils/get-search.js'; 16 | 17 | const INTERNALS = Symbol('Request internals'); 18 | 19 | const forbiddenMethods = new Set(["CONNECT", "TRACE", "TRACK"]); 20 | const normalizedMethods = new Set(["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"]); 21 | 22 | /** 23 | * Check if `obj` is an instance of Request. 24 | * 25 | * @param {any} object 26 | * @return {object is Request} 27 | */ 28 | const isRequest = object => { 29 | return ( 30 | typeof object === 'object' && 31 | typeof object[INTERNALS] === 'object' 32 | ); 33 | }; 34 | 35 | 36 | /** 37 | * Request class 38 | * @implements {globalThis.Request} 39 | * 40 | * @typedef {Object} RequestState 41 | * @property {string} method 42 | * @property {RequestRedirect} redirect 43 | * @property {globalThis.Headers} headers 44 | * @property {RequestCredentials} credentials 45 | * @property {URL} parsedURL 46 | * @property {AbortSignal|null} signal 47 | * 48 | * @typedef {Object} RequestExtraOptions 49 | * @property {number} [follow] 50 | * @property {boolean} [compress] 51 | * @property {number} [size] 52 | * @property {number} [counter] 53 | * @property {Agent} [agent] 54 | * @property {number} [highWaterMark] 55 | * @property {boolean} [insecureHTTPParser] 56 | * 57 | * @typedef {((url:URL) => import('http').Agent | import('https').Agent) | import('http').Agent | import('https').Agent} Agent 58 | * 59 | * @typedef {Object} RequestOptions 60 | * @property {string} [method] 61 | * @property {ReadableStream|null} [body] 62 | * @property {globalThis.Headers} [headers] 63 | * @property {RequestRedirect} [redirect] 64 | * 65 | */ 66 | export default class Request extends Body { 67 | /** 68 | * @param {string|Request|URL} info Url or Request instance 69 | * @param {RequestInit & RequestExtraOptions} init Custom options 70 | */ 71 | constructor(info, init = {}) { 72 | let parsedURL; 73 | /** @type {RequestOptions & RequestExtraOptions} */ 74 | let settings 75 | 76 | // Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245) 77 | if (isRequest(info)) { 78 | parsedURL = new URL(info.url); 79 | settings = (info) 80 | } else { 81 | parsedURL = new URL(info); 82 | settings = {}; 83 | } 84 | 85 | 86 | 87 | // Normalize method: https://fetch.spec.whatwg.org/#methods 88 | let method = init.method || settings.method || 'GET'; 89 | if (forbiddenMethods.has(method.toUpperCase())) { 90 | throw new TypeError(`Failed to construct 'Request': '${method}' HTTP method is unsupported.`) 91 | } else if (normalizedMethods.has(method.toUpperCase())) { 92 | method = method.toUpperCase(); 93 | } 94 | 95 | const inputBody = init.body != null 96 | ? init.body 97 | : (isRequest(info) && info.body !== null) 98 | ? clone(info) 99 | : null; 100 | 101 | // eslint-disable-next-line no-eq-null, eqeqeq 102 | if (inputBody != null && (method === 'GET' || method === 'HEAD')) { 103 | throw new TypeError('Request with GET/HEAD method cannot have body'); 104 | } 105 | 106 | super(inputBody, { 107 | size: init.size || settings.size || 0 108 | }); 109 | const input = settings 110 | 111 | 112 | const headers = /** @type {globalThis.Headers} */ 113 | (new Headers(init.headers || input.headers || {})); 114 | 115 | if (inputBody !== null && !headers.has('Content-Type')) { 116 | const contentType = extractContentType(this); 117 | if (contentType) { 118 | headers.append('Content-Type', contentType); 119 | } 120 | } 121 | 122 | let signal = 'signal' in init 123 | ? init.signal 124 | : isRequest(input) 125 | ? input.signal 126 | : null; 127 | 128 | // eslint-disable-next-line no-eq-null, eqeqeq 129 | if (signal != null && !isAbortSignal(signal)) { 130 | throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget'); 131 | } 132 | 133 | if (!signal) { 134 | let AbortControllerConstructor = typeof AbortController != "undefined" 135 | ? AbortController 136 | : AbortControllerPolyfill; 137 | /** @type {any} */ 138 | let newSignal = new AbortControllerConstructor().signal; 139 | signal = newSignal; 140 | } 141 | 142 | /** @type {RequestState} */ 143 | this[INTERNALS] = { 144 | method, 145 | redirect: init.redirect || input.redirect || 'follow', 146 | headers, 147 | credentials: init.credentials || 'same-origin', 148 | parsedURL, 149 | signal: signal || null 150 | }; 151 | 152 | /** @type {boolean} */ 153 | this.keepalive 154 | 155 | // Node-fetch-only options 156 | /** @type {number} */ 157 | this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow; 158 | /** @type {boolean} */ 159 | this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress; 160 | /** @type {number} */ 161 | this.counter = init.counter || input.counter || 0; 162 | /** @type {Agent|undefined} */ 163 | this.agent = init.agent || input.agent; 164 | /** @type {number} */ 165 | this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; 166 | /** @type {boolean} */ 167 | this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; 168 | } 169 | 170 | /** 171 | * @type {RequestCache} 172 | */ 173 | get cache() { 174 | return "default" 175 | } 176 | 177 | /** 178 | * @type {RequestCredentials} 179 | */ 180 | 181 | get credentials() { 182 | return this[INTERNALS].credentials 183 | } 184 | 185 | /** 186 | * @type {RequestDestination} 187 | */ 188 | get destination() { 189 | return "" 190 | } 191 | 192 | get integrity() { 193 | return "" 194 | } 195 | 196 | /** @type {RequestMode} */ 197 | get mode() { 198 | return "cors" 199 | } 200 | 201 | /** @type {string} */ 202 | get referrer() { 203 | return "" 204 | } 205 | 206 | /** @type {ReferrerPolicy} */ 207 | get referrerPolicy() { 208 | return "" 209 | } 210 | get method() { 211 | return this[INTERNALS].method; 212 | } 213 | 214 | /** 215 | * @type {string} 216 | */ 217 | get url() { 218 | return formatUrl(this[INTERNALS].parsedURL); 219 | } 220 | 221 | /** 222 | * @type {globalThis.Headers} 223 | */ 224 | get headers() { 225 | return this[INTERNALS].headers; 226 | } 227 | 228 | get redirect() { 229 | return this[INTERNALS].redirect; 230 | } 231 | 232 | /** 233 | * @returns {AbortSignal} 234 | */ 235 | get signal() { 236 | // @ts-ignore 237 | return this[INTERNALS].signal; 238 | } 239 | 240 | /** 241 | * Clone this request 242 | * 243 | * @return {globalThis.Request} 244 | */ 245 | clone() { 246 | return new Request(this); 247 | } 248 | 249 | get [Symbol.toStringTag]() { 250 | return 'Request'; 251 | } 252 | } 253 | 254 | Object.defineProperties(Request.prototype, { 255 | method: {enumerable: true}, 256 | url: {enumerable: true}, 257 | headers: {enumerable: true}, 258 | redirect: {enumerable: true}, 259 | clone: {enumerable: true}, 260 | signal: {enumerable: true} 261 | }); 262 | 263 | /** 264 | * Convert a Request to Node.js http request options. 265 | * The options object to be passed to http.request 266 | * 267 | * @param {Request & Record} request - A Request instance 268 | */ 269 | export const getNodeRequestOptions = request => { 270 | const {parsedURL} = request[INTERNALS]; 271 | const headers = new Headers(request[INTERNALS].headers); 272 | 273 | // Fetch step 1.3 274 | if (!headers.has('Accept')) { 275 | headers.set('Accept', '*/*'); 276 | } 277 | 278 | // HTTP-network-or-cache fetch steps 2.4-2.7 279 | let contentLengthValue = null; 280 | if (request.body === null && /^(post|put)$/i.test(request.method)) { 281 | contentLengthValue = '0'; 282 | } 283 | 284 | if (request.body !== null) { 285 | const totalBytes = getTotalBytes(request); 286 | // Set Content-Length if totalBytes is a number (that is not NaN) 287 | if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) { 288 | contentLengthValue = String(totalBytes); 289 | } 290 | } 291 | 292 | if (contentLengthValue) { 293 | headers.set('Content-Length', contentLengthValue); 294 | } 295 | 296 | // HTTP-network-or-cache fetch step 2.11 297 | if (!headers.has('User-Agent')) { 298 | headers.set('User-Agent', 'node-fetch'); 299 | } 300 | 301 | // HTTP-network-or-cache fetch step 2.15 302 | if (request.compress && !headers.has('Accept-Encoding')) { 303 | headers.set('Accept-Encoding', 'gzip,deflate,br'); 304 | } 305 | 306 | let {agent} = request; 307 | if (typeof agent === 'function') { 308 | agent = agent(parsedURL); 309 | } 310 | 311 | if (!headers.has('Connection') && !agent) { 312 | headers.set('Connection', 'close'); 313 | } 314 | 315 | // HTTP-network fetch step 4.2 316 | // chunked encoding is handled by Node.js 317 | 318 | const search = getSearch(parsedURL); 319 | 320 | // Manually spread the URL object instead of spread syntax 321 | const requestOptions = { 322 | path: parsedURL.pathname + search, 323 | pathname: parsedURL.pathname, 324 | hostname: parsedURL.hostname, 325 | protocol: parsedURL.protocol, 326 | port: parsedURL.port, 327 | hash: parsedURL.hash, 328 | search: parsedURL.search, 329 | // @ts-ignore - it does not has a query 330 | query: parsedURL.query, 331 | href: parsedURL.href, 332 | method: request.method, 333 | // @ts-ignore - not sure what this supposed to do 334 | headers: headers[Symbol.for('nodejs.util.inspect.custom')](), 335 | insecureHTTPParser: request.insecureHTTPParser, 336 | agent 337 | }; 338 | 339 | return requestOptions; 340 | }; 341 | -------------------------------------------------------------------------------- /packages/fetch/src/response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Response.js 3 | * 4 | * Response class provides content decoding 5 | */ 6 | 7 | import Headers from './headers.js'; 8 | import Body, {clone, extractContentType} from './body.js'; 9 | import {isRedirect} from './utils/is-redirect.js'; 10 | 11 | const INTERNALS = Symbol('Response internals'); 12 | 13 | /** 14 | * Response class 15 | * 16 | * @typedef {Object} Ext 17 | * @property {number} [size] 18 | * @property {string} [url] 19 | * @property {number} [counter] 20 | * @property {number} [highWaterMark] 21 | * 22 | * @implements {globalThis.Response} 23 | */ 24 | export default class Response extends Body { 25 | /** 26 | * @param {BodyInit|import('stream').Stream|null} [body] - Readable stream 27 | * @param {ResponseInit & Ext} [options] - Response options 28 | */ 29 | constructor(body = null, options = {}) { 30 | super(body, options); 31 | 32 | const status = options.status || 200; 33 | const headers = new Headers(options.headers); 34 | 35 | if (body !== null && !headers.has('Content-Type')) { 36 | const contentType = extractContentType(this); 37 | if (contentType) { 38 | headers.append('Content-Type', contentType); 39 | } 40 | } 41 | 42 | /** 43 | * @private 44 | */ 45 | this[INTERNALS] = { 46 | url: options.url, 47 | status, 48 | statusText: options.statusText || '', 49 | headers, 50 | counter: options.counter || 0, 51 | highWaterMark: options.highWaterMark 52 | }; 53 | } 54 | 55 | /** 56 | * @type {ResponseType} 57 | */ 58 | get type() { 59 | return "default" 60 | } 61 | 62 | get url() { 63 | return this[INTERNALS].url || ''; 64 | } 65 | 66 | get status() { 67 | return this[INTERNALS].status; 68 | } 69 | 70 | /** 71 | * Convenience property representing if the request ended normally 72 | */ 73 | get ok() { 74 | return this[INTERNALS].status >= 200 && this[INTERNALS].status < 300; 75 | } 76 | 77 | get redirected() { 78 | return this[INTERNALS].counter > 0; 79 | } 80 | 81 | get statusText() { 82 | return this[INTERNALS].statusText; 83 | } 84 | 85 | /** 86 | * @type {Headers} 87 | */ 88 | get headers() { 89 | return this[INTERNALS].headers; 90 | } 91 | 92 | get highWaterMark() { 93 | return this[INTERNALS].highWaterMark; 94 | } 95 | 96 | /** 97 | * Clone this response 98 | * 99 | * @returns {Response} 100 | */ 101 | clone() { 102 | return new Response(clone(this), { 103 | url: this.url, 104 | status: this.status, 105 | statusText: this.statusText, 106 | headers: this.headers, 107 | size: this.size 108 | }); 109 | } 110 | 111 | /** 112 | * @param {string} url The URL that the new response is to originate from. 113 | * @param {number} status An optional status code for the response (e.g., 302.) 114 | * @returns {Response} A Response object. 115 | */ 116 | static redirect(url, status = 302) { 117 | if (!isRedirect(status)) { 118 | throw new RangeError('Failed to execute "redirect" on "response": Invalid status code'); 119 | } 120 | 121 | return new Response(null, { 122 | headers: { 123 | location: new URL(url).toString() 124 | }, 125 | status 126 | }); 127 | } 128 | 129 | get [Symbol.toStringTag]() { 130 | return 'Response'; 131 | } 132 | } 133 | 134 | Object.defineProperties(Response.prototype, { 135 | url: {enumerable: true}, 136 | status: {enumerable: true}, 137 | ok: {enumerable: true}, 138 | redirected: {enumerable: true}, 139 | statusText: {enumerable: true}, 140 | headers: {enumerable: true}, 141 | clone: {enumerable: true} 142 | }); 143 | 144 | -------------------------------------------------------------------------------- /packages/fetch/src/utils/form-data.js: -------------------------------------------------------------------------------- 1 | import {randomBytes} from 'crypto'; 2 | import { iterateMultipart } from '@web3-storage/multipart-parser'; 3 | import { FormData, File } from '../package.js'; 4 | import { isBlob } from './is.js'; 5 | 6 | const carriage = '\r\n'; 7 | const dashes = '-'.repeat(2); 8 | const carriageLength = Buffer.byteLength(carriage); 9 | 10 | /** 11 | * @param {string} boundary 12 | */ 13 | const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; 14 | 15 | /** 16 | * @param {string} boundary 17 | * @param {string} name 18 | * @param {*} field 19 | * 20 | * @return {string} 21 | */ 22 | function getHeader(boundary, name, field) { 23 | let header = ''; 24 | 25 | header += `${dashes}${boundary}${carriage}`; 26 | header += `Content-Disposition: form-data; name="${name}"`; 27 | 28 | if (isBlob(field)) { 29 | const { name = 'blob', type } = /** @type {Blob & {name?:string}} */ (field); 30 | header += `; filename="${name}"${carriage}`; 31 | header += `Content-Type: ${type || 'application/octet-stream'}`; 32 | } 33 | 34 | return `${header}${carriage.repeat(2)}`; 35 | } 36 | 37 | /** 38 | * @return {string} 39 | */ 40 | export const getBoundary = () => randomBytes(8).toString('hex'); 41 | 42 | /** 43 | * @param {FormData} form 44 | * @param {string} boundary 45 | */ 46 | export async function * formDataIterator(form, boundary) { 47 | const encoder = new TextEncoder(); 48 | for (const [name, value] of form) { 49 | yield encoder.encode(getHeader(boundary, name, value)); 50 | 51 | if (isBlob(value)) { 52 | // @ts-ignore - we know our streams implement aysnc iteration 53 | yield * value.stream(); 54 | } else { 55 | yield encoder.encode(value); 56 | } 57 | 58 | yield encoder.encode(carriage); 59 | } 60 | 61 | yield encoder.encode(getFooter(boundary)); 62 | } 63 | 64 | /** 65 | * @param {FormData} form 66 | * @param {string} boundary 67 | */ 68 | export function getFormDataLength(form, boundary) { 69 | let length = 0; 70 | 71 | for (const [name, value] of form) { 72 | length += Buffer.byteLength(getHeader(boundary, name, value)); 73 | 74 | if (isBlob(value)) { 75 | length += value.size; 76 | } else { 77 | length += Buffer.byteLength(String(value)); 78 | } 79 | 80 | length += carriageLength; 81 | } 82 | 83 | length += Buffer.byteLength(getFooter(boundary)); 84 | 85 | return length; 86 | } 87 | 88 | /** 89 | * @param {Body & {headers?:Headers}} source 90 | */ 91 | export const toFormData = async (source) => { 92 | let { body, headers } = source; 93 | const contentType = headers?.get('Content-Type') || '' 94 | 95 | if (contentType.startsWith('application/x-www-form-urlencoded') && body != null) { 96 | const form = new FormData(); 97 | let bodyText = await source.text(); 98 | new URLSearchParams(bodyText).forEach((v, k) => form.append(k, v)); 99 | return form; 100 | } 101 | 102 | const [type, boundary] = contentType.split(/\s*;\s*boundary=/) 103 | if (type === 'multipart/form-data' && boundary != null && body != null) { 104 | const form = new FormData() 105 | const parts = iterateMultipart(body, boundary) 106 | for await (const { name, data, filename, contentType } of parts) { 107 | if (typeof filename === 'string') { 108 | form.append(name, new File([data], filename, { type: contentType })) 109 | } else if (typeof filename !== 'undefined') { 110 | form.append(name, new File([], '', { type: contentType })) 111 | } else { 112 | form.append(name, new TextDecoder().decode(data), filename) 113 | } 114 | } 115 | return form 116 | } else { 117 | throw new TypeError('Could not parse content as FormData.') 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/fetch/src/utils/get-search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {URL} parsedURL 3 | * @returns {string} 4 | */ 5 | export const getSearch = parsedURL => { 6 | if (parsedURL.search) { 7 | return parsedURL.search; 8 | } 9 | 10 | const lastOffset = parsedURL.href.length - 1; 11 | const hash = parsedURL.hash || (parsedURL.href[lastOffset] === '#' ? '#' : ''); 12 | return parsedURL.href[lastOffset - hash.length] === '?' ? '?' : ''; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/fetch/src/utils/is-redirect.js: -------------------------------------------------------------------------------- 1 | const redirectStatus = new Set([301, 302, 303, 307, 308]); 2 | 3 | /** 4 | * Redirect code matching 5 | * 6 | * @param {number} code - Status code 7 | * @return {boolean} 8 | */ 9 | export const isRedirect = code => { 10 | return redirectStatus.has(code); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/fetch/src/utils/is.js: -------------------------------------------------------------------------------- 1 | import Stream from "stream"; 2 | 3 | /** 4 | * Is.js 5 | * 6 | * Object type checks. 7 | */ 8 | 9 | const NAME = Symbol.toStringTag; 10 | 11 | /** 12 | * Check if `obj` is a URLSearchParams object 13 | * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143 14 | * 15 | * @param {any} object 16 | * @return {obj is URLSearchParams} 17 | */ 18 | export const isURLSearchParameters = (object) => { 19 | return ( 20 | typeof object === "object" && 21 | typeof object.append === "function" && 22 | typeof object.delete === "function" && 23 | typeof object.get === "function" && 24 | typeof object.getAll === "function" && 25 | typeof object.has === "function" && 26 | typeof object.set === "function" && 27 | typeof object.sort === "function" && 28 | object[NAME] === "URLSearchParams" 29 | ); 30 | }; 31 | 32 | /** 33 | * Check if `object` is a W3C `Blob` object (which `File` inherits from) 34 | * 35 | * @param {*} object 36 | * @return {object is Blob} 37 | */ 38 | export const isBlob = (object) => { 39 | return ( 40 | typeof object === "object" && 41 | typeof object.arrayBuffer === "function" && 42 | typeof object.type === "string" && 43 | typeof object.stream === "function" && 44 | typeof object.constructor === "function" && 45 | /^(Blob|File)$/.test(object[NAME]) 46 | ); 47 | }; 48 | 49 | /** 50 | * Check if `obj` is a spec-compliant `FormData` object 51 | * 52 | * @param {*} object 53 | * @return {object is FormData} 54 | */ 55 | export function isFormData(object) { 56 | return ( 57 | typeof object === "object" && 58 | typeof object.append === "function" && 59 | typeof object.set === "function" && 60 | typeof object.get === "function" && 61 | typeof object.getAll === "function" && 62 | typeof object.delete === "function" && 63 | typeof object.keys === "function" && 64 | typeof object.values === "function" && 65 | typeof object.entries === "function" && 66 | typeof object.constructor === "function" && 67 | object[NAME] === "FormData" 68 | ); 69 | } 70 | 71 | /** 72 | * Detect form data input from form-data module 73 | * 74 | * @param {any} value 75 | * @returns {value is Stream & {getBoundary():string, hasKnownLength():boolean, getLengthSync():number|null}} 76 | */ 77 | export const isMultipartFormDataStream = (value) => { 78 | return ( 79 | value instanceof Stream === true && 80 | typeof value.getBoundary === "function" && 81 | typeof value.hasKnownLength === "function" && 82 | typeof value.getLengthSync === "function" 83 | ); 84 | }; 85 | 86 | /** 87 | * Check if `obj` is an instance of AbortSignal. 88 | * 89 | * @param {any} object 90 | * @return {obj is AbortSignal} 91 | */ 92 | export const isAbortSignal = (object) => { 93 | return ( 94 | typeof object === "object" && 95 | (object[NAME] === "AbortSignal" || object[NAME] === "EventTarget") 96 | ); 97 | }; 98 | 99 | /** 100 | * Check if `value` is a ReadableStream. 101 | * 102 | * @param {*} value 103 | * @returns {value is ReadableStream} 104 | */ 105 | export const isReadableStream = (value) => { 106 | return ( 107 | typeof value === "object" && 108 | typeof value.getReader === "function" && 109 | typeof value.cancel === "function" && 110 | typeof value.tee === "function" 111 | ); 112 | }; 113 | 114 | /** 115 | * 116 | * @param {any} value 117 | * @returns {value is Iterable} 118 | */ 119 | export const isIterable = (value) => value && Symbol.iterator in value; 120 | -------------------------------------------------------------------------------- /packages/fetch/src/utils/utf8.js: -------------------------------------------------------------------------------- 1 | import {TextEncoder, TextDecoder} from 'util'; 2 | 3 | const encoder = new TextEncoder(); 4 | const decoder = new TextDecoder(); 5 | 6 | /** 7 | * @param {string} text 8 | */ 9 | export const encode = text => encoder.encode(text); 10 | 11 | /** 12 | * @param {Uint8Array} bytes 13 | */ 14 | export const decode = bytes => decoder.decode(bytes); 15 | -------------------------------------------------------------------------------- /packages/fetch/test/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /packages/fetch/test/commonjs/test-artifact.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | const assert = require('assert'); 4 | const fetch = require('@remix-run/web-fetch'); 5 | assert.strictEqual( 6 | typeof fetch, 7 | 'function', 8 | 'default import must be a function' 9 | ); 10 | 11 | const {Request, Response, Headers} = require('@remix-run/web-fetch'); 12 | 13 | assert.ok( 14 | new Request('https://www.test.com').headers instanceof Headers, 15 | 'Request class is not exposing correct functionality' 16 | ); 17 | assert.strictEqual( 18 | new Response(null, {headers: {a: 'a'}}).headers.get('a'), 19 | 'a', 20 | 'Response class is not exposing correct functionality' 21 | ); 22 | 23 | fetch( 24 | `data:text/plain;base64,${Buffer.from('Hello World!').toString('base64')}` 25 | ) 26 | .then(res => res.text()) 27 | .then(text => assert.strictEqual(text, 'Hello World!')) 28 | .then(() => { 29 | console.log('CommonJS build artifact fitness tests successfully'); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/fetch/test/external-encoding.js: -------------------------------------------------------------------------------- 1 | import fetch from '@remix-run/web-fetch'; 2 | import chai from 'chai'; 3 | 4 | const {expect} = chai; 5 | 6 | describe('external encoding', () => { 7 | describe('data uri', () => { 8 | it('should accept base64-encoded gif data uri', () => { 9 | return fetch('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').then(r => { 10 | expect(r.status).to.equal(200); 11 | expect(r.headers.get('Content-Type')).to.equal('image/gif'); 12 | 13 | return r.arrayBuffer().then(b => { 14 | expect(b).to.be.an.instanceOf(ArrayBuffer); 15 | }); 16 | }); 17 | }); 18 | 19 | it('should accept data uri with specified charset', async () => { 20 | const r = await fetch('data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678'); 21 | expect(r.status).to.equal(200); 22 | expect(r.headers.get('Content-Type')).to.equal('text/plain;charset=UTF-8;page=21'); 23 | 24 | const b = await r.text(); 25 | expect(b).to.equal('the data:1234,5678'); 26 | }); 27 | 28 | it('should accept data uri of plain text', () => { 29 | return fetch('data:,Hello%20World!').then(r => { 30 | expect(r.status).to.equal(200); 31 | expect(r.headers.get('Content-Type')).to.equal('text/plain;charset=US-ASCII'); 32 | return r.text().then(t => expect(t).to.equal('Hello World!')); 33 | }); 34 | }); 35 | 36 | it('should reject invalid data uri', () => { 37 | return fetch('data:@@@@').catch(error => { 38 | expect(error).to.exist; 39 | expect(error.message).to.include('malformed data: URI'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/fetch/test/file.js: -------------------------------------------------------------------------------- 1 | import fetch from '@remix-run/web-fetch' 2 | import { assert } from "chai" 3 | describe("can fetch local files", () => { 4 | it("can fetch local file", async () => { 5 | const response = await fetch(import.meta.url) 6 | assert.equal(response.headers.get('content-type'), "application/javascript") 7 | const code = await response.text() 8 | 9 | assert.ok(code.includes('it("can fetch local file"')) 10 | }) 11 | }) 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/fetch/test/form-data.js: -------------------------------------------------------------------------------- 1 | import FormData from 'formdata-node'; 2 | import {Blob} from '@remix-run/web-blob'; 3 | 4 | import chai from 'chai'; 5 | 6 | import read from './utils/read-stream.js'; 7 | 8 | import {getFormDataLength, getBoundary, formDataIterator} from '../src/utils/form-data.js'; 9 | 10 | const {expect} = chai; 11 | 12 | const carriage = '\r\n'; 13 | 14 | const getFooter = boundary => `--${boundary}--${carriage.repeat(2)}`; 15 | 16 | describe('FormData', () => { 17 | it('should return a length for empty form-data', () => { 18 | const form = new FormData(); 19 | const boundary = getBoundary(); 20 | 21 | expect(getFormDataLength(form, boundary)).to.be.equal(Buffer.byteLength(getFooter(boundary))); 22 | }); 23 | 24 | it('should add a Blob field\'s size to the FormData length', () => { 25 | const form = new FormData(); 26 | const boundary = getBoundary(); 27 | 28 | const string = 'Hello, world!'; 29 | const expected = Buffer.byteLength( 30 | `--${boundary}${carriage}` + 31 | `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + 32 | string + 33 | `${carriage}${getFooter(boundary)}` 34 | ); 35 | 36 | form.set('field', string); 37 | 38 | expect(getFormDataLength(form, boundary)).to.be.equal(expected); 39 | }); 40 | 41 | it('should return a length for a Blob field', () => { 42 | const form = new FormData(); 43 | const boundary = getBoundary(); 44 | 45 | const blob = new Blob(['Hello, world!'], {type: 'text/plain'}); 46 | 47 | form.set('blob', blob); 48 | 49 | const expected = blob.size + Buffer.byteLength( 50 | `--${boundary}${carriage}` + 51 | 'Content-Disposition: form-data; name="blob"; ' + 52 | `filename="blob"${carriage}Content-Type: text/plain` + 53 | `${carriage.repeat(3)}${getFooter(boundary)}` 54 | ); 55 | 56 | expect(getFormDataLength(form, boundary)).to.be.equal(expected); 57 | }); 58 | 59 | it('should create a body from empty form-data', async () => { 60 | const form = new FormData(); 61 | const boundary = getBoundary(); 62 | 63 | expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(getFooter(boundary)); 64 | }); 65 | 66 | it('should set default content-type', async () => { 67 | const form = new FormData(); 68 | const boundary = getBoundary(); 69 | 70 | form.set('blob', new Blob([])); 71 | 72 | expect(String(await read(formDataIterator(form, boundary)))).to.contain('Content-Type: application/octet-stream'); 73 | }); 74 | 75 | it('should create a body with a FormData field', async () => { 76 | const form = new FormData(); 77 | const boundary = getBoundary(); 78 | const string = 'Hello, World!'; 79 | 80 | form.set('field', string); 81 | 82 | const expected = `--${boundary}${carriage}` + 83 | `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + 84 | string + 85 | `${carriage}${getFooter(boundary)}`; 86 | 87 | expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected); 88 | }); 89 | 90 | it('should create a body with a FormData Blob field', async () => { 91 | const form = new FormData(); 92 | const boundary = getBoundary(); 93 | 94 | const expected = `--${boundary}${carriage}` + 95 | 'Content-Disposition: form-data; name="blob"; ' + 96 | `filename="blob"${carriage}Content-Type: text/plain${carriage.repeat(2)}` + 97 | 'Hello, World!' + 98 | `${carriage}${getFooter(boundary)}`; 99 | 100 | form.set('blob', new Blob(['Hello, World!'], {type: 'text/plain'})); 101 | 102 | expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /packages/fetch/test/headers.js: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | import {Headers} from '@remix-run/web-fetch'; 3 | import chai from 'chai'; 4 | import chaiIterator from 'chai-iterator'; 5 | 6 | chai.use(chaiIterator); 7 | 8 | const {expect} = chai; 9 | 10 | describe('Headers', () => { 11 | it('should have attributes conforming to Web IDL', () => { 12 | const headers = new Headers(); 13 | expect(Object.getOwnPropertyNames(headers)).to.be.empty; 14 | const enumerableProperties = []; 15 | 16 | for (const property in headers) { 17 | enumerableProperties.push(property); 18 | } 19 | 20 | for (const toCheck of [ 21 | 'append', 22 | 'delete', 23 | 'entries', 24 | 'forEach', 25 | 'get', 26 | 'has', 27 | 'keys', 28 | 'set', 29 | 'values' 30 | ]) { 31 | expect(enumerableProperties).to.contain(toCheck); 32 | } 33 | }); 34 | 35 | it('should allow iterating through all headers with forEach', () => { 36 | const headers = new Headers([ 37 | ['b', '2'], 38 | ['c', '4'], 39 | ['b', '3'], 40 | ['a', '1'] 41 | ]); 42 | expect(headers).to.have.property('forEach'); 43 | 44 | const result = []; 45 | headers.forEach((value, key) => { 46 | result.push([key, value]); 47 | }); 48 | 49 | expect(result).to.deep.equal([ 50 | ['a', '1'], 51 | ['b', '2, 3'], 52 | ['c', '4'] 53 | ]); 54 | }); 55 | 56 | it('should be iterable with forEach', () => { 57 | const headers = new Headers(); 58 | headers.append('Accept', 'application/json'); 59 | headers.append('Accept', 'text/plain'); 60 | headers.append('Content-Type', 'text/html'); 61 | 62 | const results = []; 63 | headers.forEach((value, key, object) => { 64 | results.push({value, key, object}); 65 | }); 66 | 67 | expect(results.length).to.equal(2); 68 | expect({key: 'accept', value: 'application/json, text/plain', object: headers}).to.deep.equal(results[0]); 69 | expect({key: 'content-type', value: 'text/html', object: headers}).to.deep.equal(results[1]); 70 | }); 71 | 72 | it('should allow iterating through multiple set-cookie headers with forEach', () => { 73 | let headers = new Headers([ 74 | ['a', '1'], 75 | ['Set-Cookie', 'b=2'] 76 | ]); 77 | headers.append('Set-Cookie', 'c=3'); 78 | expect(headers.entries()).to.be.iterable; 79 | 80 | const results = []; 81 | headers.forEach((value, key, object) => { 82 | results.push({value, key, object}); 83 | }); 84 | 85 | expect(results).to.deep.equal([ 86 | { value: '1', key: 'a', object: headers }, 87 | { value: 'b=2', key: 'set-cookie', object: headers }, 88 | { value: 'c=3', key: 'set-cookie', object: headers }, 89 | ]); 90 | }) 91 | 92 | it('should set "this" to undefined by default on forEach', () => { 93 | const headers = new Headers({Accept: 'application/json'}); 94 | headers.forEach(function () { 95 | expect(this).to.be.undefined; 96 | }); 97 | }); 98 | 99 | it('should accept thisArg as a second argument for forEach', () => { 100 | const headers = new Headers({Accept: 'application/json'}); 101 | const thisArg = {}; 102 | headers.forEach(function () { 103 | expect(this).to.equal(thisArg); 104 | }, thisArg); 105 | }); 106 | 107 | it('should allow iterating through all headers with for-of loop', () => { 108 | const headers = new Headers([ 109 | ['b', '2'], 110 | ['c', '4'], 111 | ['a', '1'] 112 | ]); 113 | headers.append('b', '3'); 114 | expect(headers).to.be.iterable; 115 | 116 | const result = []; 117 | for (const pair of headers) { 118 | result.push(pair); 119 | } 120 | 121 | expect(result).to.deep.equal([ 122 | ['a', '1'], 123 | ['b', '2, 3'], 124 | ['c', '4'] 125 | ]); 126 | 127 | }); 128 | 129 | it('should allow iterating through multiple set-cookie headers with for-of loop', () => { 130 | let headers = new Headers([ 131 | ['a', '1'], 132 | ['Set-Cookie', 'b=2'] 133 | ]); 134 | headers.append('Set-Cookie', 'c=3'); 135 | expect(headers.entries()).to.be.iterable; 136 | 137 | const result = []; 138 | for (const pair of headers) { 139 | result.push(pair); 140 | } 141 | 142 | expect(result).to.deep.equal([['a', '1'], ['set-cookie', 'b=2'], ['set-cookie', 'c=3']]); 143 | }) 144 | 145 | it('should allow iterating through all headers with entries()', () => { 146 | const headers = new Headers([ 147 | ['b', '2'], 148 | ['c', '4'], 149 | ['a', '1'] 150 | ]); 151 | headers.append('b', '3'); 152 | 153 | expect(headers.entries()).to.be.iterable 154 | .and.to.deep.iterate.over([ 155 | ['a', '1'], 156 | ['b', '2, 3'], 157 | ['c', '4'] 158 | ]); 159 | }); 160 | 161 | it('should allow iterating through multiple set-cookie headers with entries()', ()=> { 162 | let headers = new Headers([ 163 | ['a', '1'], 164 | ['Set-Cookie', 'b=2'] 165 | ]); 166 | headers.append('Set-Cookie', 'c=3'); 167 | expect(headers.entries()).to.be.iterable 168 | .and.to.deep.iterate.over([['a', '1'], ['set-cookie', 'b=2'], ['set-cookie', 'c=3']]); 169 | }) 170 | 171 | it('should allow iterating through all headers with keys()', () => { 172 | const headers = new Headers([ 173 | ['b', '2'], 174 | ['c', '4'], 175 | ['a', '1'] 176 | ]); 177 | headers.append('b', '3'); 178 | 179 | expect(headers.keys()).to.be.iterable 180 | .and.to.iterate.over(['a', 'b', 'c']); 181 | }); 182 | 183 | it('should allow iterating through all headers with values()', () => { 184 | const headers = new Headers([ 185 | ['b', '2'], 186 | ['c', '4'], 187 | ['a', '1'] 188 | ]); 189 | headers.append('b', '3'); 190 | 191 | expect(headers.values()).to.be.iterable 192 | .and.to.iterate.over(['1', '2, 3', '4']); 193 | }); 194 | 195 | it('should allow iterating through multiple set-cookie headers with values()', ()=> { 196 | let headers = new Headers([ 197 | ['a', '1'], 198 | ['Set-Cookie', 'b=2'] 199 | ]); 200 | headers.append('Set-Cookie', 'c=3'); 201 | expect(headers.values()).to.be.iterable 202 | .and.to.iterate.over(['1', 'b=2', 'c=3']); 203 | }) 204 | 205 | it('should reject illegal header', () => { 206 | const headers = new Headers(); 207 | expect(() => new Headers({'He y': 'ok'})).to.throw(TypeError); 208 | expect(() => new Headers({'Hé-y': 'ok'})).to.throw(TypeError); 209 | expect(() => new Headers({'He-y': 'ăk'})).to.throw(TypeError); 210 | expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError); 211 | expect(() => headers.delete('Hé-y')).to.throw(TypeError); 212 | expect(() => headers.get('Hé-y')).to.throw(TypeError); 213 | expect(() => headers.has('Hé-y')).to.throw(TypeError); 214 | expect(() => headers.set('Hé-y', 'ok')).to.throw(TypeError); 215 | // Should reject empty header 216 | expect(() => headers.append('', 'ok')).to.throw(TypeError); 217 | }); 218 | 219 | it('should allow HTTP2 pseudo-headers', () => { 220 | let headers = new Headers({':authority': 'something'}); 221 | headers.append(":method", "something else") 222 | 223 | const result = []; 224 | for (const pair of headers) { 225 | result.push(pair); 226 | } 227 | 228 | expect(result).to.deep.equal([[':authority', 'something'], [':method', 'something else']]); 229 | 230 | }) 231 | 232 | it('should ignore unsupported attributes while reading headers', () => { 233 | const FakeHeader = function () { }; 234 | // Prototypes are currently ignored 235 | // This might change in the future: #181 236 | FakeHeader.prototype.z = 'fake'; 237 | 238 | const res = new FakeHeader(); 239 | res.a = 'string'; 240 | res.b = ['1', '2']; 241 | res.c = ''; 242 | res.d = []; 243 | res.e = 1; 244 | res.f = [1, 2]; 245 | res.g = {a: 1}; 246 | res.h = undefined; 247 | res.i = null; 248 | res.j = Number.NaN; 249 | res.k = true; 250 | res.l = false; 251 | res.m = Buffer.from('test'); 252 | 253 | const h1 = new Headers(res); 254 | h1.set('n', [1, 2]); 255 | h1.append('n', ['3', 4]); 256 | 257 | const h1Raw = h1.raw(); 258 | 259 | expect(h1Raw.a).to.include('string'); 260 | expect(h1Raw.b).to.include('1,2'); 261 | expect(h1Raw.c).to.include(''); 262 | expect(h1Raw.d).to.include(''); 263 | expect(h1Raw.e).to.include('1'); 264 | expect(h1Raw.f).to.include('1,2'); 265 | expect(h1Raw.g).to.include('[object Object]'); 266 | expect(h1Raw.h).to.include('undefined'); 267 | expect(h1Raw.i).to.include('null'); 268 | expect(h1Raw.j).to.include('NaN'); 269 | expect(h1Raw.k).to.include('true'); 270 | expect(h1Raw.l).to.include('false'); 271 | expect(h1Raw.m).to.include('test'); 272 | expect(h1Raw.n).to.include('1,2'); 273 | expect(h1Raw.n).to.include('3,4'); 274 | 275 | expect(h1Raw.z).to.be.undefined; 276 | }); 277 | 278 | it('should wrap headers', () => { 279 | const h1 = new Headers({ 280 | a: '1' 281 | }); 282 | const h1Raw = h1.raw(); 283 | 284 | const h2 = new Headers(h1); 285 | h2.set('b', '1'); 286 | const h2Raw = h2.raw(); 287 | 288 | const h3 = new Headers(h2); 289 | h3.append('a', '2'); 290 | const h3Raw = h3.raw(); 291 | 292 | expect(h1Raw.a).to.include('1'); 293 | expect(h1Raw.a).to.not.include('2'); 294 | 295 | expect(h2Raw.a).to.include('1'); 296 | expect(h2Raw.a).to.not.include('2'); 297 | expect(h2Raw.b).to.include('1'); 298 | 299 | expect(h3Raw.a).to.include('1'); 300 | expect(h3Raw.a).to.include('2'); 301 | expect(h3Raw.b).to.include('1'); 302 | }); 303 | 304 | it('should accept headers as an iterable of tuples', () => { 305 | let headers; 306 | 307 | headers = new Headers([ 308 | ['a', '1'], 309 | ['b', '2'], 310 | ['a', '3'] 311 | ]); 312 | expect(headers.get('a')).to.equal('1, 3'); 313 | expect(headers.get('b')).to.equal('2'); 314 | 315 | headers = new Headers([ 316 | new Set(['a', '1']), 317 | ['b', '2'], 318 | new Map([['a', null], ['3', null]]).keys() 319 | ]); 320 | expect(headers.get('a')).to.equal('1, 3'); 321 | expect(headers.get('b')).to.equal('2'); 322 | 323 | headers = new Headers(new Map([ 324 | ['a', '1'], 325 | ['b', '2'] 326 | ])); 327 | expect(headers.get('a')).to.equal('1'); 328 | expect(headers.get('b')).to.equal('2'); 329 | }); 330 | 331 | it('should throw a TypeError if non-tuple exists in a headers initializer', () => { 332 | expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError); 333 | expect(() => new Headers(['b2'])).to.throw(TypeError); 334 | expect(() => new Headers('b2')).to.throw(TypeError); 335 | expect(() => new Headers({[Symbol.iterator]: 42})).to.throw(TypeError); 336 | }); 337 | 338 | it('should use a custom inspect function', () => { 339 | const headers = new Headers([ 340 | ['Host', 'thehost'], 341 | ['Host', 'notthehost'], 342 | ['a', '1'], 343 | ['b', '2'], 344 | ['a', '3'] 345 | ]); 346 | 347 | // eslint-disable-next-line quotes 348 | expect(util.format(headers)).to.equal("{ a: [ '1', '3' ], b: '2', host: 'thehost' }"); 349 | }); 350 | }); 351 | -------------------------------------------------------------------------------- /packages/fetch/test/response.js: -------------------------------------------------------------------------------- 1 | 2 | import {TextEncoder} from 'util'; 3 | import chai from 'chai'; 4 | import {Blob} from '@remix-run/web-blob'; 5 | import {Response} from '@remix-run/web-fetch'; 6 | import TestServer from './utils/server.js'; 7 | import { ReadableStream } from '../src/package.js'; 8 | 9 | const {expect} = chai; 10 | 11 | describe('Response', () => { 12 | const local = new TestServer(); 13 | let base; 14 | 15 | before(async () => { 16 | await local.start(); 17 | base = `http://${local.hostname}:${local.port}/`; 18 | }); 19 | 20 | after(async () => { 21 | return local.stop(); 22 | }); 23 | 24 | it('should have attributes conforming to Web IDL', () => { 25 | const res = new Response(); 26 | const enumerableProperties = []; 27 | for (const property in res) { 28 | enumerableProperties.push(property); 29 | } 30 | 31 | for (const toCheck of [ 32 | 'body', 33 | 'bodyUsed', 34 | 'arrayBuffer', 35 | 'blob', 36 | 'json', 37 | 'text', 38 | 'url', 39 | 'status', 40 | 'ok', 41 | 'redirected', 42 | 'statusText', 43 | 'headers', 44 | 'clone' 45 | ]) { 46 | expect(enumerableProperties).to.contain(toCheck); 47 | } 48 | 49 | for (const toCheck of [ 50 | 'body', 51 | 'bodyUsed', 52 | 'url', 53 | 'status', 54 | 'ok', 55 | 'redirected', 56 | 'statusText', 57 | 'headers' 58 | ]) { 59 | expect(() => { 60 | res[toCheck] = 'abc'; 61 | }).to.throw(); 62 | } 63 | }); 64 | 65 | it('should support empty options', () => { 66 | const res = new Response(streamFromString('a=1')); 67 | return res.text().then(result => { 68 | expect(result).to.equal('a=1'); 69 | }); 70 | }); 71 | 72 | it('should support parsing headers', () => { 73 | const res = new Response(null, { 74 | headers: { 75 | a: '1' 76 | } 77 | }); 78 | expect(res.headers.get('a')).to.equal('1'); 79 | }); 80 | 81 | it('should support text() method', () => { 82 | const res = new Response('a=1'); 83 | return res.text().then(result => { 84 | expect(result).to.equal('a=1'); 85 | }); 86 | }); 87 | 88 | it('should support json() method', () => { 89 | const res = new Response('{"a":1}'); 90 | return res.json().then(result => { 91 | expect(result.a).to.equal(1); 92 | }); 93 | }); 94 | 95 | it('should support blob() method', () => { 96 | const res = new Response('a=1', { 97 | method: 'POST', 98 | headers: { 99 | 'Content-Type': 'text/plain' 100 | } 101 | }); 102 | return res.blob().then(result => { 103 | expect(result).to.be.an.instanceOf(Blob); 104 | expect(result.size).to.equal(3); 105 | expect(result.type).to.equal('text/plain'); 106 | }); 107 | }); 108 | 109 | it('should support clone() method', () => { 110 | const body = streamFromString('a=1'); 111 | const res = new Response(body, { 112 | headers: { 113 | a: '1' 114 | }, 115 | url: base, 116 | status: 346, 117 | statusText: 'production' 118 | }); 119 | const cl = res.clone(); 120 | expect(cl.headers.get('a')).to.equal('1'); 121 | expect(cl.url).to.equal(base); 122 | expect(cl.status).to.equal(346); 123 | expect(cl.statusText).to.equal('production'); 124 | expect(cl.ok).to.be.false; 125 | // Clone body shouldn't be the same body 126 | expect(cl.body).to.not.equal(body); 127 | return cl.text().then(result => { 128 | expect(result).to.equal('a=1'); 129 | }); 130 | }); 131 | 132 | it('should support clone() method with null body', () => { 133 | const res = new Response(null, { 134 | headers: { 135 | a: '1' 136 | }, 137 | url: base, 138 | status: 346, 139 | statusText: 'production' 140 | }); 141 | const cl = res.clone(); 142 | expect(cl.headers.get('a')).to.equal('1'); 143 | expect(cl.url).to.equal(base); 144 | expect(cl.status).to.equal(346); 145 | expect(cl.statusText).to.equal('production'); 146 | expect(cl.ok).to.be.false; 147 | // Clone body should also be null 148 | expect(cl.body).to.equal(null); 149 | return cl.text().then(result => { 150 | expect(result).to.equal(''); 151 | }); 152 | }); 153 | 154 | it('should support stream as body', () => { 155 | const body = streamFromString('a=1'); 156 | const res = new Response(body); 157 | return res.text().then(result => { 158 | expect(result).to.equal('a=1'); 159 | }); 160 | }); 161 | 162 | it('should support string as body', () => { 163 | const res = new Response('a=1'); 164 | return res.text().then(result => { 165 | expect(result).to.equal('a=1'); 166 | }); 167 | }); 168 | 169 | it('should support buffer as body', () => { 170 | const res = new Response(Buffer.from('a=1')); 171 | return res.text().then(result => { 172 | expect(result).to.equal('a=1'); 173 | }); 174 | }); 175 | 176 | it('should support ArrayBuffer as body', () => { 177 | const encoder = new TextEncoder(); 178 | const res = new Response(encoder.encode('a=1')); 179 | return res.text().then(result => { 180 | expect(result).to.equal('a=1'); 181 | }); 182 | }); 183 | 184 | it('should support blob as body', () => { 185 | const res = new Response(new Blob(['a=1'])); 186 | return res.text().then(result => { 187 | expect(result).to.equal('a=1'); 188 | }); 189 | }); 190 | 191 | it('should support Uint8Array as body', () => { 192 | const encoder = new TextEncoder(); 193 | const res = new Response(encoder.encode('a=1')); 194 | return res.text().then(result => { 195 | expect(result).to.equal('a=1'); 196 | }); 197 | }); 198 | 199 | it('should support DataView as body', () => { 200 | const encoder = new TextEncoder(); 201 | const res = new Response(new DataView(encoder.encode('a=1').buffer)); 202 | return res.text().then(result => { 203 | expect(result).to.equal('a=1'); 204 | }); 205 | }); 206 | 207 | it('should default to null as body', () => { 208 | const res = new Response(); 209 | expect(res.body).to.equal(null); 210 | 211 | return res.text().then(result => expect(result).to.equal('')); 212 | }); 213 | 214 | it('should default to 200 as status code', () => { 215 | const res = new Response(null); 216 | expect(res.status).to.equal(200); 217 | }); 218 | 219 | it('should default to empty string as url', () => { 220 | const res = new Response(); 221 | expect(res.url).to.equal(''); 222 | }); 223 | }); 224 | 225 | const streamFromString = text => new ReadableStream({ 226 | start(controller) { 227 | controller.enqueue(Buffer.from(text)); 228 | controller.close(); 229 | } 230 | }); 231 | -------------------------------------------------------------------------------- /packages/fetch/test/utils/chai-timeout.js: -------------------------------------------------------------------------------- 1 | import pTimeout from 'p-timeout'; 2 | 3 | export default ({Assertion}, utils) => { 4 | utils.addProperty(Assertion.prototype, 'timeout', async function () { 5 | let timeouted = false; 6 | await pTimeout(this._obj, 150, () => { 7 | timeouted = true; 8 | }); 9 | return this.assert( 10 | timeouted, 11 | 'expected promise to timeout but it was resolved', 12 | 'expected promise not to timeout but it timed out' 13 | ); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/fetch/test/utils/dummy.txt: -------------------------------------------------------------------------------- 1 | i am a dummy -------------------------------------------------------------------------------- /packages/fetch/test/utils/read-stream.js: -------------------------------------------------------------------------------- 1 | export default async function readStream(stream) { 2 | const chunks = []; 3 | 4 | for await (const chunk of stream) { 5 | chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)); 6 | } 7 | 8 | return Buffer.concat(chunks); 9 | } 10 | -------------------------------------------------------------------------------- /packages/fetch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "paths": { 6 | "@remix-run/web-fetch": [ 7 | "packages/fetch/src/lib.node.js" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/file/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Export CJS version for browser ([807fc63](https://github.com/remix-run/web-std-io/commit/807fc63)) 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - `@remix-run/web-blob@3.1.0` 13 | 14 | ## 3.1.0-pre.0 15 | 16 | ### Minor Changes 17 | 18 | - 807fc63: Export CJS version for browser 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [807fc63] 23 | - @remix-run/web-blob@3.1.0-pre.0 24 | 25 | ## 3.0.3 26 | 27 | ### Patch Changes 28 | 29 | - 43c6ce2: Move types conditional export to the top of the list to align with [the node guidance](https://nodejs.org/api/packages.html#community-conditions-definitions) 30 | - Updated dependencies [43c6ce2] 31 | - @remix-run/web-blob@3.0.5 32 | 33 | ## [3.0.2](https://www.github.com/web-std/io/compare/file-v3.0.1...file-v3.0.2) (2022-01-21) 34 | 35 | ### Changes 36 | 37 | - **file:** update blob dep version ([767988b](https://www.github.com/web-std/io/commit/767988b9dade84ee04b8cda515c114cba8a1f659)) 38 | 39 | ## [3.0.1](https://www.github.com/web-std/io/compare/file-v3.0.0...file-v3.0.1) (2022-01-19) 40 | 41 | ### Bug Fixes 42 | 43 | - ship less files to address TSC issues ([#35](https://www.github.com/web-std/io/issues/35)) ([0651e62](https://www.github.com/web-std/io/commit/0651e62ae42d17eae2db89858c9e44f3342c304c)) 44 | 45 | ## 3.0.0 (2021-11-05) 46 | 47 | ### Features 48 | 49 | - revamp the repo ([#19](https://www.github.com/web-std/io/issues/19)) ([90624cf](https://www.github.com/web-std/io/commit/90624cfd2d4253c2cbc316d092f26e77b5169f47)) 50 | 51 | ### Changes 52 | 53 | - align package versions ([09c8676](https://www.github.com/web-std/io/commit/09c8676348619313d9df24d9597cea0eb82704d2)) 54 | - bump versions ([#27](https://www.github.com/web-std/io/issues/27)) ([0fe5224](https://www.github.com/web-std/io/commit/0fe5224124e318f560dcfbd8a234d05367c9fbcb)) 55 | -------------------------------------------------------------------------------- /packages/file/License.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Irakli Gozalishvili. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /packages/file/Readme.md: -------------------------------------------------------------------------------- 1 | # web-file 2 | 3 | [![ci][ci.icon]][ci.url] 4 | [![package][version.icon] ![downloads][downloads.icon]][package.url] 5 | [![styled with prettier][prettier.icon]][prettier.url] 6 | 7 | Web API compatible [File][] for nodejs. 8 | 9 | ### Usage 10 | 11 | ```js 12 | import { File, Blob } from "@remix-run/web-file" 13 | const file = new File(["hello", new TextEncoder().encode("world")], "hello") 14 | for await (const chunk of blob.stream()) { 15 | console.log(chunk) 16 | } 17 | ``` 18 | 19 | ### Usage from Typescript 20 | 21 | This library makes use of [typescript using JSDOC annotations][ts-jsdoc] and 22 | also generates type difinitions along with typed definition maps. So you should 23 | be able to get all the type innference out of the box. 24 | 25 | ## Install 26 | 27 | npm install @remix-run/web-file 28 | 29 | [ci.icon]: https://github.com/web-std/io/workflows/file/badge.svg 30 | [ci.url]: https://github.com/web-std/io/actions/workflows/file.yml 31 | [version.icon]: https://img.shields.io/npm/v/@remix-run/web-file.svg 32 | [downloads.icon]: https://img.shields.io/npm/dm/@remix-run/web-file.svg 33 | [package.url]: https://npmjs.org/package/@remix-run/web-file 34 | [downloads.image]: https://img.shields.io/npm/dm/@remix-run/web-file.svg 35 | [downloads.url]: https://npmjs.org/package/@remix-run/web-file 36 | [prettier.icon]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg 37 | [prettier.url]: https://github.com/prettier/prettier 38 | [blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob 39 | [fetch-blob]: https://github.com/node-fetch/fetch-blob 40 | [readablestream]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream 41 | [readable]: https://nodejs.org/api/stream.html#stream_readable_streams 42 | [file]: https://w3c.github.io/FileAPI/ 43 | [for await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of 44 | [buffer]: https://nodejs.org/api/buffer.html 45 | [weakmap]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap 46 | [ts-jsdoc]: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html 47 | -------------------------------------------------------------------------------- /packages/file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remix-run/web-file", 3 | "version": "3.1.0", 4 | "description": "Web API compatible File implementation for node", 5 | "files": [ 6 | "src", 7 | "dist/src", 8 | "License.md", 9 | "Readme.md" 10 | ], 11 | "keywords": [ 12 | "file", 13 | "blob", 14 | "typed", 15 | "typescript" 16 | ], 17 | "type": "module", 18 | "module": "./src/lib.js", 19 | "main": "./dist/src/lib.node.cjs", 20 | "browser": { 21 | "./src/lib.node.js": "./src/lib.js" 22 | }, 23 | "types": "./dist/src/lib.d.ts", 24 | "exports": { 25 | ".": { 26 | "types": "./dist/src/lib.d.ts", 27 | "browser": { 28 | "require": "./dist/src/lib.cjs", 29 | "import": "./src/lib.js" 30 | }, 31 | "require": "./dist/src/lib.node.cjs", 32 | "node": "./src/lib.node.js" 33 | } 34 | }, 35 | "dependencies": { 36 | "@remix-run/web-blob": "^3.1.0" 37 | }, 38 | "author": "Irakli Gozalishvili (https://gozala.io)", 39 | "repository": "https://github.com/remix-run/web-std-io", 40 | "license": "MIT", 41 | "devDependencies": { 42 | "@remix-run/web-fetch": "^4.4.2-pre.0", 43 | "@types/node": "15.0.2", 44 | "git-validate": "2.2.4", 45 | "husky": "^6.0.0", 46 | "lint-staged": "^11.0.0", 47 | "playwright-test": "^7.2.0", 48 | "prettier": "^2.3.0", 49 | "rimraf": "3.0.2", 50 | "rollup": "2.47.0", 51 | "rollup-plugin-multi-input": "1.2.0", 52 | "typescript": "^4.4.4", 53 | "uvu": "0.5.2" 54 | }, 55 | "scripts": { 56 | "typecheck": "tsc", 57 | "build": "npm run build:cjs && npm run build:types", 58 | "build:cjs": "rollup --config rollup.config.js", 59 | "build:types": "tsc --build", 60 | "prepare": "npm run build", 61 | "test:es": "uvu test all.spec.js", 62 | "test:web": "playwright-test -r uvu test/web.spec.js", 63 | "test:cjs": "rimraf dist && npm run build && node dist/test/all.spec.cjs", 64 | "test": "npm run test:es && npm run test:web && npm run test:cjs", 65 | "precommit": "lint-staged" 66 | }, 67 | "lint-staged": { 68 | "*.js": [ 69 | "prettier --no-semi --write", 70 | "git add" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/file/rollup.config.js: -------------------------------------------------------------------------------- 1 | import multiInput from "rollup-plugin-multi-input" 2 | 3 | const config = [ 4 | ["test", "dist/test"], 5 | ["src", "dist/src"], 6 | ].map(([base, dest]) => ({ 7 | input: [`${base}/**/*.js`], 8 | output: { 9 | dir: dest, 10 | preserveModules: true, 11 | sourcemap: true, 12 | format: "cjs", 13 | entryFileNames: "[name].cjs", 14 | }, 15 | plugins: [multiInput({ relative: base })], 16 | })) 17 | export default config 18 | -------------------------------------------------------------------------------- /packages/file/src/file.js: -------------------------------------------------------------------------------- 1 | import { Blob } from "./package.js" 2 | 3 | /** 4 | * @implements {globalThis.File} 5 | */ 6 | export class File extends Blob { 7 | /** 8 | * 9 | * @param {BlobPart[]} init 10 | * @param {string} name - A USVString representing the file name or the path 11 | * to the file. 12 | * @param {FilePropertyBag} [options] 13 | */ 14 | constructor( 15 | init, 16 | name = panic(new TypeError("File constructor requires name argument")), 17 | options = {} 18 | ) { 19 | super(init, options) 20 | // Per File API spec https://w3c.github.io/FileAPI/#file-constructor 21 | // Every "/" character of file name must be replaced with a ":". 22 | /** @private */ 23 | this._name = name 24 | // It appears that browser do not follow the spec here. 25 | // String(name).replace(/\//g, ":") 26 | /** @private */ 27 | this._lastModified = options.lastModified || Date.now() 28 | } 29 | 30 | /** 31 | * The name of the file referenced by the File object. 32 | * @type {string} 33 | */ 34 | get name() { 35 | return this._name 36 | } 37 | 38 | /** 39 | * The path the URL of the File is relative to. 40 | * @type {string} 41 | */ 42 | get webkitRelativePath() { 43 | return "" 44 | } 45 | 46 | /** 47 | * Returns the last modified time of the file, in millisecond since the UNIX 48 | * epoch (January 1st, 1970 at Midnight). 49 | * @returns {number} 50 | */ 51 | get lastModified() { 52 | return this._lastModified 53 | } 54 | 55 | get [Symbol.toStringTag]() { 56 | return "File" 57 | } 58 | } 59 | 60 | /** 61 | * @param {*} error 62 | * @returns {never} 63 | */ 64 | const panic = error => { 65 | throw error 66 | } 67 | -------------------------------------------------------------------------------- /packages/file/src/lib.js: -------------------------------------------------------------------------------- 1 | export const File = globalThis.File 2 | export const Blob = globalThis.Blob 3 | -------------------------------------------------------------------------------- /packages/file/src/lib.node.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | import { Blob } from "./package.js" 4 | import { File as WebFile } from "./file.js" 5 | 6 | // Electron-renderer should get the browser implementation instead of node 7 | // Browser configuration is not enough 8 | 9 | // Marking export as a DOM File object instead of custom class. 10 | /** @type {typeof globalThis.File} */ 11 | const File = WebFile 12 | 13 | export { File, Blob } 14 | -------------------------------------------------------------------------------- /packages/file/src/package.js: -------------------------------------------------------------------------------- 1 | export { Blob } from "@remix-run/web-blob"; 2 | -------------------------------------------------------------------------------- /packages/file/test/all.spec.js: -------------------------------------------------------------------------------- 1 | import { test as fileTest } from "./file.spec.js" 2 | import { test as fetchTest } from "./fetch.spec.js" 3 | 4 | import { test } from "./test.js" 5 | 6 | fileTest(test) 7 | fetchTest(test) 8 | test.run() 9 | -------------------------------------------------------------------------------- /packages/file/test/fetch.spec.js: -------------------------------------------------------------------------------- 1 | import { Response } from "@remix-run/web-fetch"; 2 | import { File } from "@remix-run/web-file"; 3 | import { assert } from "./test.js"; 4 | 5 | /** 6 | * @param {import('./test').Test} test 7 | */ 8 | export const test = test => { 9 | test("node-fetch recognizes blobs", async () => { 10 | const response = new Response(new File(["hello"], "path/file.txt")); 11 | 12 | assert.equal(await response.text(), "hello"); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/file/test/file.spec.js: -------------------------------------------------------------------------------- 1 | import * as lib from "@remix-run/web-file"; 2 | import { File } from "@remix-run/web-file"; 3 | import { assert } from "./test.js"; 4 | 5 | /** 6 | * @param {import('./test').Test} test 7 | */ 8 | export const test = test => { 9 | test("test baisc", async () => { 10 | assert.isEqual(typeof lib.Blob, "function"); 11 | assert.isEqual(typeof lib.File, "function"); 12 | }); 13 | 14 | if (globalThis.window === globalThis) { 15 | test("uses built-ins", async () => { 16 | assert.isEqual(lib.File, globalThis.File); 17 | assert.isEqual(lib.Blob, globalThis.Blob); 18 | }); 19 | } 20 | 21 | test("new File", async () => { 22 | // @ts-expect-error 23 | assert.throws(() => new File(), TypeError); 24 | // @ts-expect-error 25 | assert.throws(() => new File([]), TypeError); 26 | 27 | const before = Date.now(); 28 | await new Promise(resolve => setTimeout(resolve, 3)); 29 | const file = new File(["test"], "name"); 30 | await new Promise(resolve => setTimeout(resolve, 3)); 31 | const after = Date.now(); 32 | assert.equal(file.size, 4); 33 | assert.equal(file.name, "name"); 34 | assert.equal(typeof file.lastModified, "number"); 35 | assert.equal(file.lastModified > before, true); 36 | assert.equal(file.lastModified < after, true); 37 | assert.equal(file.type, ""); 38 | 39 | const chunks = []; 40 | const reader = file.stream().getReader(); 41 | while (true) { 42 | const chunk = await reader.read(); 43 | if (chunk.done) { 44 | reader.releaseLock(); 45 | break; 46 | } else { 47 | chunks.push(chunk.value); 48 | } 49 | } 50 | 51 | assert.deepEqual(chunks, [new TextEncoder().encode("test")]); 52 | }); 53 | 54 | test("File with lastModified", async () => { 55 | const file = new File(["test"], "name", { lastModified: 1594672000418 }); 56 | 57 | assert.equal(file.size, 4); 58 | assert.equal(file.name, "name"); 59 | assert.equal(file.lastModified, 1594672000418); 60 | assert.equal(file.type, ""); 61 | }); 62 | 63 | test("File with type", async () => { 64 | const file = new File(["test"], "name", { 65 | lastModified: 1594672000418, 66 | type: "text/plain" 67 | }); 68 | 69 | assert.equal(file.size, 4); 70 | assert.equal(file.name, "name"); 71 | assert.equal(file.lastModified, 1594672000418); 72 | assert.equal(file.type, "text/plain"); 73 | }); 74 | 75 | test("File type is normalized", async () => { 76 | const file = new File(["test"], "name", { 77 | type: "Text/Plain" 78 | }); 79 | 80 | assert.equal(file.size, 4); 81 | assert.equal(file.name, "name"); 82 | assert.equal(file.type, "text/plain"); 83 | }); 84 | 85 | test("File name is (not) escaped", async () => { 86 | const file = new File(["test"], "dir/name"); 87 | 88 | assert.equal(file.size, 4); 89 | // occording to spec it's former but in pratice it seems later 🤷‍♂️ 90 | assert.equal(file.name === "dir:name" || file.name === "dir/name", true); 91 | assert.equal(file.type, ""); 92 | }); 93 | }; 94 | -------------------------------------------------------------------------------- /packages/file/test/test.js: -------------------------------------------------------------------------------- 1 | import * as uvu from "uvu" 2 | import * as uvuassert from "uvu/assert" 3 | 4 | const deepEqual = uvuassert.equal 5 | const isEqual = uvuassert.equal 6 | const isEquivalent = uvuassert.equal 7 | export const assert = { ...uvuassert, deepEqual, isEqual, isEquivalent } 8 | export const test = uvu.test 9 | 10 | 11 | /** 12 | * @typedef {uvu.Test} Test 13 | */ 14 | -------------------------------------------------------------------------------- /packages/file/test/web.spec.js: -------------------------------------------------------------------------------- 1 | import { test as fileTest } from "./file.spec.js" 2 | import { test } from "./test.js" 3 | 4 | fileTest(test) 5 | test.run() 6 | -------------------------------------------------------------------------------- /packages/file/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "paths": { 6 | "@remix-run/web-file": ["packages/file/src/lib.js"] 7 | } 8 | }, 9 | "references": [ 10 | { 11 | "path": "../blob" 12 | }, 13 | { 14 | "path": "../fetch" 15 | } 16 | ], 17 | "include": [ 18 | "src", 19 | "test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /packages/form-data/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Export CJS version for browser ([807fc63](https://github.com/remix-run/web-std-io/commit/807fc63)) 8 | 9 | ## 3.0.5 10 | 11 | ### Patch Changes 12 | 13 | - 43c6ce2: Move types conditional export to the top of the list to align with [the node guidance](https://nodejs.org/api/packages.html#community-conditions-definitions) 14 | 15 | ## 3.0.4 16 | 17 | ### Patch Changes 18 | 19 | - c4a9738: allow passing a form to FormData. implementation taken from https://github.com/tchak/remix-router-turbo/blob/main/test/setup-test-env.ts, thank you @tchak 20 | 21 | ## 3.0.3 22 | 23 | ### Patch Changes 24 | 25 | - 6521895: Fix submitting form data when file input is empty. Addresses https://github.com/remix-run/remix/pull/3576 26 | 27 | ## [3.0.2](https://www.github.com/web-std/io/compare/form-data-v3.0.1...form-data-v3.0.2) (2022-01-21) 28 | 29 | ### Changes 30 | 31 | - bump form-data version ([b7ac808](https://www.github.com/web-std/io/commit/b7ac808ba8ae6488d5c2dc6d0d441412a7a8e2b8)) 32 | 33 | ## [3.0.1](https://www.github.com/web-std/io/compare/form-data-v3.0.0...form-data-v3.0.1) (2022-01-19) 34 | 35 | ### Bug Fixes 36 | 37 | - ship less files to address TSC issues ([#35](https://www.github.com/web-std/io/issues/35)) ([0651e62](https://www.github.com/web-std/io/commit/0651e62ae42d17eae2db89858c9e44f3342c304c)) 38 | 39 | ## 3.0.0 (2021-11-08) 40 | 41 | ### Features 42 | 43 | - revamp the repo ([#19](https://www.github.com/web-std/io/issues/19)) ([90624cf](https://www.github.com/web-std/io/commit/90624cfd2d4253c2cbc316d092f26e77b5169f47)) 44 | 45 | ### Changes 46 | 47 | - align package versions ([09c8676](https://www.github.com/web-std/io/commit/09c8676348619313d9df24d9597cea0eb82704d2)) 48 | - bump versions ([#27](https://www.github.com/web-std/io/issues/27)) ([0fe5224](https://www.github.com/web-std/io/commit/0fe5224124e318f560dcfbd8a234d05367c9fbcb)) 49 | -------------------------------------------------------------------------------- /packages/form-data/License.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Irakli Gozalishvili. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /packages/form-data/Readme.md: -------------------------------------------------------------------------------- 1 | # @remix-run/web-form-data 2 | 3 | [![ci][ci.icon]][ci.url] 4 | [![package][version.icon] ![downloads][downloads.icon]][package.url] 5 | [![styled with prettier][prettier.icon]][prettier.url] 6 | 7 | Web API compatible [FormData][] for nodejs. 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install @remix-run/web-form-data 13 | ``` 14 | 15 | [ci.icon]: https://github.com/web-std/io/workflows/form-data/badge.svg 16 | [ci.url]: https://github.com/web-std/io/actions/workflows/form-data.yml 17 | [version.icon]: https://img.shields.io/npm/v/@remix-run/web-form-data.svg 18 | [downloads.icon]: https://img.shields.io/npm/dm/@remix-run/web-form-data.svg 19 | [package.url]: https://npmjs.org/package/@remix-run/web-form-data 20 | [downloads.image]: https://img.shields.io/npm/dm/@remix-run/web-form-data.svg 21 | [downloads.url]: https://npmjs.org/package/@remix-run/web-form-data 22 | [prettier.icon]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg 23 | [prettier.url]: https://github.com/prettier/prettier 24 | [formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData 25 | -------------------------------------------------------------------------------- /packages/form-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remix-run/web-form-data", 3 | "version": "3.1.0", 4 | "description": "Web API compatible Form Data implementation", 5 | "files": [ 6 | "src", 7 | "dist/src", 8 | "License.md", 9 | "Readme.md" 10 | ], 11 | "keywords": [ 12 | "formdata", 13 | "typed" 14 | ], 15 | "type": "module", 16 | "module": "./src/lib.js", 17 | "main": "./dist/src/lib.node.cjs", 18 | "types": "./dist/src/lib.d.ts", 19 | "browser": { 20 | "./src/lib.node.js": "./src/lib.js" 21 | }, 22 | "exports": { 23 | ".": { 24 | "types": "./dist/src/lib.d.ts", 25 | "browser": { 26 | "require": "./dist/src/lib.cjs", 27 | "import": "./src/lib.js" 28 | }, 29 | "require": "./dist/src/lib.node.cjs", 30 | "import": "./src/lib.node.js" 31 | } 32 | }, 33 | "dependencies": { 34 | "web-encoding": "1.1.5" 35 | }, 36 | "author": "Irakli Gozalishvili (https://gozala.io)", 37 | "repository": "https://github.com/remix-run/web-std-io", 38 | "license": "MIT", 39 | "devDependencies": { 40 | "@remix-run/web-blob": "^3.1.0", 41 | "@remix-run/web-fetch": "^4.4.2-pre.0", 42 | "@remix-run/web-file": "^3.1.0", 43 | "@types/node": "15.0.2", 44 | "git-validate": "2.2.4", 45 | "husky": "^6.0.0", 46 | "lint-staged": "^11.0.0", 47 | "playwright-test": "^7.2.0", 48 | "prettier": "^2.3.0", 49 | "rimraf": "3.0.2", 50 | "rollup": "2.47.0", 51 | "rollup-plugin-multi-input": "1.2.0", 52 | "typescript": "^4.4.4", 53 | "uvu": "0.5.2" 54 | }, 55 | "scripts": { 56 | "typecheck": "tsc", 57 | "build": "npm run build:cjs && npm run build:types", 58 | "build:cjs": "rollup --config rollup.config.js", 59 | "build:types": "tsc --build", 60 | "prepare": "npm run build", 61 | "test:es": "uvu test all.spec.js", 62 | "test:web": "playwright-test -r uvu test/web.spec.js", 63 | "test:cjs": "rimraf dist && npm run build && node dist/test/all.spec.cjs", 64 | "test": "npm run test:es && npm run test:cjs", 65 | "precommit": "lint-staged" 66 | }, 67 | "lint-staged": { 68 | "*.js": [ 69 | "prettier --no-semi --write", 70 | "git add" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/form-data/rollup.config.js: -------------------------------------------------------------------------------- 1 | import multiInput from "rollup-plugin-multi-input" 2 | 3 | const config = [ 4 | ["test", "dist/test"], 5 | ["src", "dist/src"], 6 | ].map(([base, dest]) => ({ 7 | input: [`${base}/**/*.js`], 8 | output: { 9 | dir: dest, 10 | preserveModules: true, 11 | sourcemap: true, 12 | format: "cjs", 13 | entryFileNames: "[name].cjs", 14 | }, 15 | plugins: [multiInput({ relative: base })], 16 | })) 17 | export default config 18 | -------------------------------------------------------------------------------- /packages/form-data/src/form-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @implements {globalThis.FormData} 3 | */ 4 | export class FormData { 5 | /** 6 | * @param {HTMLFormElement} [form] 7 | */ 8 | constructor(form) { 9 | /** 10 | * @private 11 | * @readonly 12 | * @type {Array<[string, FormDataEntryValue]>} 13 | */ 14 | this._entries = []; 15 | 16 | Object.defineProperty(this, "_entries", { enumerable: false }); 17 | 18 | if (isHTMLFormElement(form)) { 19 | for (const element of form.elements) { 20 | if (isSelectElement(element)) { 21 | for (const option of element.options) { 22 | if (option.selected) { 23 | this.append(element.name, option.value); 24 | } 25 | } 26 | } else if ( 27 | isInputElement(element) && 28 | (element.checked || !["radio", "checkbox"].includes(element.type)) && 29 | element.name 30 | ) { 31 | this.append(element.name, element.value); 32 | } 33 | } 34 | } 35 | } 36 | get [Symbol.toStringTag]() { 37 | return "FormData"; 38 | } 39 | 40 | /** 41 | * Appends a new value onto an existing key inside a FormData object, or adds 42 | * the key if it does not already exist. 43 | * 44 | * The difference between `set` and `append` is that if the specified key 45 | * already exists, `set` will overwrite all existing values with the new one, 46 | * whereas `append` will append the new value onto the end of the existing 47 | * set of values. 48 | * 49 | * @param {string} name 50 | * @param {string|Blob|File} value - The name of the field whose data is 51 | * contained in value. 52 | * @param {string} [filename] - The filename reported to the server, when a 53 | * value is a `Blob` or a `File`. The default filename for a `Blob` objects is 54 | * `"blob"`. The default filename for a `File` is the it's name. 55 | */ 56 | append( 57 | name, 58 | value = panic( 59 | new TypeError("FormData.append: requires at least 2 arguments") 60 | ), 61 | filename 62 | ) { 63 | this._entries.push([name, toEntryValue(value, filename)]); 64 | } 65 | 66 | /** 67 | * Deletes a key and all its values from a FormData object. 68 | * 69 | * @param {string} name 70 | */ 71 | delete( 72 | name = panic(new TypeError("FormData.delete: requires string argument")) 73 | ) { 74 | const entries = this._entries; 75 | let index = 0; 76 | while (index < entries.length) { 77 | const [entryName] = /** @type {[string, FormDataEntryValue]}*/ ( 78 | entries[index] 79 | ); 80 | if (entryName === name) { 81 | entries.splice(index, 1); 82 | } else { 83 | index++; 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * Returns the first value associated with a given key from within a 90 | * FormData object. 91 | * 92 | * @param {string} name 93 | * @returns {FormDataEntryValue|null} 94 | */ 95 | 96 | get(name = panic(new TypeError("FormData.get: requires string argument"))) { 97 | for (const [entryName, value] of this._entries) { 98 | if (entryName === name) { 99 | return value; 100 | } 101 | } 102 | return null; 103 | } 104 | 105 | /** 106 | * Returns an array of all the values associated with a given key from within 107 | * a FormData. 108 | * 109 | * @param {string} name 110 | * @returns {FormDataEntryValue[]} 111 | */ 112 | getAll( 113 | name = panic(new TypeError("FormData.getAll: requires string argument")) 114 | ) { 115 | const values = []; 116 | for (const [entryName, value] of this._entries) { 117 | if (entryName === name) { 118 | values.push(value); 119 | } 120 | } 121 | return values; 122 | } 123 | 124 | /** 125 | * Returns a boolean stating whether a FormData object contains a certain key. 126 | * 127 | * @param {string} name 128 | */ 129 | 130 | has(name = panic(new TypeError("FormData.has: requires string argument"))) { 131 | for (const [entryName] of this._entries) { 132 | if (entryName === name) { 133 | return true; 134 | } 135 | } 136 | return false; 137 | } 138 | 139 | /** 140 | * Sets a new value for an existing key inside a FormData object, or adds the 141 | * key/value if it does not already exist. 142 | * 143 | * @param {string} name 144 | * @param {string|Blob|File} value 145 | * @param {string} [filename] 146 | */ 147 | 148 | set( 149 | name, 150 | value = panic(new TypeError("FormData.set: requires at least 2 arguments")), 151 | filename 152 | ) { 153 | let index = 0; 154 | const { _entries: entries } = this; 155 | const entryValue = toEntryValue(value, filename); 156 | let wasSet = false; 157 | while (index < entries.length) { 158 | const entry = /** @type {[string, FormDataEntryValue]}*/ (entries[index]); 159 | if (entry[0] === name) { 160 | if (wasSet) { 161 | entries.splice(index, 1); 162 | } else { 163 | wasSet = true; 164 | entry[1] = entryValue; 165 | index++; 166 | } 167 | } else { 168 | index++; 169 | } 170 | } 171 | 172 | if (!wasSet) { 173 | entries.push([name, entryValue]); 174 | } 175 | } 176 | 177 | /** 178 | * Method returns an iterator allowing to go through all key/value pairs 179 | * contained in this object. 180 | */ 181 | entries() { 182 | return this._entries.values(); 183 | } 184 | 185 | /** 186 | * Returns an iterator allowing to go through all keys of the key/value pairs 187 | * contained in this object. 188 | * 189 | * @returns {IterableIterator} 190 | */ 191 | *keys() { 192 | for (const [name] of this._entries) { 193 | yield name; 194 | } 195 | } 196 | 197 | /** 198 | * Returns an iterator allowing to go through all values contained in this 199 | * object. 200 | * 201 | * @returns {IterableIterator} 202 | */ 203 | *values() { 204 | for (const [_, value] of this._entries) { 205 | yield value; 206 | } 207 | } 208 | 209 | [Symbol.iterator]() { 210 | return this._entries.values(); 211 | } 212 | 213 | /** 214 | * @param {(value: FormDataEntryValue, key: string, parent: globalThis.FormData) => void} fn 215 | * @param {any} [thisArg] 216 | * @returns {void} 217 | */ 218 | forEach(fn, thisArg) { 219 | for (const [key, value] of this._entries) { 220 | fn.call(thisArg, value, key, this); 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * @param {any} value 227 | * @returns {value is HTMLFormElement} 228 | */ 229 | const isHTMLFormElement = (value) => 230 | Object.prototype.toString.call(value) === "[object HTMLFormElement]"; 231 | 232 | /** 233 | * @param {string|Blob|File} value 234 | * @param {string} [filename] 235 | * @returns {FormDataEntryValue} 236 | */ 237 | const toEntryValue = (value, filename) => { 238 | if (isFile(value)) { 239 | return filename != null ? new BlobFile([value], filename, value) : value; 240 | } else if (isBlob(value)) { 241 | return new BlobFile([value], filename != null ? filename : "blob"); 242 | } else { 243 | if (filename != null && filename != "") { 244 | throw new TypeError( 245 | "filename is only supported when value is Blob or File" 246 | ); 247 | } 248 | return `${value}`; 249 | } 250 | }; 251 | 252 | /** 253 | * @param {any} value 254 | * @returns {value is File} 255 | */ 256 | const isFile = (value) => 257 | Object.prototype.toString.call(value) === "[object File]" && 258 | typeof value.name === "string"; 259 | 260 | /** 261 | * @param {any} value 262 | * @returns {value is Blob} 263 | */ 264 | const isBlob = (value) => 265 | Object.prototype.toString.call(value) === "[object Blob]"; 266 | 267 | /** 268 | * Simple `File` implementation that just wraps a given blob. 269 | * @implements {globalThis.File} 270 | */ 271 | const BlobFile = class File { 272 | /** 273 | * @param {[Blob]} parts 274 | * @param {string} name 275 | * @param {FilePropertyBag} [options] 276 | */ 277 | constructor([blob], name, { lastModified = Date.now() } = {}) { 278 | this.blob = blob; 279 | this.name = name; 280 | this.lastModified = lastModified; 281 | } 282 | get webkitRelativePath() { 283 | return ""; 284 | } 285 | get size() { 286 | return this.blob.size; 287 | } 288 | get type() { 289 | return this.blob.type; 290 | } 291 | /** 292 | * 293 | * @param {number} [start] 294 | * @param {number} [end] 295 | * @param {string} [contentType] 296 | */ 297 | slice(start, end, contentType) { 298 | return this.blob.slice(start, end, contentType); 299 | } 300 | stream() { 301 | return this.blob.stream(); 302 | } 303 | text() { 304 | return this.blob.text(); 305 | } 306 | arrayBuffer() { 307 | return this.blob.arrayBuffer(); 308 | } 309 | get [Symbol.toStringTag]() { 310 | return "File"; 311 | } 312 | }; 313 | 314 | /** 315 | * @param {*} error 316 | * @returns {never} 317 | */ 318 | const panic = (error) => { 319 | throw error; 320 | }; 321 | 322 | /** 323 | * 324 | * @param {Element} element 325 | * @returns {element is HTMLSelectElement} 326 | */ 327 | function isSelectElement(element) { 328 | return element.tagName === "SELECT"; 329 | } 330 | 331 | /** 332 | * 333 | * @param {Element} element 334 | * @returns {element is HTMLInputElement} 335 | */ 336 | function isInputElement(element) { 337 | return element.tagName === "INPUT" || element.tagName === "TEXTAREA"; 338 | } 339 | -------------------------------------------------------------------------------- /packages/form-data/src/lib.js: -------------------------------------------------------------------------------- 1 | export const { FormData } = globalThis 2 | -------------------------------------------------------------------------------- /packages/form-data/src/lib.node.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | "use strict" 3 | 4 | import * as polyfill from "./form-data.js" 5 | 6 | export const FormData = polyfill.FormData 7 | -------------------------------------------------------------------------------- /packages/form-data/test/all.spec.js: -------------------------------------------------------------------------------- 1 | import { test as libTest } from "./form-data.spec.js" 2 | import { test as fetchTest } from "./fetch.spec.js" 3 | import { test } from "./test.js" 4 | 5 | libTest(test) 6 | fetchTest(test) 7 | test.run() 8 | -------------------------------------------------------------------------------- /packages/form-data/test/fetch.spec.js: -------------------------------------------------------------------------------- 1 | import { FormData } from "@remix-run/web-form-data"; 2 | import { Blob } from "@remix-run/web-file"; 3 | import { Response } from "@remix-run/web-fetch"; 4 | import { assert } from "./test.js"; 5 | 6 | /** 7 | * @param {import('./test').Test} test 8 | */ 9 | export const test = test => { 10 | test("node-fetch recognizes form-data", async () => { 11 | const data = new FormData(); 12 | data.set("file", new Blob(["hello"])); 13 | // @ts-ignore 14 | const response = new Response(data); 15 | 16 | assert.equal(response.headers.has("content-type"), true); 17 | const type = response.headers.get("content-type") || ""; 18 | assert.equal( 19 | /multipart\/form-data;\s*boundary=/.test(type), 20 | true, 21 | "multipart/form-data content type" 22 | ); 23 | 24 | const text = await response.text(); 25 | assert.equal(text.includes("hello"), true); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/form-data/test/form-data.spec.js: -------------------------------------------------------------------------------- 1 | import { FormData } from "@remix-run/web-form-data"; 2 | import * as lib from "@remix-run/web-form-data"; 3 | import { File, Blob } from "@remix-run/web-file"; 4 | import { assert } from "./test.js"; 5 | 6 | /** 7 | * @param {import('./test').Test} test 8 | */ 9 | export const test = (test) => { 10 | test("test basic", async () => { 11 | assert.equal(typeof FormData, "function"); 12 | assert.isEqual(typeof lib.FormData, "function"); 13 | }); 14 | 15 | if (globalThis.window === globalThis) { 16 | test("exports built-ins", () => { 17 | assert.equal(lib.FormData, globalThis.FormData); 18 | }); 19 | } 20 | 21 | // @see https://github.com/jsdom/jsdom/blob/5279cfda5fe4d52f04b2eb6a801c98d81f9b55da/test/web-platform-tests/to-upstream/XMLHttpRequest/formdata-set-blob.HTMLFormElement 22 | test("blob without type", () => { 23 | const formData = new FormData(); 24 | formData.set("blob-1", new Blob()); 25 | const blob1 = /** @type {File} */ (formData.get("blob-1")); 26 | assert.equal(blob1.constructor.name, "File"); 27 | assert.equal(blob1.name, "blob"); 28 | assert.equal(blob1.type, ""); 29 | assert.isLessThan( 30 | Math.abs(blob1.lastModified - Date.now()), 31 | 200, 32 | "lastModified should be now" 33 | ); 34 | }); 35 | 36 | test("blob with type", () => { 37 | const formData = new FormData(); 38 | formData.set("blob-2", new Blob([], { type: "text/plain" })); 39 | const blob2 = /** @type {File} */ (formData.get("blob-2")); 40 | assert.equal(blob2.constructor.name, "File"); 41 | assert.equal(blob2.name, "blob"); 42 | assert.equal(blob2.type, "text/plain"); 43 | assert.isLessThan( 44 | Math.abs(blob2.lastModified - Date.now()), 45 | 200, 46 | "lastModified should be now" 47 | ); 48 | }); 49 | 50 | test("blob with custom name", () => { 51 | const formData = new FormData(); 52 | formData.set("blob-3", new Blob(), "custom name"); 53 | const blob3 = /** @type {File} */ (formData.get("blob-3")); 54 | assert.equal(blob3.constructor.name, "File"); 55 | assert.equal(blob3.name, "custom name"); 56 | assert.equal(blob3.type, ""); 57 | assert.isLessThan( 58 | Math.abs(blob3.lastModified - Date.now()), 59 | 200, 60 | "lastModified should be now" 61 | ); 62 | }); 63 | 64 | test("file without lastModified or custom name", () => { 65 | const formData = new FormData(); 66 | formData.set("file-1", new File([], "name")); 67 | const file1 = /** @type {File} */ (formData.get("file-1")); 68 | assert.equal(file1.constructor.name, "File"); 69 | assert.equal(file1.name, "name"); 70 | assert.equal(file1.type, ""); 71 | assert.isLessThan( 72 | Math.abs(file1.lastModified - Date.now()), 73 | 200, 74 | "lastModified should be now" 75 | ); 76 | }); 77 | 78 | test("file with lastModified and custom name", () => { 79 | const formData = new FormData(); 80 | formData.set( 81 | "file-2", 82 | new File([], "name", { lastModified: 123 }), 83 | "custom name" 84 | ); 85 | const file2 = /** @type {File} */ (formData.get("file-2")); 86 | assert.equal(file2.constructor.name, "File"); 87 | assert.equal(file2.name, "custom name"); 88 | assert.equal(file2.type, ""); 89 | assert.equal(file2.lastModified, 123, "lastModified should be 123"); 90 | }); 91 | 92 | // This mimics the payload sent by a browser when a file input 93 | // exists but is not filled out. 94 | test("filename on string contents", () => { 95 | const formData = new FormData(); 96 | formData.set("file-3", new Blob([]), ""); 97 | const file3 = /** @type {File} */ (formData.get("file-3")); 98 | assert.equal(file3.constructor.name, "File"); 99 | assert.equal(file3.name, ""); 100 | assert.equal(file3.type, ""); 101 | }); 102 | 103 | test("throws on few args", () => { 104 | const data = new FormData(); 105 | // @ts-expect-error 106 | assert.throws(() => data.append("key")); 107 | // @ts-expect-error 108 | assert.throws(() => data.set("key")); 109 | // @ts-expect-error 110 | assert.throws(() => data.get()); 111 | // @ts-expect-error 112 | assert.throws(() => data.getAll()); 113 | // @ts-expect-error 114 | assert.throws(() => data.delete()); 115 | }); 116 | 117 | test("only value", () => { 118 | const data = new FormData(); 119 | data.set("key", "value1"); 120 | assert.equal(data.get("key"), "value1"); 121 | }); 122 | 123 | test("second value", () => { 124 | const data = new FormData(); 125 | data.set("key", "value1"); 126 | data.append("key", "value2"); 127 | assert.equal(data.get("key"), "value1"); 128 | }); 129 | 130 | test("null value", () => { 131 | const data = new FormData(); 132 | // @ts-expect-error 133 | data.set("key", null); 134 | assert.equal(data.get("key"), "null"); 135 | }); 136 | 137 | test("has", () => { 138 | var data = new FormData(); 139 | data.append("n1", "value"); 140 | assert.equal(data.has("n1"), true); 141 | assert.equal(data.has("n2"), false); 142 | data.append("n2", "value"); 143 | assert.equal(data.has("n1"), true); 144 | assert.equal(data.has("n2"), true); 145 | data.append("n3", new Blob(["content"])); 146 | assert.equal(data.has("n3"), true); 147 | }); 148 | 149 | test("should return the keys/values/entres as they are appended", () => { 150 | const data = new FormData(); 151 | data.append("keyA", "val1"); 152 | data.append("keyA", "val2"); 153 | data.append("keyB", "val3"); 154 | data.append("keyA", "val4"); 155 | 156 | assert.deepEqual([...data.keys()], ["keyA", "keyA", "keyB", "keyA"]); 157 | assert.deepEqual([...data.values()], ["val1", "val2", "val3", "val4"]); 158 | assert.deepEqual( 159 | [...data], 160 | [ 161 | ["keyA", "val1"], 162 | ["keyA", "val2"], 163 | ["keyB", "val3"], 164 | ["keyA", "val4"], 165 | ] 166 | ); 167 | }); 168 | 169 | test("overwrite first matching key", () => { 170 | const data = new FormData(); 171 | data.append("keyA", "val1"); 172 | data.append("keyA", "val2"); 173 | data.append("keyB", "val3"); 174 | data.append("keyA", "val4"); 175 | 176 | data.set("keyA", "val3"); 177 | assert.deepEqual( 178 | [...data], 179 | [ 180 | ["keyA", "val3"], 181 | ["keyB", "val3"], 182 | ] 183 | ); 184 | }); 185 | 186 | test("appends value when no matching", () => { 187 | const data = new FormData(); 188 | data.append("keyB", "val3"); 189 | data.set("keyA", "val3"); 190 | 191 | assert.deepEqual( 192 | [...data], 193 | [ 194 | ["keyB", "val3"], 195 | ["keyA", "val3"], 196 | ] 197 | ); 198 | }); 199 | 200 | test("FormData.delete()", () => { 201 | var data = new FormData(); 202 | data.append("name", "value"); 203 | assert.equal(data.has("name"), true); 204 | data.delete("name"); 205 | assert.equal(data.has("name"), false); 206 | 207 | data.append("name", new Blob(["content"])); 208 | assert.equal(data.has("name"), true); 209 | data.delete("name"); 210 | assert.equal(data.has("name"), false); 211 | 212 | data.append("n1", "v1"); 213 | data.append("n2", "v2"); 214 | data.append("n1", "v3"); 215 | data.delete("n1"); 216 | assert.equal(data.has("n1"), false); 217 | 218 | assert.deepEqual([...data], [["n2", "v2"]]); 219 | }); 220 | 221 | test("Should return correct filename with File", () => { 222 | const data = new FormData(); 223 | data.set("key", new File([], "doc.txt")); 224 | const file = /** @type {File} */ (data.get("key")); 225 | assert.equal("doc.txt", file.name); 226 | }); 227 | 228 | test("Should return correct filename with Blob filename", () => { 229 | const data = new FormData(); 230 | data.append("key", new Blob(), "doc.txt"); 231 | const file = /** @type {File} */ (data.get("key")); 232 | assert.equal("doc.txt", file.name); 233 | }); 234 | 235 | test("Should return correct filename with just Blob", () => { 236 | const data = new FormData(); 237 | data.append("key", new Blob()); 238 | const file = /** @type {File} */ (data.get("key")); 239 | assert.equal("blob", file.name); 240 | }); 241 | 242 | test.skip("complicated form", () => { 243 | const data = new FormData(); 244 | data.append("blobs", new Blob(["basic"])); 245 | data.append("blobs", new Blob(["with-type"], { type: "text/plain" })); 246 | data.append( 247 | "blobs", 248 | new Blob(["with-name"], { type: "text/markdown" }), 249 | "file.md" 250 | ); 251 | data.append("files", new File(["basic"], "basic")); 252 | data.append( 253 | "files", 254 | new File(["with-type"], "file.txt", { type: "text/plain" }) 255 | ); 256 | data.append( 257 | "files", 258 | new File(["renamed"], "orig.txt", { type: "text/plain" }), 259 | "rename.md" 260 | ); 261 | }); 262 | 263 | test("Should allow passing a form element", () => { 264 | /** @type {globalThis.HTMLFormElement} */ 265 | let form; 266 | 267 | if (typeof window === 'undefined') { 268 | /** @implements {globalThis.HTMLFormElement} */ 269 | class FakeForm { 270 | get [Symbol.toStringTag]() { 271 | return "HTMLFormElement"; 272 | } 273 | 274 | toString() { 275 | return `
`; 276 | } 277 | 278 | // @ts-ignore 279 | get elements() { 280 | return [ 281 | { 282 | tagName: "INPUT", 283 | name: "inside", 284 | value: "", 285 | }, 286 | { 287 | tagName: "INPUT", 288 | name: "outside", 289 | value: "", 290 | form: "my-form", 291 | }, 292 | { 293 | tagName: "INPUT", 294 | name: "remember-me", 295 | value: "on", 296 | checked: true, 297 | } 298 | ] 299 | } 300 | 301 | get id() { 302 | return "my-form" 303 | } 304 | } 305 | 306 | form = /** @type {globalThis.HTMLFormElement} */ (/** @type {unknown} */ (new FakeForm())) 307 | } else { 308 | form = document.createElement('form'); 309 | let inside = document.createElement('input') 310 | let outside = document.createElement('input') 311 | let checkbox = document.createElement('input') 312 | 313 | form.id = 'my-form' 314 | inside.name = 'inside' 315 | outside.name = 'outside' 316 | outside.setAttribute('form', 'my-form') 317 | checkbox.name = "remember-me" 318 | checkbox.type = 'checkbox' 319 | checkbox.checked = true; 320 | 321 | form.appendChild(inside); 322 | form.appendChild(checkbox); 323 | document.body.appendChild(form); 324 | document.body.appendChild(outside); 325 | } 326 | 327 | const formData = new FormData(form); 328 | assert.equal(formData.has("inside"), true) 329 | assert.equal(formData.has("outside"), true) 330 | assert.equal(formData.get("remember-me"), "on") 331 | }) 332 | }; 333 | -------------------------------------------------------------------------------- /packages/form-data/test/test.js: -------------------------------------------------------------------------------- 1 | import * as uvu from "uvu" 2 | import * as uvuassert from "uvu/assert" 3 | 4 | const deepEqual = uvuassert.equal 5 | const isEqual = uvuassert.equal 6 | const isEquivalent = uvuassert.equal 7 | 8 | /** 9 | * @param {number} value 10 | * @param {number} number 11 | * @param {string} [description] 12 | */ 13 | const isLessThan = (value, number, description) => 14 | uvuassert.ok(value < number, description) 15 | export const assert = { 16 | ...uvuassert, 17 | deepEqual, 18 | isEqual, 19 | isEquivalent, 20 | isLessThan, 21 | } 22 | export const test = uvu.test 23 | 24 | /** 25 | * @typedef {uvu.Test} Test 26 | */ 27 | -------------------------------------------------------------------------------- /packages/form-data/test/web.spec.js: -------------------------------------------------------------------------------- 1 | import { test as formTest } from "./form-data.spec.js" 2 | import { test } from "./test.js" 3 | 4 | formTest(test) 5 | 6 | test.run() 7 | -------------------------------------------------------------------------------- /packages/form-data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "paths": { 6 | "@remix-run/web-form-data": ["packages/form-data/src/lib.js"] 7 | } 8 | }, 9 | "references": [ 10 | { 11 | "path": "../file" 12 | }, 13 | { 14 | "path": "../blob" 15 | } 16 | ], 17 | "include": [ 18 | "src", 19 | "test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /packages/stream/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Export CJS version for browser ([807fc63](https://github.com/remix-run/web-std-io/commit/807fc63)) 8 | 9 | ## 1.0.4 10 | 11 | ### Patch Changes 12 | 13 | - 43c6ce2: Move types conditional export to the top of the list to align with [the node guidance](https://nodejs.org/api/packages.html#community-conditions-definitions) 14 | 15 | ## [1.0.2](https://www.github.com/web-std/io/compare/stream-v1.0.1...stream-v1.0.2) (2022-04-13) 16 | 17 | ### Bug Fixes 18 | 19 | - **packages/fetch:** only export what's needed so TS doesn't mess up the imports in the output files ([30ad037](https://www.github.com/web-std/io/commit/30ad0377a88ebffc3a998616e3b774ce5bcc584a)) 20 | - **packages/stream:** no initializers in ambient contexts ([30ad037](https://www.github.com/web-std/io/commit/30ad0377a88ebffc3a998616e3b774ce5bcc584a)) 21 | - typescript types ([#56](https://www.github.com/web-std/io/issues/56)) ([30ad037](https://www.github.com/web-std/io/commit/30ad0377a88ebffc3a998616e3b774ce5bcc584a)) 22 | 23 | ## [1.0.1](https://www.github.com/web-std/io/compare/stream-v1.0.0...stream-v1.0.1) (2022-01-19) 24 | 25 | ### Bug Fixes 26 | 27 | - ship less files to address TSC issues ([#35](https://www.github.com/web-std/io/issues/35)) ([0651e62](https://www.github.com/web-std/io/commit/0651e62ae42d17eae2db89858c9e44f3342c304c)) 28 | 29 | ## 1.0.0 (2021-11-05) 30 | 31 | ### Features 32 | 33 | - Factor out streams into separate package ([#19](https://www.github.com/web-std/io/issues/19)) ([90624cf](https://www.github.com/web-std/io/commit/90624cfd2d4253c2cbc316d092f26e77b5169f47)) 34 | -------------------------------------------------------------------------------- /packages/stream/Readme.md: -------------------------------------------------------------------------------- 1 | # @remix-run/web-stream 2 | 3 | [![ci][ci.icon]][ci.url] 4 | [![package][version.icon] ![downloads][downloads.icon]][package.url] 5 | [![styled with prettier][prettier.icon]][prettier.url] 6 | 7 | Web streams APIs across web & node. In browsers this library just exports stream constructors, in node it exports [native web stream implementations][node webstreams] when available and [web-streams-polyfill][] 8 | 9 | > ⚠️ Please note that library makes no attempt to polyfill `WritableStream` or `TransforStream` in web browsers that do not have them. 10 | 11 | ### Usage 12 | 13 | ```js 14 | import { 15 | ReadableStream, 16 | WritableStream, 17 | TransformStream, 18 | } from "@remix-run/web-stream" 19 | ``` 20 | 21 | ### Usage from Typescript 22 | 23 | This library makes use of [typescript using JSDOC annotations][ts-jsdoc] and 24 | also generates type definitions along with typed definition maps. So you should 25 | be able to get all the type inference out of the box. 26 | 27 | ## Install 28 | 29 | npm install @remix-run/web-stream 30 | 31 | [ci.icon]: https://github.com/web-std/io/workflows/stream/badge.svg 32 | [ci.url]: https://github.com/web-std/io/actions/workflows/stream.yml 33 | [version.icon]: https://img.shields.io/npm/v/@remix-run/web-stream.svg 34 | [downloads.icon]: https://img.shields.io/npm/dm/@remix-run/web-stream.svg 35 | [package.url]: https://npmjs.org/package/@remix-run/web-stream 36 | [downloads.image]: https://img.shields.io/npm/dm/@remix-run/web-stream.svg 37 | [downloads.url]: https://npmjs.org/package/@remix-run/web-stream 38 | [prettier.icon]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg 39 | [prettier.url]: https://github.com/prettier/prettier 40 | [ts-jsdoc]: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html 41 | [node-webstreams]: https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html 42 | [web-streams-polyfill]: https://www.npmjs.com/package/web-streams-polyfill 43 | -------------------------------------------------------------------------------- /packages/stream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remix-run/web-stream", 3 | "version": "1.1.0", 4 | "description": "Web API compatible streams for node/web", 5 | "files": [ 6 | "src", 7 | "dist/src", 8 | "License.md", 9 | "Readme.md" 10 | ], 11 | "keywords": [ 12 | "web-stream", 13 | "whatwg-stream", 14 | "stream", 15 | "typescript" 16 | ], 17 | "type": "module", 18 | "module": "./src/lib.js", 19 | "main": "./src/stream.cjs", 20 | "types": "./src/lib.d.ts", 21 | "browser": { 22 | "./src/lib.node.js": "./src/lib.js" 23 | }, 24 | "exports": { 25 | ".": { 26 | "types": "./src/lib.d.ts", 27 | "browser": { 28 | "require": "./src/stream.cjs", 29 | "import": "./src/lib.js" 30 | }, 31 | "require": "./src/stream.cjs", 32 | "import": "./src/lib.node.js" 33 | } 34 | }, 35 | "author": "Irakli Gozalishvili (https://gozala.io)", 36 | "repository": "https://github.com/remix-run/web-std-io", 37 | "license": "MIT", 38 | "dependencies": { 39 | "web-streams-polyfill": "^3.1.1" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "15.0.2", 43 | "git-validate": "2.2.4", 44 | "husky": "^6.0.0", 45 | "lint-staged": "^11.0.0", 46 | "playwright-test": "^7.2.0", 47 | "prettier": "^2.3.0", 48 | "rimraf": "3.0.2", 49 | "typescript": "^4.4.4", 50 | "uvu": "0.5.2" 51 | }, 52 | "scripts": { 53 | "typecheck": "tsc", 54 | "test:es": "uvu test all.spec.js", 55 | "test:web": "playwright-test -r uvu test/web.spec.js", 56 | "test:cjs": "node test/node.spec.cjs", 57 | "test": "npm run test:es && npm run test:web && npm run test:cjs", 58 | "precommit": "lint-staged" 59 | }, 60 | "lint-staged": { 61 | "*.js": [ 62 | "prettier --no-semi --write", 63 | "git add" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/stream/src/lib.d.ts: -------------------------------------------------------------------------------- 1 | declare var ReadableStreamExport: typeof ReadableStream; 2 | declare var ReadableStreamDefaultReaderExport: typeof ReadableStreamDefaultReader; 3 | // @ts-ignore 4 | declare var ReadableStreamBYOBReaderExport: typeof ReadableStreamBYOBReader; 5 | // @ts-ignore 6 | declare var ReadableStreamBYOBRequestExport: typeof ReadableStreamBYOBRequest; 7 | // @ts-ignore 8 | declare var ReadableByteStreamControllerExport: typeof ReadableByteStreamController; 9 | declare var ReadableStreamDefaultControllerExport: typeof ReadableStreamDefaultController; 10 | declare var TransformStreamExport: typeof TransformStream; 11 | declare var TransformStreamDefaultControllerExport: typeof TransformStreamDefaultController; 12 | declare var WritableStreamExport: typeof WritableStream; 13 | declare var WritableStreamDefaultWriterExport: typeof WritableStreamDefaultWriter; 14 | declare var WritableStreamDefaultControllerExport: typeof WritableStreamDefaultController; 15 | declare var ByteLengthQueuingStrategyExport: typeof ByteLengthQueuingStrategy; 16 | declare var CountQueuingStrategyExport: typeof CountQueuingStrategy; 17 | declare var TextEncoderStreamExport: typeof TextEncoderStream; 18 | declare var TextDecoderStreamExport: typeof TextDecoderStream; 19 | 20 | export { 21 | ReadableStreamExport as ReadableStream, 22 | ReadableStreamDefaultReaderExport as ReadableStreamDefaultReader, 23 | ReadableStreamBYOBReaderExport as ReadableStreamBYOBReader, 24 | ReadableStreamBYOBRequestExport as ReadableStreamBYOBRequest, 25 | ReadableByteStreamControllerExport as ReadableByteStreamController, 26 | ReadableStreamDefaultControllerExport as ReadableStreamDefaultController, 27 | TransformStreamExport as TransformStream, 28 | TransformStreamDefaultControllerExport as TransformStreamDefaultController, 29 | WritableStreamExport as WritableStream, 30 | WritableStreamDefaultWriterExport as WritableStreamDefaultWriter, 31 | WritableStreamDefaultControllerExport as WritableStreamDefaultController, 32 | ByteLengthQueuingStrategyExport as ByteLengthQueuingStrategy, 33 | CountQueuingStrategyExport as CountQueuingStrategy, 34 | TextEncoderStreamExport as TextEncoderStream, 35 | TextDecoderStreamExport as TextDecoderStream, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/stream/src/lib.js: -------------------------------------------------------------------------------- 1 | export const { 2 | ReadableStream, 3 | ReadableStreamDefaultReader, 4 | // @ts-ignore 5 | ReadableStreamBYOBReader, 6 | // @ts-ignore 7 | ReadableStreamBYOBRequest, 8 | // @ts-ignore 9 | ReadableByteStreamController, 10 | ReadableStreamDefaultController, 11 | TransformStream, 12 | TransformStreamDefaultController, 13 | WritableStream, 14 | WritableStreamDefaultWriter, 15 | WritableStreamDefaultController, 16 | ByteLengthQueuingStrategy, 17 | CountQueuingStrategy, 18 | TextEncoderStream, 19 | TextDecoderStream, 20 | } = globalThis 21 | -------------------------------------------------------------------------------- /packages/stream/src/lib.node.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import streams from "./stream.cjs" 3 | export const { 4 | ReadableStream, 5 | ReadableStreamDefaultReader, 6 | ReadableStreamBYOBReader, 7 | ReadableStreamBYOBRequest, 8 | ReadableByteStreamController, 9 | ReadableStreamDefaultController, 10 | TransformStream, 11 | TransformStreamDefaultController, 12 | WritableStream, 13 | WritableStreamDefaultWriter, 14 | WritableStreamDefaultController, 15 | ByteLengthQueuingStrategy, 16 | CountQueuingStrategy, 17 | TextEncoderStream, 18 | TextDecoderStream, 19 | } = streams 20 | -------------------------------------------------------------------------------- /packages/stream/src/stream.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("web-streams-polyfill/ponyfill"); 2 | -------------------------------------------------------------------------------- /packages/stream/test/all.spec.js: -------------------------------------------------------------------------------- 1 | import { test as libTest } from "./lib.spec.js" 2 | 3 | import { test } from "./test.js" 4 | 5 | libTest(test) 6 | test.run() 7 | -------------------------------------------------------------------------------- /packages/stream/test/lib.spec.cjs: -------------------------------------------------------------------------------- 1 | const lib = require("@remix-run/web-stream"); 2 | 3 | /** 4 | * @param {import('./test').Test} test 5 | */ 6 | exports.test = async ({ test, assert }) => { 7 | console.log("define tests"); 8 | test("test baisc", async () => { 9 | console.log("test basic"); 10 | 11 | assert.isEqual(typeof lib.ReadableStream, "function"); 12 | }); 13 | 14 | if (globalThis.window === globalThis) { 15 | test("uses built-ins", async () => { 16 | assert.isEqual(lib.ReadableStream, globalThis.ReadableStream); 17 | assert.isEqual(lib.WritableStream, globalThis.WritableStream); 18 | assert.isEqual(lib.TransformStream, globalThis.TransformStream); 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/stream/test/lib.spec.js: -------------------------------------------------------------------------------- 1 | import * as lib from "@remix-run/web-stream"; 2 | import { assert } from "./test.js"; 3 | 4 | /** 5 | * @param {import('./test').Test} test 6 | */ 7 | export const test = test => { 8 | test("test baisc", async () => { 9 | assert.isEqual(typeof lib.ReadableStream, "function"); 10 | }); 11 | 12 | if (globalThis.window === globalThis) { 13 | test("uses built-ins", async () => { 14 | assert.isEqual(lib.ReadableStream, globalThis.ReadableStream); 15 | assert.isEqual(lib.WritableStream, globalThis.WritableStream); 16 | assert.isEqual(lib.TransformStream, globalThis.TransformStream); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/stream/test/node.spec.cjs: -------------------------------------------------------------------------------- 1 | const { test: libTest } = require("./lib.spec.cjs") 2 | 3 | /** 4 | * @typedef {uvu.Test} Test 5 | */ 6 | 7 | const test = async () => { 8 | const { test } = await import("./test.js") 9 | await libTest(test) 10 | test.run() 11 | } 12 | 13 | test() 14 | -------------------------------------------------------------------------------- /packages/stream/test/test.js: -------------------------------------------------------------------------------- 1 | import * as uvu from "uvu" 2 | import * as uvuassert from "uvu/assert" 3 | 4 | const deepEqual = uvuassert.equal 5 | const isEqual = uvuassert.equal 6 | const isEquivalent = uvuassert.equal 7 | export const assert = { ...uvuassert, deepEqual, isEqual, isEquivalent } 8 | export const test = Object.assign(uvu.test, { 9 | test: uvu.test, 10 | assert, 11 | }) 12 | 13 | /** 14 | * @typedef {test} Test 15 | */ 16 | -------------------------------------------------------------------------------- /packages/stream/test/web.spec.js: -------------------------------------------------------------------------------- 1 | import { test as libTest } from "./lib.spec.js" 2 | import { test } from "./test.js" 3 | 4 | libTest(test) 5 | test.run() 6 | -------------------------------------------------------------------------------- /packages/stream/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": false, 5 | "noEmit": true, 6 | "outDir": "dist" 7 | }, 8 | "include": [ 9 | "src", 10 | "test", 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | "incremental": true, /* Enable incremental compilation */ 6 | "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | 12 | /* Language and Environment */ 13 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 14 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 15 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 16 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 17 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 18 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 19 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 20 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 21 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 22 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 23 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 24 | 25 | /* Modules */ 26 | "module": "ESNext", /* Specify what module code is generated. */ 27 | // "rootDir": "./", /* Specify the root folder within your source files. */ 28 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 29 | "baseUrl": "./", 30 | "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 31 | 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | "stripInternal": true, 49 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 50 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 51 | // "outDir": "dist", /* Specify an output folder for all emitted files. */ 52 | // "removeComments": true, /* Disable emitting comments. */ 53 | // "noEmit": true, /* Disable emitting files from a compilation. */ 54 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 55 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 56 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 57 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 60 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 61 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 62 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 63 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 64 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 65 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 66 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 67 | // "declarationDir": "./@types", /* Specify the output directory for generated declaration files. */ 68 | /* Interop Constraints */ 69 | "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 74 | /* Type Checking */ 75 | "strict": true, /* Enable all strict type-checking options. */ 76 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 77 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 78 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 79 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 80 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 81 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 82 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 83 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 84 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 85 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 86 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 87 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 88 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 89 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 90 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 91 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 92 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 93 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 94 | /* Completeness */ 95 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 96 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 97 | }, 98 | "exclude": [ 99 | "dist", 100 | "node_modules" 101 | ] 102 | } 103 | --------------------------------------------------------------------------------