Compare commits

...
Author SHA1 Message Date
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
8 changed files with 647 additions and 610 deletions
+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: ["16", "15", "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
+5 -10
View File
@@ -5,22 +5,16 @@ 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 The format is based on [Keep a Changelog](http://keepachangelog.com/) and this
project adheres to [Semantic Versioning](http://semver.org/). project adheres to [Semantic Versioning](http://semver.org/).
(Unreleased) ## v3.1.3
==================
### Removed
-
### Changed
-
### Added
-
### Fixed
- Allow usage of iterable object in Blob constructor. [#108] - Allow usage of iterable object in Blob constructor. [#108]
- Run test WPT test against our impl [#109] - Run test WPT test against our impl [#109]
- File name are now casted to string [#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] - 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 ## v3.1.2
- Improved typing - Improved typing
- Fixed a bug where position in iterator did not increase - Fixed a bug where position in iterator did not increase
@@ -101,3 +95,4 @@ project adheres to [Semantic Versioning](http://semver.org/).
[#108]: https://github.com/node-fetch/fetch-blob/pull/108 [#108]: https://github.com/node-fetch/fetch-blob/pull/108
[#109]: https://github.com/node-fetch/fetch-blob/pull/109 [#109]: https://github.com/node-fetch/fetch-blob/pull/109
[#114]: https://github.com/node-fetch/fetch-blob/pull/114
+22 -18
View File
@@ -1,40 +1,44 @@
import Blob from './index.js'; import Blob from './index.js'
const _File = class File extends Blob { const _File = class File extends Blob {
#lastModified = 0; #lastModified = 0
#name = ''; #name = ''
/** /**
* @param {*[]} fileBits * @param {*[]} fileBits
* @param {string} fileName * @param {string} fileName
* @param {{lastModified?: number, type?: string}} options * @param {{lastModified?: number, type?: string}} options
*/// @ts-ignore */// @ts-ignore
constructor(fileBits, fileName, options = {}) { constructor (fileBits, fileName, options = {}) {
if (arguments.length < 2) { if (arguments.length < 2) {
throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`); throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`)
} }
super(fileBits, options); super(fileBits, options)
if (options === null) options = {}; if (options === null) options = {}
const modified = Number(options.lastModified); // Simulate WebIDL type casting for NaN value in lastModified option.
this.#lastModified = Number.isNaN(modified) ? Date.now() : modified const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified)
this.#name = String(fileName); if (!Number.isNaN(lastModified)) {
this.#lastModified = lastModified
}
this.#name = String(fileName)
} }
get name() { get name () {
return this.#name; return this.#name
} }
get lastModified() { get lastModified () {
return this.#lastModified; return this.#lastModified
} }
get [Symbol.toStringTag]() { get [Symbol.toStringTag] () {
return "File"; return 'File'
} }
} }
/** @type {typeof globalThis.File} */// @ts-ignore /** @type {typeof globalThis.File} */// @ts-ignore
export const File = _File; export const File = _File
export default File; export default File
+58 -59
View File
@@ -1,58 +1,57 @@
import {statSync, createReadStream, promises as fs} from 'fs'; import { statSync, createReadStream, promises as fs } from 'node:fs'
import {basename} from 'path'; import { basename } from 'node:path'
import {MessageChannel} from 'worker_threads'; import { MessageChannel } from 'node:worker_threads'
import File from './file.js'; import File from './file.js'
import Blob from './index.js'; import Blob from './index.js'
const {stat} = fs; const { stat } = fs
const DOMException = globalThis.DOMException || (() => { const DOMException = globalThis.DOMException || (() => {
const port = new MessageChannel().port1 const port = new MessageChannel().port1
const ab = new ArrayBuffer(0) const ab = new ArrayBuffer(0)
try { port.postMessage(ab, [ab, ab]) } try { port.postMessage(ab, [ab, ab]) } catch (err) { return err.constructor }
catch (err) { return err.constructor }
})() })()
/** /**
* @param {string} path filepath on the disk * @param {string} path filepath on the disk
* @param {string} [type] mimetype to use * @param {string} [type] mimetype to use
*/ */
const blobFromSync = (path, type) => fromBlob(statSync(path), path, type); const blobFromSync = (path, type) => fromBlob(statSync(path), path, type)
/** /**
* @param {string} path filepath on the disk * @param {string} path filepath on the disk
* @param {string} [type] mimetype to use * @param {string} [type] mimetype to use
*/ */
const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type)); const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type))
/** /**
* @param {string} path filepath on the disk * @param {string} path filepath on the disk
* @param {string} [type] mimetype to use * @param {string} [type] mimetype to use
*/ */
const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type)); const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type))
/** /**
* @param {string} path filepath on the disk * @param {string} path filepath on the disk
* @param {string} [type] mimetype to use * @param {string} [type] mimetype to use
*/ */
const fileFromSync = (path, type) => fromFile(statSync(path), path, type); const fileFromSync = (path, type) => fromFile(statSync(path), path, type)
// @ts-ignore // @ts-ignore
const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({ const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
path, path,
size: stat.size, size: stat.size,
lastModified: stat.mtimeMs, lastModified: stat.mtimeMs,
start: 0 start: 0
})], {type}); })], { type })
// @ts-ignore // @ts-ignore
const fromFile = (stat, path, type = '') => new File([new BlobDataItem({ const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
path, path,
size: stat.size, size: stat.size,
lastModified: stat.mtimeMs, lastModified: stat.mtimeMs,
start: 0 start: 0
})], basename(path), { type, lastModified: stat.mtimeMs }); })], 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
@@ -62,44 +61,44 @@ const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
* @private * @private
*/ */
class BlobDataItem { class BlobDataItem {
#path; #path
#start; #start
constructor(options) { constructor (options) {
this.#path = options.path; this.#path = options.path
this.#start = options.start; this.#start = options.start
this.size = options.size; this.size = options.size
this.lastModified = options.lastModified this.lastModified = options.lastModified
} }
/** /**
* Slicing arguments is first validated and formatted * Slicing arguments is first validated and formatted
* to not be out of range by Blob.prototype.slice * to not be out of range by Blob.prototype.slice
*/ */
slice(start, end) { slice (start, end) {
return new BlobDataItem({ return new BlobDataItem({
path: this.#path, path: this.#path,
lastModified: this.lastModified, lastModified: this.lastModified,
size: end - start, size: end - start,
start start
}); })
} }
async * stream() { async * stream () {
const {mtimeMs} = await stat(this.#path) const { mtimeMs } = await stat(this.#path)
if (mtimeMs > this.lastModified) { 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'); 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, { yield * createReadStream(this.#path, {
start: this.#start, start: this.#start,
end: this.#start + this.size - 1 end: this.#start + this.size - 1
}); })
} }
get [Symbol.toStringTag]() { get [Symbol.toStringTag] () {
return 'Blob'; return 'Blob'
} }
} }
export default blobFromSync; export default blobFromSync
export {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync}; export { File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync }
+207 -202
View File
@@ -1,246 +1,251 @@
/*! 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) // TODO (jimmywarting): in the feature use conditional loading with top level await (requires 14.x)
// Node has recently added whatwg stream into core // Node has recently added whatwg stream into core
import './streams.cjs'; import './streams.cjs'
/** @typedef {import('buffer').Blob} NodeBlob} */ /** @typedef {import('buffer').Blob} NodeBlob} */
// 64 KiB (same size chrome slice theirs blob into Uint8array's) // 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536; const POOL_SIZE = 65536
/** @param {(Blob | NodeBlob | Uint8Array)[]} parts */ /** @param {(Blob | NodeBlob | Uint8Array)[]} parts */
async function * toIterator (parts, clone = true) { async function * toIterator (parts, clone = true) {
for (let part of 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)) { } else if (ArrayBuffer.isView(part)) {
if (clone) { if (clone) {
let position = part.byteOffset; let position = part.byteOffset
let end = part.byteOffset + part.byteLength; const end = part.byteOffset + part.byteLength
while (position !== end) { while (position !== end) {
const size = Math.min(end - position, POOL_SIZE); const size = Math.min(end - position, POOL_SIZE)
const chunk = part.buffer.slice(position, position + size); const chunk = part.buffer.slice(position, position + size)
position += chunk.byteLength; position += chunk.byteLength
yield new Uint8Array(chunk); yield new Uint8Array(chunk)
} }
} else { } else {
yield part; yield part
} }
} else { } else {
/* c8 ignore start */ /* c8 ignore start */
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob) // For blobs that have arrayBuffer but no stream method (nodes buffer.Blob)
let position = 0; let position = 0
while (position !== part.size) { while (position !== part.size) {
const chunk = part.slice(position, Math.min(part.size, position + POOL_SIZE)); const chunk = part.slice(position, Math.min(part.size, position + POOL_SIZE))
const buffer = await chunk.arrayBuffer(); const buffer = await chunk.arrayBuffer()
position += buffer.byteLength; position += buffer.byteLength
yield new Uint8Array(buffer); yield new Uint8Array(buffer)
} }
/* c8 ignore end */ /* c8 ignore end */
} }
} }
} }
const _Blob = class Blob { const _Blob = class Blob {
/** @type {Array.<(Blob|Uint8Array)>} */
#parts = []
#type = ''
#size = 0
/** @type {Array.<(Blob|Uint8Array)>} */ /**
#parts = []; * The Blob() constructor returns a new Blob object. The content
#type = ''; * of the blob consists of the concatenation of the values given
#size = 0; * in the parameter array.
*
* @param {*} blobParts
* @param {{ type?: string }} [options]
*/
constructor (blobParts = [], options = {}) {
if (typeof blobParts !== 'object' || blobParts === null) {
throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.')
}
/** if (typeof blobParts[Symbol.iterator] !== 'function') {
* The Blob() constructor returns a new Blob object. The content throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.')
* of the blob consists of the concatenation of the values given }
* in the parameter array.
*
* @param {*} blobParts
* @param {{ type?: string }} [options]
*/
constructor(blobParts = [], options = {}) {
const parts = [];
let size = 0;
if (typeof blobParts !== 'object') {
throw new TypeError(`Failed to construct 'Blob': parameter 1 is not an iterable object.`);
}
if (typeof options !== 'object' && typeof options !== 'function') { if (typeof options !== 'object' && typeof options !== 'function') {
throw new TypeError(`Failed to construct 'Blob': parameter 2 cannot convert to dictionary.`); throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.')
} }
if (options === null) options = {}; if (options === null) options = {}
for (const element of blobParts) { const encoder = new TextEncoder()
let part; for (const element of blobParts) {
if (ArrayBuffer.isView(element)) { let part
part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength)); if (ArrayBuffer.isView(element)) {
} else if (element instanceof ArrayBuffer) { part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength))
part = new Uint8Array(element.slice(0)); } else if (element instanceof ArrayBuffer) {
} else if (element instanceof Blob) { part = new Uint8Array(element.slice(0))
part = element; } else if (element instanceof Blob) {
} else { part = element
part = new TextEncoder().encode(element); } else {
} part = encoder.encode(element)
}
size += ArrayBuffer.isView(part) ? part.byteLength : part.size; this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size
parts.push(part); this.#parts.push(part)
} }
const type = options.type === undefined ? '' : String(options.type); const type = options.type === undefined ? '' : String(options.type)
this.#type = /^[\x20-\x7E]*$/.test(type) ? type : ''; this.#type = /^[\x20-\x7E]*$/.test(type) ? type : ''
this.#size = size; }
this.#parts = parts;
}
/** /**
* The Blob interface's size property returns the * The Blob interface's size property returns the
* size of the Blob in bytes. * size of the Blob in bytes.
*/ */
get size() { get size () {
return this.#size; return 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 this.#type
} }
/** /**
* The text() method in the Blob interface returns a Promise * The text() method in the Blob interface returns a Promise
* that resolves with a string containing the contents of * that resolves with a string containing the contents of
* the blob, interpreted as UTF-8. * the blob, interpreted as UTF-8.
* *
* @return {Promise<string>} * @return {Promise<string>}
*/ */
async text() { async text () {
// More optimized than using this.arrayBuffer() // More optimized than using this.arrayBuffer()
// that requires twice as much ram // that requires twice as much ram
const decoder = new TextDecoder(); const decoder = new TextDecoder()
let str = ''; let str = ''
for await (let part of toIterator(this.#parts, false)) { for await (const part of toIterator(this.#parts, false)) {
str += decoder.decode(part, { stream: true }); str += decoder.decode(part, { stream: true })
} }
// Remaining // Remaining
str += decoder.decode(); str += decoder.decode()
return str; return str
} }
/** /**
* The arrayBuffer() method in the Blob interface returns a * The arrayBuffer() method in the Blob interface returns a
* Promise that resolves with the contents of the blob as * Promise that resolves with the contents of the blob as
* binary data contained in an ArrayBuffer. * binary data contained in an ArrayBuffer.
* *
* @return {Promise<ArrayBuffer>} * @return {Promise<ArrayBuffer>}
*/ */
async arrayBuffer() { async arrayBuffer () {
// Easier way... Just a unnecessary overhead // Easier way... Just a unnecessary overhead
// const view = new Uint8Array(this.size); // const view = new Uint8Array(this.size);
// await this.stream().getReader({mode: 'byob'}).read(view); // await this.stream().getReader({mode: 'byob'}).read(view);
// return view.buffer; // return view.buffer;
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 toIterator(this.#parts, false)) {
data.set(chunk, offset); data.set(chunk, offset)
offset += chunk.length; offset += chunk.length
} }
return data.buffer; return data.buffer
} }
stream() { stream () {
const it = toIterator(this.#parts, true); const it = toIterator(this.#parts, true)
return new ReadableStream({ return new globalThis.ReadableStream({
type: 'bytes', type: 'bytes',
async pull(ctrl) { async pull (ctrl) {
const chunk = await it.next(); const chunk = await it.next()
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value); chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value)
} },
})
}
/** async cancel () {
* The Blob interface's slice() method creates and returns a await it.return()
* 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;
let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); /**
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); * 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
const span = Math.max(relativeEnd - relativeStart, 0); let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size)
const parts = this.#parts; let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size)
const blobParts = [];
let added = 0;
for (const part of parts) { const span = Math.max(relativeEnd - relativeStart, 0)
// don't add the overflow to new blobParts const parts = this.#parts
if (added >= span) { const blobParts = []
break; let added = 0
}
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size; for (const part of parts) {
if (relativeStart && size <= relativeStart) { // don't add the overflow to new blobParts
// Skip the beginning and change the relative if (added >= span) {
// start & end position as we skip the unwanted parts break
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()}); const size = ArrayBuffer.isView(part) ? part.byteLength : part.size
blob.#size = span; if (relativeStart && size <= relativeStart) {
blob.#parts = blobParts; // 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
}
}
return blob; const blob = new Blob([], { type: String(type).toLowerCase() })
} blob.#size = span
blob.#parts = blobParts
get [Symbol.toStringTag]() { return blob
return 'Blob'; }
}
static [Symbol.hasInstance](object) { get [Symbol.toStringTag] () {
return ( return 'Blob'
object && }
typeof object === 'object' &&
typeof object.constructor === 'function' && static [Symbol.hasInstance] (object) {
( return (
typeof object.stream === 'function' || object &&
typeof object.arrayBuffer === 'function' typeof object === 'object' &&
) && typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[Symbol.toStringTag]) (
); 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 }
}); })
/** @type {typeof globalThis.Blob} */ /** @type {typeof globalThis.Blob} */
export const Blob = _Blob; export const Blob = _Blob
export default Blob; export default Blob
+3 -29
View File
@@ -1,6 +1,6 @@
{ {
"name": "fetch-blob", "name": "fetch-blob",
"version": "3.1.2", "version": "3.1.3",
"description": "Blob & File 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", "type": "module",
@@ -14,9 +14,8 @@
"streams.cjs" "streams.cjs"
], ],
"scripts": { "scripts": {
"lint": "xo test.js",
"test-wpt": "node --experimental-loader ./test/http-loader.js ./test/test-wpt-in-node.js", "test-wpt": "node --experimental-loader ./test/http-loader.js ./test/test-wpt-in-node.js",
"test": "npm run lint && ava test.js", "test": "ava test.js",
"report": "c8 --reporter json --reporter text ava test.js", "report": "c8 --reporter json --reporter text ava test.js",
"coverage": "c8 --reporter json --reporter text ava test.js && codecov -f coverage/coverage-final.json", "coverage": "c8 --reporter json --reporter text ava test.js && codecov -f coverage/coverage-final.json",
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js from.js" "prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js from.js"
@@ -36,37 +35,12 @@
"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": {
"rules": {
"unicorn/prefer-node-protocol": "off",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-spread": "off",
"unicorn/prefer-number-properties": "off",
"import/extensions": [
"error",
"always",
{
"ignorePackages": true
}
]
},
"overrides": [
{
"files": "test.js",
"rules": {
"node/no-unsupported-features/es-syntax": 0,
"node/no-unsupported-features/node-builtins": 0
}
}
]
},
"devDependencies": { "devDependencies": {
"ava": "^3.15.0", "ava": "^3.15.0",
"c8": "^7.7.2", "c8": "^7.7.2",
"codecov": "^3.8.2", "codecov": "^3.8.2",
"node-fetch": "^3.0.0-beta.9", "node-fetch": "^3.0.0-beta.9",
"typescript": "^4.3.2", "typescript": "^4.3.2"
"xo": "^0.40.1"
}, },
"funding": [ "funding": [
{ {
+35 -21
View File
@@ -1,37 +1,51 @@
/* c8 ignore start */ /* c8 ignore start */
// 64 KiB (same size chrome slice theirs blob into Uint8array's) // 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536; const POOL_SIZE = 65536
if (!globalThis.ReadableStream) { 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 { try {
Object.assign(globalThis, require('stream/web')) 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) { } catch (error) {
// TODO: Remove when only supporting node >= 16.5.0 // fallback to polyfill implementation
Object.assign(globalThis, require('web-streams-polyfill/dist/ponyfill.es2018.js')) Object.assign(globalThis, require('web-streams-polyfill/dist/ponyfill.es2018.js'))
} }
} }
try { try {
const {Blob} = require('buffer') // 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) { if (Blob && !Blob.prototype.stream) {
Blob.prototype.stream = function name(params) { Blob.prototype.stream = function name (params) {
let position = 0; let position = 0
const blob = this; const blob = this
return new ReadableStream({ return new ReadableStream({
type: 'bytes', type: 'bytes',
async pull(ctrl) { async pull (ctrl) {
const chunk = blob.slice(position, Math.min(blob.size, position + POOL_SIZE)); const chunk = blob.slice(position, Math.min(blob.size, position + POOL_SIZE))
const buffer = await chunk.arrayBuffer(); const buffer = await chunk.arrayBuffer()
position += buffer.byteLength; position += buffer.byteLength
ctrl.enqueue(new Uint8Array(buffer)) ctrl.enqueue(new Uint8Array(buffer))
if (position === blob.size) { if (position === blob.size) {
ctrl.close() ctrl.close()
} }
} }
}) })
} }
} }
} catch (error) {} } catch (error) {}
/* c8 ignore end */ /* c8 ignore end */
+316 -270
View File
@@ -1,396 +1,442 @@
import fs from 'fs'; import fs from 'fs'
import {Readable} from 'stream'; import { Readable } from 'stream'
import buffer from 'buffer'; import buffer from 'buffer'
import test from 'ava'; import test from 'ava'
import {Response} from 'node-fetch'; import { Response } from 'node-fetch'
import syncBlob, {blobFromSync, blobFrom, fileFromSync, fileFrom} from './from.js'; import syncBlob, { blobFromSync, blobFrom, fileFromSync, fileFrom } from './from.js'
import File from './file.js'; import File from './file.js'
import Blob from './index.js'; import Blob from './index.js'
const license = fs.readFileSync('./LICENSE', 'utf-8'); 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
t.pass(); t.pass()
}); })
test('new Blob(parts)', t => { test('new Blob(parts)', t => {
const data = 'a=1'; const data = 'a=1'
const blob = new Blob([data]); // eslint-disable-line no-unused-vars const blob = new Blob([data]) // eslint-disable-line no-unused-vars
t.pass(); t.pass()
}); })
test('Blob ctor parts', async t => { test('Blob ctor parts', async t => {
const parts = [ const parts = [
'a', 'a',
new Uint8Array([98]), new Uint8Array([98]),
new Uint16Array([25699]), new Uint16Array([25699]),
new Uint8Array([101]).buffer, new Uint8Array([101]).buffer,
Buffer.from('f'), Buffer.from('f'),
new Blob(['g']), new Blob(['g']),
{}, {},
new URLSearchParams('foo') 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]foo=')
}); })
test('Blob ctor threats an object with @@iterator as a sequence', async t => { test('Blob ctor threats an object with @@iterator as a sequence', async t => {
const blob = new Blob({[Symbol.iterator]: Array.prototype[Symbol.iterator]}); const blob = new Blob({ [Symbol.iterator]: Array.prototype[Symbol.iterator] })
t.is(blob.size, 0); t.is(blob.size, 0)
t.is(await blob.text(), ''); t.is(await blob.text(), '')
}); })
test('Blob ctor reads blob parts from object with @@iterator', async t => { test('Blob ctor reads blob parts from object with @@iterator', async t => {
const input = ['one', 'two', 'three']; const input = ['one', 'two', 'three']
const expected = input.join(''); const expected = input.join('')
const blob = new Blob({ const blob = new Blob({
* [Symbol.iterator]() { * [Symbol.iterator] () {
yield * input; yield * input
} }
}); })
t.is(blob.size, new TextEncoder().encode(expected).byteLength); t.is(blob.size, new TextEncoder().encode(expected).byteLength)
t.is(await blob.text(), expected); t.is(await blob.text(), expected)
}); })
test('Blob ctor throws a string', t => { test('Blob ctor throws a string', t => {
t.throws(() => new Blob('abc')); t.throws(() => new Blob('abc'), {
}); instanceOf: TypeError,
message: 'Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'
})
})
test('Blob ctor throws an error for an object that does not have @@iterable method', t => {
t.throws(() => new Blob({}), {
instanceOf: TypeError,
message: 'Failed to construct \'Blob\': The object must have a callable @@iterator property.'
})
})
test('Blob ctor threats Uint8Array as a sequence', async t => { test('Blob ctor threats Uint8Array as a sequence', async t => {
const input = [1, 2, 3]; const input = [1, 2, 3]
const blob = new Blob(new Uint8Array(input)); const blob = new Blob(new Uint8Array(input))
t.is(await blob.text(), input.join('')); t.is(await blob.text(), input.join(''))
}); })
test('Blob size', t => { test('Blob size', t => {
const data = 'a=1'; const data = 'a=1'
const blob = new Blob([data]); const blob = new Blob([data])
t.is(blob.size, data.length); t.is(blob.size, data.length)
}); })
test('Blob type', t => { test('Blob type', t => {
const type = 'text/plain'; const type = 'text/plain'
const blob = new Blob([], {type}); const blob = new Blob([], { type })
t.is(blob.type, type); t.is(blob.type, type)
}); })
test('Blob slice type', t => { test('Blob slice type', t => {
const type = 'text/plain'; const type = 'text/plain'
const blob = new Blob().slice(0, 0, type); const blob = new Blob().slice(0, 0, type)
t.is(blob.type, type); t.is(blob.type, type)
}); })
test('invalid Blob type', t => { test('invalid Blob type', t => {
const blob = new Blob([], {type: '\u001Ftext/plain'}); const blob = new Blob([], { type: '\u001Ftext/plain' })
t.is(blob.type, ''); t.is(blob.type, '')
}); })
test('invalid Blob slice type', t => { test('invalid Blob slice type', t => {
const blob = new Blob().slice(0, 0, '\u001Ftext/plain'); const blob = new Blob().slice(0, 0, '\u001Ftext/plain')
t.is(blob.type, ''); t.is(blob.type, '')
}); })
test('Blob text()', async t => { test('Blob 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 })
t.is(await blob.text(), data); t.is(await blob.text(), data)
}); })
test('Blob arrayBuffer()', async t => { test('Blob arrayBuffer()', 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 decoder = new TextDecoder('utf-8'); const decoder = new TextDecoder('utf-8')
const buffer = await blob.arrayBuffer(); const buffer = await blob.arrayBuffer()
t.is(decoder.decode(buffer), data); t.is(decoder.decode(buffer), data)
}); })
test('Blob stream()', async t => { 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 })
for await (const chunk of blob.stream()) { for await (const chunk of blob.stream()) {
t.is(chunk.join(), [97, 61, 49].join()); t.is(chunk.join(), [97, 61, 49].join())
} }
}); })
test('Blob stream() can be cancelled', async t => {
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()
t.true(done)
t.is(chunk, undefined)
})
test('Blob toString()', t => { test('Blob toString()', 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 })
t.is(blob.toString(), '[object Blob]'); t.is(blob.toString(), '[object Blob]')
}); })
test('Blob slice()', async t => { test('Blob slice()', async t => {
const data = 'abcdefgh'; const data = 'abcdefgh'
const blob = new Blob([data]).slice(); const blob = new Blob([data]).slice()
t.is(await blob.text(), data); t.is(await blob.text(), data)
}); })
test('Blob slice(0, 1)', async t => { test('Blob slice(0, 1)', async t => {
const data = 'abcdefgh'; const data = 'abcdefgh'
const blob = new Blob([data]).slice(0, 1); const blob = new Blob([data]).slice(0, 1)
t.is(await blob.text(), 'a'); t.is(await blob.text(), 'a')
}); })
test('Blob slice(-1)', async t => { test('Blob slice(-1)', async t => {
const data = 'abcdefgh'; const data = 'abcdefgh'
const blob = new Blob([data]).slice(-1); const blob = new Blob([data]).slice(-1)
t.is(await blob.text(), 'h'); t.is(await blob.text(), 'h')
}); })
test('Blob slice(0, -1)', async t => { test('Blob slice(0, -1)', async t => {
const data = 'abcdefgh'; const data = 'abcdefgh'
const blob = new Blob([data]).slice(0, -1); const blob = new Blob([data]).slice(0, -1)
t.is(await blob.text(), 'abcdefg'); t.is(await blob.text(), 'abcdefg')
}); })
test('Blob(["hello ", "world"]).slice(5)', async t => { test('Blob(["hello ", "world"]).slice(5)', async t => {
const parts = ['hello ', 'world']; const parts = ['hello ', 'world']
const blob = new Blob(parts); const blob = new Blob(parts)
t.is(await blob.slice(5).text(), ' world'); 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')
}); })
test('Blob works with node-fetch Response.blob()', async t => { 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(Readable.from(blob.stream()))
const blob2 = await response.blob(); const blob2 = await response.blob()
t.is(await blob2.text(), data); t.is(await blob2.text(), data)
}); })
test('Blob works with node-fetch Response.text()', async t => { 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(Readable.from(blob.stream()))
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 => { test('blob part backed up by filesystem', async t => {
const blob = blobFromSync('./LICENSE'); const blob = blobFromSync('./LICENSE')
t.is(await blob.slice(0, 3).text(), license.slice(0, 3)); t.is(await blob.slice(0, 3).text(), license.slice(0, 3))
t.is(await blob.slice(4, 11).text(), license.slice(4, 11)); t.is(await blob.slice(4, 11).text(), license.slice(4, 11))
}); })
test('Reading after modified should fail', async t => { test('Reading after modified should fail', async t => {
const blob = blobFromSync('./LICENSE'); const blob = blobFromSync('./LICENSE')
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, 500); setTimeout(resolve, 500)
}); })
fs.closeSync(fs.openSync('./LICENSE', 'a')); fs.closeSync(fs.openSync('./LICENSE', 'a'))
const error = await t.throwsAsync(blob.text()); const error = await t.throwsAsync(blob.text())
t.is(error.constructor.name, 'DOMException'); t.is(error.constructor.name, 'DOMException')
t.is(error instanceof Error, true); t.is(error instanceof Error, true)
t.is(error.name, 'NotReadableError'); t.is(error.name, 'NotReadableError')
const file = fileFromSync('./LICENSE'); const file = fileFromSync('./LICENSE')
// Above test updates the last modified date to now // Above test updates the last modified date to now
t.is(typeof file.lastModified, 'number'); t.is(typeof file.lastModified, 'number')
// The lastModifiedDate is deprecated and removed from spec // The lastModifiedDate is deprecated and removed from spec
t.false('lastModifiedDate' in file); t.false('lastModifiedDate' in file)
const mod = file.lastModified - Date.now(); const mod = file.lastModified - Date.now()
t.true(mod <= 0 && mod >= -500); // Close to tolerance: 0.500ms t.true(mod <= 0 && mod >= -500) // Close to tolerance: 0.500ms
}); })
test('Reading file after modified should fail', async t => { test('Reading file after modified should fail', async t => {
const file = fileFromSync('./LICENSE'); const file = fileFromSync('./LICENSE')
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, 100); setTimeout(resolve, 100)
}); })
const now = new Date(); const now = new Date()
// Change modified time // Change modified time
fs.utimesSync('./LICENSE', now, now); fs.utimesSync('./LICENSE', now, now)
const error = await t.throwsAsync(file.text()); const error = await t.throwsAsync(file.text())
t.is(error.constructor.name, 'DOMException'); t.is(error.constructor.name, 'DOMException')
t.is(error instanceof Error, true); t.is(error instanceof Error, true)
t.is(error.name, 'NotReadableError'); t.is(error.name, 'NotReadableError')
}); })
test('Reading from the stream created by blobFrom', async t => { test('Reading from the stream created by blobFrom', async t => {
const blob = blobFromSync('./LICENSE'); const blob = blobFromSync('./LICENSE')
const actual = await blob.text(); const actual = await blob.text()
t.is(actual, license); t.is(actual, license)
}); })
test('create a blob from path asynchronous', async t => { test('create a blob from path asynchronous', async t => {
const blob = await blobFrom('./LICENSE'); const blob = await blobFrom('./LICENSE')
const actual = await blob.text(); const actual = await blob.text()
t.is(actual, license); t.is(actual, license)
}); })
test('Reading empty blobs', async t => { test('Reading empty blobs', async t => {
const blob = blobFromSync('./LICENSE').slice(0, 0); const blob = blobFromSync('./LICENSE').slice(0, 0)
const actual = await blob.text(); const actual = await blob.text()
t.is(actual, ''); t.is(actual, '')
}); })
test('Blob-ish class is an instance of Blob', t => { test('Blob-ish class is an instance of Blob', t => {
class File { class File {
stream() {} stream () {}
get [Symbol.toStringTag]() { get [Symbol.toStringTag] () {
return 'File'; return 'File'
} }
} }
t.true(new File() instanceof Blob); t.true(new File() instanceof Blob)
}); })
test('Instanceof check returns false for nullish values', t => { test('Instanceof check returns false for nullish values', t => {
t.false(null instanceof Blob); t.false(null instanceof Blob)
}); })
/** @see https://github.com/w3c/FileAPI/issues/43 - important to keep boundary value */ /** @see https://github.com/w3c/FileAPI/issues/43 - important to keep boundary value */
test('Dose not lowercase the blob values', t => { test('Dose not lowercase the blob values', t => {
const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd'; const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd'
t.is(new Blob([], {type}).type, type); t.is(new Blob([], { type }).type, type)
}); })
test('Parts are immutable', async t => { test('Parts are immutable', async t => {
const buf = new Uint8Array([97]); const buf = new Uint8Array([97])
const blob = new Blob([buf]); const blob = new Blob([buf])
buf[0] = 98; buf[0] = 98
t.is(await blob.text(), 'a'); t.is(await blob.text(), 'a')
}); })
test('Blobs are immutable', async t => { test('Blobs are immutable', async t => {
const buf = new Uint8Array([97]); const buf = new Uint8Array([97])
const blob = new Blob([buf]); const blob = new Blob([buf])
const chunk = await blob.stream().getReader().read(); const chunk = await blob.stream().getReader().read()
t.is(chunk.value[0], 97); t.is(chunk.value[0], 97)
chunk.value[0] = 98; chunk.value[0] = 98
t.is(await blob.text(), 'a'); t.is(await blob.text(), 'a')
}); })
// This was necessary to avoid large ArrayBuffer clones (slice) // This was necessary to avoid large ArrayBuffer clones (slice)
test('Large chunks are divided into smaller chunks', async t => { test('Large chunks are divided into smaller chunks', async t => {
const buf = new Uint8Array(65590); const buf = new Uint8Array(65590)
const blob = new Blob([buf]); const blob = new Blob([buf])
let i = 0; let i = 0
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
for await (const chunk of blob.stream()) { for await (const chunk of blob.stream()) {
i++; i++
} }
t.is(i === 2, true); t.is(i === 2, true)
}); })
test('Can use named import - as well as default', async t => { test('Can use named import - as well as default', async t => {
// eslint-disable-next-line node/no-unsupported-features/es-syntax // eslint-disable-next-line node/no-unsupported-features/es-syntax
const {Blob, default: def} = await import('./index.js'); const { Blob, default: def } = await import('./index.js')
t.is(Blob, def); t.is(Blob, def)
}); })
test('default from.js exports blobFromSync', t => { test('default from.js exports blobFromSync', t => {
t.is(blobFromSync, syncBlob); t.is(blobFromSync, syncBlob)
}); })
if (buffer.Blob) { if (buffer.Blob) {
test('Can wrap buffer.Blob to a fetch-blob', async t => { test('Can wrap buffer.Blob to a fetch-blob', async t => {
const blob1 = new buffer.Blob(['blob part']); const blob1 = new buffer.Blob(['blob part'])
const blob2 = new Blob([blob1]); const blob2 = new Blob([blob1])
t.is(await blob2.text(), 'blob part'); t.is(await blob2.text(), 'blob part')
}); })
} }
test('File is a instance of blob', t => { test('File is a instance of blob', t => {
t.true(new File([], '') instanceof Blob); t.true(new File([], '') instanceof Blob)
}); })
test('fileFrom returns the name', async t => { test('fileFrom returns the name', async t => {
t.is((await fileFrom('./LICENSE')).name, 'LICENSE'); t.is((await fileFrom('./LICENSE')).name, 'LICENSE')
}); })
test('fileFromSync returns the name', t => { test('fileFromSync returns the name', t => {
t.is(fileFromSync('./LICENSE').name, 'LICENSE'); t.is(fileFromSync('./LICENSE').name, 'LICENSE')
}); })
test('fileFromSync(path, type) sets the type', t => { test('fileFromSync(path, type) sets the type', t => {
t.is(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain'); t.is(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain')
}); })
test('blobFromSync(path, type) sets the type', t => { test('blobFromSync(path, type) sets the type', t => {
t.is(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain'); t.is(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain')
}); })
test('fileFrom(path, type) sets the type', async t => { test('fileFrom(path, type) sets the type', async t => {
const file = await fileFrom('./LICENSE', 'text/plain'); const file = await fileFrom('./LICENSE', 'text/plain')
t.is(file.type, 'text/plain'); t.is(file.type, 'text/plain')
}); })
test('new File(,,{lastModified: 100})', t => { test('new File(,,{lastModified: 100})', t => {
const mod = new File([], '', {lastModified: 100}).lastModified; const mod = new File([], '', { lastModified: 100 }).lastModified
t.is(mod, 100); t.is(mod, 100)
}); })
test('new File(,,{lastModified: "200"})', t => { test('new File(,,{lastModified: "200"})', t => {
const mod = new File([], '', {lastModified: '200'}).lastModified; const mod = new File([], '', { lastModified: '200' }).lastModified
t.is(mod, 200); t.is(mod, 200)
}); })
test('new File(,,{lastModified: true})', t => { test('new File(,,{lastModified: true})', t => {
const mod = new File([], '', {lastModified: true}).lastModified; const mod = new File([], '', { lastModified: true }).lastModified
t.is(mod, 1); t.is(mod, 1)
}); })
test('new File(,,{lastModified: new Date()})', t => { test('new File(,,{lastModified: new Date()})', t => {
const mod = new File([], '', {lastModified: new Date()}).lastModified - Date.now(); const mod = new File([], '', { lastModified: new Date() }).lastModified - Date.now()
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms t.true(mod <= 0 && mod >= -20) // Close to tolerance: 0.020ms
}); })
test('new File(,,{lastModified: undefined})', t => {
const mod = new File([], '', { lastModified: undefined }).lastModified - Date.now()
t.true(mod <= 0 && mod >= -20) // Close to tolerance: 0.020ms
})
test('new File(,,{lastModified: null})', t => {
const mod = new File([], '', { lastModified: null }).lastModified
t.is(mod, 0)
})
test('Interpretes NaN value in lastModified option as 0', t => {
t.plan(3)
const values = ['Not a Number', [], {}]
// 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(['Some content'], 'file.txt', { lastModified })
t.is(file.lastModified, 0)
}
})
test('new File(,,{}) sets current time', t => { test('new File(,,{}) sets current time', t => {
const mod = new File([], '').lastModified - Date.now(); const mod = new File([], '').lastModified - Date.now()
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms t.true(mod <= 0 && mod >= -20) // Close to tolerance: 0.020ms
}); })
test('blobFrom(path, type) sets the type', async t => { test('blobFrom(path, type) sets the type', async t => {
const blob = await blobFrom('./LICENSE', 'text/plain'); const blob = await blobFrom('./LICENSE', 'text/plain')
t.is(blob.type, 'text/plain'); t.is(blob.type, 'text/plain')
}); })
test('blobFrom(path) sets empty type', async t => { test('blobFrom(path) sets empty type', async t => {
const blob = await blobFrom('./LICENSE'); const blob = await blobFrom('./LICENSE')
t.is(blob.type, ''); t.is(blob.type, '')
}); })
test('new File() throws with too few args', t => { test('new File() throws with too few args', t => {
t.throws(() => new File(), { t.throws(() => new File(), {
instanceOf: TypeError, instanceOf: TypeError,
message: 'Failed to construct \'File\': 2 arguments required, but only 0 present.' message: 'Failed to construct \'File\': 2 arguments required, but only 0 present.'
}); })
}); })
test('can slice zero sized blobs', async t => { test('can slice zero sized blobs', async t => {
const blob = new Blob(); const blob = new Blob()
const txt = await blob.slice(0, 0).text(); const txt = await blob.slice(0, 0).text()
t.is(txt, ''); t.is(txt, '')
}); })
test('returns a readable stream', t => { test('returns a readable stream', t => {
const stream = new File([], '').stream(); const stream = new File([], '').stream()
t.true(typeof stream.getReader === 'function'); t.true(typeof stream.getReader === 'function')
}); })
test('checking instanceof blob#stream', async t => { test('checking instanceof blob#stream', t => {
// eslint-disable-next-line node/no-unsupported-features/es-syntax const stream = new File([], '').stream()
const {ReadableStream} = await import('stream/web').catch(_ => import('web-streams-polyfill/dist/ponyfill.es2018.js')); t.true(stream instanceof globalThis.ReadableStream)
const stream = new File([], '').stream(); })
t.true(stream instanceof ReadableStream);
});