Compare commits

..
Author SHA1 Message Date
Konstantin Vyatkin cd619c7694 lint 2020-06-11 00:17:58 -04:00
Konstantin Vyatkin bc6ca02caa import TextDecoder from util 2020-06-11 00:16:48 -04:00
Konstantin Vyatkin b098249bea update badge 2020-06-11 00:13:20 -04:00
Konstantin Vyatkin ecdabc9879 add GitHub Actions 2020-06-11 00:01:56 -04:00
Konstantin Vyatkin 71600df374 switch coverage to c8 2020-06-11 00:01:51 -04:00
Konstantin Vyatkin f9be7e7d7b add codecov partial line coverage 2020-06-10 23:52:25 -04:00
Konstantin Vyatkin fcf91af996 remove travis 2020-06-10 23:50:57 -04:00
10 changed files with 105 additions and 649 deletions
-25
View File
@@ -1,25 +0,0 @@
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
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
node: ["16", "15", "14", engines] node: ["14", "12", engines]
exclude: exclude:
# On Windows, run tests with only the LTS environments. # On Windows, run tests with only the LTS environments.
- os: windows-latest - os: windows-latest
+1 -1
View File
@@ -62,4 +62,4 @@ typings/
# dotenv environment variables file # dotenv environment variables file
.env .env
*.d.ts index.d.ts
-30
View File
@@ -1,36 +1,6 @@
Changelog Changelog
========= =========
## 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
- Fix: .slice has an implementation bug (#54).
- Added blob backed up by filesystem (#55)
## v2.0.1
- Fix: remove upper bound for node engine semver (#49).
## v2.0.0 ## v2.0.0
> Note: This release was previously published as `1.0.7`, but as it contains breaking changes, we renamed it to `2.0.0`. > Note: This release was previously published as `1.0.7`, but as it contains breaking changes, we renamed it to `2.0.0`.
+9 -118
View File
@@ -13,129 +13,21 @@ 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 a simple generator function that yields Uint8Array (Breaking change)
(Read "Differences from other blobs" for more info.)
All of this changes have made it dependency free of any core node modules, so it would be possible to just import it using http-import from a CDN without any bundling
</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 AsyncGeneratorFunction that yields Uint8Arrays
The reasoning behind `Blob.prototype.stream()` is that NodeJS readable stream
isn't spec compatible with whatwg streams and we didn't want to import the hole whatwg stream polyfill for node
or browserify NodeJS streams for the browsers and picking any flavor over the other. So we decided to opted out
of any stream and just implement the bear minium of what both streams have in common which is the asyncIterator
that both yields Uint8Array. this is the most isomorphic way with the use of `for-await-of` loops.
It would be redundant to convert anything to whatwg streams and than convert it back to
node streams since you work inside of Node.
It will probably stay like this until nodejs get native support for whatwg<sup>[1][https://github.com/nodejs/whatwg-stream]</sup> streams and whatwg stream add the node
equivalent for `Readable.from(iterable)`<sup>[2](https://github.com/whatwg/streams/issues/1018)</sup>
But for now if you really need a Node Stream then you can do so using this transformation
```js
import {Readable} from 'stream'
const stream = Readable.from(blob.stream())
```
But if you don't need it to be a stream then you can just use the asyncIterator part of it that is isomorphic.
```js
for await (const chunk of blob.stream()) {
console.log(chunk) // uInt8Array
}
```
If you need to make some feature detection to fix this different behavior
```js
if (Blob.prototype.stream?.constructor?.name === 'AsyncGeneratorFunction') {
// not spec compatible, monkey patch it...
// (Alternative you could extend the Blob and use super.stream())
let orig = Blob.prototype.stream
Blob.prototype.stream = function () {
const iterator = orig.call(this)
return new ReadableStream({
async pull (ctrl) {
const next = await iterator.next()
return next.done ? ctrl.close() : ctrl.enqueue(next.value)
}
})
}
}
```
Possible feature whatwg version: `ReadableStream.from(iterator)`
It's also possible to delete this method and instead use `.slice()` and `.arrayBuffer()` since it has both a public and private stream method
</details>
## Usage ## Usage
```js ```js
// Ways to import const Blob = require('fetch-blob');
// (PS it's dependency free ESM package so regular http-import from CDN works too) const fetch = require('node-fetch');
import Blob from 'fetch-blob'
import File from 'fetch-blob/file.js'
import {Blob} from 'fetch-blob' fetch('https://httpbin.org/post', {
import {File} from 'fetch-blob/file.js' method: 'POST',
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()) { ... }
// turn the async iterator into a node stream
stream.Readable.from(blob.stream())
// turn the async iterator into a whatwg stream (feature)
globalThis.ReadableStream.from(blob.stream())
``` ```
### Blob part backed up by filesystem 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.
`fetch-blob/from.js` comes packed with tools to convert any filepath into either a Blob or a File
It will not read the content into memory. It will only stat the file for last modified date and file size.
```js
// The default export is sync and use fs.stat to retrieve size & last modified as a blob
import blobFromSync from 'fetch-blob/from.js'
import {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync} from 'fetch-blob/from.js'
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
const blob = new Blob([fsFile, fsBlob, 'memory', new Uint8Array(10)])
console.log(blob.size) // ~4 GiB
```
`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
@@ -145,4 +37,3 @@ 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
-36
View File
@@ -1,36 +0,0 @@
import Blob from './index.js';
export default 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);
const modified = Number(options.lastModified);
this.#lastModified = Number.isNaN(modified) ? Date.now() : modified
this.#name = fileName;
}
get name() {
return this.#name;
}
get lastModified() {
return this.#lastModified;
}
get [Symbol.toStringTag]() {
return "File";
}
}
export { File };
-103
View File
@@ -1,103 +0,0 @@
import {statSync, createReadStream} from 'fs';
import {stat} from 'fs/promises';
import {basename} from 'path';
import File from './file.js';
import Blob from './index.js';
import {MessageChannel} from 'worker_threads';
const DOMException = globalThis.DOMException || (() => {
const port = new MessageChannel().port1
const ab = new ArrayBuffer(0)
try { port.postMessage(ab, [ab, ab]) }
catch (err) { return err.constructor }
})()
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
const blobFromSync = (path, type) => fromBlob(statSync(path), path, type);
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
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
*/
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);
const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
path,
size: stat.size,
lastModified: stat.mtimeMs,
start: 0
})], {type});
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
* with minium requirement. Its wrapped around a Blob as a blobPart
* so you have no direct access to this.
*
* @private
*/
class BlobDataItem {
#path;
#start;
constructor(options) {
this.#path = options.path;
this.#start = options.start;
this.size = options.size;
this.lastModified = options.lastModified
}
/**
* Slicing arguments is first validated and formatted
* 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
});
}
async * stream() {
const {mtimeMs} = await stat(this.#path)
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');
}
if (this.size) {
yield * createReadStream(this.#path, {
start: this.#start,
end: this.#start + this.size - 1
});
}
}
get [Symbol.toStringTag]() {
return 'Blob';
}
}
export default blobFromSync;
export {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync};
+64 -93
View File
@@ -1,76 +1,71 @@
// 64 KiB (same size chrome slice theirs blob into Uint8array's) const {Readable} = require('stream');
const POOL_SIZE = 65536;
/** @param {(Blob | Uint8Array)[]} parts */ /**
async function * toIterator (parts, clone = true) { * @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>}
for (let part of parts) { */
const wm = new WeakMap();
async function * read(parts) {
for (const part of parts) {
if ('stream' in part) { if ('stream' in part) {
yield * part.stream(); yield * part.stream();
} else if (ArrayBuffer.isView(part)) {
if (clone) {
let position = part.byteOffset;
let end = part.byteOffset + part.byteLength;
while (position !== end) {
const size = Math.min(end - position, POOL_SIZE);
const chunk = part.buffer.slice(position, position + size);
yield new Uint8Array(chunk);
position += chunk.byteLength;
}
} else { } else {
yield part; yield part;
} }
} else {
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob)
let position = 0;
while (position !== part.size) {
const chunk = part.slice(position, Math.min(part.size, position + POOL_SIZE));
const buffer = await chunk.arrayBuffer();
position += buffer.byteLength;
yield new Uint8Array(buffer);
}
}
} }
} }
export default class Blob { /**
* @template T
/** @type {Array.<(Blob|Uint8Array)>} */ * @param {T} object
#parts = []; * @returns {T is Blob}
#type = ''; */
#size = 0; const isBlob = object => {
return (
typeof object === 'object' &&
typeof object.stream === 'function' &&
typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[Symbol.toStringTag])
);
};
class Blob {
/** /**
* The Blob() constructor returns a new Blob object. The content * The Blob() constructor returns a new Blob object. The content
* of the blob consists of the concatenation of the values given * of the blob consists of the concatenation of the values given
* in the parameter array. * in the parameter array.
* *
* @param {*} blobParts * @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
* @param {{ type?: string }} [options] * @param {{ type?: string }} [options]
*/ */
constructor(blobParts = [], options = {}) { constructor(blobParts = [], options = {type: ''}) {
let size = 0; let size = 0;
const parts = blobParts.map(element => { const parts = blobParts.map(element => {
let part; let buffer;
if (ArrayBuffer.isView(element)) { if (Buffer.isBuffer(element)) {
part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength)); buffer = element;
} else if (ArrayBuffer.isView(element)) {
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
} else if (element instanceof ArrayBuffer) { } else if (element instanceof ArrayBuffer) {
part = new Uint8Array(element.slice(0)); buffer = Buffer.from(element);
} else if (element instanceof Blob) { } else if (isBlob(element)) {
part = element; buffer = element;
} else { } else {
part = new TextEncoder().encode(String(element)); buffer = Buffer.from(typeof element === 'string' ? element : String(element));
} }
size += ArrayBuffer.isView(part) ? part.byteLength : part.size; size += buffer.length || buffer.size || 0;
return part; return buffer;
}); });
const type = options.type === undefined ? '' : String(options.type); const type = options.type === undefined ? '' : String(options.type).toLowerCase();
this.#type = /[^\u0020-\u007E]/.test(type) ? '' : type; wm.set(this, {
this.#size = size; type: /[^\u0020-\u007E]/.test(type) ? '' : type,
this.#parts = parts; size,
parts
});
} }
/** /**
@@ -78,14 +73,14 @@ export default class Blob {
* size of the Blob in bytes. * size of the Blob in bytes.
*/ */
get size() { get size() {
return this.#size; return wm.get(this).size;
} }
/** /**
* The type property of a Blob object returns the MIME type of the file. * The type property of a Blob object returns the MIME type of the file.
*/ */
get type() { get type() {
return this.#type; return wm.get(this).type;
} }
/** /**
@@ -96,16 +91,7 @@ export default class Blob {
* @return {Promise<string>} * @return {Promise<string>}
*/ */
async text() { async text() {
// More optimized than using this.arrayBuffer() return Buffer.from(await this.arrayBuffer()).toString();
// that requires twice as much ram
const decoder = new TextDecoder();
let str = '';
for await (let part of toIterator(this.#parts, false)) {
str += decoder.decode(part, { stream: true });
}
// Remaining
str += decoder.decode();
return str;
} }
/** /**
@@ -118,7 +104,7 @@ export default class Blob {
async arrayBuffer() { async arrayBuffer() {
const data = new Uint8Array(this.size); const data = new Uint8Array(this.size);
let offset = 0; let offset = 0;
for await (const chunk of toIterator(this.#parts, false)) { for await (const chunk of this.stream()) {
data.set(chunk, offset); data.set(chunk, offset);
offset += chunk.length; offset += chunk.length;
} }
@@ -127,13 +113,13 @@ export default class Blob {
} }
/** /**
* The Blob stream() implements partial support of the whatwg stream * The Blob interface's stream() method is difference from native
* by only being async iterable. * and uses node streams instead of whatwg streams.
* *
* @returns {AsyncGenerator<Uint8Array>} * @returns {Readable} Node readable stream
*/ */
async * stream() { stream() {
yield * toIterator(this.#parts, true); return Readable.from(read(wm.get(this).parts));
} }
/** /**
@@ -152,7 +138,7 @@ export default class Blob {
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
const span = Math.max(relativeEnd - relativeStart, 0); const span = Math.max(relativeEnd - relativeStart, 0);
const parts = this.#parts; const parts = wm.get(this).parts.values();
const blobParts = []; const blobParts = [];
let added = 0; let added = 0;
@@ -164,16 +150,10 @@ export default class Blob {
relativeStart -= size; relativeStart -= size;
relativeEnd -= size; relativeEnd -= size;
} else { } else {
let chunk const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
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
}
blobParts.push(chunk); blobParts.push(chunk);
relativeStart = 0; // All next sequential parts should start at 0 added += size;
relativeStart = 0; // All next sequental parts should start at 0
// don't add the overflow to new blobParts // don't add the overflow to new blobParts
if (added >= span) { if (added >= span) {
@@ -182,27 +162,11 @@ export default class Blob {
} }
} }
const blob = new Blob([], {type: String(type).toLowerCase()}); const blob = new Blob([], {type});
blob.#size = span; Object.assign(wm.get(blob), {size: span, parts: blobParts});
blob.#parts = blobParts;
return blob; return blob;
} }
get [Symbol.toStringTag]() {
return 'Blob';
}
static [Symbol.hasInstance](object) {
return (
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, {
@@ -211,4 +175,11 @@ Object.defineProperties(Blob.prototype, {
slice: {enumerable: true} slice: {enumerable: true}
}); });
export { Blob }; Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
value: 'Blob',
writable: false,
enumerable: false,
configurable: true
});
module.exports = Blob;
+18 -44
View File
@@ -1,52 +1,43 @@
{ {
"name": "fetch-blob", "name": "fetch-blob",
"version": "3.0.0", "version": "2.0.0",
"description": "Blob & File implementation in Node.js, originally from node-fetch.", "description": "A Blob implementation in Node.js, originally from node-fetch.",
"main": "index.js", "main": "index.js",
"type": "module",
"files": [ "files": [
"from.js",
"file.js",
"file.d.ts",
"index.js", "index.js",
"index.d.ts", "index.d.ts"
"from.d.ts"
], ],
"scripts": { "scripts": {
"lint": "xo test.js", "lint": "xo",
"test": "npm run lint && ava", "test": "xo && ava",
"report": "c8 --reporter json --reporter text ava", "report": "c8 --reporter json --reporter text ava",
"coverage": "c8 --reporter json --reporter text ava && 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": ">=14.0.0" "node": "^10.17.0"
}, },
"author": "Jimmy Wärting <jimmy@warting.se> (https://jimmy.warting.se)", "author": "David Frank",
"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",
"xo": { "devDependencies": {
"rules": { "ava": "^3.8.2",
"unicorn/prefer-node-protocol": "off", "c8": "^7.2.0",
"unicorn/numeric-separators-style": "off", "codecov": "^3.7.0",
"unicorn/prefer-spread": "off", "get-stream": "^5.1.0",
"import/extensions": [ "node-fetch": "^2.6.0",
"error", "typescript": "^3.9.5",
"always", "xo": "^0.32.0"
{
"ignorePackages": true
}
]
}, },
"xo": {
"overrides": [ "overrides": [
{ {
"files": "test.js", "files": "test.js",
@@ -57,22 +48,5 @@
} }
] ]
}, },
"devDependencies": { "dependencies": {}
"ava": "^3.15.0",
"c8": "^7.7.2",
"codecov": "^3.8.2",
"node-fetch": "^3.0.0-beta.9",
"typescript": "^4.3.2",
"xo": "^0.40.1"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
]
} }
+11 -197
View File
@@ -1,13 +1,8 @@
import fs from 'fs'; const test = require('ava');
import {Readable} from 'stream'; const Blob = require('.');
import buffer from 'buffer'; const getStream = require('get-stream');
import test from 'ava'; const {Response} = require('node-fetch');
import {Response} from 'node-fetch'; const {TextDecoder} = require('util');
import syncBlob, {blobFromSync, blobFrom, fileFromSync, fileFrom} from './from.js';
import File from './file.js';
import Blob from './index.js';
const license = fs.readFileSync('./LICENSE', 'utf-8');
test('new Blob()', t => { test('new Blob()', t => {
const blob = new Blob(); // eslint-disable-line no-unused-vars const blob = new Blob(); // eslint-disable-line no-unused-vars
@@ -28,12 +23,11 @@ test('Blob ctor parts', async t => {
new Uint8Array([101]).buffer, new Uint8Array([101]).buffer,
Buffer.from('f'), Buffer.from('f'),
new Blob(['g']), new Blob(['g']),
{}, {}
new URLSearchParams('foo')
]; ];
const blob = new Blob(parts); const blob = new Blob(parts);
t.is(await blob.text(), 'abcdefg[object Object]foo='); t.is(await blob.text(), 'abcdefg[object Object]');
}); });
test('Blob size', t => { test('Blob size', t => {
@@ -85,10 +79,8 @@ test('Blob stream()', async t => {
const data = 'a=1'; const data = 'a=1';
const type = 'text/plain'; const type = 'text/plain';
const blob = new Blob([data], {type}); const blob = new Blob([data], {type});
const result = await getStream(blob.stream());
for await (const chunk of blob.stream()) { t.is(result, data);
t.is(chunk.join(), [97, 61, 49].join());
}
}); });
test('Blob toString()', t => { test('Blob toString()', t => {
@@ -122,12 +114,6 @@ test('Blob slice(0, -1)', async t => {
t.is(await blob.text(), 'abcdefg'); 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 => { test('throw away unwanted parts', async t => {
const blob = new Blob(['a', 'b', 'c']).slice(1, 2); const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
t.is(await blob.text(), 'b'); t.is(await blob.text(), 'b');
@@ -137,7 +123,7 @@ test('Blob works with node-fetch Response.blob()', async t => {
const data = 'a=1'; const data = 'a=1';
const type = 'text/plain'; const type = 'text/plain';
const blob = new Blob([data], {type}); const blob = new Blob([data], {type});
const response = new Response(Readable.from(blob.stream())); const response = new Response(blob);
const blob2 = await response.blob(); const blob2 = await response.blob();
t.is(await blob2.text(), data); t.is(await blob2.text(), data);
}); });
@@ -146,179 +132,7 @@ test('Blob works with node-fetch Response.text()', async t => {
const data = 'a=1'; const data = 'a=1';
const type = 'text/plain'; const type = 'text/plain';
const blob = new Blob([data], {type}); const blob = new Blob([data], {type});
const response = new Response(Readable.from(blob.stream())); const response = new Response(blob);
const text = await response.text(); const text = await response.text();
t.is(text, data); t.is(text, data);
}); });
test('blob part backed up by filesystem', async t => {
const blob = blobFromSync('./LICENSE');
t.is(await blob.slice(0, 3).text(), license.slice(0, 3));
t.is(await blob.slice(4, 11).text(), license.slice(4, 11));
});
test('Reading after modified should fail', async t => {
const blob = blobFromSync('./LICENSE');
await new Promise(resolve => {
setTimeout(resolve, 100);
});
const now = new Date();
// Change modified time
fs.utimesSync('./LICENSE', now, now);
const error = await t.throwsAsync(blob.text());
t.is(error.constructor.name, 'DOMException');
t.is(error instanceof Error, true);
t.is(error.name, 'NotReadableError');
});
test('Reading file after modified should fail', async t => {
const file = fileFromSync('./LICENSE');
await new Promise(resolve => {
setTimeout(resolve, 100);
});
const now = new Date();
// Change modified time
fs.utimesSync('./LICENSE', now, now);
const error = await t.throwsAsync(file.text());
t.is(error.constructor.name, 'DOMException');
t.is(error instanceof Error, true);
t.is(error.name, 'NotReadableError');
});
test('Reading from the stream created by blobFrom', async t => {
const blob = blobFromSync('./LICENSE');
const actual = await blob.text();
t.is(actual, license);
});
test('create a blob from path asynchronous', async t => {
const blob = await blobFrom('./LICENSE');
const actual = await blob.text();
t.is(actual, license);
});
test('Reading empty blobs', async t => {
const blob = blobFromSync('./LICENSE').slice(0, 0);
const actual = await blob.text();
t.is(actual, '');
});
test('Blob-ish class is an instance of Blob', t => {
class File {
stream() {}
get [Symbol.toStringTag]() {
return 'File';
}
}
t.true(new File() instanceof Blob);
});
test('Instanceof check returns false for nullish values', t => {
t.false(null instanceof Blob);
});
/** @see https://github.com/w3c/FileAPI/issues/43 - important to keep boundary value */
test('Dose not lowercase the blob values', t => {
const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd';
t.is(new Blob([], {type}).type, type);
});
test('Parts are immutable', async t => {
const buf = new Uint8Array([97]);
const blob = new Blob([buf]);
buf[0] = 98;
t.is(await blob.text(), 'a');
});
test('Blobs are immutable', async t => {
const buf = new Uint8Array([97]);
const blob = new Blob([buf]);
const chunk = await blob.stream().next();
t.is(chunk.value[0], 97);
chunk.value[0] = 98;
t.is(await blob.text(), 'a');
});
// This was necessary to avoid large ArrayBuffer clones (slice)
test('Large chunks are divided into smaller chunks', 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++;
}
t.is(i === 2, true);
});
test('Can use named import - as well as default', async t => {
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const {Blob, default: def} = await import('./index.js');
t.is(Blob, def);
});
test('default from.js exports blobFromSync', t => {
t.is(blobFromSync, syncBlob);
});
if (buffer.Blob) {
test('Can wrap buffer.Blob to a fetch-blob', async t => {
const blob1 = new buffer.Blob(['blob part']);
const blob2 = new Blob([blob1]);
t.is(await blob2.text(), 'blob part');
});
}
test('File is a instance of blob', t => {
t.true(new File([], '') instanceof Blob);
});
test('fileFrom returns the name', async t => {
t.is((await fileFrom('./LICENSE')).name, 'LICENSE');
});
test('fileFromSync returns the name', t => {
t.is(fileFromSync('./LICENSE').name, 'LICENSE');
});
test('fileFromSync(path, type) sets the type', t => {
t.is(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain');
});
test('blobFromSync(path, type) sets the type', t => {
t.is(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain');
});
test('fileFrom(path, type) sets the type', async t => {
const file = await fileFrom('./LICENSE', 'text/plain');
t.is(file.type, 'text/plain');
});
test('fileFrom(path, type) read/sets the lastModified ', async t => {
const file = await fileFrom('./LICENSE', 'text/plain');
// Earlier test updates the last modified date to now
t.is(typeof file.lastModified, 'number');
// The lastModifiedDate is deprecated and removed from spec
t.false('lastModifiedDate' in file);
t.is(file.lastModified > Date.now() - 60000, true);
});
test('blobFrom(path, type) sets the type', async t => {
const blob = await blobFrom('./LICENSE', 'text/plain');
t.is(blob.type, 'text/plain');
});
test('blobFrom(path) sets empty type', async t => {
const blob = await blobFrom('./LICENSE');
t.is(blob.type, '');
});
test('new File() throws with too few args', t => {
t.throws(() => new File(), {
instanceOf: TypeError,
message: 'Failed to construct \'File\': 2 arguments required, but only 0 present.'
});
});