Parcel appears to produce invalid code when a JavaScript builtin is reassigned (monkey-patched) within a module
🐛 bug report
If an ES6 module reassigns window.
🎛 Configuration (.babelrc, package.json, cli command)
(No babel configuration)
.postcssrc
{
"plugins": {
"tailwindcss": {}
}
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['src/*.html'],
theme: {
extend: {},
},
plugins: [],
}
package.json
{
"devDependencies": {
"http-server": "^14.1.1",
"parcel": "^2.6.2",
"postcss": "^8.4.14",
"tailwindcss": "^3.1.6"
}
}
cli command: parcel build src/index.html
🤔 Expected Behavior
(This is an example, such reassignment should also work for any builtin) setTimeout() should be monkey-patched when window.setTimeout is reassigned to a different function.
Potential valid use-cases for this behavior:
- Monkey-patching Math.random() to support random seeds without any code changes
- Debugging or profiling existing code (my use)
😯 Current Behavior
The code compiles (and Parcel makes no attempt to bail out), but proceeds to crash at runtime with an exception.
The error looks something like this:
Uncaught TypeError: require(...)(...) is undefined
a3umm runtime-0052737e6b0644e4.js:1
newRequire index.4d6bcbeb.js:71
localRequire index.4d6bcbeb.js:84
gLLPy main.js:3
newRequire index.4d6bcbeb.js:71
<anonymous> index.4d6bcbeb.js:122
<anonymous> index.4d6bcbeb.js:145
💁 Possible Solution
This appears to be due to scope hoisting? Maybe "bail out" if something like this is done, or simply put a note in the docs stating that this behavior is not supported? This may be fixable as well by treating browser-supplied objects such as window and document as special when compiling to a browser environment.
🔦 Context
I have code that uses setTimeout() frequently and sometimes want to profile it by printing a log message containing the number of setTimeout() handlers currently active. To do this, I reassigned window.setTimeout() to another function that called the setTimeout() function (while obviously including code to track how many setTimeout handlers were active). This code works when executed normally. However, when minified through parcel, it throws an error.
💻 Code Sample
Minimal code sample that reproduces the issue:
index.html
<html>
<head>
<script src="main.js" type="module"></script>
</head>
<body>
</body>
</html>
main.js
import './debug_events.js';
debug_events.js
const setTimeoutOld = window.setTimeout;
var eventsWaitingCount = 0;
window.setTimeout = function(func, delay){
setTimeoutOld(()=>{
eventsWaitingCount--;
console.log("setTimeout handler called. There are " + eventsWaitingCount + " event handlers in operation.");
func();
}, delay);
eventsWaitingCount++;
console.log("setTimeout handler created. There are " + eventsWaitingCount + " event handlers in operation.");
}
export {setTimeoutOld};
🌍 Your Environment
(I was compiling a web app using Parcel, on a Mac. My environment also used PostCSS and Tailwind CSS, as shown above.)
| Software | Version(s) |
|---|---|
| Parcel | 2.6.2 |
| Node | 18.4.0 |
| npm/Yarn | 8.12.1 |
| Operating System | macOS Monterey |
Hi @omduggineni,
as a workaround restructuring your code to this instead seems to work.
I'd generally advice using Proxy and Reflect for "monkey patching" in javascript as the code below will preserve the behavior of the original setTimeout function much closer. I.e. the exception behavior given invalid arguments will be unchanged, code relying on the return value of setTimeout to remove the timeout with clearTimeout will still work and code relying on the optional parameters passed to set timeout (see param1, …, paramN here won't break.
Also, having side-effects in modules is generally a bad idea as it will prevent tree-shaking of unused functions.
debug_events.js
export function proxy_set_timeout() {
let events_waiting_count = 0;
window.setTimeout = new Proxy(window.setTimeout, {
apply(target, this_arg, arg_list) {
if (typeof arg_list[0] === "function") {
arg_list[0] = new Proxy(arg_list[0], {
apply(timeout_target, timeout_this_arg, timeout_arg_list) {
events_waiting_count--;
console.log(
"setTimeout handler called. There are " + events_waiting_count + " event handlers in operation."
);
return Reflect.apply(timeout_target, timeout_this_arg, timeout_arg_list);
},
});
}
const return_value = Reflect.apply(target, this_arg, arg_list);
events_waiting_count++;
console.log("setTimeout handler created. There are " + events_waiting_count + " event handlers in operation.");
return return_value;
},
});
}
main.js:
import { proxy_set_timeout } from "./debug_events";
proxy_set_timeout();
Proof that it works:
14:29:40.532 setTimeout(() => console.log("hi"), 1000)
14:29:40.549 setTimeout handler created. There are 1 event handlers in operation. [index.8b76918f.js:1:308](http://localhost:1234/index.8b76918f.js)
14:29:40.551 3
14:29:41.551 setTimeout handler called. There are 0 event handlers in operation. [index.8b76918f.js:1:156](http://localhost:1234/index.8b76918f.js)
14:29:41.552 hi [debugger eval code:1:26](chrome://devtools/content/webconsole/debugger%20eval%20code)
Working folder: aa.zip
@danieltroger Ah ok, I did think there was a better way to do what I was doing! Thanks! (I hadn't really looked into coding practices because this was only used briefly as a debugging/profiling tool and not in production - also, I started using modules in my code somewhat recently). I mostly opened this issue to document that it doesn't behave as expected in Parcel.
Not sure if it's related, but I may have run into a similar issue to this with the @floating-ui library that is used by Mantine's Tooltip React component.
I did some more digging in the @floating-ui library and found that the module useFloating.ts is exporting a name that is the same as one that is being imported and aliased.
import {useFloating as usePosition} from '@floating-ui/react-dom';
export function useFloating<RT extends ReferenceType = ReferenceType>({
As a test, renaming the useFloating() function to useFloating2() in the useFloating.ts module, and then changing the import in the Mantine usetooltip module fixes the issue with Parcel scope hoisting that I am seeing.
@JennaSys Can you please provide a reproduction (ideally in a new issue), floating-ui and mantine appears to be working correctly for me:
import { Tooltip, Button } from "@mantine/core";
function App() {
return (
<Tooltip label="Tooltip">
<Button variant="outline">Button with tooltip</Button>
</Tooltip>
);
}
or
import { useFloating } from "@floating-ui/react-dom-interactions";
function App() {
const { x, y, reference, floating, strategy } = useFloating();
return (
<>
<button ref={reference}>Button</button>
<div
ref={floating}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: "max-content",
}}
>
Tooltip
</div>
</>
);
}
Yes, I'll try and put together a standalone use case and post a new issue.
@mischnic I get the error even with your first example:
import { Tooltip, Button } from "@mantine/core";
function App() {
return (
<Tooltip label="Tooltip">
<Button variant="outline">Button with tooltip</Button>
</Tooltip>
);
}
I suppose there could be something else in my tech stack that could be problematic, but when I use this package.json build script:
"build": "NODE_ENV=production parcel build index.html"
I get a runtime error:
TypeError: Cannot destructure property 'open' of 'e' as it is undefined.
at j (floating-ui.react-dom-interactions.esm.js:1085:5)
at d (use-tooltip.ts:56:5)
at Tooltip.tsx:133:19
But when I remove scope hoisting it works;
"build": "NODE_ENV=production parcel build index.html --no-scope-hoist"
Buidling and running in dev mode also works:
"start": "NODE_ENV=development parcel index.html"
@JennaSys Here's what I tried: https://github.com/mischnic/parcel-issue-8310.
Hmmm, that repo works for me too. My build system is admittedly a bit different, but it's odd that the error I'm getting is from an issue with this specific library. I'll try and pinpoint how my generated JS is different from this one. It may very well be a side effect from something else happening further up my build stack, I just don't see how yet.
@mischnic I've isolated a test case that fails outside of my build stack and will create a separate issue for it. I still don't know why it's failing, but it is reproducible. TBH it may be an edge case that wouldn't normally happen for most people, but I'll also give some background on my dev environment when I post the issue that will explain why it's happening in my particular case.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs.