Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
owowed opened this issue Aug 24, 2024 · 7 comments
Open

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

owowed opened this issue Aug 24, 2024 · 7 comments

Comments

@owowed
Copy link

owowed commented Aug 24, 2024

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/[email protected]/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/[email protected]/index.min.js"
                },
            }
        })
    ]
});

(Edited for more clarity)

@owowed
Copy link
Author

owowed commented Aug 26, 2024

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
Copy link
Owner

lisonge commented Aug 26, 2024

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
Copy link
Author

owowed commented Aug 26, 2024

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
Copy link
Owner

lisonge commented Aug 26, 2024

sorry, it should be GM_getResourceText

/**
* @see https://www.tampermonkey.net/documentation.php#api:GM_getResourceText
* @see https://violentmonkey.github.io/api/gm/#gm_getresourcetext
*/
export const GM_getResourceText = /* @__PURE__ */ (() =>
monkeyWindow.GM_getResourceText)();

@lisonge
Copy link
Owner

lisonge commented Aug 26, 2024

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
Copy link
Author

owowed commented Aug 26, 2024

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 owowed changed the title Support for ESM-only libraries by using dynamic imports [Feature] Support for building with ESM-only libraries Aug 26, 2024
@owowed
Copy link
Author

owowed commented Aug 26, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants