@@ -48,6 +48,22 @@ const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency
4848 * @property {string } context
4949 */
5050
51+ /**
52+ * Per-`const NAME = require(LITERAL)` binding state used to forward
53+ * member-access references on `NAME` to the `CommonJsRequireDependency`
54+ * created for the `require()` call.
55+ * @typedef {object } RequireBindingData
56+ * @property {RawReferencedExports } referencedExports mutable list shared with the dependency; pushed to as `NAME.x.y` accesses are walked
57+ * @property {InstanceType<typeof import("./CommonJsRequireDependency")> | null } dep dependency for the `require()` call (assigned during walk)
58+ */
59+
60+ /** @type {WeakMap<CallExpression, RequireBindingData> } */
61+ const requireBindingData = new WeakMap ( ) ;
62+
63+ const REQUIRE_BINDING_TAG = Symbol (
64+ "CommonJsImportsParserPlugin require binding"
65+ ) ;
66+
5167const PLUGIN_NAME = "CommonJsImportsParserPlugin" ;
5268
5369/**
@@ -152,17 +168,26 @@ const createRequireCallHandler = (parser, options, getContext) => {
152168 */
153169 const processRequireItem = ( expr , param ) => {
154170 if ( param . isString ( ) ) {
155- const referencedExports = getRequireReferencedExportsFromDestructuring (
171+ let referencedExports = getRequireReferencedExportsFromDestructuring (
156172 parser ,
157173 expr
158174 ) ;
175+ const binding = requireBindingData . get (
176+ /** @type {CallExpression } */ ( expr )
177+ ) ;
178+ if ( binding && ! referencedExports ) {
179+ // `const NAME = require(LITERAL)` — let later member-access walks
180+ // on `NAME` populate the dependency's referenced exports.
181+ referencedExports = binding . referencedExports ;
182+ }
159183 const dep = new CommonJsRequireDependency (
160184 /** @type {string } */ ( param . string ) ,
161185 /** @type {Range } */ ( param . range ) ,
162186 getContext ( ) ,
163187 referencedExports ,
164188 /** @type {Range } */ ( expr . range )
165189 ) ;
190+ if ( binding ) binding . dep = dep ;
166191 dep . loc = /** @type {DependencyLocation } */ ( expr . loc ) ;
167192 dep . optional = Boolean ( parser . scope . inTry ) ;
168193 parser . state . current . addDependency ( dep ) ;
@@ -662,6 +687,88 @@ class CommonJsImportsParserPlugin {
662687 . tap ( PLUGIN_NAME , callChainHandler ) ;
663688 // #endregion
664689
690+ // #region Require bound to a const variable
691+ // Track `const NAME = require(LITERAL)` so that static member accesses on
692+ // `NAME` (e.g. `NAME.foo`, `NAME.foo()`) are forwarded to the same
693+ // `CommonJsRequireDependency` as referenced exports — enabling tree
694+ // shaking of CommonJS modules that are imported into a named binding
695+ // rather than destructured.
696+ parser . hooks . preDeclarator . tap ( PLUGIN_NAME , ( declarator , statement ) => {
697+ if ( statement . kind !== "const" ) return ;
698+ if ( declarator . id . type !== "Identifier" ) return ;
699+ if ( ! declarator . init || declarator . init . type !== "CallExpression" ) {
700+ return ;
701+ }
702+ const init = declarator . init ;
703+ if (
704+ init . callee . type !== "Identifier" ||
705+ init . callee . name !== "require" ||
706+ init . arguments . length !== 1
707+ ) {
708+ return ;
709+ }
710+ const arg = init . arguments [ 0 ] ;
711+ if ( arg . type !== "Literal" || typeof arg . value !== "string" ) return ;
712+ // Only attach binding state when `require` resolves to the free
713+ // `require` (i.e. it isn't shadowed in the current scope).
714+ const requireInfo = parser . getFreeInfoFromVariable ( "require" ) ;
715+ if ( ! requireInfo || requireInfo . name !== "require" ) return ;
716+ /** @type {RequireBindingData } */
717+ const binding = {
718+ referencedExports : [ ] ,
719+ dep : null
720+ } ;
721+ requireBindingData . set ( init , binding ) ;
722+ parser . tagVariable ( declarator . id . name , REQUIRE_BINDING_TAG , binding ) ;
723+ return true ;
724+ } ) ;
725+
726+ parser . hooks . expression . for ( REQUIRE_BINDING_TAG ) . tap ( PLUGIN_NAME , ( ) => {
727+ const binding =
728+ /** @type {RequireBindingData } */
729+ ( parser . currentTagData ) ;
730+ if ( binding && binding . dep ) {
731+ // `NAME` is read as a value (not as the object of a static member
732+ // chain), so we have to assume the whole exports object is used.
733+ binding . dep . referencedExports = null ;
734+ }
735+ } ) ;
736+
737+ parser . hooks . expressionMemberChain
738+ . for ( REQUIRE_BINDING_TAG )
739+ . tap ( PLUGIN_NAME , ( _expr , members ) => {
740+ const binding =
741+ /** @type {RequireBindingData } */
742+ ( parser . currentTagData ) ;
743+ if ( binding && binding . dep && binding . dep . referencedExports ) {
744+ binding . dep . referencedExports . push ( members ) ;
745+ }
746+ // Returning truthy suppresses the parser's fallback chain (which
747+ // would otherwise walk `NAME` as a bare expression and trigger our
748+ // `expression` hook above, marking the whole namespace as used).
749+ return true ;
750+ } ) ;
751+
752+ parser . hooks . callMemberChain
753+ . for ( REQUIRE_BINDING_TAG )
754+ . tap ( PLUGIN_NAME , ( expr , members ) => {
755+ const binding =
756+ /** @type {RequireBindingData } */
757+ ( parser . currentTagData ) ;
758+ if ( binding && binding . dep && binding . dep . referencedExports ) {
759+ if ( members . length === 0 ) {
760+ // `NAME(...)` — calling the require result directly; the
761+ // whole exports object is observable.
762+ binding . dep . referencedExports = null ;
763+ } else {
764+ binding . dep . referencedExports . push ( members ) ;
765+ }
766+ }
767+ if ( expr . arguments ) parser . walkExpressions ( expr . arguments ) ;
768+ return true ;
769+ } ) ;
770+ // #endregion
771+
665772 // #region Require.resolve
666773 /**
667774 * Processes the provided expr.
0 commit comments