Safari: Inconsistent ESM module variable state - undefined then defined after timeout
Opened this issue · 2 comments
I've encountered an unusual behavior specifically in Safari (WebKit) where a variable imported from a dynamically loaded ESM module is initially undefined
, but becomes defined after a small timeout. This doesn't occur in other browsers.
Reproduction
import * as mod from "./chunk-WEXA2WHQ.js";
await new Promise((res) => {
console.log(1, mod);
setTimeout(() => {
res(console.log(2, mod));
}, 10);
});
// Safari output:
// 1 undefined
// 2 {...}
// Chrome/Firefox output:
// 1 {...}
// 2 {...}
Build Configuration
Using esbuild with the following config:
{
entryPoints,
entryNames: "[name]-[hash]",
outdir: "...",
bundle: true,
splitting: true,
metafile: true,
treeShaking: true,
write: false,
minify: true,
format: "esm",
jsx: "automatic"
}
The dependency graph:
strict digraph "dependency-cruiser output"{
rankdir="LR" splines="true" overlap="false" nodesep="0.16" ranksep="0.18" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true"
node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"]
edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"]
"chunk-XJRNTRWC.js" -> "chunk-QPHLPAA5.js"
"chunk-XJRNTRWC.js" -> "chunk-U67V476Y.js"
"chunk-XJRNTRWC.js" -> "chunk-WCUTOVXG.js"
"chunk-XJRNTRWC.js" -> "chunk-WEXA2WHQ.js"
"chunk-WEXA2WHQ.js" -> "chunk-5VTA2WAL.js"
"chunk-WEXA2WHQ.js" -> "chunk-AEZVEXA2.js"
"chunk-WEXA2WHQ.js" -> "chunk-AXFVPSDH.js"
"chunk-WEXA2WHQ.js" -> "chunk-EZEAVRPG.js"
"chunk-WEXA2WHQ.js" -> "chunk-QPHLPAA5.js"
"chunk-WEXA2WHQ.js" -> "chunk-TFVA3VVS.js"
"chunk-WEXA2WHQ.js" -> "chunk-U67V476Y.js"
# Files that depend on XJRNTRWC or WEXA2WHQ
"cart-OH52QW3I.js" -> "chunk-XJRNTRWC.js"
"cart-OH52QW3I.js" -> "chunk-WEXA2WHQ.js"
"dialog-3WPGJR7Y.js" -> "chunk-XJRNTRWC.js"
"dialog-3WPGJR7Y.js" -> "chunk-WEXA2WHQ.js"
"sheet-PWMR3JFM.js" -> "chunk-WEXA2WHQ.js"
}
Questions
- Is this behavior compliant with the ESM spec?
- Could this be related to how code splitting works in Safari?
- Is there a recommended way to ensure consistent module loading behavior across browsers?
Environment
- Browser: Safari (stable release)
- esbuild version: 0.24.0
- OS: macOS / iOS
Note: The issue appears to be Safari-specific as it works as expected in Chrome and Firefox.
Current hack
I'm adding this code:
await new Promise((res) => {
let i = 0;
(function check() {
if (mod || ++i > 100) res(null);
else setTimeout(check, 5);
})();
});
Interesting. Is there any top-level await used anywhere? I could see browsers having timing bugs and/or differences with something like that. Also I assume the variable in question is initialized inline instead of asynchronously, right?
It's surprising but there is actually only one top-level await used which is never evaluated in the browser but needed because it's evaluated on the server, and the moment I remove it (using a Response Override in Safari) the issue doesn't seems to rise again. Yes the variable is defined inline, here a snippet of this file:
// chunk-WEXA2WHQ.js
import {
Overlay,
} from "./chunk-XJRNTRWC.js";
import {
__toESM,
forwardRef,
require_jsx_runtime
} from "./chunk-QPHLPAA5.js"; // <-- the top level await is in this file
// app/components/ui/dialog.tsx
var import_jsx_runtime = __toESM(require_jsx_runtime());
var DialogOverlay = forwardRef(
({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
Overlay,
{
ref,
...props
}
)
);
DialogOverlay.displayName = Overlay.displayName;
export {
DialogOverlay,
};
The top level await in chunk-QPHLPAA5.js is this:
var module = globalThis.document
? reactClient.default
: await import("@bureaudouble/rsc-engine/react.react-server");
// it's never evaluated on the browser, and doesn't lead to anything in the browser, only in the server
Also, currently, I don't succeed to produce a really minimal example to share, without all the libs I use, it seems to be dependent of things I'm not aware of at this point.