diff --git a/file.js b/file.js index e16e299..dad091c 100644 --- a/file.js +++ b/file.js @@ -1,44 +1,44 @@ -import Blob from './index.js'; +import Blob from './index.js' const _File = class File extends Blob { - #lastModified = 0; - #name = ''; + #lastModified = 0 + #name = '' /** * @param {*[]} fileBits * @param {string} fileName * @param {{lastModified?: number, type?: string}} options */// @ts-ignore - constructor(fileBits, fileName, options = {}) { + constructor (fileBits, fileName, options = {}) { 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 = {} // Simulate WebIDL type casting for NaN value in lastModified option. - const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified); + const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified) if (!Number.isNaN(lastModified)) { - this.#lastModified = lastModified; + this.#lastModified = lastModified } - this.#name = String(fileName); + this.#name = String(fileName) } - get name() { - return this.#name; + get name () { + return this.#name } - get lastModified() { - return this.#lastModified; + get lastModified () { + return this.#lastModified } - get [Symbol.toStringTag]() { - return "File"; + get [Symbol.toStringTag] () { + return 'File' } } /** @type {typeof globalThis.File} */// @ts-ignore -export const File = _File; -export default File; +export const File = _File +export default File diff --git a/from.js b/from.js index 02e938e..430b071 100644 --- a/from.js +++ b/from.js @@ -1,58 +1,57 @@ -import {statSync, createReadStream, promises as fs} from 'node:fs'; -import {basename} from 'node:path'; -import {MessageChannel} from 'node:worker_threads'; +import { statSync, createReadStream, promises as fs } from 'node:fs' +import { basename } from 'node:path' +import { MessageChannel } from 'node:worker_threads' -import File from './file.js'; -import Blob from './index.js'; +import File from './file.js' +import Blob from './index.js' -const {stat} = fs; +const { stat } = fs 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 } + 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); +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)); +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)); +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 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}); + 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 }); + 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 @@ -62,44 +61,44 @@ const fromFile = (stat, path, type = '') => new File([new BlobDataItem({ * @private */ class BlobDataItem { - #path; - #start; + #path + #start - constructor(options) { - this.#path = options.path; - this.#start = options.start; - this.size = options.size; - this.lastModified = options.lastModified - } + 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 - }); - } + /** + * 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'); - } - yield * createReadStream(this.#path, { - start: this.#start, - end: this.#start + this.size - 1 - }); - } + 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') + } + yield * createReadStream(this.#path, { + start: this.#start, + end: this.#start + this.size - 1 + }) + } - get [Symbol.toStringTag]() { - return 'Blob'; - } + get [Symbol.toStringTag] () { + return 'Blob' + } } -export default blobFromSync; -export {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync}; +export default blobFromSync +export { File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync } diff --git a/index.js b/index.js index fccae07..2148d73 100644 --- a/index.js +++ b/index.js @@ -3,250 +3,249 @@ // TODO (jimmywarting): in the feature use conditional loading with top level await (requires 14.x) // Node has recently added whatwg stream into core -import './streams.cjs'; +import './streams.cjs' /** @typedef {import('buffer').Blob} NodeBlob} */ // 64 KiB (same size chrome slice theirs blob into Uint8array's) -const POOL_SIZE = 65536; +const POOL_SIZE = 65536 /** @param {(Blob | NodeBlob | Uint8Array)[]} parts */ async function * toIterator (parts, clone = true) { - for (let part of parts) { - if ('stream' in part) { - 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); - position += chunk.byteLength; - yield new Uint8Array(chunk); - } - } else { - yield part; - } - } else { - /* c8 ignore start */ - // 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); - } - /* c8 ignore end */ - } - } + for (const part of parts) { + if ('stream' in part) { + yield * 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 + } + } else { + /* c8 ignore start */ + // 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) + } + /* c8 ignore end */ + } + } } const _Blob = class Blob { + /** @type {Array.<(Blob|Uint8Array)>} */ + #parts = [] + #type = '' + #size = 0 - /** @type {Array.<(Blob|Uint8Array)>} */ - #parts = []; - #type = ''; - #size = 0; + /** + * The Blob() constructor returns a new Blob object. The content + * of the blob consists of the concatenation of the values given + * 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.') + } - /** - * The Blob() constructor returns a new Blob object. The content - * of the blob consists of the concatenation of the values given - * 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') { + throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.') + } - if (typeof blobParts[Symbol.iterator] !== "function") { - throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.'); - } + if (typeof options !== 'object' && typeof options !== 'function') { + throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.') + } - if (typeof options !== 'object' && typeof options !== 'function') { - throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.'); - } + if (options === null) options = {} - if (options === null) options = {}; + const encoder = new TextEncoder() + for (const element of blobParts) { + let part + if (ArrayBuffer.isView(element)) { + part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength)) + } else if (element instanceof ArrayBuffer) { + part = new Uint8Array(element.slice(0)) + } else if (element instanceof Blob) { + part = element + } else { + part = encoder.encode(element) + } - const encoder = new TextEncoder() - for (const element of blobParts) { - let part; - if (ArrayBuffer.isView(element)) { - part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength)); - } 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 + this.#parts.push(part) + } - this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size; - 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 : ''; - } + /** + * The Blob interface's size property returns the + * size of the Blob in bytes. + */ + get size () { + return this.#size + } - /** - * The Blob interface's size property returns the - * size of the Blob in bytes. - */ - get size() { - return this.#size; - } + /** + * The type property of a Blob object returns the MIME type of the file. + */ + get type () { + return this.#type + } - /** - * The type property of a Blob object returns the MIME type of the file. - */ - get type() { - return this.#type; - } + /** + * The text() method in the Blob interface returns a Promise + * that resolves with a string containing the contents of + * the blob, interpreted as UTF-8. + * + * @return {Promise} + */ + 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 text() method in the Blob interface returns a Promise - * that resolves with a string containing the contents of - * the blob, interpreted as UTF-8. - * - * @return {Promise} - */ - async text() { - // More optimized than using this.arrayBuffer() - // 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; - } + /** + * The arrayBuffer() method in the Blob interface returns a + * Promise that resolves with the contents of the blob as + * binary data contained in an ArrayBuffer. + * + * @return {Promise} + */ + async arrayBuffer () { + // Easier way... Just a unnecessary overhead + // const view = new Uint8Array(this.size); + // await this.stream().getReader({mode: 'byob'}).read(view); + // return view.buffer; - /** - * The arrayBuffer() method in the Blob interface returns a - * Promise that resolves with the contents of the blob as - * binary data contained in an ArrayBuffer. - * - * @return {Promise} - */ - async arrayBuffer() { - // Easier way... Just a unnecessary overhead - // const view = new Uint8Array(this.size); - // await this.stream().getReader({mode: 'byob'}).read(view); - // return view.buffer; + const data = new Uint8Array(this.size) + let offset = 0 + for await (const chunk of toIterator(this.#parts, false)) { + data.set(chunk, offset) + offset += chunk.length + } - const data = new Uint8Array(this.size); - let offset = 0; - for await (const chunk of toIterator(this.#parts, false)) { - data.set(chunk, offset); - offset += chunk.length; - } + return data.buffer + } - return data.buffer; - } + stream () { + const it = toIterator(this.#parts, true) - stream() { - const it = toIterator(this.#parts, true); + return new globalThis.ReadableStream({ + type: 'bytes', + async pull (ctrl) { + const chunk = await it.next() + chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value) + }, - return new ReadableStream({ - type: 'bytes', - async pull(ctrl) { - const chunk = await it.next(); - chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value); - }, + async cancel () { + await it.return() + } + }) + } - async cancel() { - await it.return() - } - }) - } + /** + * 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 - /** - * 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; + 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) - 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); + const span = Math.max(relativeEnd - relativeStart, 0) + const parts = this.#parts + const blobParts = [] + let added = 0 - const span = Math.max(relativeEnd - relativeStart, 0); - const parts = this.#parts; - const blobParts = []; - let added = 0; + for (const part of parts) { + // don't add the overflow to new blobParts + if (added >= span) { + break + } - for (const part of parts) { - // 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 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 - const blob = new Blob([], {type: String(type).toLowerCase()}); - blob.#size = span; - blob.#parts = blobParts; + return blob + } - return blob; - } + get [Symbol.toStringTag] () { + 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]) - ); - } + 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, { - size: {enumerable: true}, - type: {enumerable: true}, - slice: {enumerable: true} -}); + size: { enumerable: true }, + type: { enumerable: true }, + slice: { enumerable: true } +}) /** @type {typeof globalThis.Blob} */ -export const Blob = _Blob; -export default Blob; +export const Blob = _Blob +export default Blob diff --git a/test.js b/test.js index 876c975..a2968fc 100644 --- a/test.js +++ b/test.js @@ -1,444 +1,442 @@ -import fs from 'fs'; -import {Readable} from 'stream'; -import buffer from 'buffer'; -import test from 'ava'; -import {Response} from 'node-fetch'; -import syncBlob, {blobFromSync, blobFrom, fileFromSync, fileFrom} from './from.js'; -import File from './file.js'; -import Blob from './index.js'; +import fs from 'fs' +import { Readable } from 'stream' +import buffer from 'buffer' +import test from 'ava' +import { Response } from 'node-fetch' +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'); +const license = fs.readFileSync('./LICENSE', 'utf-8') test('new Blob()', t => { - const blob = new Blob(); // eslint-disable-line no-unused-vars - t.pass(); -}); + 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(); -}); + 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']), - {}, - new URLSearchParams('foo') - ]; + const parts = [ + 'a', + new Uint8Array([98]), + new Uint16Array([25699]), + new Uint8Array([101]).buffer, + Buffer.from('f'), + new Blob(['g']), + {}, + new URLSearchParams('foo') + ] - const blob = new Blob(parts); - t.is(await blob.text(), 'abcdefg[object Object]foo='); -}); + const blob = new Blob(parts) + t.is(await blob.text(), 'abcdefg[object Object]foo=') +}) 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(await blob.text(), ''); -}); + t.is(blob.size, 0) + t.is(await blob.text(), '') +}) test('Blob ctor reads blob parts from object with @@iterator', async t => { - const input = ['one', 'two', 'three']; - const expected = input.join(''); + const input = ['one', 'two', 'three'] + const expected = input.join('') - const blob = new Blob({ - * [Symbol.iterator]() { - yield * input; - } - }); + const blob = new Blob({ + * [Symbol.iterator] () { + yield * input + } + }) - t.is(blob.size, new TextEncoder().encode(expected).byteLength); - t.is(await blob.text(), expected); -}); + t.is(blob.size, new TextEncoder().encode(expected).byteLength) + t.is(await blob.text(), expected) +}) test('Blob ctor throws a string', t => { - t.throws(() => new Blob('abc'), { - instanceOf: TypeError, - message: 'Failed to construct \'Blob\': The provided value cannot be converted to a sequence.' - }); -}); + 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.' - }); -}); + 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 => { - const input = [1, 2, 3]; - const blob = new Blob(new Uint8Array(input)); + const input = [1, 2, 3] + 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 => { - const data = 'a=1'; - const blob = new Blob([data]); - t.is(blob.size, data.length); -}); + 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); -}); + 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); -}); + 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, ''); -}); + 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, ''); -}); + 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); -}); + 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 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); -}); + 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 data = 'a=1' + const type = 'text/plain' + const blob = new Blob([data], { type }) - for await (const chunk of blob.stream()) { - t.is(chunk.join(), [97, 61, 49].join()); - } -}); + for await (const chunk of blob.stream()) { + t.is(chunk.join(), [97, 61, 49].join()) + } +}) test('Blob stream() can be cancelled', async t => { - const stream = new Blob(['Some content']).stream(); + const stream = new Blob(['Some content']).stream() - // Cancel the stream before start reading, or this will throw an error - await stream.cancel(); + // Cancel the stream before start reading, or this will throw an error + await stream.cancel() - const reader = stream.getReader(); + const reader = stream.getReader() - const {done, value: chunk} = await reader.read(); + const { done, value: chunk } = await reader.read() - t.true(done); - t.is(chunk, undefined); -}); + t.true(done) + t.is(chunk, undefined) +}) test('Blob toString()', t => { - const data = 'a=1'; - const type = 'text/plain'; - const blob = new Blob([data], {type}); - t.is(blob.toString(), '[object Blob]'); -}); + 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); -}); + 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'); -}); + 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'); -}); + 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'); -}); + 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'); -}); + 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'); -}); + 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(Readable.from(blob.stream())); - const blob2 = await response.blob(); - t.is(await blob2.text(), data); -}); + const data = 'a=1' + const type = 'text/plain' + const blob = new Blob([data], { type }) + const response = new Response(Readable.from(blob.stream())) + 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(Readable.from(blob.stream())); - const text = await response.text(); - t.is(text, data); -}); + const data = 'a=1' + const type = 'text/plain' + const blob = new Blob([data], { type }) + const response = new Response(Readable.from(blob.stream())) + const text = await response.text() + 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)); -}); + 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, 500); - }); - fs.closeSync(fs.openSync('./LICENSE', 'a')); - const error = await t.throwsAsync(blob.text()); - t.is(error.constructor.name, 'DOMException'); - t.is(error instanceof Error, true); - t.is(error.name, 'NotReadableError'); + const blob = blobFromSync('./LICENSE') + await new Promise(resolve => { + setTimeout(resolve, 500) + }) + fs.closeSync(fs.openSync('./LICENSE', 'a')) + const error = await t.throwsAsync(blob.text()) + t.is(error.constructor.name, 'DOMException') + t.is(error instanceof Error, true) + t.is(error.name, 'NotReadableError') - const file = fileFromSync('./LICENSE'); - // Above 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); - const mod = file.lastModified - Date.now(); - t.true(mod <= 0 && mod >= -500); // Close to tolerance: 0.500ms -}); + const file = fileFromSync('./LICENSE') + // Above 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) + const mod = file.lastModified - Date.now() + t.true(mod <= 0 && mod >= -500) // Close to tolerance: 0.500ms +}) 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'); -}); + 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); -}); + 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); -}); + 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, ''); -}); + 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() {} + class File { + stream () {} - get [Symbol.toStringTag]() { - return 'File'; - } - } + get [Symbol.toStringTag] () { + return 'File' + } + } - t.true(new File() instanceof Blob); -}); + t.true(new File() instanceof Blob) +}) 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 */ test('Dose not lowercase the blob values', t => { - const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd'; - t.is(new Blob([], {type}).type, type); -}); + 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'); -}); + 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().getReader().read(); - t.is(chunk.value[0], 97); - chunk.value[0] = 98; - t.is(await blob.text(), 'a'); -}); + const buf = new Uint8Array([97]) + const blob = new Blob([buf]) + const chunk = await blob.stream().getReader().read() + 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++; - } + 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); -}); + 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); -}); + // 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); -}); + 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('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); -}); + t.true(new File([], '') instanceof Blob) +}) 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 => { - t.is(fileFromSync('./LICENSE').name, 'LICENSE'); -}); + t.is(fileFromSync('./LICENSE').name, 'LICENSE') +}) 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 => { - 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 => { - const file = await fileFrom('./LICENSE', 'text/plain'); - t.is(file.type, 'text/plain'); -}); + const file = await fileFrom('./LICENSE', 'text/plain') + t.is(file.type, 'text/plain') +}) test('new File(,,{lastModified: 100})', t => { - const mod = new File([], '', {lastModified: 100}).lastModified; - t.is(mod, 100); -}); + const mod = new File([], '', { lastModified: 100 }).lastModified + t.is(mod, 100) +}) test('new File(,,{lastModified: "200"})', t => { - const mod = new File([], '', {lastModified: '200'}).lastModified; - t.is(mod, 200); -}); + const mod = new File([], '', { lastModified: '200' }).lastModified + t.is(mod, 200) +}) test('new File(,,{lastModified: true})', t => { - const mod = new File([], '', {lastModified: true}).lastModified; - t.is(mod, 1); -}); + const mod = new File([], '', { lastModified: true }).lastModified + t.is(mod, 1) +}) test('new File(,,{lastModified: new Date()})', t => { - const mod = new File([], '', {lastModified: new Date()}).lastModified - Date.now(); - t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms -}); + const mod = new File([], '', { lastModified: new Date() }).lastModified - Date.now() + 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 -}); + 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); -}); + const mod = new File([], '', { lastModified: null }).lastModified + t.is(mod, 0) +}) test('Interpretes NaN value in lastModified option as 0', t => { - t.plan(3); + t.plan(3) - const values = ['Not a Number', [], {}]; + 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}); + // 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); - } -}); + t.is(file.lastModified, 0) + } +}) test('new File(,,{}) sets current time', t => { - const mod = new File([], '').lastModified - Date.now(); - t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms -}); + const mod = new File([], '').lastModified - Date.now() + t.true(mod <= 0 && mod >= -20) // Close to tolerance: 0.020ms +}) test('blobFrom(path, type) sets the type', async t => { - const blob = await blobFrom('./LICENSE', 'text/plain'); - t.is(blob.type, 'text/plain'); -}); + 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, ''); -}); + 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.' - }); -}); + t.throws(() => new File(), { + instanceOf: TypeError, + message: 'Failed to construct \'File\': 2 arguments required, but only 0 present.' + }) +}) test('can slice zero sized blobs', async t => { - const blob = new Blob(); - const txt = await blob.slice(0, 0).text(); - t.is(txt, ''); -}); + const blob = new Blob() + const txt = await blob.slice(0, 0).text() + t.is(txt, '') +}) test('returns a readable stream', t => { - const stream = new File([], '').stream(); - t.true(typeof stream.getReader === 'function'); -}); + const stream = new File([], '').stream() + t.true(typeof stream.getReader === 'function') +}) -test('checking instanceof blob#stream', async t => { - // eslint-disable-next-line node/no-unsupported-features/es-syntax - const {ReadableStream} = await import('node:stream/web').catch(_ => import('web-streams-polyfill/dist/ponyfill.es2018.js')); - const stream = new File([], '').stream(); - t.true(stream instanceof ReadableStream); -}); +test('checking instanceof blob#stream', t => { + const stream = new File([], '').stream() + t.true(stream instanceof globalThis.ReadableStream) +})