Compatibility

Does Obfuscation Break UMD and Module Wrappers?

If you publish a library rather than an application, the first few lines of your bundle are a small piece of environment detection: is there an AMD loader here, is this CommonJS, or should I hang myself off the global object? That code reads identifiers that are never declared anywhere in your source, which is exactly the shape that makes people nervous about a renaming transform. We measured it, and the wrapper is fine. The way to break it is narrower and quieter than the thing people worry about.

What was measured

The sample is a conventional UMD wrapper: an immediately invoked function that tests typeof define === 'function' && define.amd, falls back to typeof exports === 'object' together with a check on module.exports, and finally assigns the factory result onto a root object. The factory returns a small library with a version string and an add function.

One wrapper is not enough to test the thing that matters, because a single run only ever exercises one branch. The sample therefore runs the wrapper three times against three different host shapes: plain CommonJS as node supplies it, a synthetic AMD loader installed as a global with an amd marker on it, and a bare object with neither loader present so the global fallback has to be taken. Each run records which branch it chose and then calls the exported add through whichever channel that branch used.

That sample was run unprotected in node, then protected in five configurations and run again, with output diffed line by line. The five are the ES5 target on default options, the modern target, the two identifier-renaming presets our emit-validation gate uses, and a string-table preset that moves and encodes every literal.

All five produced output identical to the original. The branch sequence stayed commonjs, amd, global. The exported function returned 5, 9 and 13 through the CommonJS export, the AMD callback and the global object respectively. The line that reports typeof define, typeof exports and typeof module came back as undefined,object,object in every configuration, which is the same answer the unprotected file gives.

Why the free identifiers survive

The reassuring result has a mechanical reason worth understanding, because it tells you which future change would be safe and which would not.

define, exports and module in a UMD wrapper are free identifiers. Your file never declares them; it only reads them, and it guards each read with typeof so that reading one that does not exist returns the string undefined rather than throwing. Identifier renaming rewrites bindings that the file owns. A name your file never declares is not a binding it owns, so there is nothing to rewrite, and the read continues to resolve against whatever the host supplies at load time.

This is the same property that makes typeof on a genuinely undeclared identifier safe, and it is why the global-fallback branch keeps working: the root object is passed in as an argument, so it is an ordinary parameter that gets renamed along with every use of it inside the function.

It also means the reassurance is specific rather than general. The wrapper survives because of how it reads those names, not because the engine has special knowledge of UMD. Nothing in the transform recognises a module wrapper as such.

The one way to break it, and it is silent

Member renaming is a separate option from identifier renaming, and it is the one that can reach this code. It takes a regular expression and renames matching property names. Point that regular expression at exports and the wrapper stops working, on both targets.

The measured result is a branch flip. The run that should report commonjs reports global, and the line that should report the exported function returning 5 reports that there was nothing to call. Nothing throws. The process exits successfully.

The mechanism is visible in the emitted output. typeof exports === 'object' is untouched, because that exports is a free identifier rather than a member access. Every module.exports in the file, though, is a member access, and all eight occurrences were rewritten to module followed by a generated name. So the guard evaluates its first clause as true, finds module defined, then reads a property that does not exist on node's real module object, gets undefined, treats the whole condition as false and falls through to the branch that assigns onto the root object instead. The package loads. It simply exports nothing.

This is the general rule this site keeps arriving at, in a new costume: renaming is safe when a name is reached only by code you control, and unsafe when the name is half of a contract with something outside your file. Here the other half of the contract is node's module record, which no transform of your source can rename.

What was safe to rename, which is most of it

Three other regular expressions were measured against the same sample and all three came back identical to the original.

Renaming the library's own surface, version and add, changed nothing observable, because the factory that defines those names and the call sites that use them are both inside the file. They are renamed together and stay consistent. That is the ordinary, intended case.

Renaming MyLib, the property the global fallback assigns onto the root object, also came back identical, for the same reason: the sample writes it and reads it in one file. In a real deployment this is the case to think about rather than to dismiss, because your consumers read that global by its original name from their own files, which the engine never sees. The measurement says the transform is self-consistent; it cannot say your consumers are.

Renaming amd came back identical too, and this one is worth a sentence because it looks like it should fail. The sample installs the marker with define.amd = {} and the wrapper reads it with define.amd. Both are member accesses in the same file, so both were renamed, and the detection still matched. Against a real AMD loader the write would live in the loader and only the read would be renamed, and the branch would be missed. Our sample models the loader, and a model can only tell you about the half it contains.

How to check your own bundle

The check is cheap and does not need our engine to be part of it. Protect your bundle, then load the protected file three ways: with an AMD loader present, as a CommonJS require, and as a plain script tag with neither. Assert that each path reaches your exports and that your public functions still return what they returned before. If you publish a package, the CommonJS assertion is simply requiring your own tarball in a scratch directory and calling one function.

If you use member renaming, scope the regular expression so it cannot match exports, and treat the same care as applying to any name your consumers type. An anchored allowlist of your own internal field names is far safer than a pattern that happens to catch a word the module system also uses.

The wider point for library authors is unchanged by this measurement: the names your callers depend on are a published interface, and a transform that cannot see your callers cannot know which names those are. Protecting a library is a scoping exercise before it is a configuration exercise.

The short version

A UMD wrapper keeps choosing the correct branch after protection in every configuration we measured, because the identifiers it probes are ones your file never declares and the transform therefore never rewrites. The AMD path, the CommonJS path and the global fallback all reached their exports and returned the same values.

The failure worth guarding against is not syntactic and not loud. Let member renaming match exports and your published package silently stops exporting, because module.exports becomes a property node has never heard of. Scope that regular expression, load the protected bundle all three ways once, and this area stops being a question.

Frequently asked questions

Does obfuscation break a UMD wrapper?

Not in anything we measured. The wrapper chose the same branch before and after protection in all five configurations, and the AMD, CommonJS and global-fallback paths each reached the exported function and returned the same value. The identifiers a wrapper probes are free identifiers your file never declares, so identifier renaming has no binding to rewrite.

Why does my protected package export nothing?

The most likely cause is a member-renaming regular expression that matches exports. We measured that exact case: every module.exports in the file becomes a generated property name, the CommonJS guard reads undefined, and the wrapper falls through to the global branch. Nothing throws and the process exits successfully, so it presents as an empty import rather than an error.

Are define, exports and module renamed by identifier renaming?

No. Those are free identifiers, meaning your source reads them without ever declaring them. Identifier renaming rewrites bindings the file owns, so it leaves them alone and they keep resolving against whatever the host provides. We confirmed this by reading typeof for all three out of the protected build and getting the same answer as the original.

Is it safe to use member renaming on a published library?

Only with a carefully scoped pattern. Renaming the library's own internal names measured clean, because both the definition and the use sites are inside the file. Any name your consumers type is different: they read it from files the engine never sees, so renaming it changes your published interface. Anchor the pattern to your internal fields rather than writing something broad.

Does the AMD branch really work, or did the sample just avoid it?

It was exercised. The sample installs a synthetic AMD loader as a global with an amd marker, runs the wrapper again, and calls the function the loader received. That path returned the same value before and after protection in every configuration. Note that our loader lives in the same file as the wrapper, so it models rather than reproduces a real loader boundary.

What is the cheapest way to be sure my bundle still loads?

Load the protected file three ways once: under an AMD loader, through a CommonJS require, and as a plain script with neither present, asserting that a public function returns what it should in each. For a package, requiring your own packed tarball in a scratch directory covers the case that matters most.

Related reading