Engineering Journal
Table Formatter
Table Formatter

The AMD Loader Conflict That Silently Broke DOMPurify

2026-05-30

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:

  1. AMD loader runs first → installs define globally
  2. UMD library runs → detects define, tries to register as AMD module
  3. AMD loader rejects it (wrong origin, wrong timing, anonymous define limit)
  4. Library silently fails to register
  5. Downstream code hits undefined, falls back, or crashes
The fix is always the same: UMD libraries that you want as globals must load before the AMD loader. Once the AMD loader is installed, any UMD library that checks for 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:

For GSAP, which is also loaded in TAFNE: it checks for 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.

Read this post in the full Engineering Journal →