ClojureScript :advanced optimizations — keeping JS-interop property names intact without an externs file
0 reputation · 25 Apr 2023, 07:54 UTC
0 reputation · 25 Apr 2023, 07:54 UTC
I am configuring a ClojureScript build that compiles with :optimizations :advanced via the Google Closure Compiler. The goal is a minified production bundle where calls into a plain JavaScript library — accessed through the js/ prefix and goog.object — still resolve correctly at runtime.
The constraint I am unsure about is the externs boundary. My understanding is that advanced compilation renames properties and strips code it considers dead, so any external API reached by string-based or dynamic property access needs to be protected. What is not clear to me is which of these approaches is the intended, well-supported configuration for a project that consumes a third-party JS library: maintaining a hand-written externs file, using ^:export metadata, or relying on goog.object/get with string keys. I also do not know how this differs between the standard ClojureScript compiler pipeline and shadow-cljs, which I believe handles npm dependencies and externs inference differently.
Assume a recent ClojureScript release; I can verify behavior by diffing the compiled output between :none and :advanced.
:advanced, or does the compiler infer externs from js/ usage?goog.object/get with a string key fully protect a property from renaming?32775 reputation · 25 Apr 2023, 19:49 UTC
Under :optimizations :advanced the Google Closure Compiler renames property names that are accessed via dot notation (e.g. (.foo js-lib) or js/lib.foo). To keep those names intact you must either protect them with an externs file, use string‑key access via goog.object/get (and goog.object/set for writes), or give the compiler enough type information to infer externs. The standard ClojureScript compiler does not infer externs from plain js/ usage; shadow‑cljs does this automatically for npm dependencies, which often removes the need for a hand‑written externs file.
. or js/) is renamed unless the name appears in an externs file.(goog.object/get obj "propName") protects that read because the string literal is not renamed; writes need goog.object/set or aset with a string key.^:export only protects names you define in ClojureScript for external consumption; it does not protect third‑party property names.cljs.main/clojurescript pipeline does not infer externs from js/ unless you add ^js hints and set :infer-externs true.js/ usages (both hinted and many unhinted).If you rely on plain js/ dot access without hints, the compiler will treat those properties as internal and rename them, leading to runtime errors like "undefined is not a function" only in the advanced build. Using goog.object/get avoids renaming per‑access, while adding ^js hints lets the compiler infer externs (standard) or lets shadow‑cljs do it automatically.
:externs.^js hints to every js/ value you interop with and set :infer-externs true in your build config.goog.object/get (reads) and goog.object/set or aset (writes) using string keys.externs.js to verify that the properties you rely on are present.^js hint to the corresponding js/ value or supply a small externs file for that library.Are you using ^js type hints on the js/ values you interop with? Knowing this determines whether you need to add hints, rely on inference, or fall back to an externs file.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.