lisonge / vite-plugin-monkey

A vite plugin server and build your.user.js for userscript engine like Tampermonkey, Violentmonkey, Greasemonkey, ScriptCat
MIT License
1.33k stars 70 forks source link

[Feature] Support for building with ESM-only libraries #180

Open owowed opened 3 weeks ago

owowed commented 3 weeks ago

I'd like support for building with packages that only provide ESM dist, one example is jsx-dom, which doesn't provide UMD/IIFE dist, but only providing ESM.

We can already use ESM dist in the userscript via dynamic import in async IIFE:

// ==UserScript==
// @name       esm-only packages support
// @namespace  vite-plugin-monkey
// @version    0.0.1
// @author     owowed
// @license    ISC
// @match      https://www.google.com/
// ==/UserScript==

(async function () {
    'use strict';

    var React = await import("https://cdn.jsdelivr.net/npm/jsx-dom@8.1.5/index.min.js");

    const test = /* @__PURE__ */ React.createElement("div", null);
    document.body.appendChild(test);
    console.log(React);

})();

Maybe we can add a new build.externalDynamicImports configuration that'll automatically resolve ESM-only external modules to dynamic import:

import { defineConfig } from "vite";
import monkey from "vite-plugin-monkey";

export default defineConfig({
    plugins: [
        monkey({
            entry: "./src/main.js",
            userscript: {
                match: [
                    "https://www.google.com/"
                ]
            },
            build: {
                externalDynamicImports: { // similar to externalGlobals
                     // replace any "jsx-dom" import in the source code using `await import(url)`
                    "jsx-dom": "https://cdn.jsdelivr.net/npm/jsx-dom@8.1.5/index.min.js"
                },
            }
        })
    ]
});

(Edited for more clarity)

owowed commented 2 weeks ago

Since dynamic import has to follow CORS rules, here is an implementation to workaround that using GM_fetch/GM_xhr:

async function importShim<ModuleType>(url: string): Promise<ModuleType> {
    const script = await GM_fetch(url).then(res => res.text()); // recommend @trim21/gm-fetch
    const scriptBlob = new Blob([script], { type: "text/javascript" });
    const importUrl = URL.createObjectURL(scriptBlob);
    return import(importUrl);
}

Of course, this will only work if CSP allows dynamic import from blob URLs. Don't forget there is also GM_import API that can bypass this, but only available in FireMonkey (and unfortunately, no other userscript manager supports it).

lisonge commented 2 weeks ago

you can use GM_getText instead of fetch to load module text

but it is not supported that target module text import another remote relative module

owowed commented 2 weeks ago

you can use GM_getText instead of fetch to load module text

I've never seen GM_getText API before, and its not in the Violentmonkey or Tampermonkey docs. Could you perhaps provide link to the documentation?

but it is not supported that target module text import another remote relative module

The importShim function doesn't import the remote module directly, it first fetches the remote module via GM_fetch, and then import that using Blob and URL.createObjectURL. Because of that, importShim imports from a blob URL that's coming from the same origin, which should run on the website without violating CORS.

lisonge commented 2 weeks ago

sorry, it should be GM_getResourceText

https://github.com/lisonge/vite-plugin-monkey/blob/d8127bd6d86966df810b93031bbac8a83f9963b6/packages/vite-plugin-monkey/src/client/index.ts#L66-L71

lisonge commented 2 weeks ago

but it is not supported that target module text import another remote relative module

if your target module is the following code

// it import another remote relative module
export * from './'

your importShim will not work

owowed commented 2 weeks ago

You're right. The importShim function is designed to import from external module, like CDNs (jsdelivr, unpkg, cdnjs etc.) Users can still choose to use the normal import for normal relative imports.

The naming for importShim is kind of confusing since shims are meant for polyfills. Sorry, I'll refer to them as importExternal from now on 😅

owowed commented 2 weeks ago

Actually, we can create importShim that can handle both of these cases, and support GM_getResourceText:

async function importShim(url) {
    // for importing external modules outside of its own origin or using GM_getResourceText
    if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("gm-resource:")) {
        let scriptText = url.startsWith("gm-resource:")
            ? await (GM_getResourceText || GM.getResourceText)(url.split(":")[1])
            : await GM_fetch(url).then(res => res.text());
        const scriptBlob = new Blob([scriptText], { type: "text/javascript" });
        const importUrl = URL.createObjectURL(scriptBlob);
        return import(importUrl);
    }
    return import(url);
}

GM_fetch is still useful in case the user doesn't want to manually add resource entries.