jszip icon indicating copy to clipboard operation
jszip copied to clipboard

generateAsync does not work with Tampermonkey

Open bmacho opened this issue 1 year ago • 6 comments

JSZip doesn't work with Tampermonkey, at least the current newest version, v3.10.1. Version v3.9.1 works just fine.

It is a known problem, see https://github.com/Stuk/jszip/issues/814#issuecomment-1139378561 and https://github.com/cvzi/pinterest-Backup-Original-Files/issues/4 .

I opened this issue, because I've failed to find the conversations above when searching my issue. I only was able to find them when I already knew that this was the problem.

I hope this issue will be easier to find.

Reverting from v3.10.1 to v3.9.1 solved my problem, and it solved their problems too.

bmacho avatar Oct 29 '24 17:10 bmacho

It is a regression, and it breaks many existing userscripts in the wild. I suggest, maybe you could revert it?

bmacho avatar Oct 29 '24 18:10 bmacho

JSZip doesn't work with Tampermonkey, at least the current newest version, v3.10.1. Version v3.9.1 works just fine.

It is a known problem, see #814 (comment) and cvzi/pinterest-Backup-Original-Files#4 .

I opened this issue, because I've failed to find the conversations above when searching my issue. I only was able to find them when I already knew that this was the problem.

I hope this issue will be easier to find.

Reverting from v3.10.1 to v3.9.1 solved my problem, and it solved their problems too.

There is indeed this issue, but unfortunately it hasn't been updated for a long time.

JoeanAmier avatar Dec 30 '24 15:12 JoeanAmier

here is solution -> https://github.com/lisonge/vite-plugin-monkey/issues/216#issuecomment-2745140560

lisonge avatar Mar 22 '25 08:03 lisonge

Confirming that I am also experiencing this issue where zip.generateAsync hangs indefinitely when run inside a Tampermonkey userscript.

My environment details:

  • Browser: Google Chrome (Latest Stable Version)
  • Operating System: Windows 11 24H2
  • Tampermonkey Version: 5.3.3

JSZip versions tested:

  • v3.9.1: Works correctly. generateAsync resolves and the file is generated/saved.
  • v3.10.1: Fails. The script hangs immediately after calling zip.generateAsync({ type: 'blob' }). The promise never resolves or rejects.

The issue can be reproduced reliably using the following minimal standalone Tampermonkey test script:

// ==UserScript==
// @name         JSZip Standalone Functionality Test (for GitHub Issue #934)
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Standalone test for JSZip generateAsync hang in Tampermonkey
// @author       Test User
// @match        https://example.com/
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js
// @grant        GM_addStyle
// ==/UserScript==

/* global JSZip, saveAs */

(function() {
    'use strict';

    console.log("JSZip Test Script: Initializing...");

    // --- Add styles ---
    GM_addStyle(`
        #jszip-test-button {
            position: fixed; bottom: 20px; right: 20px; z-index: 9999;
            padding: 10px 15px; background-color: #FF6347; color: white;
            border: none; border-radius: 5px; cursor: pointer; font-size: 14px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        #jszip-test-button:disabled {
            background-color: #cccccc; cursor: not-allowed;
        }
    `);

    // --- Create test button ---
    let testButton = document.createElement('button');
    testButton.id = 'jszip-test-button';
    testButton.innerText = 'Run JSZip Test';
    document.body.appendChild(testButton);

    // --- Button click handler ---
    testButton.addEventListener('click', function() {
        console.log("JSZip Test Button Clicked!");
        testButton.disabled = true;
        testButton.innerText = 'Testing...';

        try {
            console.log("1. Creating JSZip instance...");
            const zip = new JSZip();

            console.log("2. Creating folder 'test_files'...");
            const folder = zip.folder("test_files");

            console.log("3. Creating test Blob...");
            const testContent = `Hello from JSZip Standalone Test!\nTime: ${new Date().toISOString()}\nRandom: ${Math.random()}`;
            const testBlob = new Blob([testContent], { type: 'text/plain;charset=utf-8' });
            console.log("   Test Blob created, size:", testBlob.size);

            console.log("4. Converting Blob to ArrayBuffer...");
            testBlob.arrayBuffer().then(buffer => {
                console.log("   ArrayBuffer conversion success, adding to ZIP...");
                try {
                    folder.file("hello.txt", buffer); // Add file to folder
                    console.log("5. File 'test_files/hello.txt' added to JSZip object.");
                } catch(addFileError) {
                    console.error("   Error adding file to zip:", addFileError);
                    alert("Error adding file to zip! Check console.");
                    testButton.disabled = false; testButton.innerText = 'Run JSZip Test';
                    return;
                }

                console.log("6. Preparing to call zip.generateAsync({ type: 'blob' })...");
                // Note: The @require line above uses 3.10.1 by default.
                // Change it to 3.9.1 to observe the working behavior.
                zip.generateAsync({ type: "blob" }) // Simple invocation
                    .then(generatedBlob => {
                        // This block is NOT reached with 3.10.1
                        console.log("7. SUCCESS: zip.generateAsync .then() reached!");
                        console.log("   Generated Blob size:", generatedBlob.size);
                        console.log("8. Calling saveAs...");
                        try {
                            saveAs(generatedBlob, "jszip_standalone_test.zip");
                            console.log("9. saveAs call completed.");
                            alert("JSZip test SUCCESSFUL! File download triggered.");
                        } catch (saveAsError) {
                             console.error("   Error during saveAs:", saveAsError);
                             alert("saveAs failed! Check console.");
                        }
                    })
                    .catch(err => {
                        // This block is NOT reached when 3.10.1 hangs
                        console.error("E1: zip.generateAsync FAILED (.catch):", err);
                        alert("zip.generateAsync failed! Check console.");
                    })
                    .finally(() => {
                        // This block is NOT reached when 3.10.1 hangs
                        console.log("10. zip.generateAsync .finally() reached.");
                        testButton.disabled = false;
                        testButton.innerText = 'Run JSZip Test';
                    });
                // This is the last log message observed when using JSZip 3.10.1
                console.log("   Call to zip.generateAsync has been issued (asynchronous).");

            }).catch(bufferErr => {
                console.error("E2: Error converting Blob to ArrayBuffer:", bufferErr);
                alert("Error creating test data! Check console.");
                testButton.disabled = false; testButton.innerText = 'Run JSZip Test';
            });

        } catch (initialError) {
            console.error("E3: Initial script error:", initialError);
            alert("Test script error! Check console.");
            testButton.disabled = false; testButton.innerText = 'Run JSZip Test';
        }
    });

    console.log("JSZip Test Script: Initialized and ready.");

})();

