Compare commits

...
44 Commits
Author SHA1 Message Date
Jimmy Wärting dc29588366 fix: misc updates (#132)
* misc updates

* bring back c8 for test coverage

* use new loader

* don't use engines

* simplify ci

* fix double slice problem
2022-01-20 14:00:15 +01:00
Jimmy Wärting 8ab587d340 removed xo 2021-10-30 13:52:13 +02:00
Jimmy Wärting f61ee01955 bumped minor patch 2021-10-30 13:50:21 +02:00
Jimmy Wärting 5dbcbb91b4 standard.js style 2021-10-30 13:50:21 +02:00
Jimmy Wärting c5c2b52154 fix(warning): Supress warnings when importing experimental web stream from NodeJS (#125)
* Suppress warnings when importing web stream
* added node: prefix
* updated changelog
* apply `standard streams.cjs --fix`
2021-10-30 13:04:02 +02:00
Ben McCann 836709fc65 fix: prefix stream/web import with node: (#122) 2021-10-24 17:27:58 +02:00
Nick K 2c11647222 fix(blob, file, tests): Minor improvements for Blob and File implementations (#120)
* Use private class fields in parts initialization directly.

* Move encoder initialization outside the loop

* Improve errors reported by Blob constructor.

* Simulate WebIDL type casting for lastModified in File constructor.

* Add cancel method for Blob.stream() underlying source.

* Fix indentation inconsistency.

* Use getReader in blob stream cancellation test instead of Symbol.asyncIterator
2021-10-24 14:56:15 +02:00
Jimmy Wärting 9022f8fc70 Update index.js 2021-09-29 17:14:07 +02:00
Jimmy Wärting 0e07457246 Added v12 to test 2021-09-07 17:16:24 +02:00
Jimmy Wärting 46476faf49 Update CHANGELOG.md 2021-07-31 11:30:20 +02:00
Jimmy Wärting b86b1409de found/fixed some errors while testing agains WPT (#109)
* found/fixed some errors while testing agains WPT
* only run test.js with ava
2021-07-31 11:24:15 +02:00
Jimmy Wärting a08f394f1b Create PULL_REQUEST_TEMPLATE.md 2021-07-31 11:08:57 +02:00
Nick K c2903ca530 Allow usage of iterable object in Blob constructor. (#108)
* Allow usage of iterable object in Blob constructor.

* Fix ESLint errors
2021-07-29 15:42:42 +02:00
Jimmy Wärting 0b028432d0 bumped patch 2021-07-16 11:59:39 +02:00
Jimmy Wärting f64c20694f added streams.cjs 2021-07-16 11:58:08 +02:00
Jimmy Wärting 613160ba58 bumped patch 2021-07-16 11:57:08 +02:00
Jimmy Wärting efdad69698 bumped patch 2021-07-16 11:55:56 +02:00
Jimmy Wärting e00f849f6d improve typing (#105)
* improve typing

* bug fix

* don't test cjs
2021-07-16 11:51:34 +02:00
Jimmy Wärting 52f296c283 Conditional loading (#104)
use good old cjs to conditionally load stuff in node only
2021-07-16 00:33:00 +02:00
Jimmy Wärting 701103ffd2 Update CHANGELOG.md 2021-07-15 22:55:57 +02:00
Jimmy Wärting ada23da6ee Update package.json 2021-07-15 22:50:37 +02:00
Jimmy Wärting 16f312c543 use whatwg streams (#103)
* use whatwg streams
* could not use optional changing in v12
* could not use fs/promise in v12
2021-07-15 22:49:58 +02:00
Jimmy Wärting 5ba554efd2 Lowered targeted node version to those who support ESM
closes #98
2021-06-19 11:51:54 +02:00
Jimmy Wärting 4747497b52 fixed lastModified 2021-06-10 10:51:16 +02:00
Jimmy Wärting 09fd40448a Update file.js 2021-06-10 10:33:00 +02:00
Jimmy Wärting 8d14e88040 Created a File Class (#97)
* Created a File Class
* Test the untested parts
* ignore any d.ts file
2021-05-29 11:17:26 +02:00
dependabot-preview[bot]anddependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> 8ef89adad4 Upgrade to GitHub-native Dependabot (#89)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-05-14 11:01:14 +02:00
Jimmy Wärting a3b0d62b9d corrected the release name 2021-05-07 11:45:20 +02:00
Jimmy Wärting 97a084b5e8 Revamp (#83)
* See CHANGELOG for v3.0.0

* removed get-stream

* enabled linter just for test.js & from.js

* Any blob part is acceptable (jsdoc)
since the default fallback is to cast unknown items into strings

* async stat version

* import from fs/promise instead

* Updated the Readme and code examples

* known differences, buffer.Blob support, private stream, more test

* require node 14

* use fetch v3
2021-05-07 11:42:01 +02:00
Jimmy Wärting a8fed4e338 Return empty a empty stream for zero sized blob (#86)
Return empty a empty stream for zero sized blob
2021-04-29 14:01:01 +02:00
Nick K 483973cf25 Fix for the 'start' range of BlobDataItem when reading its stream (#85)
Set default value for 'start' option
2021-04-26 12:06:27 +02:00
Jimmy Wärting cc929f3714 2.1.1 was already publish on npm 9 month ago 2021-04-20 17:52:55 +02:00
Jimmy Wärting 5584886f0f no message 2021-04-20 17:46:23 +02:00
Jimmy Wärting 0d2cd8b9b7 Updated dependencies
Gosh i hate linters. Want to remove them...
2021-04-20 16:32:18 +02:00
Jimmy Wärting 149030ddbe use extension 2021-04-20 16:15:04 +02:00
Nick K f08e6a6773 Add generated typings for from.js file. (#80)
* Generate typings for from.js
* Add generated typings to files section and fix linter issue.
* Remove domexception installation from ci workflow
* Fix for .d.ts output
Co-authored-by: Jimmy Wärting <jimmy@warting.se>
2021-04-20 16:10:46 +02:00
Nick K eed6a1dddd Add nullish values checking in Symbol.hasInstance (#82)
* Add nullish values checking in Symbol.hasInstance
* Fix for a test description
* Add an extension.
* Disable unicorn/import-index rule.
* Fix a typo
2021-04-20 16:07:38 +02:00
Jimmy Wärting 3b12a39ea2 cast type to string 2021-02-13 15:50:02 +01:00
Jimmy Wärting 591a28ed94 Update from.js 2021-02-13 15:01:14 +01:00
Richie Bendall 40f4568c95 Revert 689bee122b 2021-01-29 02:46:12 +13:00
Richie Bendall a74fb90d95 Update ci.yml 2021-01-28 13:57:22 +13:00
Richie Bendall 689bee122b Use native Blob when possible 2021-01-28 13:55:33 +13:00
Richie Bendall 50b89fcdce Make domexception an optional peer dependency 2021-01-18 22:23:51 +13:00
Jimmy Wärting 40c6ff45df Update from.js 2021-01-04 01:00:19 +01:00
16 changed files with 1041 additions and 457 deletions
+24
View File
@@ -0,0 +1,24 @@
<!-- Thanks for contributing! -->
## The purpose of this PR is:
...
## This is what had to change:
...
## This is what like reviewers to know:
...
-------------------------------------------------------------------------------------------------
<!-- Mark what you have done, Remove unnecessary ones. Add new tasks that may fit (like TODO's) -->
- [ ] I prefixed the PR-title with `docs: `, `fix(area): `, `feat(area): ` or `breaking(area): `
- [ ] I updated ./CHANGELOG.md with a link to this PR or Issue
- [ ] I updated the README.md
- [ ] I Added unit test(s)
-------------------------------------------------------------------------------------------------
<!-- Add a `- fix #_NUMBER_` line for every Issue this PR solves. Do not comma separate them -->
- fix #000
+25
View File
@@ -0,0 +1,25 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: get-stream
versions:
- 6.0.0
- 6.0.1
- dependency-name: typescript
versions:
- 4.1.3
- 4.1.4
- 4.1.5
- 4.2.2
- 4.2.3
- 4.2.4
- dependency-name: xo
versions:
- 0.37.1
- 0.38.1
- 0.38.2
+5 -30
View File
@@ -14,43 +14,18 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
node: ["14", "12", engines] node: ["17.3"]
exclude:
# On Windows, run tests with only the LTS environments.
- os: windows-latest
node: engines
- os: windows-latest
node: "14"
# On macOS, run tests with only the LTS environments.
- os: macOS-latest
node: engines
- os: macOS-latest
node: "14"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Get Node.JS version from package.json
if: matrix.node == 'engines'
id: get-version
run: echo ::set-output name=node::$(npx --q minimum-node-version)
- uses: actions/setup-node@v2-beta
if: matrix.node != 'engines'
with: with:
node-version: ${{ matrix.node }} node-version: '17.3'
- uses: actions/setup-node@v2-beta
if: matrix.node == 'engines'
with:
node-version: ${{steps.get-version.outputs.node}}
- run: npm install - run: npm install
- run: npm install domexception - run: npm test
- run: npm run report -- --colors - run: npm run report -- --colors
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
with: with:
-20
View File
@@ -1,20 +0,0 @@
name: CI
on:
pull_request:
paths:
- "**.js"
- "**eslint**"
- "package.json"
- ".github/workflows/lint.yml"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2-beta
with:
node-version: 14
- run: npm install
- run: npm run lint
+7 -3
View File
@@ -16,6 +16,8 @@ pids
*.seed *.seed
*.pid.lock *.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
@@ -47,8 +49,8 @@ typings/
# Optional npm cache directory # Optional npm cache directory
.npm .npm
# Optional eslint cache # Optional cache
.eslintcache .cache
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
@@ -62,4 +64,6 @@ typings/
# dotenv environment variables file # dotenv environment variables file
.env .env
index.d.ts *.d.ts
*.d.cts
.DS_Store
+50 -2
View File
@@ -1,5 +1,49 @@
Changelog # Changelog
=========
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this
project adheres to [Semantic Versioning](http://semver.org/).
## v3.1.3
- Allow usage of iterable object in Blob constructor. [#108]
- Run test WPT test against our impl [#109]
- File name are now casted to string [#109]
- Slicing in the middle of multiple parts added more bytes than what what it should have [#109]
- Prefixed `stream/web` import with `node:` to allow easier static analysis detection of Node built-ins [#122]
- Added `node:` prefix in `from.js` as well [#114]
- Suppress warning when importing `stream/web` [#114]
## v3.1.2
- Improved typing
- Fixed a bug where position in iterator did not increase
## v3.1.0
- started to use real whatwg streams
- degraded fs/promise to fs.promise to support node v12
- degraded optional changing to support node v12
## v3.0.0
- Changed WeakMap for private field (require node 12)
- Switch to ESM
- blob.stream() return a subset of whatwg stream which is the async iterable part
(it no longer return a node stream)
- Reduced the dependency of Buffer by changing to global TextEncoder/Decoder (require node 11)
- Disabled xo since it could understand private fields (#)
- No longer transform the type to lowercase (https://github.com/w3c/FileAPI/issues/43)
This is more loose than strict, keys should be lowercased, but values should not.
It would require a more proper mime type parser - so we just made it loose.
- index.js and file.js can now be imported by browser & deno since it no longer depends on any
core node features (but why would you?)
- Implemented a File class
## v2.1.2
- Fixed a bug where `start` in BlobDataItem was undefined (#85)
## v2.1.1
- Add nullish values checking in Symbol.hasInstance (#82)
- Add generated typings for from.js file (#80)
- Updated dev dependencies
## v2.1.0 ## v2.1.0
- Fix: .slice has an implementation bug (#54). - Fix: .slice has an implementation bug (#54).
@@ -48,3 +92,7 @@ Changelog
## v1.0.0 ## v1.0.0
- Major: initial release - Major: initial release
[#108]: https://github.com/node-fetch/fetch-blob/pull/108
[#109]: https://github.com/node-fetch/fetch-blob/pull/109
[#114]: https://github.com/node-fetch/fetch-blob/pull/114
+68 -19
View File
@@ -13,39 +13,87 @@ A Blob implementation in Node.js, originally from [node-fetch](https://github.co
npm install fetch-blob npm install fetch-blob
``` ```
<details>
<summary>Upgrading from 2x to 3x</summary>
Updating from 2 to 3 should be a breeze since there is not many changes to the blob specification.
The major cause of a major release is coding standards.
- internal WeakMaps was replaced with private fields
- internal Buffer.from was replaced with TextEncoder/Decoder
- internal buffers was replaced with Uint8Arrays
- CommonJS was replaced with ESM
- The node stream returned by calling `blob.stream()` was replaced with whatwg streams
- (Read "Differences from other blobs" for more info.)
</details>
<details>
<summary>Differences from other Blobs</summary>
- Unlike NodeJS `buffer.Blob` (Added in: v15.7.0) and browser native Blob this polyfilled version can't be sent via PostMessage
- This blob version is more arbitrary, it can be constructed with blob parts that isn't a instance of itself
it has to look and behave as a blob to be accepted as a blob part.
- The benefit of this is that you can create other types of blobs that don't contain any internal data that has to be read in other ways, such as the `BlobDataItem` created in `from.js` that wraps a file path into a blob-like item and read lazily (nodejs plans to [implement this][fs-blobs] as well)
- The `blob.stream()` is the most noticeable differences. It returns a WHATWG stream now. to keep it as a node stream you would have to do:
```js
import {Readable} from 'stream'
const stream = Readable.from(blob.stream())
```
</details>
## Usage ## Usage
```js ```js
const Blob = require('fetch-blob'); // Ways to import
const fetch = require('node-fetch'); // (PS it's dependency free ESM package so regular http-import from CDN works too)
import Blob from 'fetch-blob'
import File from 'fetch-blob/file.js'
fetch('https://httpbin.org/post', { import {Blob} from 'fetch-blob'
method: 'POST', import {File} from 'fetch-blob/file.js'
body: new Blob(['Hello World'], { type: 'text/plain' })
}) const {Blob} = await import('fetch-blob')
.then(res => res.json());
.then(json => console.log(json));
// Ways to read the blob:
const blob = new Blob(['hello, world'])
await blob.text()
await blob.arrayBuffer()
for await (let chunk of blob.stream()) { ... }
blob.stream().getReader().read()
blob.stream().getReader({mode: 'byob'}).read(view)
``` ```
### Blob part backed up by filesystem ### Blob part backed up by filesystem
To use, install [domexception](https://github.com/jsdom/domexception).
```sh `fetch-blob/from.js` comes packed with tools to convert any filepath into either a Blob or a File
npm install fetch-blob domexception It will not read the content into memory. It will only stat the file for last modified date and file size.
```
```js ```js
const blobFrom = require('fetch-blob/from.js'); // The default export is sync and use fs.stat to retrieve size & last modified as a blob
const blob1 = blobFrom('./2-GiB-file.bin'); import blobFromSync from 'fetch-blob/from.js'
const blob2 = blobFrom('./2-GiB-file.bin'); import {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync} from 'fetch-blob/from.js'
// Not a 4 GiB memory snapshot, just holds 3 references const fsFile = fileFromSync('./2-GiB-file.bin', 'application/octet-stream')
const fsBlob = await blobFrom('./2-GiB-file.mp4')
// Not a 4 GiB memory snapshot, just holds references
// points to where data is located on the disk // points to where data is located on the disk
const blob = new Blob([blob1, blob2]); const blob = new Blob([fsFile, fsBlob, 'memory', new Uint8Array(10)])
console.log(blob.size) // 4 GiB console.log(blob.size) // ~4 GiB
``` ```
See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [tests](https://github.com/node-fetch/fetch-blob/blob/master/test.js) for more details. `blobFrom|blobFromSync|fileFrom|fileFromSync(path, [mimetype])`
### Creating Blobs backed up by other async sources
Our Blob & File class are more generic then any other polyfills in the way that it can accept any blob look-a-like item
An example of this is that our blob implementation can be constructed with parts coming from [BlobDataItem](https://github.com/node-fetch/fetch-blob/blob/8ef89adad40d255a3bbd55cf38b88597c1cd5480/from.js#L32) (aka a filepath) or from [buffer.Blob](https://nodejs.org/api/buffer.html#buffer_new_buffer_blob_sources_options), It dose not have to implement all the methods - just enough that it can be read/understood by our Blob implementation. The minium requirements is that it has `Symbol.toStringTag`, `size`, `slice()` and either a `stream()` or a `arrayBuffer()` method. If you then wrap it in our Blob or File `new Blob([blobDataItem])` then you get all of the other methods that should be implemented in a blob or file
An example of this could be to create a file or blob like item coming from a remote HTTP request. Or from a DataBase
See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [tests](https://github.com/node-fetch/fetch-blob/blob/master/test.js) for more details of how to use the Blob.
[npm-image]: https://flat.badgen.net/npm/v/fetch-blob [npm-image]: https://flat.badgen.net/npm/v/fetch-blob
[npm-url]: https://www.npmjs.com/package/fetch-blob [npm-url]: https://www.npmjs.com/package/fetch-blob
@@ -55,3 +103,4 @@ See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blo
[codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob [codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob [install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
[install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob [install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob
[fs-blobs]: https://github.com/nodejs/node/issues/37340
+49
View File
@@ -0,0 +1,49 @@
import Blob from './index.js'
const _File = class File extends Blob {
#lastModified = 0
#name = ''
/**
* @param {*[]} fileBits
* @param {string} fileName
* @param {{lastModified?: number, type?: string}} options
*/// @ts-ignore
constructor (fileBits, fileName, options = {}) {
if (arguments.length < 2) {
throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`)
}
super(fileBits, options)
if (options === null) options = {}
// Simulate WebIDL type casting for NaN value in lastModified option.
const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified)
if (!Number.isNaN(lastModified)) {
this.#lastModified = lastModified
}
this.#name = String(fileName)
}
get name () {
return this.#name
}
get lastModified () {
return this.#lastModified
}
get [Symbol.toStringTag] () {
return 'File'
}
static [Symbol.hasInstance] (object) {
return !!object && object instanceof Blob &&
/^(File)$/.test(object[Symbol.toStringTag])
}
}
/** @type {typeof globalThis.File} */// @ts-ignore
export const File = _File
export default File
+82 -39
View File
@@ -1,57 +1,100 @@
const {statSync, createReadStream} = require('fs'); import { statSync, createReadStream, promises as fs } from 'node:fs'
const Blob = require('.'); import { basename } from 'node:path'
const DOMException = require('domexception'); import DOMException from 'node-domexception'
import File from './file.js'
import Blob from './index.js'
const { stat } = fs
/** /**
* @param {string} path filepath on the disk * @param {string} path filepath on the disk
* @returns {Blob} * @param {string} [type] mimetype to use
*/ */
function blobFrom(path) { const blobFromSync = (path, type) => fromBlob(statSync(path), path, type)
const {size, mtime} = statSync(path);
const blob = new BlobDataItem({path, size, mtime});
return new Blob([blob]); /**
} * @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
* @returns {Promise<Blob>}
*/
const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type))
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
* @returns {Promise<File>}
*/
const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type))
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
const fileFromSync = (path, type) => fromFile(statSync(path), path, type)
// @ts-ignore
const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
path,
size: stat.size,
lastModified: stat.mtimeMs,
start: 0
})], { type })
// @ts-ignore
const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
path,
size: stat.size,
lastModified: stat.mtimeMs,
start: 0
})], basename(path), { type, lastModified: stat.mtimeMs })
/** /**
* This is a blob backed up by a file on the disk * This is a blob backed up by a file on the disk
* with minium requirement * with minium requirement. Its wrapped around a Blob as a blobPart
* so you have no direct access to this.
* *
* @private * @private
*/ */
class BlobDataItem { class BlobDataItem {
constructor(options) { #path
this.size = options.size; #start
this.path = options.path;
this.start = options.start;
this.mtime = options.mtime;
}
// Slicing arguments is validated and formated constructor (options) {
// by Blob.prototype.slice this.#path = options.path
slice(start, end) { this.#start = options.start
return new BlobDataItem({ this.size = options.size
path: this.path, this.lastModified = options.lastModified
start, }
mtime: this.mtime,
size: end - start
});
}
stream() { /**
if (statSync(this.path).mtime > this.mtime) { * Slicing arguments is first validated and formatted
throw new DOMException('The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.', 'NotReadableError'); * to not be out of range by Blob.prototype.slice
} */
slice (start, end) {
return new BlobDataItem({
path: this.#path,
lastModified: this.lastModified,
size: end - start,
start: this.#start + start
})
}
return createReadStream(this.path, { async * stream () {
start: this.start, const { mtimeMs } = await stat(this.#path)
end: this.start + this.size - 1 if (mtimeMs > this.lastModified) {
}); throw new DOMException('The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.', 'NotReadableError')
} }
yield * createReadStream(this.#path, {
start: this.#start,
end: this.#start + this.size - 1
})
}
get [Symbol.toStringTag]() { get [Symbol.toStringTag] () {
return 'Blob'; return 'Blob'
} }
} }
module.exports = blobFrom; export default blobFromSync
export { File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync }
+224 -152
View File
@@ -1,178 +1,250 @@
const {Readable} = require('stream'); /*! fetch-blob. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
/** // TODO (jimmywarting): in the feature use conditional loading with top level await (requires 14.x)
* @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>} // Node has recently added whatwg stream into core
*/
const wm = new WeakMap();
async function * read(parts) { import './streams.cjs'
for (const part of parts) {
if ('stream' in part) { // 64 KiB (same size chrome slice theirs blob into Uint8array's)
yield * part.stream(); const POOL_SIZE = 65536
} else {
yield part; /** @param {(Blob | Uint8Array)[]} parts */
} async function * toIterator (parts, clone = true) {
} for (const part of parts) {
if ('stream' in part) {
yield * (/** @type {AsyncIterableIterator<Uint8Array>} */ (part.stream()))
} else if (ArrayBuffer.isView(part)) {
if (clone) {
let position = part.byteOffset
const end = part.byteOffset + part.byteLength
while (position !== end) {
const size = Math.min(end - position, POOL_SIZE)
const chunk = part.buffer.slice(position, position + size)
position += chunk.byteLength
yield new Uint8Array(chunk)
}
} else {
yield part
}
/* c8 ignore next 10 */
} else {
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob)
let position = 0, b = (/** @type {Blob} */ (part))
while (position !== b.size) {
const chunk = b.slice(position, Math.min(b.size, position + POOL_SIZE))
const buffer = await chunk.arrayBuffer()
position += buffer.byteLength
yield new Uint8Array(buffer)
}
}
}
} }
class Blob { const _Blob = class Blob {
/** /** @type {Array.<(Blob|Uint8Array)>} */
* The Blob() constructor returns a new Blob object. The content #parts = []
* of the blob consists of the concatenation of the values given #type = ''
* in the parameter array. #size = 0
* #endings = 'transparent'
* @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
* @param {{ type?: string }} [options]
*/
constructor(blobParts = [], options = {type: ''}) {
let size = 0;
const parts = blobParts.map(element => { /**
let buffer; * The Blob() constructor returns a new Blob object. The content
if (element instanceof Buffer) { * of the blob consists of the concatenation of the values given
buffer = element; * in the parameter array.
} else if (ArrayBuffer.isView(element)) { *
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); * @param {*} blobParts
} else if (element instanceof ArrayBuffer) { * @param {{ type?: string, endings?: string }} [options]
buffer = Buffer.from(element); */
} else if (element instanceof Blob) { constructor (blobParts = [], options = {}) {
buffer = element; if (typeof blobParts !== 'object' || blobParts === null) {
} else { throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.')
buffer = Buffer.from(typeof element === 'string' ? element : String(element)); }
}
size += buffer.length || buffer.size || 0; if (typeof blobParts[Symbol.iterator] !== 'function') {
return buffer; throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.')
}); }
const type = options.type === undefined ? '' : String(options.type).toLowerCase(); if (typeof options !== 'object' && typeof options !== 'function') {
throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.')
}
wm.set(this, { if (options === null) options = {}
type: /[^\u0020-\u007E]/.test(type) ? '' : type,
size,
parts
});
}
/** const encoder = new TextEncoder()
* The Blob interface's size property returns the for (const element of blobParts) {
* size of the Blob in bytes. let part
*/ if (ArrayBuffer.isView(element)) {
get size() { part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength))
return wm.get(this).size; } else if (element instanceof ArrayBuffer) {
} part = new Uint8Array(element.slice(0))
} else if (element instanceof Blob) {
part = element
} else {
part = encoder.encode(`${element}`)
}
/** this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size
* The type property of a Blob object returns the MIME type of the file. this.#parts.push(part)
*/ }
get type() {
return wm.get(this).type;
}
/** this.#endings = `${options.endings === undefined ? 'transparent' : options.endings}`
* The text() method in the Blob interface returns a Promise const type = options.type === undefined ? '' : String(options.type)
* that resolves with a string containing the contents of this.#type = /^[\x20-\x7E]*$/.test(type) ? type : ''
* the blob, interpreted as UTF-8. }
*
* @return {Promise<string>}
*/
async text() {
return Buffer.from(await this.arrayBuffer()).toString();
}
/** /**
* The arrayBuffer() method in the Blob interface returns a * The Blob interface's size property returns the
* Promise that resolves with the contents of the blob as * size of the Blob in bytes.
* binary data contained in an ArrayBuffer. */
* get size () {
* @return {Promise<ArrayBuffer>} return this.#size
*/ }
async arrayBuffer() {
const data = new Uint8Array(this.size);
let offset = 0;
for await (const chunk of this.stream()) {
data.set(chunk, offset);
offset += chunk.length;
}
return data.buffer; /**
} * The type property of a Blob object returns the MIME type of the file.
*/
get type () {
return this.#type
}
/** /**
* The Blob interface's stream() method is difference from native * The text() method in the Blob interface returns a Promise
* and uses node streams instead of whatwg streams. * that resolves with a string containing the contents of
* * the blob, interpreted as UTF-8.
* @returns {Readable} Node readable stream *
*/ * @return {Promise<string>}
stream() { */
return Readable.from(read(wm.get(this).parts)); async text () {
} // More optimized than using this.arrayBuffer()
// that requires twice as much ram
const decoder = new TextDecoder()
let str = ''
for await (const part of toIterator(this.#parts, false)) {
str += decoder.decode(part, { stream: true })
}
// Remaining
str += decoder.decode()
return str
}
/** /**
* The Blob interface's slice() method creates and returns a * The arrayBuffer() method in the Blob interface returns a
* new Blob object which contains data from a subset of the * Promise that resolves with the contents of the blob as
* blob on which it's called. * binary data contained in an ArrayBuffer.
* *
* @param {number} [start] * @return {Promise<ArrayBuffer>}
* @param {number} [end] */
* @param {string} [type] async arrayBuffer () {
*/ // Easier way... Just a unnecessary overhead
slice(start = 0, end = this.size, type = '') { // const view = new Uint8Array(this.size);
const {size} = this; // await this.stream().getReader({mode: 'byob'}).read(view);
// return view.buffer;
let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); const data = new Uint8Array(this.size)
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); let offset = 0
for await (const chunk of toIterator(this.#parts, false)) {
data.set(chunk, offset)
offset += chunk.length
}
const span = Math.max(relativeEnd - relativeStart, 0); return data.buffer
const parts = wm.get(this).parts.values(); }
const blobParts = [];
let added = 0;
for (const part of parts) { stream () {
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size; const it = toIterator(this.#parts, true)
if (relativeStart && size <= relativeStart) {
// Skip the beginning and change the relative
// start & end position as we skip the unwanted parts
relativeStart -= size;
relativeEnd -= size;
} else {
const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
blobParts.push(chunk);
added += ArrayBuffer.isView(chunk) ? chunk.byteLength : chunk.size;
relativeStart = 0; // All next sequental parts should start at 0
// don't add the overflow to new blobParts return new globalThis.ReadableStream({
if (added >= span) { // @ts-ignore
break; type: 'bytes',
} async pull (ctrl) {
} const chunk = await it.next()
} chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value)
},
const blob = new Blob([], {type}); async cancel () {
Object.assign(wm.get(blob), {size: span, parts: blobParts}); await it.return()
}
})
}
return blob; /**
} * The Blob interface's slice() method creates and returns a
* new Blob object which contains data from a subset of the
* blob on which it's called.
*
* @param {number} [start]
* @param {number} [end]
* @param {string} [type]
*/
slice (start = 0, end = this.size, type = '') {
const { size } = this
get [Symbol.toStringTag]() { let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size)
return 'Blob'; let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size)
}
static [Symbol.hasInstance](object) { const span = Math.max(relativeEnd - relativeStart, 0)
return ( const parts = this.#parts
typeof object === 'object' && const blobParts = []
typeof object.stream === 'function' && let added = 0
object.stream.length === 0 &&
typeof object.constructor === 'function' && for (const part of parts) {
/^(Blob|File)$/.test(object[Symbol.toStringTag]) // don't add the overflow to new blobParts
); if (added >= span) {
} break
}
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size
if (relativeStart && size <= relativeStart) {
// Skip the beginning and change the relative
// start & end position as we skip the unwanted parts
relativeStart -= size
relativeEnd -= size
} else {
let chunk
if (ArrayBuffer.isView(part)) {
chunk = part.subarray(relativeStart, Math.min(size, relativeEnd))
added += chunk.byteLength
} else {
chunk = part.slice(relativeStart, Math.min(size, relativeEnd))
added += chunk.size
}
relativeEnd -= size
blobParts.push(chunk)
relativeStart = 0 // All next sequential parts should start at 0
}
}
const blob = new Blob([], { type: String(type).toLowerCase() })
blob.#size = span
blob.#parts = blobParts
return blob
}
get [Symbol.toStringTag] () {
return 'Blob'
}
static [Symbol.hasInstance] (object) {
return (
object &&
typeof object === 'object' &&
typeof object.constructor === 'function' &&
(
typeof object.stream === 'function' ||
typeof object.arrayBuffer === 'function'
) &&
/^(Blob|File)$/.test(object[Symbol.toStringTag])
)
}
} }
Object.defineProperties(Blob.prototype, { Object.defineProperties(_Blob.prototype, {
size: {enumerable: true}, size: { enumerable: true },
type: {enumerable: true}, type: { enumerable: true },
slice: {enumerable: true} slice: { enumerable: true }
}); })
module.exports = Blob; /** @type {typeof globalThis.Blob} */
export const Blob = _Blob
export default Blob
+31 -30
View File
@@ -1,55 +1,56 @@
{ {
"name": "fetch-blob", "name": "fetch-blob",
"version": "2.1.0", "version": "3.1.4",
"description": "A Blob implementation in Node.js, originally from node-fetch.", "description": "Blob & File implementation in Node.js, originally from node-fetch.",
"main": "index.js", "main": "index.js",
"type": "module",
"files": [ "files": [
"from.js", "from.js",
"file.js",
"file.d.ts",
"index.js", "index.js",
"index.d.ts" "index.d.ts",
"from.d.ts",
"streams.cjs"
], ],
"scripts": { "scripts": {
"lint": "xo", "test": "node --experimental-loader ./test/http-loader.js ./test/test-wpt-in-node.js",
"test": "xo && ava", "report": "c8 --reporter json --reporter text npm run test",
"report": "c8 --reporter json --reporter text ava", "coverage": "npm run report && codecov -f coverage/coverage-final.json",
"coverage": "c8 --reporter json --reporter text ava && codecov -f coverage/coverage-final.json", "prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js from.js"
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js"
}, },
"repository": "https://github.com/node-fetch/fetch-blob.git", "repository": "https://github.com/node-fetch/fetch-blob.git",
"keywords": [ "keywords": [
"blob", "blob",
"file",
"node-fetch" "node-fetch"
], ],
"engines": { "engines": {
"node": "^10.17.0 || >=12.3.0" "node": "^12.20 || >= 14.13"
}, },
"author": "David Frank", "author": "Jimmy Wärting <jimmy@warting.se> (https://jimmy.warting.se)",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/node-fetch/fetch-blob/issues" "url": "https://github.com/node-fetch/fetch-blob/issues"
}, },
"homepage": "https://github.com/node-fetch/fetch-blob#readme", "homepage": "https://github.com/node-fetch/fetch-blob#readme",
"devDependencies": { "devDependencies": {
"ava": "^3.8.2", "@types/node": "^17.0.9",
"c8": "^7.2.0", "c8": "^7.11.0",
"codecov": "^3.7.0", "typescript": "^4.5.4"
"get-stream": "^5.1.0",
"node-fetch": "^2.6.0",
"typescript": "^3.9.5",
"xo": "^0.32.0"
}, },
"xo": { "funding": [
"overrides": [ {
{ "type": "github",
"files": "test.js", "url": "https://github.com/sponsors/jimmywarting"
"rules": { },
"node/no-unsupported-features/es-syntax": 0, {
"node/no-unsupported-features/node-builtins": 0 "type": "paypal",
} "url": "https://paypal.me/jimmywarting"
} }
] ],
}, "dependencies": {
"peerDependencies": { "node-domexception": "^1.0.0",
"domexception": "^2.0.1" "web-streams-polyfill": "^3.0.3"
} }
} }
+51
View File
@@ -0,0 +1,51 @@
/* c8 ignore start */
// 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536
if (!globalThis.ReadableStream) {
// `node:stream/web` got introduced in v16.5.0 as experimental
// and it's preferred over the polyfilled version. So we also
// suppress the warning that gets emitted by NodeJS for using it.
try {
const process = require('node:process')
const { emitWarning } = process
try {
process.emitWarning = () => {}
Object.assign(globalThis, require('node:stream/web'))
process.emitWarning = emitWarning
} catch (error) {
process.emitWarning = emitWarning
throw error
}
} catch (error) {
// fallback to polyfill implementation
Object.assign(globalThis, require('web-streams-polyfill/dist/ponyfill.es2018.js'))
}
}
try {
// Don't use node: prefix for this, require+node: is not supported until node v14.14
// Only `import()` can use prefix in 12.20 and later
const { Blob } = require('buffer')
if (Blob && !Blob.prototype.stream) {
Blob.prototype.stream = function name (params) {
let position = 0
const blob = this
return new ReadableStream({
type: 'bytes',
async pull (ctrl) {
const chunk = blob.slice(position, Math.min(blob.size, position + POOL_SIZE))
const buffer = await chunk.arrayBuffer()
position += buffer.byteLength
ctrl.enqueue(new Uint8Array(buffer))
if (position === blob.size) {
ctrl.close()
}
}
})
}
}
} catch (error) {}
/* c8 ignore end */
-162
View File
@@ -1,162 +0,0 @@
const fs = require('fs');
const test = require('ava');
const Blob = require('.');
const blobFrom = require('./from');
const getStream = require('get-stream');
const {Response} = require('node-fetch');
const {TextDecoder} = require('util');
test('new Blob()', t => {
const blob = new Blob(); // eslint-disable-line no-unused-vars
t.pass();
});
test('new Blob(parts)', t => {
const data = 'a=1';
const blob = new Blob([data]); // eslint-disable-line no-unused-vars
t.pass();
});
test('Blob ctor parts', async t => {
const parts = [
'a',
new Uint8Array([98]),
new Uint16Array([25699]),
new Uint8Array([101]).buffer,
Buffer.from('f'),
new Blob(['g']),
{}
];
const blob = new Blob(parts);
t.is(await blob.text(), 'abcdefg[object Object]');
});
test('Blob size', t => {
const data = 'a=1';
const blob = new Blob([data]);
t.is(blob.size, data.length);
});
test('Blob type', t => {
const type = 'text/plain';
const blob = new Blob([], {type});
t.is(blob.type, type);
});
test('Blob slice type', t => {
const type = 'text/plain';
const blob = new Blob().slice(0, 0, type);
t.is(blob.type, type);
});
test('invalid Blob type', t => {
const blob = new Blob([], {type: '\u001Ftext/plain'});
t.is(blob.type, '');
});
test('invalid Blob slice type', t => {
const blob = new Blob().slice(0, 0, '\u001Ftext/plain');
t.is(blob.type, '');
});
test('Blob text()', async t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
t.is(await blob.text(), data);
});
test('Blob arrayBuffer()', async t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
const decoder = new TextDecoder('utf-8');
const buffer = await blob.arrayBuffer();
t.is(decoder.decode(buffer), data);
});
test('Blob stream()', async t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
const result = await getStream(blob.stream());
t.is(result, data);
});
test('Blob toString()', t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
t.is(blob.toString(), '[object Blob]');
});
test('Blob slice()', async t => {
const data = 'abcdefgh';
const blob = new Blob([data]).slice();
t.is(await blob.text(), data);
});
test('Blob slice(0, 1)', async t => {
const data = 'abcdefgh';
const blob = new Blob([data]).slice(0, 1);
t.is(await blob.text(), 'a');
});
test('Blob slice(-1)', async t => {
const data = 'abcdefgh';
const blob = new Blob([data]).slice(-1);
t.is(await blob.text(), 'h');
});
test('Blob slice(0, -1)', async t => {
const data = 'abcdefgh';
const blob = new Blob([data]).slice(0, -1);
t.is(await blob.text(), 'abcdefg');
});
test('Blob(["hello ", "world"]).slice(5)', async t => {
const parts = ['hello ', 'world'];
const blob = new Blob(parts);
t.is(await blob.slice(5).text(), ' world');
});
test('throw away unwanted parts', async t => {
const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
t.is(await blob.text(), 'b');
});
test('Blob works with node-fetch Response.blob()', async t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
const response = new Response(blob);
const blob2 = await response.blob();
t.is(await blob2.text(), data);
});
test('Blob works with node-fetch Response.text()', async t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
const response = new Response(blob);
const text = await response.text();
t.is(text, data);
});
test('blob part backed up by filesystem', async t => {
const blob = blobFrom('./LICENSE');
t.is(await blob.slice(0, 3).text(), 'MIT');
t.is(await blob.slice(4, 11).text(), 'License');
});
test('Reading after modified should fail', async t => {
const blob = blobFrom('./LICENSE');
await new Promise(resolve => setTimeout(resolve, 100));
const now = new Date();
// Change modified time
fs.utimesSync('./LICENSE', now, now);
const error = await blob.text().catch(error => error);
t.is(error.name, 'NotReadableError');
});
+65
View File
@@ -0,0 +1,65 @@
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import { get } from 'node:https'
const fetch = url => new Promise(rs => get(url, rs))
const cache = new URL('./.cache/', import.meta.url)
/**
* @param {string} specifier
* @param {{
* conditions: !Array<string>,
* parentURL: !(string | undefined),
* }} context
* @param {Function} defaultResolve
* @returns {Promise<{ url: string }>}
*/
export async function resolve (specifier, context, defaultResolve) {
const { parentURL = null } = context
// Normally Node.js would error on specifiers starting with 'https://', so
// this hook intercepts them and converts them into absolute URLs to be
// passed along to the later hooks below.
if (specifier.startsWith('https://')) {
return {
url: specifier
}
} else if (parentURL && parentURL.startsWith('https://')) {
return {
url: new URL(specifier, parentURL).href
}
}
// Let Node.js handle all other specifiers.
return defaultResolve(specifier, context, defaultResolve)
}
export async function load (url, context, defaultLoad) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://')) {
const uuid = Buffer.from(url).toString('hex')
const cachedFile = new URL(uuid, cache)
let data = ''
// cache remote files for 1h
if (fs.existsSync(cachedFile) && fs.statSync(cachedFile).mtimeMs > Date.now() - 1000 * 60 * 60) {
data = fs.readFileSync(cachedFile, 'utf8')
} else {
const res = await fetch(url).catch(err => err)
for await (const chunk of res) data += chunk
fs.mkdirSync(cache, { recursive: true })
fs.writeFileSync(cachedFile, data)
}
return {
// This example assumes all network-provided JavaScript is ES module
// code.
format: 'module',
source: data
}
}
// Let Node.js handle all other URLs.
return defaultLoad(url, context, defaultLoad)
}
+217
View File
@@ -0,0 +1,217 @@
// @ts-nocheck
// @ts-ignore
import fs from 'node:fs'
import buffer from 'node:buffer'
import syncBlob, { blobFromSync, blobFrom, fileFromSync, fileFrom } from '../from.js'
const license = fs.readFileSync('./LICENSE')
test_blob(() => new Blob([
'a',
new Uint8Array([98]),
new Uint16Array([25699]),
new Uint8Array([101]).buffer,
Buffer.from('f'),
new Blob(['g']),
{},
new URLSearchParams('foo')
]), {
desc: 'Blob ctor parts',
expected: 'abcdefg[object Object]foo=',
type: '',
length: 26
})
promise_test(async () => {
assert_equals(fileFromSync('./LICENSE').name, 'LICENSE')
assert_equals((await fileFrom('./LICENSE')).name, 'LICENSE')
}, 'file from returns the same name')
// Could not find similar test on WPT
test(() => {
const now = new Date()
assert_equals(new File([], '', { lastModified: now }).lastModified, +now)
assert_equals(new File([], '', { lastModified: +now }).lastModified, +now)
assert_equals(new File([], '', { lastModified: 100 }).lastModified, 100)
assert_equals(new File([], '', { lastModified: '200' }).lastModified, 200)
assert_equals(new File([], '', { lastModified: true }).lastModified, 1)
assert_equals(new File([], '', { lastModified: false }).lastModified, 0)
assert_approx_equals(new File([], '').lastModified, Date.now(), 0.020)
assert_approx_equals(new File([], '', { lastModified: undefined }).lastModified, Date.now(), 0.020)
}, 'File sets current time')
// Could not find similar test on WPT
test(() => {
const values = ['Not a Number', [], {}, null]
// I can't really see anything about this in the spec,
// but this is how browsers handle type casting for this option...
for (const lastModified of values) {
const file = new File([], '', { lastModified })
assert_equals(file.lastModified, 0)
}
}, 'Interpretes NaN value in lastModified option as 0')
test(() => {
assert_equals(blobFromSync, syncBlob)
}, 'default export is named exported blobFromSync')
promise_test(async () => {
const { Blob, default: def } = await import('../index.js')
assert_equals(Blob, def)
}, 'Can use named import - as well as default')
// This was necessary to avoid large ArrayBuffer clones (slice)
promise_test(async t => {
const buf = new Uint8Array(65590)
const blob = new Blob([buf])
let i = 0
// eslint-disable-next-line no-unused-vars
for await (const chunk of blob.stream()) {
i++
}
assert_equals(i, 2)
}, 'Large chunks are divided into smaller chunks')
// Couldn't find a test for this on WPT
promise_test(async () => {
const buf = new Uint8Array([97])
const blob = new Blob([buf])
const chunk = await blob.stream().getReader().read()
assert_equals(chunk.value[0], 97)
chunk.value[0] = 98
assert_equals(await blob.text(), 'a')
}, 'Blobs are immutable')
/**
* Deviation from WPT: it's important to keep boundary value
* so we don't lowercase the type
* @see https://github.com/w3c/FileAPI/issues/43
*/
test(() => {
const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd'
assert_equals(new Blob([], { type }).type, type)
assert_equals(new File([], '', { type }).type, type)
}, 'Dose not lowercase the type')
test( // Because we have symbol.hasInstance it's best to test it...
() => (assert_false(null instanceof Blob), assert_false(null instanceof File)),
'Instanceof check returns false for nullish values'
)
test( // Because browser normally never tries things taken for granted
() => assert_equals(new Blob().toString(), '[object Blob]'),
'blob.toString() returns [object Blob]'
)
test( // Because browser normally never tries things taken for granted
() => assert_equals(new File([], '').toString(), '[object File]'),
'file.toString() returns [object File]'
)
// fetch-blob uniques is that it supports arbitrary blobs too
test(() => {
class File {
stream () {}
get [Symbol.toStringTag] () { return 'File' }
}
assert_true(new File() instanceof Blob)
}, 'Blob-ish class is an instance of Blob')
// fetch-blob uniques is that it supports arbitrary blobs too
if (buffer.Blob) {
test_blob(() => new Blob([new buffer.Blob(['blob part'])]), {
desc: 'Can wrap buffer.Blob to a fetch-blob',
expected: 'blob part',
type: '',
})
}
/**
* Test if Blob can be constructed with BOM and keep it when casted to string
* Test if blob.text() can correctly remove BOM - `buffer.toString()` is bad
*/
promise_test(async () => {
const text = '{"foo": "bar"}'
const blob = new Blob([`\uFEFF${text}`])
assert_equals(blob.size, 17)
assert_equals(await blob.text(), text)
const ab = await blob.slice(0, 3).arrayBuffer()
assert_equals_typed_array(new Uint8Array(ab), new Uint8Array([0xEF, 0xBB, 0xBF]))
}, 'Can wrap buffer.Blob to a fetch-blob')
// Here to make sure our `toIterator` is working as intended
promise_test(async () => {
const stream = new Blob(['Some content']).stream()
// Cancel the stream before start reading, or this will throw an error
await stream.cancel()
const reader = stream.getReader()
const { done, value: chunk } = await reader.read()
assert_true(done)
assert_equals(chunk, undefined)
}, 'Blob stream() can be cancelled')
/******************************************************************************/
/* */
/* Test Blobs backed up by the filesystem */
/* */
/******************************************************************************/
promise_test(async () => {
assert_equals(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain')
assert_equals(fileFromSync('./LICENSE').type, '')
assert_equals(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain')
assert_equals(blobFromSync('./LICENSE').type, '')
assert_equals((await fileFrom('./LICENSE', 'text/plain')).type, 'text/plain')
assert_equals((await fileFrom('./LICENSE')).type, '')
assert_equals((await blobFrom('./LICENSE', 'text/plain')).type, 'text/plain')
assert_equals((await blobFrom('./LICENSE')).type, '')
}, 'from utilities sets correct type')
promise_test(async () => {
assert_equals(await blobFromSync('./LICENSE').text(), license.toString())
assert_equals(await fileFromSync('./LICENSE').text(), license.toString())
assert_equals(await (await blobFrom('./LICENSE')).text(), license.toString())
assert_equals(await (await fileFrom('./LICENSE')).text(), license.toString())
}, 'blob part backed up by filesystem can be read')
promise_test(async () => {
assert_equals(await blobFromSync('./LICENSE').text(), license.toString())
assert_equals(await fileFromSync('./LICENSE').text(), license.toString())
assert_equals(await (await blobFrom('./LICENSE')).text(), license.toString())
assert_equals(await (await fileFrom('./LICENSE')).text(), license.toString())
}, 'blob part backed up by filesystem slice correctly')
test(async () => {
const blob = blobFromSync('./LICENSE')
await new Promise(resolve => setTimeout(resolve, 2000))
const now = new Date()
// Change modified time
fs.utimesSync('./LICENSE', now, now)
const error = await blob.text().then(assert_unreached, e => e)
assert_equals(error.constructor.name, 'DOMException')
assert_equals(error instanceof Error, true)
assert_equals(error.name, 'NotReadableError')
const file = fileFromSync('./LICENSE')
// Above test updates the last modified date to now
assert_equals(typeof file.lastModified, 'number')
// The lastModifiedDate is deprecated and removed from spec
assert_false('lastModifiedDate' in file)
assert_approx_equals(file.lastModified, +now, 1000)
}, 'Reading after modified should fail')
promise_test(async () => {
assert_equals(await blobFromSync('./LICENSE').slice(0, 0).text(), '')
assert_equals(await blobFromSync('./LICENSE').slice(0, 3).text(), license.slice(0, 3).toString())
assert_equals(await blobFromSync('./LICENSE').slice(4, 11).text(), license.slice(4, 11).toString())
assert_equals(await blobFromSync('./LICENSE').slice(-11).text(), license.slice(-11).toString())
assert_equals(await blobFromSync('./LICENSE').slice(4, 11).slice(2, 5).text(), license.slice(4, 11).slice(2, 5).toString())
}, 'slicing blobs backed up by filesystem returns correct string')
+143
View File
@@ -0,0 +1,143 @@
// Don't want to use the FileReader, don't want to lowerCase the type either
// import from 'https://wpt.live/resources/testharnessreport.js'
import { File, Blob } from '../from.js'
let hasFailed
globalThis.self = globalThis
await import('https://wpt.live/resources/testharness.js')
setup({
explicit_timeout: true,
explicit_done: true
})
function test_blob (fn, expectations) {
const expected = expectations.expected
const type = expectations.type
const desc = expectations.desc
const length = expectations.length
const t = async_test(desc)
t.step(async function () {
const blob = fn()
assert_true(blob instanceof Blob)
assert_false(blob instanceof File)
assert_equals(blob.type.toLowerCase(), type)
assert_equals(await blob.text(), expected)
t.done()
})
}
function test_blob_binary (fn, expectations) {
const expected = expectations.expected
const type = expectations.type
const desc = expectations.desc
const t = async_test(desc)
t.step(async function () {
const blob = fn()
assert_true(blob instanceof Blob)
assert_false(blob instanceof File)
assert_equals(blob.type.toLowerCase(), type)
const result = await blob.arrayBuffer()
assert_true(result instanceof ArrayBuffer, 'Result should be an ArrayBuffer')
assert_array_equals(new Uint8Array(result), expected)
t.done()
})
}
// Assert that two TypedArray objects have the same byte values
globalThis.assert_equals_typed_array = (array1, array2) => {
const [view1, view2] = [array1, array2].map((array) => {
assert_true(array.buffer instanceof ArrayBuffer,
'Expect input ArrayBuffers to contain field `buffer`')
return new DataView(array.buffer, array.byteOffset, array.byteLength)
})
assert_equals(view1.byteLength, view2.byteLength,
'Expect both arrays to be of the same byte length')
const byteLength = view1.byteLength
for (let i = 0; i < byteLength; ++i) {
assert_equals(view1.getUint8(i), view2.getUint8(i),
`Expect byte at buffer position ${i} to be equal`)
}
}
globalThis.add_result_callback((test, ...args) => {
if ([
'Blob with type "A"',
'Blob with type "TEXT/HTML"',
'Getters and value conversions should happen in order until an exception is thrown.',
'Using type in File constructor: TEXT/PLAIN',
'Using type in File constructor: text/plain;charset=UTF-8'
].includes(test.name)) return
const INDENT_SIZE = 2
const reporter = {}
reporter.startSuite = name => console.log(`\n ${(name)}\n`)
reporter.pass = message => console.log((indent(('√ ') + message, INDENT_SIZE)))
reporter.fail = message => console.log((indent('\u00D7 ' + message, INDENT_SIZE)))
reporter.reportStack = stack => console.log((indent(stack, INDENT_SIZE * 2)))
function indent (string, times) {
const prefix = ' '.repeat(times)
return string.split('\n').map(l => prefix + l).join('\n')
}
if (test.status === 0) {
reporter.pass(test.name)
} else if (test.status === 1) {
reporter.fail(`${test.name}\n`)
reporter.reportStack(`${test.message}\n${test.stack}`)
hasFailed = true
} else if (test.status === 2) {
reporter.fail(`${test.name} (timeout)\n`)
reporter.reportStack(`${test.message}\n${test.stack}`)
hasFailed = true
} else if (test.status === 3) {
reporter.fail(`${test.name} (incomplete)\n`)
reporter.reportStack(`${test.message}\n${test.stack}`)
hasFailed = true
} else if (test.status === 4) {
reporter.fail(`${test.name} (precondition failed)\n`)
reporter.reportStack(`${test.message}\n${test.stack}`)
hasFailed = true
} else {
reporter.fail(`unknown test status: ${test.status}`)
hasFailed = true
}
})
globalThis.File = File
globalThis.Blob = Blob
globalThis.garbageCollect = () => {}
globalThis.test_blob = test_blob
globalThis.test_blob_binary = test_blob_binary
// Cuz WPT don't clean up after itself
globalThis.MessageChannel = class extends MessageChannel {
constructor () {
super()
setTimeout(() => {
this.port1.close()
this.port2.close()
this.port1.onmessage = this.port2.onmessage = null
}, 100)
}
}
import('https://wpt.live/FileAPI/file/File-constructor.any.js')
import('https://wpt.live/FileAPI/blob/Blob-constructor.any.js')
import('https://wpt.live/FileAPI/blob/Blob-array-buffer.any.js')
import('https://wpt.live/FileAPI/blob/Blob-slice-overflow.any.js')
import('https://wpt.live/FileAPI/blob/Blob-slice.any.js')
import('https://wpt.live/FileAPI/blob/Blob-stream.any.js')
import('https://wpt.live/FileAPI/blob/Blob-text.any.js')
import('./own-misc-test.js')
hasFailed && process.exit(1)