[Feature] Support for building with ESM-only libraries
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)
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).
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
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.
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
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
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 😅
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.
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
importShimwill not work
This case can be resolved?
See this issue: https://github.com/Tampermonkey/tampermonkey/issues/1396
Based on the issue and my own tests, user scripts can use dynamic import directly. For example, this code works:
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://www.google.com/
// @icon https://www.google.com/s2/favicons?domain=github.com
// ==/UserScript==
(async function () {
'use strict'
console.log('start')
// Import an ESM module directly from a CDN
// Example using esm.sh for a library like 'uuid'
try {
const { v4: uuidv4 } = await import('https://esm.sh/uuid')
console.log('Generated UUID:', uuidv4())
} catch (error) {
console.error('Failed to import or use ESM module:', error)
}
// Another example with a different CDN and library
// const { createPicker } = await import('https://unpkg.com/picmo@latest/dist/index.js');
// createPicker(...);
})()
However, this will be blocked if the page has a Content Security Policy (CSP). I checked that resources listed in @require are injected into userscript.html at runtime, so they are not blocked by CSP.
If you want to support dynamic import but the userscript engine itself doesn’t allow it, you would have to bundle all ESM code into the userscript at build time to avoid CSP issues. That is a build-time workaround, so it doesn't reduce the final bundle size and its benefit is limited. For example, Greasy Fork limits scripts to 2 MB — if your bundled script is larger than that, you still exceed the limit. It's better to use proper tree shaking during bundling.
In the end, true support for dynamic import should come from the userscript engine. That's my current understanding.
@yunsii
The issue is only valuable if we consider ES importing as the browser's import api, as when you do nothing, Vite will bundle those dependencies into your dist file by default, whether they are CJS or ES modules.
While I believe that's the engine's responsibility, there should be some current workarounds for developers, as Tampermonkey is proprietary and old-fashioned in a way (for DX). Like TM doesn't support dev servers, that's why Vite exists. ;)