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
8 changed files with 42 additions and 210 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
node: ["15", "14", "12", engines] node: ["14", "12", engines]
exclude: exclude:
# On Windows, run tests with only the LTS environments. # On Windows, run tests with only the LTS environments.
- os: windows-latest - os: windows-latest
-1
View File
@@ -63,4 +63,3 @@ typings/
.env .env
index.d.ts index.d.ts
from.d.ts
-19
View File
@@ -1,25 +1,6 @@
Changelog Changelog
========= =========
## next
- Fixed a bug where in BlobDataItem when the file was empty (#86)
## 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).
## v2.0.0 ## v2.0.0
> Note: This release was previously published as `1.0.7`, but as it contains breaking changes, we renamed it to `2.0.0`. > Note: This release was previously published as `1.0.7`, but as it contains breaking changes, we renamed it to `2.0.0`.
-18
View File
@@ -27,24 +27,6 @@ fetch('https://httpbin.org/post', {
.then(json => console.log(json)); .then(json => console.log(json));
``` ```
### Blob part backed up by filesystem
To use, install [domexception](https://github.com/jsdom/domexception).
```sh
npm install fetch-blob domexception
```
```js
const blobFrom = require('fetch-blob/from.js');
const blob1 = blobFrom('./2-GiB-file.bin');
const blob2 = blobFrom('./2-GiB-file.bin');
// Not a 4 GiB memory snapshot, just holds 3 references
// points to where data is located on the disk
const blob = new Blob([blob1, blob2]);
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. 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-image]: https://flat.badgen.net/npm/v/fetch-blob
-61
View File
@@ -1,61 +0,0 @@
const {statSync, createReadStream} = require('fs');
const Blob = require('./index.js');
const DOMException = require('domexception');
/**
* @param {string} path filepath on the disk
* @returns {Blob}
*/
function blobFrom(path) {
const {size, mtime} = statSync(path);
const blob = new BlobDataItem({path, size, mtime});
return new Blob([blob]);
}
/**
* This is a blob backed up by a file on the disk
* with minium requirement
*
* @private
*/
class BlobDataItem {
constructor(options) {
this.size = options.size;
this.path = options.path;
this.start = options.start || 0;
this.mtime = options.mtime;
}
// Slicing arguments is first validated and formated
// to not be out of range by Blob.prototype.slice
slice(start, end) {
return new BlobDataItem({
path: this.path,
start,
mtime: this.mtime,
size: end - start
});
}
stream() {
if (statSync(this.path).mtime > this.mtime) {
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) {
return new Blob().stream();
}
return createReadStream(this.path, {
start: this.start,
end: this.start + this.size - 1
});
}
get [Symbol.toStringTag]() {
return 'Blob';
}
}
module.exports = blobFrom;
+26 -21
View File
@@ -15,6 +15,20 @@ async function * read(parts) {
} }
} }
/**
* @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 { class Blob {
/** /**
* The Blob() constructor returns a new Blob object. The content * The Blob() constructor returns a new Blob object. The content
@@ -24,24 +38,23 @@ class Blob {
* @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts * @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
* @param {{ type?: string }} [options] * @param {{ type?: string }} [options]
*/ */
constructor(blobParts = [], options = {}) { constructor(blobParts = [], options = {type: ''}) {
let size = 0; let size = 0;
const parts = blobParts.map(element => { const parts = blobParts.map(element => {
let buffer; let buffer;
if (element instanceof Buffer) { if (Buffer.isBuffer(element)) {
buffer = element; buffer = element;
} else if (ArrayBuffer.isView(element)) { } else if (ArrayBuffer.isView(element)) {
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
} else if (element instanceof ArrayBuffer) { } else if (element instanceof ArrayBuffer) {
buffer = Buffer.from(element); buffer = Buffer.from(element);
} else if (element instanceof Blob) { } else if (isBlob(element)) {
buffer = element; buffer = element;
} else { } else {
buffer = Buffer.from(typeof element === 'string' ? element : String(element)); buffer = Buffer.from(typeof element === 'string' ? element : String(element));
} }
// eslint-disable-next-line unicorn/explicit-length-check
size += buffer.length || buffer.size || 0; size += buffer.length || buffer.size || 0;
return buffer; return buffer;
}); });
@@ -139,7 +152,7 @@ class Blob {
} else { } else {
const chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
blobParts.push(chunk); blobParts.push(chunk);
added += ArrayBuffer.isView(chunk) ? chunk.byteLength : chunk.size; added += size;
relativeStart = 0; // All next sequental parts should start at 0 relativeStart = 0; // All next sequental parts should start at 0
// don't add the overflow to new blobParts // don't add the overflow to new blobParts
@@ -149,26 +162,11 @@ class Blob {
} }
} }
const blob = new Blob([], {type: String(type).toLowerCase()}); const blob = new Blob([], {type});
Object.assign(wm.get(blob), {size: span, parts: blobParts}); Object.assign(wm.get(blob), {size: span, parts: blobParts});
return blob; return blob;
} }
get [Symbol.toStringTag]() {
return 'Blob';
}
static [Symbol.hasInstance](object) {
return (
object &&
typeof object === 'object' &&
typeof object.stream === 'function' &&
object.stream.length === 0 &&
typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[Symbol.toStringTag])
);
}
} }
Object.defineProperties(Blob.prototype, { Object.defineProperties(Blob.prototype, {
@@ -177,4 +175,11 @@ Object.defineProperties(Blob.prototype, {
slice: {enumerable: true} slice: {enumerable: true}
}); });
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
value: 'Blob',
writable: false,
enumerable: false,
configurable: true
});
module.exports = Blob; module.exports = Blob;
+14 -31
View File
@@ -1,20 +1,18 @@
{ {
"name": "fetch-blob", "name": "fetch-blob",
"version": "2.1.2", "version": "2.0.0",
"description": "A Blob implementation in Node.js, originally from node-fetch.", "description": "A Blob implementation in Node.js, originally from node-fetch.",
"main": "index.js", "main": "index.js",
"files": [ "files": [
"from.js",
"index.js", "index.js",
"index.d.ts", "index.d.ts"
"from.d.ts"
], ],
"scripts": { "scripts": {
"lint": "xo", "lint": "xo",
"test": "xo && ava", "test": "xo && ava",
"report": "c8 --reporter json --reporter text ava", "report": "c8 --reporter json --reporter text ava",
"coverage": "c8 --reporter json --reporter text ava && codecov -f coverage/coverage-final.json", "coverage": "c8 --reporter json --reporter text ava && codecov -f coverage/coverage-final.json",
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js from.js" "prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js"
}, },
"repository": "https://github.com/node-fetch/fetch-blob.git", "repository": "https://github.com/node-fetch/fetch-blob.git",
"keywords": [ "keywords": [
@@ -22,7 +20,7 @@
"node-fetch" "node-fetch"
], ],
"engines": { "engines": {
"node": "^10.17.0 || >=12.3.0" "node": "^10.17.0"
}, },
"author": "David Frank", "author": "David Frank",
"license": "MIT", "license": "MIT",
@@ -30,17 +28,16 @@
"url": "https://github.com/node-fetch/fetch-blob/issues" "url": "https://github.com/node-fetch/fetch-blob/issues"
}, },
"homepage": "https://github.com/node-fetch/fetch-blob#readme", "homepage": "https://github.com/node-fetch/fetch-blob#readme",
"xo": { "devDependencies": {
"rules": { "ava": "^3.8.2",
"unicorn/import-index": "off", "c8": "^7.2.0",
"import/extensions": [ "codecov": "^3.7.0",
"error", "get-stream": "^5.1.0",
"always", "node-fetch": "^2.6.0",
{ "typescript": "^3.9.5",
"ignorePackages": true "xo": "^0.32.0"
}
]
}, },
"xo": {
"overrides": [ "overrides": [
{ {
"files": "test.js", "files": "test.js",
@@ -51,19 +48,5 @@
} }
] ]
}, },
"peerDependenciesMeta": { "dependencies": {}
"domexception": {
"optional": true
}
},
"devDependencies": {
"ava": "^3.15.0",
"c8": "^7.7.1",
"codecov": "^3.8.1",
"domexception": "^2.0.1",
"get-stream": "^6.0.1",
"node-fetch": "^2.6.1",
"typescript": "^4.2.4",
"xo": "^0.38.2"
}
} }
+1 -58
View File
@@ -1,10 +1,8 @@
const fs = require('fs');
const test = require('ava'); const test = require('ava');
const Blob = require('.');
const getStream = require('get-stream'); const getStream = require('get-stream');
const {Response} = require('node-fetch'); const {Response} = require('node-fetch');
const {TextDecoder} = require('util'); const {TextDecoder} = require('util');
const Blob = require('./index.js');
const blobFrom = require('./from.js');
test('new Blob()', t => { test('new Blob()', t => {
const blob = new Blob(); // eslint-disable-line no-unused-vars const blob = new Blob(); // eslint-disable-line no-unused-vars
@@ -116,12 +114,6 @@ test('Blob slice(0, -1)', async t => {
t.is(await blob.text(), 'abcdefg'); 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 => { test('throw away unwanted parts', async t => {
const blob = new Blob(['a', 'b', 'c']).slice(1, 2); const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
t.is(await blob.text(), 'b'); t.is(await blob.text(), 'b');
@@ -144,52 +136,3 @@ test('Blob works with node-fetch Response.text()', async t => {
const text = await response.text(); const text = await response.text();
t.is(text, data); t.is(text, data);
}); });
test('blob part backed up by filesystem', async t => {
const blob = blobFrom('./LICENSE');
t.is(await blob.slice(0, 3).text(), 'MIT');
t.is(await blob.slice(4, 11).text(), 'License');
});
test('Reading after modified should fail', async t => {
const blob = blobFrom('./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.name, 'NotReadableError');
});
test('Reading from the stream created by blobFrom', async t => {
const blob = blobFrom('./LICENSE');
const expected = await fs.promises.readFile('./LICENSE', 'utf-8');
const actual = await getStream(blob.stream());
t.is(actual, expected);
});
test('Reading empty blobs', async t => {
const blob = blobFrom('./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);
});