Open DerekZiemba opened 2 years ago
Why does does Tampermonkey pass undefined variables such as
module
&exports
These variables are hidden from @require
'd scripts so they don't mistakenly use a page implementation.
I'll think about a better way to achieve this after the next stable release.
just a note, i dont believe exports = ...
is valid commonjs, so you dont need such magical setter/getter if you want to mimic it. in commonjs you only assign individual fields in exports
, not the whole object. whole object assignment is only possible with module.exports
. And module
itself is also never re-assigned, so its setter-getter magic is pointless too.
just a note, i dont believe
exports = ...
is valid commonjs, so you dont need such magical setter/getter if you want to mimic it. in commonjs you only assign individual fields inexports
, not the whole object. whole object assignment is only possible withmodule.exports
. Andmodule
itself is also never re-assigned, so its setter-getter magic is pointless too.
I only realized this after I got eslint working properly in vscode. However, it still works in the browser. Create a Proxy and define an exports
variable on the global object.
Because of the linter and other complications, ultimately I did end up using module.exports
. Having module
defined is still problematic though. For example it's not straightforward to share modules between GM scripts because module
is always passed and set as undefined
. The reason I want to share modules is so I can share data across origins.
But I've been basically doing the following:
/* rome-ignore */ /* @ts-ignore */ /* eslint-disable-next-line */
module ??= unsafeWindow.__MyGMModules ?? (()=>{
const modules = { };
const rgxModulePath = /((?:\.\.?\/)+)?(.+\/?)?([a-zA-Z0-9_.-]+?)(\.(?:m?js|ts|d\.ts))?$/;
const parseModulePath = (requirePath) => { /* Get simplified name using rgxModulePath */ }
const myRequire = (path) => { return modules[parseModulePath(path)]; /* (stripped logic for brevity) */ }
const createExportProxy = (path) => {
const name = parseModulePath(path);
return modules[name] ??= new Proxy({ name }, {
ownKeys: Object.keys,
has: Object.hasOwn,
construct(target, args, newTarget) { /* (stripped logic for brevity) */ },
getPrototypeOf(target) { /* (stripped logic for brevity) */ },
defineProperty(target, name, descriptor) { /* (stripped logic for brevity) */ },
getOwnPropertyDescriptor(target, name) { /* (stripped logic for brevity) */ },
get(target, prop, receiver) { /* (stripped logic for brevity) */ },
set(target, prop, value) { /* (stripped logic for brevity) */ },
});
}
let currentName = '.';
let currentExportsProxy = createExportProxy(currentName);
return new Proxy(modules, {
ownKeys: Object.keys,
has: Object.hasOwn,
get(target, prop, receiver) {
switch(prop) {
case 'require': return myRequire;
case 'name': return currentName;
case 'exports': return currentExportsProxy;
default: return target[prop] ?? modules['.'][prop];
}
},
set(target, prop, value) {
switch(prop) {
case 'name':
currentExportsProxy = createExportProxy(currentName = value);
break;
case 'exports':
Object.assign(currentExportsProxy, value);
break;
}
},
});
})();
/* rome-ignore lint(correctness/noShadowRestrictedNames): reason */ /* @ts-ignore */ /* eslint-disable-next-line */
const require = module.require;
/// Then later:
{
const Resource = require('../path/is-ignored/only-uses-file-name.js');
// create a new export proxy (module) and names it so I can require it later;
module.name = 'name-im-calling-this-module';
module.exports.someFunction = ()=>{};
}
It seems to me that you are inventing your own module system, a bit similar to commonjs but still not able to load unmodified commonjs modules because of the need to define module.name; it also seems very sensitive to the order of @import
clauses, needing all required modules to precede the requiring modules. Instead of all that, if you're gonna have to modify your modules anyway, why don't you use standard AMD or UMD module syntax with an AMD loader which is very well suited to modules concatenated to a single file in any order? It's not hard to manually convert them (wrap in a define() function, extract dependency list and module name as arguments) but they can even be generated from commonjs by various build tools like webpack or browserify. And if you go there anyway the tools can even concatenate them for you so your userscript ends up with a single @import
and no code at all. Or even better, since @import
from local file really seems like a bad practice, just prepend the userscript header to the tool-generated output script.
The reason I want to share modules is so I can share data across origins
I do not understand what you mean here. No matter what module system you come up with, each frame will still have its own script realm with its own modules, there is no way around that. To share data, you will need postMessage/onmessage (for frames within the same tab) or GM.getValue/GM.setValue/GM.addValueChangeListener for different tabs, and that has nothing to do with this issue
Lastly, since I got a bit sidetracked, it is not clear to me if you still think there is an issue with tampermonkey itself that needs addressing here, or the issue can be closed.
Basically trying to hack around vscode so I can get better intellisense.
Solution 1: install TS and define the global variables you use https://code.visualstudio.com/docs/nodejs/working-with-javascript#_global-variables-and-type-checking
Solution 2: use standard import/require and then compile into a single bundle. There are plugins for bundlers to generate userscripts.
Issue
Tampermonkey wraps userscripts in the following block:
Problem
It passes variables such as
context
,define
,exports
,module
,fapply
all with undefined values. In particular, I needexports
&module
to either have actual values supplied by tampermonkey, or for the variables not to be passed at all.If they were omitted, I could declare them as properties on
this
and have all references get properly directed to the correct object. Otherwise,exports = 'something'
will write to the variable unless I explicitly domodule.exports = 'something'
. If it was a property onthis
, I could have theexports
setter automatically merge all exports.What I'm trying to do
Basically trying to hack around vscode so I can get better intellisense. Since vscode doesn't recognize
// @require file:///A:/Dropbox/Scripts/.js/ZZ/core/GM.js
I can put my own implementations offrequire
,import
,module
,exports
in there then just ensure it's the first@require
'ed script.The additional scripts that are loaded after that can then use typical node.js module syntax and vscode will be none the wiser that it's all fake, and will give me proper intellisense and type information..
Expected Behavior
Not to define
exports
&module
if Tampermonkey isn't going to provide something by them.Actual Behavior
Tampermonkey passes undefined parameters
exports
&module
to the wrapped script .Specifications
Script
In Tampermonkey, the script is pretty much just the header
The
// @require file:///A:/Dropbox/Scripts/.js/ZZ/core/GM.js
script would be something like this, if those undefinedexports
&module
parameters didn't throw a wrench in my plan.