Compare commits

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

* Move encoder initialization outside the loop

* Improve errors reported by Blob constructor.

* Simulate WebIDL type casting for lastModified in File constructor.

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

* Fix indentation inconsistency.

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

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

* bug fix

* don't test cjs
2021-07-16 11:51:34 +02:00
Jimmy Wärting 52f296c283 Conditional loading (#104)
use good old cjs to conditionally load stuff in node only
2021-07-16 00:33:00 +02:00
Jimmy Wärting 701103ffd2 Update CHANGELOG.md 2021-07-15 22:55:57 +02:00
Jimmy Wärting ada23da6ee Update package.json 2021-07-15 22:50:37 +02:00
Jimmy Wärting 16f312c543 use whatwg streams (#103)
* use whatwg streams
* could not use optional changing in v12
* could not use fs/promise in v12
2021-07-15 22:49:58 +02:00
Jimmy Wärting 5ba554efd2 Lowered targeted node version to those who support ESM
closes #98
2021-06-19 11:51:54 +02:00
Jimmy Wärting 4747497b52 fixed lastModified 2021-06-10 10:51:16 +02:00
Jimmy Wärting 09fd40448a Update file.js 2021-06-10 10:33:00 +02:00
Jimmy Wärting 8d14e88040 Created a File Class (#97)
* Created a File Class
* Test the untested parts
* ignore any d.ts file
2021-05-29 11:17:26 +02:00
dependabot-preview[bot]anddependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> 8ef89adad4 Upgrade to GitHub-native Dependabot (#89)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-05-14 11:01:14 +02:00
14 changed files with 1085 additions and 524 deletions
+24
View File
@@ -0,0 +1,24 @@
<!-- Thanks for contributing! -->
## The purpose of this PR is:
...
## This is what had to change:
...
## This is what like reviewers to know:
...
-------------------------------------------------------------------------------------------------
<!-- Mark what you have done, Remove unnecessary ones. Add new tasks that may fit (like TODO's) -->
- [ ] I prefixed the PR-title with `docs: `, `fix(area): `, `feat(area): ` or `breaking(area): `
- [ ] I updated ./CHANGELOG.md with a link to this PR or Issue
- [ ] I updated the README.md
- [ ] I Added unit test(s)
-------------------------------------------------------------------------------------------------
<!-- Add a `- fix #_NUMBER_` line for every Issue this PR solves. Do not comma separate them -->
- fix #000
+25
View File
@@ -0,0 +1,25 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: get-stream
versions:
- 6.0.0
- 6.0.1
- dependency-name: typescript
versions:
- 4.1.3
- 4.1.4
- 4.1.5
- 4.2.2
- 4.2.3
- 4.2.4
- dependency-name: xo
versions:
- 0.37.1
- 0.38.1
- 0.38.2
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
node: ["16", "15", "14", engines]
node: ["16", "15", "14", "12", engines]
exclude:
# On Windows, run tests with only the LTS environments.
- os: windows-latest
+1 -2
View File
@@ -62,5 +62,4 @@ typings/
# dotenv environment variables file
.env
index.d.ts
from.d.ts
*.d.ts
+33 -6
View File
@@ -1,18 +1,41 @@
Changelog
=========
# Changelog
## v3.0.0-rc.0
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this
project adheres to [Semantic Versioning](http://semver.org/).
## v3.1.3
- Allow usage of iterable object in Blob constructor. [#108]
- Run test WPT test against our impl [#109]
- File name are now casted to string [#109]
- Slicing in the middle of multiple parts added more bytes than what what it should have [#109]
- Prefixed `stream/web` import with `node:` to allow easier static analysis detection of Node built-ins [#122]
- Added `node:` prefix in `from.js` as well [#114]
- Suppress warning when importing `stream/web` [#114]
## v3.1.2
- Improved typing
- Fixed a bug where position in iterator did not increase
## v3.1.0
- started to use real whatwg streams
- degraded fs/promise to fs.promise to support node v12
- degraded optional changing to support node v12
## v3.0.0
- Changed WeakMap for private field (require node 12)
- Switch to ESM
- blob.stream() return a subset of whatwg stream which is the async iterable
- blob.stream() return a subset of whatwg stream which is the async iterable part
(it no longer return a node stream)
- Reduced the dependency of Buffer by changing to global TextEncoder/Decoder (require node 11)
- Disabled xo since it could understand private fields (#)
- No longer transform the type to lowercase (https://github.com/w3c/FileAPI/issues/43)
This is more loose than strict, keys should be lowercased, but values should not.
It would require a more proper mime type parser - so we just made it loose.
- index.js can now be imported by browser & deno since it no longer depends on any
core node features (but why would you? other environment can benefit from it)
- index.js and file.js can now be imported by browser & deno since it no longer depends on any
core node features (but why would you?)
- Implemented a File class
## v2.1.2
- Fixed a bug where `start` in BlobDataItem was undefined (#85)
@@ -69,3 +92,7 @@ Changelog
## v1.0.0
- Major: initial release
[#108]: https://github.com/node-fetch/fetch-blob/pull/108
[#109]: https://github.com/node-fetch/fetch-blob/pull/109
[#114]: https://github.com/node-fetch/fetch-blob/pull/114
+27 -59
View File
@@ -22,10 +22,8 @@ npm install fetch-blob
- internal Buffer.from was replaced with TextEncoder/Decoder
- internal buffers was replaced with Uint8Arrays
- CommonJS was replaced with ESM
- The node stream returned by calling `blob.stream()` was replaced with a simple generator function that yields Uint8Array (Breaking change)
(Read "Differences from other blobs" for more info.)
All of this changes have made it dependency free of any core node modules, so it would be possible to just import it using http-import from a CDN without any bundling
- The node stream returned by calling `blob.stream()` was replaced with whatwg streams
- (Read "Differences from other blobs" for more info.)
</details>
@@ -36,48 +34,12 @@ npm install fetch-blob
- This blob version is more arbitrary, it can be constructed with blob parts that isn't a instance of itself
it has to look and behave as a blob to be accepted as a blob part.
- The benefit of this is that you can create other types of blobs that don't contain any internal data that has to be read in other ways, such as the `BlobDataItem` created in `from.js` that wraps a file path into a blob-like item and read lazily (nodejs plans to [implement this][fs-blobs] as well)
- The `blob.stream()` is the most noticeable differences. It returns a AsyncGeneratorFunction that yields Uint8Arrays
- The `blob.stream()` is the most noticeable differences. It returns a WHATWG stream now. to keep it as a node stream you would have to do:
The reasoning behind `Blob.prototype.stream()` is that NodeJS readable stream
isn't spec compatible with whatwg streams and we didn't want to import the hole whatwg stream polyfill for node
or browserify NodeJS streams for the browsers and picking any flavor over the other. So we decided to opted out
of any stream and just implement the bear minium of what both streams have in common which is the asyncIterator
that both yields Uint8Array. this is the most isomorphic way with the use of `for-await-of` loops.
It would be redundant to convert anything to whatwg streams and than convert it back to
node streams since you work inside of Node.
It will probably stay like this until nodejs get native support for whatwg<sup>[1][https://github.com/nodejs/whatwg-stream]</sup> streams and whatwg stream add the node
equivalent for `Readable.from(iterable)`<sup>[2](https://github.com/whatwg/streams/issues/1018)</sup>
But for now if you really need a Node Stream then you can do so using this transformation
```js
import {Readable} from 'stream'
const stream = Readable.from(blob.stream())
```
But if you don't need it to be a stream then you can just use the asyncIterator part of it that is isomorphic.
```js
for await (const chunk of blob.stream()) {
console.log(chunk) // uInt8Array
}
```
If you need to make some feature detection to fix this different behavior
```js
if (Blob.prototype.stream?.constructor?.name === 'AsyncGeneratorFunction') {
// not spec compatible, monkey patch it...
// (Alternative you could extend the Blob and use super.stream())
let orig = Blob.prototype.stream
Blob.prototype.stream = function () {
const iterator = orig.call(this)
return new ReadableStream({
async pull (ctrl) {
const next = await iterator.next()
return next.done ? ctrl.close() : ctrl.enqueue(next.value)
}
})
}
}
```
Possible feature whatwg version: `ReadableStream.from(iterator)`
It's also possible to delete this method and instead use `.slice()` and `.arrayBuffer()` since it has both a public and private stream method
</details>
## Usage
@@ -86,7 +48,11 @@ npm install fetch-blob
// Ways to import
// (PS it's dependency free ESM package so regular http-import from CDN works too)
import Blob from 'fetch-blob'
import File from 'fetch-blob/file.js'
import {Blob} from 'fetch-blob'
import {File} from 'fetch-blob/file.js'
const {Blob} = await import('fetch-blob')
@@ -96,36 +62,38 @@ const blob = new Blob(['hello, world'])
await blob.text()
await blob.arrayBuffer()
for await (let chunk of blob.stream()) { ... }
// turn the async iterator into a node stream
stream.Readable.from(blob.stream())
// turn the async iterator into a whatwg stream (feature)
globalThis.ReadableStream.from(blob.stream())
blob.stream().getReader().read()
blob.stream().getReader({mode: 'byob'}).read(view)
```
### Blob part backed up by filesystem
To use, install [domexception](https://github.com/jsdom/domexception).
```sh
npm install fetch-blob domexception
```
`fetch-blob/from.js` comes packed with tools to convert any filepath into either a Blob or a File
It will not read the content into memory. It will only stat the file for last modified date and file size.
```js
// The default export is sync and use fs.stat to retrieve size & last modified
// The default export is sync and use fs.stat to retrieve size & last modified as a blob
import blobFromSync from 'fetch-blob/from.js'
import {Blob, blobFrom, blobFromSync} from 'fetch-blob/from.js'
import {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync} from 'fetch-blob/from.js'
const fsBlob1 = blobFromSync('./2-GiB-file.bin')
const fsBlob2 = await blobFrom('./2-GiB-file.bin')
const fsFile = fileFromSync('./2-GiB-file.bin', 'application/octet-stream')
const fsBlob = await blobFrom('./2-GiB-file.mp4')
// Not a 4 GiB memory snapshot, just holds 3 references
// Not a 4 GiB memory snapshot, just holds references
// points to where data is located on the disk
const blob = new Blob([fsBlob1, fsBlob2, 'memory'])
console.log(blob.size) // 4 GiB
const blob = new Blob([fsFile, fsBlob, 'memory', new Uint8Array(10)])
console.log(blob.size) // ~4 GiB
```
See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [tests](https://github.com/node-fetch/fetch-blob/blob/master/test.js) for more details.
`blobFrom|blobFromSync|fileFrom|fileFromSync(path, [mimetype])`
### Creating Blobs backed up by other async sources
Our Blob & File class are more generic then any other polyfills in the way that it can accept any blob look-a-like item
An example of this is that our blob implementation can be constructed with parts coming from [BlobDataItem](https://github.com/node-fetch/fetch-blob/blob/8ef89adad40d255a3bbd55cf38b88597c1cd5480/from.js#L32) (aka a filepath) or from [buffer.Blob](https://nodejs.org/api/buffer.html#buffer_new_buffer_blob_sources_options), It dose not have to implement all the methods - just enough that it can be read/understood by our Blob implementation. The minium requirements is that it has `Symbol.toStringTag`, `size`, `slice()` and either a `stream()` or a `arrayBuffer()` method. If you then wrap it in our Blob or File `new Blob([blobDataItem])` then you get all of the other methods that should be implemented in a blob or file
An example of this could be to create a file or blob like item coming from a remote HTTP request. Or from a DataBase
See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [tests](https://github.com/node-fetch/fetch-blob/blob/master/test.js) for more details of how to use the Blob.
[npm-image]: https://flat.badgen.net/npm/v/fetch-blob
[npm-url]: https://www.npmjs.com/package/fetch-blob
+44
View File
@@ -0,0 +1,44 @@
import Blob from './index.js'
const _File = class File extends Blob {
#lastModified = 0
#name = ''
/**
* @param {*[]} fileBits
* @param {string} fileName
* @param {{lastModified?: number, type?: string}} options
*/// @ts-ignore
constructor (fileBits, fileName, options = {}) {
if (arguments.length < 2) {
throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`)
}
super(fileBits, options)
if (options === null) options = {}
// Simulate WebIDL type casting for NaN value in lastModified option.
const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified)
if (!Number.isNaN(lastModified)) {
this.#lastModified = lastModified
}
this.#name = String(fileName)
}
get name () {
return this.#name
}
get lastModified () {
return this.#lastModified
}
get [Symbol.toStringTag] () {
return 'File'
}
}
/** @type {typeof globalThis.File} */// @ts-ignore
export const File = _File
export default File
+80 -51
View File
@@ -1,26 +1,57 @@
import {statSync, createReadStream} from 'fs';
import {stat} from 'fs/promises';
import DOMException from 'domexception';
import Blob from './index.js';
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'
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 }
})()
/**
* @param {string} path filepath on the disk
* @returns {Blob}
* @param {string} [type] mimetype to use
*/
const blobFromSync = path => from(statSync(path), path);
const blobFromSync = (path, type) => fromBlob(statSync(path), path, type)
/**
* @param {string} path filepath on the disk
* @returns {Promise<Blob>}
* @param {string} [type] mimetype to use
*/
const blobFrom = path => stat(path).then(stat => from(stat, path));
const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type))
const from = (stat, path) => new Blob([new BlobDataItem({
path,
size: stat.size,
lastModified: stat.mtimeMs,
start: 0
})]);
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type))
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
const fileFromSync = (path, type) => fromFile(statSync(path), path, type)
// @ts-ignore
const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
path,
size: stat.size,
lastModified: stat.mtimeMs,
start: 0
})], { type })
// @ts-ignore
const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
path,
size: stat.size,
lastModified: stat.mtimeMs,
start: 0
})], basename(path), { type, lastModified: stat.mtimeMs })
/**
* This is a blob backed up by a file on the disk
@@ -30,46 +61,44 @@ const from = (stat, path) => new Blob([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');
}
if (this.size) {
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 {Blob, blobFrom, blobFromSync};
export default blobFromSync
export { File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync }
+222 -187
View File
@@ -1,216 +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)
// Node has recently added whatwg stream into core
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 | Uint8Array)[]} parts */
/** @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);
yield new Uint8Array(chunk);
position += chunk.byteLength;
}
} else {
yield part;
}
} else {
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob)
let position = 0;
while (position !== part.size) {
const chunk = part.slice(position, Math.min(part.size, position + POOL_SIZE));
const buffer = await chunk.arrayBuffer();
position += buffer.byteLength;
yield new Uint8Array(buffer);
}
}
}
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 */
}
}
}
export default class Blob {
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 = {}) {
let size = 0;
if (typeof blobParts[Symbol.iterator] !== 'function') {
throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.')
}
const parts = blobParts.map(element => {
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 = new TextEncoder().encode(String(element));
}
if (typeof options !== 'object' && typeof options !== 'function') {
throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.')
}
size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
return part;
});
if (options === null) options = {}
const type = options.type === undefined ? '' : String(options.type);
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.#type = /[^\u0020-\u007E]/.test(type) ? '' : type;
this.#size = size;
this.#parts = parts;
}
this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size
this.#parts.push(part)
}
/**
* The Blob interface's size property returns the
* size of the Blob in bytes.
*/
get size() {
return this.#size;
}
const type = options.type === undefined ? '' : String(options.type)
/**
* The type property of a Blob object returns the MIME type of the file.
*/
get type() {
return this.#type;
}
this.#type = /^[\x20-\x7E]*$/.test(type) ? 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<string>}
*/
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 Blob interface's size property returns the
* size of the Blob in bytes.
*/
get size () {
return this.#size
}
/**
* 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<ArrayBuffer>}
*/
async arrayBuffer() {
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;
}
/**
* The type property of a Blob object returns the MIME type of the file.
*/
get type () {
return this.#type
}
return data.buffer;
}
/**
* 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<string>}
*/
async text () {
// More optimized than using this.arrayBuffer()
// that requires twice as much ram
const decoder = new TextDecoder()
let str = ''
for await (const part of toIterator(this.#parts, false)) {
str += decoder.decode(part, { stream: true })
}
// Remaining
str += decoder.decode()
return str
}
/**
* The Blob stream() implements partial support of the whatwg stream
* by only being async iterable.
*
* @returns {AsyncGenerator<Uint8Array>}
*/
async * stream() {
yield * toIterator(this.#parts, true);
}
/**
* 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<ArrayBuffer>}
*/
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 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 data = new Uint8Array(this.size)
let offset = 0
for await (const chunk of toIterator(this.#parts, false)) {
data.set(chunk, offset)
offset += chunk.length
}
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);
return data.buffer
}
const span = Math.max(relativeEnd - relativeStart, 0);
const parts = this.#parts;
const blobParts = [];
let added = 0;
stream () {
const it = toIterator(this.#parts, true)
for (const part of parts) {
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
}
blobParts.push(chunk);
relativeStart = 0; // All next sequental parts should start at 0
return new globalThis.ReadableStream({
type: 'bytes',
async pull (ctrl) {
const chunk = await it.next()
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value)
},
// don't add the overflow to new blobParts
if (added >= span) {
break;
}
}
}
async cancel () {
await it.return()
}
})
}
const blob = new Blob([], {type: String(type).toLowerCase()});
blob.#size = span;
blob.#parts = blobParts;
/**
* 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
return blob;
}
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)
get [Symbol.toStringTag]() {
return 'Blob';
}
const span = Math.max(relativeEnd - relativeStart, 0)
const parts = this.#parts
const blobParts = []
let added = 0
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])
);
}
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 blob = new Blob([], { type: String(type).toLowerCase() })
blob.#size = span
blob.#parts = blobParts
return blob
}
get [Symbol.toStringTag] () {
return 'Blob'
}
static [Symbol.hasInstance] (object) {
return (
object &&
typeof object === 'object' &&
typeof object.constructor === 'function' &&
(
typeof object.stream === 'function' ||
typeof object.arrayBuffer === 'function'
) &&
/^(Blob|File)$/.test(object[Symbol.toStringTag])
)
}
}
Object.defineProperties(Blob.prototype, {
size: {enumerable: true},
type: {enumerable: true},
slice: {enumerable: true}
});
Object.defineProperties(_Blob.prototype, {
size: { enumerable: true },
type: { enumerable: true },
slice: { enumerable: true }
})
export { Blob };
/** @type {typeof globalThis.Blob} */
export const Blob = _Blob
export default Blob
+29 -40
View File
@@ -1,69 +1,58 @@
{
"name": "fetch-blob",
"version": "3.0.0-rc.0",
"description": "A Blob implementation in Node.js, originally from node-fetch.",
"version": "3.1.3",
"description": "Blob & File implementation in Node.js, originally from node-fetch.",
"main": "index.js",
"type": "module",
"files": [
"from.js",
"file.js",
"file.d.ts",
"index.js",
"index.d.ts",
"from.d.ts"
"from.d.ts",
"streams.cjs"
],
"scripts": {
"lint": "xo test.js",
"test": "npm run lint && ava",
"report": "c8 --reporter json --reporter text ava",
"coverage": "c8 --reporter json --reporter text ava && codecov -f coverage/coverage-final.json",
"test-wpt": "node --experimental-loader ./test/http-loader.js ./test/test-wpt-in-node.js",
"test": "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",
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js from.js"
},
"repository": "https://github.com/node-fetch/fetch-blob.git",
"keywords": [
"blob",
"file",
"node-fetch"
],
"engines": {
"node": ">=14.0.0"
"node": "^12.20 || >= 14.13"
},
"author": "David Frank",
"author": "Jimmy Wärting <jimmy@warting.se> (https://jimmy.warting.se)",
"license": "MIT",
"bugs": {
"url": "https://github.com/node-fetch/fetch-blob/issues"
},
"homepage": "https://github.com/node-fetch/fetch-blob#readme",
"xo": {
"rules": {
"unicorn/import-index": "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
}
}
]
},
"peerDependenciesMeta": {
"domexception": {
"optional": true
}
},
"devDependencies": {
"ava": "^3.15.0",
"c8": "^7.7.1",
"codecov": "^3.8.1",
"domexception": "^2.0.1",
"c8": "^7.7.2",
"codecov": "^3.8.2",
"node-fetch": "^3.0.0-beta.9",
"typescript": "^4.2.4",
"xo": "^0.38.2"
"typescript": "^4.3.2"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"web-streams-polyfill": "^3.0.3"
}
}
+51
View File
@@ -0,0 +1,51 @@
/* c8 ignore start */
// 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536
if (!globalThis.ReadableStream) {
// `node:stream/web` got introduced in v16.5.0 as experimental
// and it's preferred over the polyfilled version. So we also
// suppress the warning that gets emitted by NodeJS for using it.
try {
const process = require('node:process')
const { emitWarning } = process
try {
process.emitWarning = () => {}
Object.assign(globalThis, require('node:stream/web'))
process.emitWarning = emitWarning
} catch (error) {
process.emitWarning = emitWarning
throw error
}
} catch (error) {
// fallback to polyfill implementation
Object.assign(globalThis, require('web-streams-polyfill/dist/ponyfill.es2018.js'))
}
}
try {
// Don't use node: prefix for this, require+node: is not supported until node v14.14
// Only `import()` can use prefix in 12.20 and later
const { Blob } = require('buffer')
if (Blob && !Blob.prototype.stream) {
Blob.prototype.stream = function name (params) {
let position = 0
const blob = this
return new ReadableStream({
type: 'bytes',
async pull (ctrl) {
const chunk = blob.slice(position, Math.min(blob.size, position + POOL_SIZE))
const buffer = await chunk.arrayBuffer()
position += buffer.byteLength
ctrl.enqueue(new Uint8Array(buffer))
if (position === blob.size) {
ctrl.close()
}
}
})
}
}
} catch (error) {}
/* c8 ignore end */
+364 -178
View File
@@ -1,256 +1,442 @@
import fs from 'fs';
import test from 'ava';
import {Response} from 'node-fetch';
import {Readable} from 'stream';
import buffer from 'buffer';
import Blob from './index.js';
import syncBlob, {blobFromSync, blobFrom} from './from.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] })
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 blob = new Blob({
* [Symbol.iterator] () {
yield * input
}
})
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.'
})
})
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 => {
const input = [1, 2, 3]
const blob = new Blob(new Uint8Array(input))
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()
// 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 => {
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, 100);
});
const now = new Date();
// Change modified time
fs.utimesSync('./LICENSE', now, now);
const error = await blob.text().catch(error => error);
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
})
test('Reading file after modified should fail', async t => {
const file = fileFromSync('./LICENSE')
await new Promise(resolve => {
setTimeout(resolve, 100)
})
const now = new Date()
// Change modified time
fs.utimesSync('./LICENSE', now, now)
const error = await t.throwsAsync(file.text())
t.is(error.constructor.name, 'DOMException')
t.is(error instanceof Error, true)
t.is(error.name, 'NotReadableError')
})
test('Reading from the stream created by blobFrom', async t => {
const blob = blobFromSync('./LICENSE');
const actual = await blob.text();
t.is(actual, license);
});
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().next();
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 => {
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)
})
test('fileFrom returns the name', async t => {
t.is((await fileFrom('./LICENSE')).name, 'LICENSE')
})
test('fileFromSync returns the name', t => {
t.is(fileFromSync('./LICENSE').name, 'LICENSE')
})
test('fileFromSync(path, type) sets the type', t => {
t.is(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain')
})
test('blobFromSync(path, type) sets the type', t => {
t.is(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain')
})
test('fileFrom(path, type) sets the type', async t => {
const file = await fileFrom('./LICENSE', 'text/plain')
t.is(file.type, 'text/plain')
})
test('new File(,,{lastModified: 100})', t => {
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)
})
test('new File(,,{lastModified: true})', t => {
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
})
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 => {
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')
})
test('blobFrom(path) sets empty type', async t => {
const blob = await blobFrom('./LICENSE')
t.is(blob.type, '')
})
test('new File() throws with too few args', t => {
t.throws(() => new File(), {
instanceOf: TypeError,
message: 'Failed to construct \'File\': 2 arguments required, but only 0 present.'
})
})
test('can slice zero sized blobs', async t => {
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')
})
test('checking instanceof blob#stream', t => {
const stream = new File([], '').stream()
t.true(stream instanceof globalThis.ReadableStream)
})
+51
View File
@@ -0,0 +1,51 @@
// https-loader.mjs
import { get } from 'https';
export function resolve(specifier, context, defaultResolve) {
const { parentURL = null } = context;
// Normally Node.js would error on specifiers starting with 'https://', so
// this hook intercepts them and converts them into absolute URLs to be
// passed along to the later hooks below.
if (specifier.startsWith('https://')) {
return {
url: specifier
};
} else if (parentURL && parentURL.startsWith('https://')) {
return {
url: new URL(specifier, parentURL).href
};
}
// Let Node.js handle all other specifiers.
return defaultResolve(specifier, context, defaultResolve);
}
export function getFormat(url, context, defaultGetFormat) {
// This loader assumes all network-provided JavaScript is ES module code.
if (url.startsWith('https://')) {
return {
format: 'module'
};
}
// Let Node.js handle all other URLs.
return defaultGetFormat(url, context, defaultGetFormat);
}
export function getSource(url, context, defaultGetSource) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://')) {
return new Promise((resolve, reject) => {
let data = ''
get(url, async res => {
for await (const chunk of res) data += chunk;
resolve({ source: data });
}).on('error', (err) => reject(err));
});
}
// Let Node.js handle all other URLs.
return defaultGetSource(url, context, defaultGetSource);
}
+133
View File
@@ -0,0 +1,133 @@
// Don't want to use the FileReader, don't want to lowerCase the type either
// import from 'https://wpt.live/resources/testharnessreport.js'
import {File, Blob} from '../from.js'
globalThis.self = globalThis
await import('https://wpt.live/resources/testharness.js')
// Should probably be fixed... should be able to compare a Blob to a File
delete Blob[Symbol.hasInstance]
setup({
explicit_timeout: true,
explicit_done: true,
});
function test_blob(fn, expectations) {
var expected = expectations.expected,
type = expectations.type,
desc = expectations.desc;
var t = async_test(desc);
t.step(async function() {
var blob = fn();
assert_true(blob instanceof Blob);
assert_false(blob instanceof File);
assert_equals(blob.type.toLowerCase(), type);
assert_equals(blob.size, expected.length);
assert_equals(await blob.text(), expected);
t.done();
});
}
function test_blob_binary(fn, expectations) {
var expected = expectations.expected,
type = expectations.type,
desc = expectations.desc;
var t = async_test(desc);
t.step(async function() {
var blob = fn();
assert_true(blob instanceof Blob);
assert_false(blob instanceof File);
assert_equals(blob.type.toLowerCase(), type);
assert_equals(blob.size, expected.length);
const result = await blob.arrayBuffer();
assert_true(result instanceof ArrayBuffer, "Result should be an ArrayBuffer");
assert_array_equals(new Uint8Array(result), expected);
t.done();
});
}
// Assert that two TypedArray objects have the same byte values
globalThis.assert_equals_typed_array = (array1, array2) => {
const [view1, view2] = [array1, array2].map((array) => {
assert_true(array.buffer instanceof ArrayBuffer,
'Expect input ArrayBuffers to contain field `buffer`');
return new DataView(array.buffer, array.byteOffset, array.byteLength);
});
assert_equals(view1.byteLength, view2.byteLength,
'Expect both arrays to be of the same byte length');
const byteLength = view1.byteLength;
for (let i = 0; i < byteLength; ++i) {
assert_equals(view1.getUint8(i), view2.getUint8(i),
`Expect byte at buffer position ${i} to be equal`);
}
}
let hasFailed
globalThis.add_result_callback(test => {
const INDENT_SIZE = 2;
var reporter = {}
if (test.name === 'Using type in File constructor: text/plain;charset=UTF-8') {
return
}
if (test.name === 'Using type in File constructor: TEXT/PLAIN') {
return
}
reporter.startSuite = name => console.log(`\n ${(name)}\n`);
reporter.pass = message => console.log((indent(("√ ") + message, INDENT_SIZE)));
reporter.fail = message => console.log((indent("\u00D7 " + message, INDENT_SIZE)));
reporter.reportStack = stack => console.log((indent(stack, INDENT_SIZE * 2)));
function indent(string, times) {
const prefix = " ".repeat(times);
return string.split("\n").map(l => prefix + l).join("\n");
}
if (test.status === 0) {
reporter.pass(test.name);
} else if (test.status === 1) {
reporter.fail(`${test.name}\n`);
reporter.reportStack(`${test.message}\n${test.stack}`);
hasFailed = true;
} else if (test.status === 2) {
reporter.fail(`${test.name} (timeout)\n`);
reporter.reportStack(`${test.message}\n${test.stack}`);
hasFailed = true;
} else if (test.status === 3) {
reporter.fail(`${test.name} (incomplete)\n`);
reporter.reportStack(`${test.message}\n${test.stack}`);
hasFailed = true;
} else if (test.status === 4) {
reporter.fail(`${test.name} (precondition failed)\n`);
reporter.reportStack(`${test.message}\n${test.stack}`);
hasFailed = true;
} else {
reporter.fail(`unknown test status: ${test.status}`);
hasFailed = true;
}
hasFailed && process.exit(1);
})
globalThis.File = File
globalThis.Blob = Blob
globalThis.garbageCollect = () => {}
globalThis.document = {body: '[object HTMLBodyElement]'}
globalThis.test_blob = test_blob;
globalThis.test_blob_binary = test_blob_binary;
import("https://wpt.live/FileAPI/file/File-constructor.any.js")
import("https://wpt.live/FileAPI/blob/Blob-array-buffer.any.js")
import("https://wpt.live/FileAPI/blob/Blob-slice-overflow.any.js")
import("https://wpt.live/FileAPI/blob/Blob-slice.any.js")
import("https://wpt.live/FileAPI/blob/Blob-stream.any.js")
import("https://wpt.live/FileAPI/blob/Blob-text.any.js")