Compare commits

..
Author SHA1 Message Date
Konstantin Vyatkin cd619c7694 lint 2020-06-11 00:17:58 -04:00
Konstantin Vyatkin bc6ca02caa import TextDecoder from util 2020-06-11 00:16:48 -04:00
Konstantin Vyatkin b098249bea update badge 2020-06-11 00:13:20 -04:00
Konstantin Vyatkin ecdabc9879 add GitHub Actions 2020-06-11 00:01:56 -04:00
Konstantin Vyatkin 71600df374 switch coverage to c8 2020-06-11 00:01:51 -04:00
Konstantin Vyatkin f9be7e7d7b add codecov partial line coverage 2020-06-10 23:52:25 -04:00
Konstantin Vyatkin fcf91af996 remove travis 2020-06-10 23:50:57 -04:00
14 changed files with 118 additions and 1010 deletions
-24
View File
@@ -1,24 +0,0 @@
<!-- 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
@@ -1,25 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: get-stream
versions:
- 6.0.0
- 6.0.1
- dependency-name: typescript
versions:
- 4.1.3
- 4.1.4
- 4.1.5
- 4.2.2
- 4.2.3
- 4.2.4
- dependency-name: xo
versions:
- 0.37.1
- 0.38.1
- 0.38.2
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
node: ["16", "15", "14", engines]
node: ["14", "12", engines]
exclude:
# On Windows, run tests with only the LTS environments.
- os: windows-latest
+1 -1
View File
@@ -62,4 +62,4 @@ typings/
# dotenv environment variables file
.env
*.d.ts
index.d.ts
+2 -63
View File
@@ -1,63 +1,5 @@
# Changelog
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/).
(Unreleased)
==================
### Removed
-
### Changed
-
### Added
-
### Fixed
- 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]
## 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 part
(it no longer return a node stream)
- Reduced the dependency of Buffer by changing to global TextEncoder/Decoder (require node 11)
- Disabled xo since it could understand private fields (#)
- No longer transform the type to lowercase (https://github.com/w3c/FileAPI/issues/43)
This is more loose than strict, keys should be lowercased, but values should not.
It would require a more proper mime type parser - so we just made it loose.
- index.js and file.js can now be imported by browser & deno since it no longer depends on any
core node features (but why would you?)
- Implemented a File class
## v2.1.2
- Fixed a bug where `start` in BlobDataItem was undefined (#85)
## v2.1.1
- Add nullish values checking in Symbol.hasInstance (#82)
- Add generated typings for from.js file (#80)
- Updated dev dependencies
## v2.1.0
- Fix: .slice has an implementation bug (#54).
- Added blob backed up by filesystem (#55)
## v2.0.1
- Fix: remove upper bound for node engine semver (#49).
Changelog
=========
## v2.0.0
@@ -98,6 +40,3 @@ project adheres to [Semantic Versioning](http://semver.org/).
## 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
+9 -76
View File
@@ -13,87 +13,21 @@ A Blob implementation in Node.js, originally from [node-fetch](https://github.co
npm install fetch-blob
```
<details>
<summary>Upgrading from 2x to 3x</summary>
Updating from 2 to 3 should be a breeze since there is not many changes to the blob specification.
The major cause of a major release is coding standards.
- internal WeakMaps was replaced with private fields
- internal Buffer.from was replaced with TextEncoder/Decoder
- internal buffers was replaced with Uint8Arrays
- CommonJS was replaced with ESM
- The node stream returned by calling `blob.stream()` was replaced with whatwg streams
- (Read "Differences from other blobs" for more info.)
</details>
<details>
<summary>Differences from other Blobs</summary>
- Unlike NodeJS `buffer.Blob` (Added in: v15.7.0) and browser native Blob this polyfilled version can't be sent via PostMessage
- This blob version is more arbitrary, it can be constructed with blob parts that isn't a instance of itself
it has to look and behave as a blob to be accepted as a blob part.
- The benefit of this is that you can create other types of blobs that don't contain any internal data that has to be read in other ways, such as the `BlobDataItem` created in `from.js` that wraps a file path into a blob-like item and read lazily (nodejs plans to [implement this][fs-blobs] as well)
- The `blob.stream()` is the most noticeable differences. It returns a WHATWG stream now. to keep it as a node stream you would have to do:
```js
import {Readable} from 'stream'
const stream = Readable.from(blob.stream())
```
</details>
## Usage
```js
// 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'
const Blob = require('fetch-blob');
const fetch = require('node-fetch');
import {Blob} from 'fetch-blob'
import {File} from 'fetch-blob/file.js'
const {Blob} = await import('fetch-blob')
// Ways to read the blob:
const blob = new Blob(['hello, world'])
await blob.text()
await blob.arrayBuffer()
for await (let chunk of blob.stream()) { ... }
blob.stream().getReader().read()
blob.stream().getReader({mode: 'byob'}).read(view)
fetch('https://httpbin.org/post', {
method: 'POST',
body: new Blob(['Hello World'], { type: 'text/plain' })
})
.then(res => res.json());
.then(json => console.log(json));
```
### Blob part backed up by filesystem
`fetch-blob/from.js` comes packed with tools to convert any filepath into either a Blob or a File
It will not read the content into memory. It will only stat the file for last modified date and file size.
```js
// The default export is sync and use fs.stat to retrieve size & last modified as a blob
import blobFromSync from 'fetch-blob/from.js'
import {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync} from 'fetch-blob/from.js'
const fsFile = fileFromSync('./2-GiB-file.bin', 'application/octet-stream')
const fsBlob = await blobFrom('./2-GiB-file.mp4')
// Not a 4 GiB memory snapshot, just holds references
// points to where data is located on the disk
const blob = new Blob([fsFile, fsBlob, 'memory', new Uint8Array(10)])
console.log(blob.size) // ~4 GiB
```
`blobFrom|blobFromSync|fileFrom|fileFromSync(path, [mimetype])`
### Creating Blobs backed up by other async sources
Our Blob & File class are more generic then any other polyfills in the way that it can accept any blob look-a-like item
An example of this is that our blob implementation can be constructed with parts coming from [BlobDataItem](https://github.com/node-fetch/fetch-blob/blob/8ef89adad40d255a3bbd55cf38b88597c1cd5480/from.js#L32) (aka a filepath) or from [buffer.Blob](https://nodejs.org/api/buffer.html#buffer_new_buffer_blob_sources_options), It dose not have to implement all the methods - just enough that it can be read/understood by our Blob implementation. The minium requirements is that it has `Symbol.toStringTag`, `size`, `slice()` and either a `stream()` or a `arrayBuffer()` method. If you then wrap it in our Blob or File `new Blob([blobDataItem])` then you get all of the other methods that should be implemented in a blob or file
An example of this could be to create a file or blob like item coming from a remote HTTP request. Or from a DataBase
See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [tests](https://github.com/node-fetch/fetch-blob/blob/master/test.js) for more details of how to use the Blob.
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.
[npm-image]: https://flat.badgen.net/npm/v/fetch-blob
[npm-url]: https://www.npmjs.com/package/fetch-blob
@@ -103,4 +37,3 @@ See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blo
[codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
[install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob
[fs-blobs]: https://github.com/nodejs/node/issues/37340
-40
View File
@@ -1,40 +0,0 @@
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 = {};
const modified = Number(options.lastModified);
this.#lastModified = Number.isNaN(modified) ? Date.now() : modified
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;
-105
View File
@@ -1,105 +0,0 @@
import {statSync, createReadStream, promises as fs} from 'fs';
import {basename} from 'path';
import {MessageChannel} from '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
* @param {string} [type] mimetype to use
*/
const blobFromSync = (path, type) => fromBlob(statSync(path), path, type);
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type));
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type));
/**
* @param {string} path filepath on the disk
* @param {string} [type] mimetype to use
*/
const fileFromSync = (path, type) => fromFile(statSync(path), path, type);
// @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
* with minium requirement. Its wrapped around a Blob as a blobPart
* so you have no direct access to this.
*
* @private
*/
class BlobDataItem {
#path;
#start;
constructor(options) {
this.#path = options.path;
this.#start = options.start;
this.size = options.size;
this.lastModified = options.lastModified
}
/**
* Slicing arguments is first validated and formatted
* to not be out of range by Blob.prototype.slice
*/
slice(start, end) {
return new BlobDataItem({
path: this.#path,
lastModified: this.lastModified,
size: end - start,
start
});
}
async * stream() {
const {mtimeMs} = await stat(this.#path)
if (mtimeMs > this.lastModified) {
throw new DOMException('The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.', 'NotReadableError');
}
yield * createReadStream(this.#path, {
start: this.#start,
end: this.#start + this.size - 1
});
}
get [Symbol.toStringTag]() {
return 'Blob';
}
}
export default blobFromSync;
export {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync};
+74 -135
View File
@@ -1,96 +1,71 @@
const {Readable} = require('stream');
// TODO (jimmywarting): in the feature use conditional loading with top level await (requires 14.x)
// Node has recently added whatwg stream into core
/**
* @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>}
*/
const wm = new WeakMap();
import './streams.cjs';
/** @typedef {import('buffer').Blob} NodeBlob} */
// 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536;
/** @param {(Blob | NodeBlob | Uint8Array)[]} parts */
async function * toIterator (parts, clone = true) {
for (let part of parts) {
async function * read(parts) {
for (const part of parts) {
if ('stream' in part) {
yield * part.stream();
} else if (ArrayBuffer.isView(part)) {
if (clone) {
let position = part.byteOffset;
let end = part.byteOffset + part.byteLength;
while (position !== end) {
const size = Math.min(end - position, POOL_SIZE);
const chunk = part.buffer.slice(position, position + size);
position += chunk.byteLength;
yield new Uint8Array(chunk);
}
} else {
yield part;
}
} else {
/* c8 ignore start */
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob)
let position = 0;
while (position !== part.size) {
const chunk = part.slice(position, Math.min(part.size, position + POOL_SIZE));
const buffer = await chunk.arrayBuffer();
position += buffer.byteLength;
yield new Uint8Array(buffer);
}
/* c8 ignore end */
yield part;
}
}
}
const _Blob = class Blob {
/** @type {Array.<(Blob|Uint8Array)>} */
#parts = [];
#type = '';
#size = 0;
/**
* @template T
* @param {T} object
* @returns {T is Blob}
*/
const isBlob = object => {
return (
typeof object === 'object' &&
typeof object.stream === 'function' &&
typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[Symbol.toStringTag])
);
};
class Blob {
/**
* The Blob() constructor returns a new Blob object. The content
* of the blob consists of the concatenation of the values given
* in the parameter array.
*
* @param {*} blobParts
* @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
* @param {{ type?: string }} [options]
*/
constructor(blobParts = [], options = {}) {
const parts = [];
constructor(blobParts = [], options = {type: ''}) {
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') {
throw new TypeError(`Failed to construct 'Blob': parameter 2 cannot convert to dictionary.`);
}
if (options === null) options = {};
for (const element of blobParts) {
let part;
if (ArrayBuffer.isView(element)) {
part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength));
const parts = blobParts.map(element => {
let buffer;
if (Buffer.isBuffer(element)) {
buffer = element;
} else if (ArrayBuffer.isView(element)) {
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
} else if (element instanceof ArrayBuffer) {
part = new Uint8Array(element.slice(0));
} else if (element instanceof Blob) {
part = element;
buffer = Buffer.from(element);
} else if (isBlob(element)) {
buffer = element;
} else {
part = new TextEncoder().encode(element);
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
}
size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
parts.push(part);
}
size += buffer.length || buffer.size || 0;
return buffer;
});
const type = options.type === undefined ? '' : String(options.type);
const type = options.type === undefined ? '' : String(options.type).toLowerCase();
this.#type = /^[\x20-\x7E]*$/.test(type) ? type : '';
this.#size = size;
this.#parts = parts;
wm.set(this, {
type: /[^\u0020-\u007E]/.test(type) ? '' : type,
size,
parts
});
}
/**
@@ -98,14 +73,14 @@ const _Blob = class Blob {
* size of the Blob in bytes.
*/
get size() {
return this.#size;
return wm.get(this).size;
}
/**
* The type property of a Blob object returns the MIME type of the file.
*/
get type() {
return this.#type;
return wm.get(this).type;
}
/**
@@ -116,16 +91,7 @@ const _Blob = class Blob {
* @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;
return Buffer.from(await this.arrayBuffer()).toString();
}
/**
@@ -136,14 +102,9 @@ const _Blob = class Blob {
* @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;
const data = new Uint8Array(this.size);
let offset = 0;
for await (const chunk of toIterator(this.#parts, false)) {
for await (const chunk of this.stream()) {
data.set(chunk, offset);
offset += chunk.length;
}
@@ -151,16 +112,14 @@ const _Blob = class Blob {
return data.buffer;
}
/**
* The Blob interface's stream() method is difference from native
* and uses node streams instead of whatwg streams.
*
* @returns {Readable} Node readable stream
*/
stream() {
const it = toIterator(this.#parts, true);
return new ReadableStream({
type: 'bytes',
async pull(ctrl) {
const chunk = await it.next();
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value);
}
})
return Readable.from(read(wm.get(this).parts));
}
/**
@@ -179,16 +138,11 @@ const _Blob = class Blob {
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
const span = Math.max(relativeEnd - relativeStart, 0);
const parts = this.#parts;
const parts = wm.get(this).parts.values();
const blobParts = [];
let added = 0;
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
@@ -196,51 +150,36 @@ const _Blob = class Blob {
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;
const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
blobParts.push(chunk);
relativeStart = 0; // All next sequential parts should start at 0
added += size;
relativeStart = 0; // All next sequental parts should start at 0
// don't add the overflow to new blobParts
if (added >= span) {
break;
}
}
}
const blob = new Blob([], {type: String(type).toLowerCase()});
blob.#size = span;
blob.#parts = blobParts;
const blob = new Blob([], {type});
Object.assign(wm.get(blob), {size: span, 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, {
Object.defineProperties(Blob.prototype, {
size: {enumerable: true},
type: {enumerable: true},
slice: {enumerable: true}
});
/** @type {typeof globalThis.Blob} */
export const Blob = _Blob;
export default Blob;
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
value: 'Blob',
writable: false,
enumerable: false,
configurable: true
});
module.exports = Blob;
+20 -52
View File
@@ -1,55 +1,43 @@
{
"name": "fetch-blob",
"version": "3.1.2",
"description": "Blob & File implementation in Node.js, originally from node-fetch.",
"version": "2.0.0",
"description": "A Blob 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",
"streams.cjs"
"index.d.ts"
],
"scripts": {
"lint": "xo test.js",
"test-wpt": "node --experimental-loader ./test/http-loader.js ./test/test-wpt-in-node.js",
"test": "npm run lint && 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"
"lint": "xo",
"test": "xo && ava",
"report": "c8 --reporter json --reporter text ava",
"coverage": "c8 --reporter json --reporter text ava && codecov -f coverage/coverage-final.json",
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js"
},
"repository": "https://github.com/node-fetch/fetch-blob.git",
"keywords": [
"blob",
"file",
"node-fetch"
],
"engines": {
"node": "^12.20 || >= 14.13"
"node": "^10.17.0"
},
"author": "Jimmy Wärting <jimmy@warting.se> (https://jimmy.warting.se)",
"author": "David Frank",
"license": "MIT",
"bugs": {
"url": "https://github.com/node-fetch/fetch-blob/issues"
},
"homepage": "https://github.com/node-fetch/fetch-blob#readme",
"devDependencies": {
"ava": "^3.8.2",
"c8": "^7.2.0",
"codecov": "^3.7.0",
"get-stream": "^5.1.0",
"node-fetch": "^2.6.0",
"typescript": "^3.9.5",
"xo": "^0.32.0"
},
"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",
@@ -60,25 +48,5 @@
}
]
},
"devDependencies": {
"ava": "^3.15.0",
"c8": "^7.7.2",
"codecov": "^3.8.2",
"node-fetch": "^3.0.0-beta.9",
"typescript": "^4.3.2",
"xo": "^0.40.1"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"web-streams-polyfill": "^4.0.0-beta.1"
}
"dependencies": {}
}
-37
View File
@@ -1,37 +0,0 @@
/* c8 ignore start */
// 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536;
if (!globalThis.ReadableStream) {
try {
Object.assign(globalThis, require('stream/web'))
} catch (error) {
// TODO: Remove when only supporting node >= 16.5.0
require('web-streams-polyfill/polyfill')
}
}
try {
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 */
+11 -267
View File
@@ -1,13 +1,8 @@
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 test = require('ava');
const Blob = require('.');
const getStream = require('get-stream');
const {Response} = require('node-fetch');
const {TextDecoder} = require('util');
test('new Blob()', t => {
const blob = new Blob(); // eslint-disable-line no-unused-vars
@@ -28,44 +23,11 @@ test('Blob ctor parts', async t => {
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=');
});
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'));
});
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(''));
t.is(await blob.text(), 'abcdefg[object Object]');
});
test('Blob size', t => {
@@ -117,10 +79,8 @@ test('Blob stream()', async t => {
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());
}
const result = await getStream(blob.stream());
t.is(result, data);
});
test('Blob toString()', t => {
@@ -154,12 +114,6 @@ test('Blob slice(0, -1)', async t => {
t.is(await blob.text(), 'abcdefg');
});
test('Blob(["hello ", "world"]).slice(5)', async t => {
const parts = ['hello ', 'world'];
const blob = new Blob(parts);
t.is(await blob.slice(5).text(), ' world');
});
test('throw away unwanted parts', async t => {
const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
t.is(await blob.text(), 'b');
@@ -169,7 +123,7 @@ 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 response = new Response(blob);
const blob2 = await response.blob();
t.is(await blob2.text(), data);
});
@@ -178,217 +132,7 @@ 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 response = new Response(blob);
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));
});
test('Reading after modified should fail', async t => {
const blob = blobFromSync('./LICENSE');
await new Promise(resolve => {
setTimeout(resolve, 500);
});
fs.closeSync(fs.openSync('./LICENSE', 'a'));
const error = await t.throwsAsync(blob.text());
t.is(error.constructor.name, 'DOMException');
t.is(error instanceof Error, true);
t.is(error.name, 'NotReadableError');
const 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);
});
test('create a blob from path asynchronous', async t => {
const blob = await blobFrom('./LICENSE');
const actual = await blob.text();
t.is(actual, license);
});
test('Reading empty blobs', async t => {
const blob = blobFromSync('./LICENSE').slice(0, 0);
const actual = await blob.text();
t.is(actual, '');
});
test('Blob-ish class is an instance of Blob', t => {
class File {
stream() {}
get [Symbol.toStringTag]() {
return 'File';
}
}
t.true(new File() instanceof Blob);
});
test('Instanceof check returns false for nullish values', t => {
t.false(null instanceof Blob);
});
/** @see https://github.com/w3c/FileAPI/issues/43 - important to keep boundary value */
test('Dose not lowercase the blob values', t => {
const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd';
t.is(new Blob([], {type}).type, type);
});
test('Parts are immutable', async t => {
const buf = new Uint8Array([97]);
const blob = new Blob([buf]);
buf[0] = 98;
t.is(await blob.text(), 'a');
});
test('Blobs are immutable', async t => {
const buf = new Uint8Array([97]);
const blob = new Blob([buf]);
const chunk = await blob.stream().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++;
}
t.is(i === 2, true);
});
test('Can use named import - as well as default', async t => {
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const {Blob, default: def} = await import('./index.js');
t.is(Blob, def);
});
test('default from.js exports blobFromSync', t => {
t.is(blobFromSync, syncBlob);
});
if (buffer.Blob) {
test('Can wrap buffer.Blob to a fetch-blob', async t => {
const blob1 = new buffer.Blob(['blob part']);
const blob2 = new Blob([blob1]);
t.is(await blob2.text(), 'blob part');
});
}
test('File is a instance of blob', t => {
t.true(new File([], '') instanceof Blob);
});
test('fileFrom returns the name', async t => {
t.is((await fileFrom('./LICENSE')).name, 'LICENSE');
});
test('fileFromSync returns the name', t => {
t.is(fileFromSync('./LICENSE').name, 'LICENSE');
});
test('fileFromSync(path, type) sets the type', t => {
t.is(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain');
});
test('blobFromSync(path, type) sets the type', t => {
t.is(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain');
});
test('fileFrom(path, type) sets the type', async t => {
const file = await fileFrom('./LICENSE', 'text/plain');
t.is(file.type, 'text/plain');
});
test('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(,,{}) 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
@@ -1,51 +0,0 @@
// 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
@@ -1,133 +0,0 @@
// 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")