Behavior observed:

  • When the @require line in the test script uses JSZip v3.10.1, running the test results in the console logging up to Call to zip.generateAsync has been issued (asynchronous).. The script then hangs indefinitely; the .then(), .catch(), and .finally() blocks attached to the generateAsync promise are never executed.
  • When the @require line is changed to use JSZip v3.9.1, the script runs correctly: generateAsync resolves, the .then() block is executed logs its messages, and saveAs successfully triggers the file download.

nerdneilsfield avatar Apr 20 '25 09:04 nerdneilsfield

Cause of error

jszip use setImmediate as polyfill

https://github.com/Stuk/jszip/blob/643714aa770afd8fe1df6cfc7e2bde945bb0ef64/lib/utils.js#L7


setImmediate has code if (event.source === global &&

https://github.com/YuzuJS/setImmediate/blob/f1ccbfdf09cb93aadf77c4aa749ea554503b9234/setImmediate.js#L106 Image

when run it tampermonkey scop without // @grant none

event.source actually is globalThis.unsafeWindow, global/globalThis is a Proxy(Object)

the condition is false, so setImmediate callback is not running

Solutions

for @require url

// @require      data:application/javascript,%3BglobalThis.setImmediate%3DsetTimeout%3B
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js

for vite

export default defineConfig(() => {
  return {
    plugins: [
      {
        name: 'fix-setImmediate',
        enforce: 'pre',
        apply: 'build',
        transform(code, id) {
          if (id.endsWith('setImmediate.js')) {
            return {
              code: code.replace(
                `if (event.source === global &&`,
                `if ((event.source === global || event.source === global.unsafeWindow) &&`,
              ),
              map: null,
            };
          }
        },
      },
    ],
  };
});

lisonge avatar Apr 20 '25 10:04 lisonge

I'm also using JSZip 3.10.1 with Tampermonkey. Therefore a dynamically load the JSZip.js

Before I used:

var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", "https://raw.githubusercontent.com/Stuk/jszip/refs/heads/main/dist/jszip.min.js", false); 
xmlHttp.send(null);
eval(xmlHttp.responseText);
console.log("JSZip loaded: " + JSZip.version);
result(new JSZip());

but then .generateAsync doesn't work. No result and no reject from the promise.

But using unsafeWindow for the evaluation does the trick:

var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", "https://raw.githubusercontent.com/Stuk/jszip/refs/heads/main/dist/jszip.min.js", false); 
xmlHttp.send(null);
unsafeWindow.eval(xmlHttp.responseText);
console.log("JSZip loaded: " + JSZip.version);
result(new JSZip());

after that everything works as intended.

demawi avatar Apr 22 '25 22:04 demawi