From e7615821aa7028202fc2c8efa0649caae2d07e97 Mon Sep 17 00:00:00 2001 From: Bjorn Stromberg Date: Sun, 30 Jun 2024 12:02:07 +0800 Subject: [PATCH] v8.0.20240630 --- .github/workflows/node.js.yml | 7 +- Blob.js | 205 ++++++++++++++++++---------------- CHANGELOG.md | 7 ++ package.json | 2 +- test/index.js | 60 +++++++--- 5 files changed, 161 insertions(+), 120 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index a180e2f..3e86233 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,14 +16,15 @@ jobs: strategy: matrix: - node-version: [12, 14, 16, 17] + node-version: [12, 14, 16, 18, 20, 22] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm i - run: npm run build --if-present + - run: npm run lint - run: npm test diff --git a/Blob.js b/Blob.js index 0ea23b4..e240452 100644 --- a/Blob.js +++ b/Blob.js @@ -8,6 +8,40 @@ * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md */ +function array2base64 (input) { + var byteToCharMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + var output = []; + + for (var i = 0; i < input.length; i += 3) { + var byte1 = input[i]; + var haveByte2 = i + 1 < input.length; + var byte2 = haveByte2 ? input[i + 1] : 0; + var haveByte3 = i + 2 < input.length; + var byte3 = haveByte3 ? input[i + 2] : 0; + + var outByte1 = byte1 >> 2; + var outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4); + var outByte3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6); + var outByte4 = byte3 & 0x3F; + + if (!haveByte3) { + outByte4 = 64; + + if (!haveByte2) { + outByte3 = 64; + } + } + + output.push( + byteToCharMap[outByte1], byteToCharMap[outByte2], + byteToCharMap[outByte3], byteToCharMap[outByte4] + ); + } + + return output.join(""); +} + (function(global) { (function (factory) { if (typeof define === "function" && define.amd) { @@ -263,39 +297,6 @@ } return view; } - function array2base64 (input) { - var byteToCharMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - - var output = []; - - for (var i = 0; i < input.length; i += 3) { - var byte1 = input[i]; - var haveByte2 = i + 1 < input.length; - var byte2 = haveByte2 ? input[i + 1] : 0; - var haveByte3 = i + 2 < input.length; - var byte3 = haveByte3 ? input[i + 2] : 0; - - var outByte1 = byte1 >> 2; - var outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4); - var outByte3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6); - var outByte4 = byte3 & 0x3F; - - if (!haveByte3) { - outByte4 = 64; - - if (!haveByte2) { - outByte3 = 64; - } - } - - output.push( - byteToCharMap[outByte1], byteToCharMap[outByte2], - byteToCharMap[outByte3], byteToCharMap[outByte4] - ); - } - - return output.join(""); - } var create = Object.create || function (a) { function c () {} @@ -384,6 +385,7 @@ this.type = this.type.toLowerCase(); } } + Blob.isPolyfill = true; Blob.prototype.arrayBuffer = function () { return Promise.resolve(this._buffer.buffer || this._buffer); @@ -415,6 +417,7 @@ return a; } + File.isPolyfill = true; File.prototype = create(Blob.prototype); File.prototype.constructor = File; @@ -430,79 +433,21 @@ return "[object File]"; }; - /********************************************************/ - /* FileReader constructor */ - /********************************************************/ - function FileReader () { - if (!(this instanceof FileReader)) { - throw new TypeError("Failed to construct 'FileReader': Please use the 'new' operator, this DOM object constructor cannot be called as a function."); - } - - var delegate = document.createDocumentFragment(); - this.addEventListener = delegate.addEventListener; - this.dispatchEvent = function (evt) { - var local = this["on" + evt.type]; - if (typeof local === "function") local(evt); - delegate.dispatchEvent(evt); - }; - this.removeEventListener = delegate.removeEventListener; - } - - function _read (fr, blob, kind) { - if (!(blob instanceof Blob)) { - throw new TypeError("Failed to execute '" + kind + "' on 'FileReader': parameter 1 is not of type 'Blob'."); - } - - fr.result = ""; - - setTimeout(function () { - this.readyState = FileReader.LOADING; - fr.dispatchEvent(new Event("load")); - fr.dispatchEvent(new Event("loadend")); - }); - } - - FileReader.EMPTY = 0; - FileReader.LOADING = 1; - FileReader.DONE = 2; - FileReader.prototype.error = null; - FileReader.prototype.onabort = null; - FileReader.prototype.onerror = null; - FileReader.prototype.onload = null; - FileReader.prototype.onloadend = null; - FileReader.prototype.onloadstart = null; - FileReader.prototype.onprogress = null; - - FileReader.prototype.readAsDataURL = function (blob) { - _read(this, blob, "readAsDataURL"); - this.result = "data:" + blob.type + ";base64," + array2base64(blob._buffer); - }; - - FileReader.prototype.readAsText = function (blob) { - _read(this, blob, "readAsText"); - this.result = textDecode(blob._buffer); - }; - - FileReader.prototype.readAsArrayBuffer = function (blob) { - _read(this, blob, "readAsText"); - // return ArrayBuffer when possible - this.result = (blob._buffer.buffer || blob._buffer).slice(); - }; - - FileReader.prototype.abort = function () {}; - /********************************************************/ /* URL */ /********************************************************/ + URL.createObjectURL = function (blob) { return blob instanceof Blob ? "data:" + blob.type + ";base64," + array2base64(blob._buffer) : createObjectURL.call(URL, blob); }; + URL.createObjectURL.isPolyfill = true; URL.revokeObjectURL = function (url) { revokeObjectURL && revokeObjectURL.call(URL, url); }; + URL.revokeObjectURL.isPolyfill = true; /********************************************************/ /* XHR */ @@ -521,12 +466,10 @@ exports.Blob = Blob; exports.File = File; - exports.FileReader = FileReader; - exports.URL = URL; } function fixFileAndXHR () { - var isIE = !!global.ActiveXObject || ( + var isIE = !!global.ActiveXObject || (typeof document !== "undefined" && "-ms-scroll-limit" in document.documentElement.style && "-ms-ime-align" in document.documentElement.style ); @@ -549,7 +492,6 @@ try { new File([], ""); exports.File = global.File; - exports.FileReader = global.FileReader; } catch (e) { try { exports.File = new Function("class File extends Blob {" + @@ -563,7 +505,7 @@ "return new File([], \"\"), File" )(); } catch (e) { - exports.File = function (b, d, c) { + exports.File = function File(b, d, c) { var blob = new Blob(b, c); var t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date(); @@ -581,6 +523,7 @@ return blob; }; } + exports.File.isPolyfill = true; } } @@ -594,6 +537,70 @@ FakeBlobBuilder(); } + /********************************************************/ + /* FileReader constructor */ + /********************************************************/ + function FileReader () { + if (!(this instanceof FileReader)) { + throw new TypeError("Failed to construct 'FileReader': Please use the 'new' operator, this DOM object constructor cannot be called as a function."); + } + + var delegate = document.createDocumentFragment(); + this.addEventListener = delegate.addEventListener; + this.dispatchEvent = function (evt) { + var local = this["on" + evt.type]; + if (typeof local === "function") local(evt); + delegate.dispatchEvent(evt); + }; + this.removeEventListener = delegate.removeEventListener; + } + + function _read (fr, blob, kind) { + if (!(blob instanceof exports.Blob)) { + throw new TypeError("Failed to execute '" + kind + "' on 'FileReader': parameter 1 is not of type 'Blob'."); + } + + fr.result = ""; + + setTimeout(function () { + this.readyState = FileReader.LOADING; + fr.dispatchEvent(new Event("load")); + fr.dispatchEvent(new Event("loadend")); + }); + } + + FileReader.EMPTY = 0; + FileReader.LOADING = 1; + FileReader.DONE = 2; + FileReader.prototype.error = null; + FileReader.prototype.onabort = null; + FileReader.prototype.onerror = null; + FileReader.prototype.onload = null; + FileReader.prototype.onloadend = null; + FileReader.prototype.onloadstart = null; + FileReader.prototype.onprogress = null; + + FileReader.prototype.readAsDataURL = function (blob) { + _read(this, blob, "readAsDataURL"); + this.result = "data:" + blob.type + ";base64," + array2base64(blob._buffer); + }; + + FileReader.prototype.readAsText = function (blob) { + _read(this, blob, "readAsText"); + this.result = textDecode(blob._buffer); + }; + + FileReader.prototype.readAsArrayBuffer = function (blob) { + _read(this, blob, "readAsText"); + // return ArrayBuffer when possible + this.result = (blob._buffer.buffer || blob._buffer).slice(); + }; + + FileReader.prototype.abort = function () {}; + + exports.FileReader = global.FileReader || FileReader; + exports.URL = global.URL || URL; + if (strTag) { if (!exports.File.prototype[strTag]) exports.File.prototype[strTag] = "File"; if (!exports.Blob.prototype[strTag]) exports.Blob.prototype[strTag] = "Blob"; diff --git a/CHANGELOG.md b/CHANGELOG.md index b973d27..35f7f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # `blob-polyfill` CHANGELOG +## v8.0.20240630 +* [Blob.js] Change Blob.prototype to global.Blob.prototype (@tmisirpash) +* [Blob.js] Make it work in environments where global.Blob exists, but global.FileReader does not (@bjornstar) +* [Blob.js] Add `isPolyfill` property to the polyfilled versions so we can differentiate them (@bjornstar) +* [test] Unskip tests and update to work in environments with global.Blob & global.File & global.URL (@bjornstar) +* [.github] Update action versions and test node v12-v22 (@bjornstar) + ## v7.0.20220408 * [Blob.js] Do not modify array that is passed into constructor (@zyrong) * [.github] Start automated tests on github (@bjornstar) diff --git a/package.json b/package.json index 6e619c8..4dd4585 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blob-polyfill", - "version": "7.0.20220408", + "version": "8.0.20240630", "description": "Blob.js implements the W3C Blob interface in browsers that do not natively support it.", "main": "Blob.js", "scripts": { diff --git a/test/index.js b/test/index.js index d106de3..07f418e 100644 --- a/test/index.js +++ b/test/index.js @@ -11,10 +11,16 @@ var URL = BlobPolyfill.URL; describe("blob-polyfill", function () { describe("Blob", function () { it("Does not pollute the global Blob definition", function () { - assert.strictEqual(typeof global.Blob, "undefined"); - assert.throws(function () { - new global.Blob(); - }, TypeError, "global.Blob should not be a constructor"); + if (typeof global.Blob === "function") { + assert(Blob === global.Blob); + assert.strictEqual(Blob.isPolyfill, undefined); + } else { + assert.strictEqual(typeof global.Blob, "undefined"); + assert.throws(function () { + new global.Blob(); + }, TypeError, "global.Blob should not be a constructor"); + assert.strictEqual(Blob.isPolyfill, true); + } }); it("At the very least, we can instantiate an empty Blob", function () { @@ -63,19 +69,24 @@ describe("blob-polyfill", function () { }); it("Does not modify the source array", function () { - var array = ['mutation']; + var array = ["mutation"]; var clone = array.slice(); - var blob = new Blob(array); + new Blob(array); assert.deepStrictEqual(array, clone); }); }); describe("File", function () { it("Does not pollute the global File definition", function () { - assert.strictEqual(typeof global.File, "undefined"); - assert.throws(function () { - new global.File(); - }, TypeError, "global.File should be undefined"); + if (typeof global.File === "function") { + assert.strictEqual(File, global.File); + assert.strictEqual(File.isPolyfill, undefined); + } else { + assert.strictEqual(typeof global.File, "undefined"); + assert.throws(function () { + new global.File(); + }, TypeError, "global.File should be undefined"); + } }); it("We can instantiate a File", function () { @@ -86,8 +97,8 @@ describe("blob-polyfill", function () { assert.strictEqual(file.name, ""); }); - it("Symbol is File", function () { - assert.strictEqual(File.prototype[Symbol.toStringTag], "File"); + it("Symbol is File or Blob", function () { + assert.ok(["Blob", "File"].includes(File.prototype[Symbol.toStringTag])); }); }); @@ -112,14 +123,29 @@ describe("blob-polyfill", function () { }); describe("URL", function () { - it.skip("Does not pollute the global URL definition", function () { + it("Modifies the global URL to always create Blobs if Blobs are not native", function () { assert.strictEqual(typeof global.URL, "function"); - assert.notStrictEqual(URL, global.URL); + assert.strictEqual(URL, global.URL); + if (typeof global.Blob === "function") { + assert.strictEqual(global.URL.createObjectURL.isPolyfill, undefined); + assert.strictEqual(global.URL.revokeObjectURL.isPolyfill, undefined); + } else { + assert.strictEqual(typeof global.URL, "function"); + assert.strictEqual(URL, global.URL); + assert.strictEqual(global.URL.createObjectURL.isPolyfill, true); + assert.strictEqual(global.URL.revokeObjectURL.isPolyfill, true); + } }); it("We can call URL.createObjectUrl", function () { - var url = URL.createObjectURL(new File(["hello world"], "hello.txt", { type: "application/plain-text" })); - assert.strictEqual(typeof url, "string"); - assert.strictEqual(url, "data:application/plain-text;base64,aGVsbG8gd29ybGQ="); + if (typeof global.Blob !== "function") { + var polyfilledUrl = URL.createObjectURL(new File(["hello world"], "hello.txt", { type: "application/plain-text" })); + assert.strictEqual(typeof polyfilledUrl, "string"); + assert.strictEqual(polyfilledUrl, "data:application/plain-text;base64,aGVsbG8gd29ybGQ="); + } else { + var nodeUrl = URL.createObjectURL(new File(["hello world"], "hello.txt", { type: "application/plain-text" })); + assert.strictEqual(typeof nodeUrl, "string"); + assert.match(nodeUrl, /blob:nodedata:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/); + } }); }); });