You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #477 (closed 2026-07-29 as completed, with no diagnosis in the thread) and to @benedikt-krieger-hyghstreet's last comment on #452. The bug is still present in 2.91.0. I hit it independently in a different Shopify theme + Parcel codebase, root-caused it, and verified a one-line fix — so I'm filing this with the details rather than reopening.
Summary
The published ESM build (lib/esm/constructorio.js, which the module field selects) throws TypeError: ConstructorioID is not a constructor when bundled with Parcel. The CommonJS entry (lib/constructorio.js) works fine.
This is not a minifier problem. parcel build --no-optimize and parcel build --no-scope-hoist fail identically — the difference is only in the error text, because the identifier is mangled in the optimized build.
Reproduction
mkdir repro && cd repro && npm init -y
npm i @constructor-io/constructorio-client-javascript@2.91.0 parcel
Evaluating the resulting bundle throws at construction time:
TypeError: r is not a constructor // optimized build
TypeError: ConstructorioID is not a constructor // --no-optimize
Deep-importing the CommonJS entry instead — '@constructor-io/constructorio-client-javascript/lib/constructorio.js' — works. That is the workaround we have been shipping.
// export module for node or environments with module loaders, such as webpackif(typeofmodule!=='undefined'&&typeofrequire!=='undefined'){module.exports=ConstructorioID;}
When scripts/build-esm.js bundles this with esbuild into an ESM output, esbuild rewrites the free require to its dynamic-require shim. In the published lib/esm/constructorio.js of 2.91.0 the guard is line 304:
__require is never actually called anywhere in the 300 KB bundle — grep -c '__require(' returns 0. It exists solely to make that guard true.
Parcel's JS transformer knows require is not defined in a browser ESM asset, so it constant-folds the initialiser. The generated bundle contains literally:
So __require === undefined → typeof __require !== "undefined" is false → module.exports = ConstructorioID never executes → esbuild's __commonJS wrapper require_constructorio_id() (called at lib/esm/constructorio.js:7085) returns {} → new ConstructorioID(...) at lib/esm/constructorio.js:7180 throws.
I haven't tested other bundlers, but this would explain why it isn't universal: a bundler that leaves the shim intact binds __require to the Proxy fallback, which is defined, so the guard passes and the bug stays hidden.
Suggested fix
The guard is asking "is a CommonJS loader present?" using a token that bundlers are entitled to rewrite or fold away. Options, best first:
Fix the guard in @constructor-io/constructorio-id — drop the typeof require half. typeof module !== 'undefined' && typeof module.exports !== 'undefined' is the conventional UMD test and does not depend on require surviving a downstream build.
Give constructorio-id a real ESM entry (export default ConstructorioID plus an exports/module field) so the ESM build never routes it through esbuild's CommonJS interop at all. Given Seperate ES6 build for a small and modern build #452's goal was a clean modern build, this seems like the right end state.
Failing both, define __require unconditionally in the ESM output (an esbuild --define or banner in scripts/build-esm.js).
Verified: patching the published lib/esm/constructorio.js:304 to
if(typeofmodule!=="undefined"){
and rebuilding makes the Parcel ESM bundle fully functional — the client constructs, search / browse / tracker are present, clientId and sessionId are generated — in the optimized build, with no other change. Because __require is never called, removing that half of the condition is safe.
The ESM build is worth having, which is why I'd like to stop pinning the CJS entry: for our entry point it comes out at 87,179 B raw / 22,090 B gzip, versus 105,519 B / 24,680 B via lib/constructorio.js.
Environment
@constructor-io/constructorio-client-javascript 2.90.0 and 2.91.0 — both reproduce
Follow-up to #477 (closed 2026-07-29 as completed, with no diagnosis in the thread) and to @benedikt-krieger-hyghstreet's last comment on #452. The bug is still present in 2.91.0. I hit it independently in a different Shopify theme + Parcel codebase, root-caused it, and verified a one-line fix — so I'm filing this with the details rather than reopening.
Summary
The published ESM build (
lib/esm/constructorio.js, which themodulefield selects) throwsTypeError: ConstructorioID is not a constructorwhen bundled with Parcel. The CommonJS entry (lib/constructorio.js) works fine.This is not a minifier problem.
parcel build --no-optimizeandparcel build --no-scope-hoistfail identically — the difference is only in the error text, because the identifier is mangled in the optimized build.Reproduction
index.js:Evaluating the resulting bundle throws at construction time:
Deep-importing the CommonJS entry instead —
'@constructor-io/constructorio-client-javascript/lib/constructorio.js'— works. That is the workaround we have been shipping.Root cause
@constructor-io/constructorio-id@2.7.1,src/constructorio-id.js:313:When
scripts/build-esm.jsbundles this with esbuild into an ESM output, esbuild rewrites the freerequireto its dynamic-require shim. In the publishedlib/esm/constructorio.jsof 2.91.0 the guard is line 304:and
__requireis declared at line 22:__requireis never actually called anywhere in the 300 KB bundle —grep -c '__require('returns 0. It exists solely to make that guard true.Parcel's JS transformer knows
requireis not defined in a browser ESM asset, so it constant-folds the initialiser. The generated bundle contains literally:So
__require === undefined→typeof __require !== "undefined"is false →module.exports = ConstructorioIDnever executes → esbuild's__commonJSwrapperrequire_constructorio_id()(called atlib/esm/constructorio.js:7085) returns{}→new ConstructorioID(...)atlib/esm/constructorio.js:7180throws.I haven't tested other bundlers, but this would explain why it isn't universal: a bundler that leaves the shim intact binds
__requireto theProxyfallback, which is defined, so the guard passes and the bug stays hidden.Suggested fix
The guard is asking "is a CommonJS loader present?" using a token that bundlers are entitled to rewrite or fold away. Options, best first:
@constructor-io/constructorio-id— drop thetypeof requirehalf.typeof module !== 'undefined' && typeof module.exports !== 'undefined'is the conventional UMD test and does not depend onrequiresurviving a downstream build.constructorio-ida real ESM entry (export default ConstructorioIDplus anexports/modulefield) so the ESM build never routes it through esbuild's CommonJS interop at all. Given Seperate ES6 build for a small and modern build #452's goal was a clean modern build, this seems like the right end state.__requireunconditionally in the ESM output (an esbuild--defineor banner inscripts/build-esm.js).Verified: patching the published
lib/esm/constructorio.js:304toand rebuilding makes the Parcel ESM bundle fully functional — the client constructs,
search/browse/trackerare present,clientIdandsessionIdare generated — in the optimized build, with no other change. Because__requireis never called, removing that half of the condition is safe.The ESM build is worth having, which is why I'd like to stop pinning the CJS entry: for our entry point it comes out at 87,179 B raw / 22,090 B gzip, versus 105,519 B / 24,680 B via
lib/constructorio.js.Environment
@constructor-io/constructorio-client-javascript2.90.0 and 2.91.0 — both reproduce@constructor-io/constructorio-id2.7.1@swc/core1.15.26, node 23.10.0["defaults and fully supports es6-module", "Chrome >= 97", "Firefox >= 96", "iOS >= 15"]