Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ab587d340 | ||
|
|
f61ee01955 | ||
|
|
5dbcbb91b4 | ||
|
|
c5c2b52154 | ||
|
|
836709fc65 | ||
|
|
2c11647222 | ||
|
|
9022f8fc70 | ||
|
|
0e07457246 |
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 */
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user