The AMD Loader Conflict That Silently Broke DOMPurify
One morning the browser console showed this:
Uncaught Error: Can only have one anonymous define call per script file
at c.enqueueDefineAnonymousModule (loader.min.js:3:4700)
at o (loader.min.js:4:1686)
at purify.min.js:2:126
at purify.min.js:2:204
Not a missing file. Not a network error. An AMD loader conflict — from two libraries that have nothing to do with each other, fighting over the same global function.
The setup
TAFNE's table editor loads Monaco Editor from the CDN. Monaco ships its own AMD module loader (loader.min.js). This loader hijacks the global define function — that's how AMD works. Once loader.min.js runs, window.define belongs to Monaco.
Separately, TAFNE sanitizes user-pasted HTML using DOMPurify. DOMPurify ships as a UMD bundle (purify.min.js) — Universal Module Definition. UMD is a format that works in AMD environments, CommonJS environments, and plain browsers. To handle AMD, the UMD preamble checks: "does define exist and is it a function? If so, call it with our module."
That check looks something like this (paraphrased from the minified source):
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module !== 'undefined') {
module.exports = factory();
} else {
root.DOMPurify = factory();
}
In a plain browser, define doesn't exist, so DOMPurify falls through to root.DOMPurify = factory() — the global. That's correct.
But the scripts were loading in this order:
<script src="loader.min.js"></script> <!-- Monaco AMD loader — installs define() -->
<script src="purify.min.js"></script> <!-- DOMPurify UMD — sees define, tries AMD path -->
By the time purify.min.js runs, define is Monaco's strict AMD loader. DOMPurify's UMD preamble sees it and calls define([], factory) — an anonymous define call.
Monaco's AMD loader allows exactly one anonymous define per script. The loader is tracking script origin to wire modules together. A second anonymous define from a different file is an error: Can only have one anonymous define call per script file.
The error fires. DOMPurify doesn't register. window.DOMPurify is undefined. Any code calling DOMPurify.sanitize() gets a silent TypeError.
Why it was silent
The call to DOMPurify.sanitize() in TAFNE wraps HTML before injecting it into the DOM. That call was guarded:
const cleanBlocks = window.DOMPurify
? window.DOMPurify.sanitize(blocksHtml, { ALLOW_DATA_ATTR: true })
: blocksHtml;
When DOMPurify is undefined, the fallback is blocksHtml — the unsanitized HTML. No crash. No visible error in the output. Just a silently neutered security control.
The AMD error in the console was easy to miss and looked unrelated to sanitization behavior. If you weren't watching for it, everything appeared to work.
The fix
Load DOMPurify before Monaco's AMD loader:
<!-- BEFORE (broken) -->
<script src="loader.min.js"></script>
<script src="purify.min.js"></script>
<!-- AFTER (correct) --> <script src="purify.min.js"></script> <script src="loader.min.js"></script>
When purify.min.js runs first, define doesn't exist yet. The UMD preamble falls through to root.DOMPurify = factory(). DOMPurify registers as a global. When loader.min.js runs next, it installs Monaco's define — but DOMPurify is already registered and doesn't touch it again.
One line moved. The conflict disappears.
The general pattern
This is a class of bugs that appears whenever you mix a strict AMD loader (Monaco, RequireJS, Dojo) with UMD-format libraries:
- AMD loader runs first → installs
defineglobally - UMD library runs → detects
define, tries to register as AMD module - AMD loader rejects it (wrong origin, wrong timing, anonymous define limit)
- Library silently fails to register
- Downstream code hits undefined, falls back, or crashes
define will try to go through AMD — and strict loaders won't cooperate.
This is documented in Monaco's own issues and in the DOMPurify FAQ, but it's easy to miss because both libraries work fine in isolation. The conflict only appears in the exact combination of: Monaco CDN + any UMD library loaded afterward.
What to watch for
If you're using Monaco from CDN and adding other libraries:
- Check if the library ships as UMD (look for
define.amdin the source) - If it does, load it before
loader.min.js - Libraries that ship as pure ES modules or pure globals are not affected
define.amd too, but it doesn't call define with a factory function — it uses a named define or module export path. In practice it hasn't conflicted. But the same rule applies: if you're unsure, put it before the AMD loader.
The AMD error message is cryptic but specific. If you ever see Can only have one anonymous define call per script file from a CDN library you didn't write, the first thing to check is load order relative to your AMD loader.