Skip to content
This repository was archived by the owner on Sep 4, 2026. It is now read-only.
This repository was archived by the owner on Sep 4, 2026. It is now read-only.

“TypeError: jquery__WEBPACK_IMPORTED_MODULE_0__ is not a function” with jQuery 4 (ESM) #256

Description

@andersk

Bug Description

jQuery 4.0.0 was released this weekend, now using ES modules, and it no longer works with expose-loader from ESM consumers.

Here’s the closest I got to making it work:

import HtmlWebpackPlugin from "html-webpack-plugin";
import resolve from "enhanced-resolve";

const myResolve = resolve.create.sync({ conditionNames: ["import", "module"] });

export default {
  entry: ["./cjs.cjs", "./esm.mjs"],
  module: {
    rules: [
      {
        test: myResolve(import.meta.dirname, "jquery"),
        loader: "expose-loader",

        // Worked with jquery@3:
        // options: { exposes: ["$"] },

        // Best attempt with jquery@4
        type: "javascript/auto",
        options: { exposes: [{ globalName: "$", moduleLocalName: "$" }] },
      },
    ],
  },
  plugins: [new HtmlWebpackPlugin()],
};

This lets window.$ and const $ = require("jquery") work as expected. However, if I do import $ from "jquery" from an ES module, I get a module object $ === { $: [function], default: [function], jQuery: [function] } rather than the expected $ === [function].

Link to Minimal Reproduction and step to reproduce

https://gist.github.com/andersk/b64e2d2c49f2db4bf294a04c27989e5f

Run npm install; npx webpack --mode=development and open dist/index.html in a browser.

Expected Behavior

Should see four paragraphs:

Added from CJS via required $

Added from CJS via global $

Added from ESM via imported $

Added from ESM via global $

Actual Behavior

Added from CJS via required $

Added from CJS via global $

Added from ESM via global $

and in the browser console:

TypeError: jquery__WEBPACK_IMPORTED_MODULE_0__ is not a function
    <anonymous> webpack:///./esm.mjs?:6
    jQuery 4
esm.mjs:8:11

Environment

System:
  OS: Linux 6.18 cpe:/o:nixos:nixos:26.05 26.05 (Yarara)
  CPU: (24) x64 AMD Ryzen AI 9 HX 370 w/ Radeon 890M
  Memory: 48.61 GB / 62.09 GB
Binaries:
  Node: 24.12.0 - /home/anders/.nix-profile/bin/node
  Yarn: 1.22.22 - /home/anders/.nix-profile/bin/yarn
  npm: 11.6.2 - /home/anders/.nix-profile/bin/npm
  pnpm: 10.28.0 - /home/anders/.nix-profile/bin/pnpm
Packages:
  expose-loader: ^5.0.1 => 5.0.1 
  html-webpack-plugin: ^5.6.6 => 5.6.6 
  webpack: ^5.104.1 => 5.104.1 
  webpack-cli: ^6.0.1 => 6.0.1

Is this a regression?

None

Last Working Version

No response

Additional Context

Probably related:

Activity

  1. anlambert commented on Jan 27, 2026

    @anlambert

    I also encountered that issue when migrating the project I am working on to jQuery 4 as I also exposed $ and jQuery globally using expose-loader.

    I managed to workaround it by using modify-source-webpack-plugin to rewrite the ESM import statements on the fly so that jQuery can be imported the correct way.

    Below is the modified webpack.config.mjs from the gist linked above.

    import {ModifySourcePlugin, ReplaceOperation} from "modify-source-webpack-plugin";
    import HtmlWebpackPlugin from "html-webpack-plugin";
    import resolve from "enhanced-resolve";
    
    const myResolve = resolve.create.sync({ conditionNames: ["import", "module"] });
    
    export default {
      entry: ["./cjs.cjs", "./esm.mjs"],
      module: {
        rules: [
          {
            test: myResolve(import.meta.dirname, "jquery"),
            loader: "expose-loader",
            type: "javascript/auto",
            options: {
              exposes: [
                {
                  globalName: "$",
                  moduleLocalName: "$",
                  override: true
                },
                {
                  globalName: "jQuery",
                  moduleLocalName: "jQuery",
                  override: true
                }]
            },
          },
        ],
      },
      plugins: [
        new HtmlWebpackPlugin(),
        // ensure jQuery 4.0 can be imported from a js module when exposed globally by webpack expose-loader
        new ModifySourcePlugin({
          rules: [
            {
              test: /\.mjs$/,
              operations: [
                new ReplaceOperation('all', "import jQuery from 'jquery'", "import {jQuery} from 'jquery'"),
                new ReplaceOperation('all', 'import jQuery from "jquery"', 'import {jQuery} from "jquery"'),
                new ReplaceOperation('all', "import \\$ from 'jquery'", "import {$$} from 'jquery'"),
                new ReplaceOperation('all', 'import \\$ from "jquery"', 'import {$$} from "jquery"')
              ]
            }
          ]
        })
      ],
    };
  2. alexander-akait commented on Jan 28, 2026

    @alexander-akait
    Member

    Just interesting why do you still use this loader (it was created for really old code), why do not use directly at the top of your entrypoint:

    import { jQuery as $ } from 'jquery';
    
    globalThis.$ = $;
  3. alexander-akait commented on Jan 28, 2026

    @alexander-akait
    Member

    There are three solution:

    1. globalThis supported and you polyfill it
    import jQuery from 'jquery';
    
    globalThis.$ = jQuery;
    globalThis.jQuery = jQuery;
    
    // ... more code or in other module
    
    $(document).ready(function(){
    	$("button").click(function(){
    		$("p").hide(); // Selects all <p> elements and hides them
    	});
    });
    1. globalThis is not supported or you don't polyfill globalThis
    import jQuery from 'jquery';
    
    var getGlobal = function () {
    	if (typeof globalThis !== 'undefined') { return globalThis; }
    	if (typeof window !== 'undefined') { return window; }
    	if (typeof self !== 'undefined') { return self; }
    	if (typeof global !== 'undefined') { return global; }
    	return (function () { return this; })() || Function('return this')()
    };
    
    const pseudoGlobalThis = getGlobal();
    
    pseudoGlobalThis.$ = jQuery;
    pseudoGlobalThis.jQuery = jQuery;
    
    $(document).ready(function(){
    	$("button").click(function(){
    		$("p").hide(); // Selects all <p> elements and hides them
    	});
    });
    1. Using such configuration (RECOMMENDED for jQuery):

    webpack.config.js

    {
      plugins: [
        new webpack.ProvidePlugin({
          "$": ["jquery", "default"],
          "jQuery": ["jquery", "default"],
        })
      ],
    }
    $(document).ready(function(){
    	$("button").click(function(){
    		$("p").hide(); // Selects all <p> elements and hides them
    	});
    });
  4. alexander-akait commented on Jan 28, 2026

    @alexander-akait
    Member

    Feel free to feedback, soon this loader will be deprecated, I will provide link in webpack examples with jQuery example for other developers

  5. anlambert commented on Jan 29, 2026

    @anlambert

    Just interesting why do you still use this loader (it was created for really old code),

    The project I am working on uses some legacy dependencies that expect $ or jQuery to be in the global scope so the use of that loader.

    why do not use directly at the top of your entrypoint:

    import { jQuery as $ } from 'jquery';

    globalThis.$ = $;

    Oh I did not know about this, thanks !

    I confirm that it works great and it simplifies my webpack config a lot.

    For those interested, see this commit migrating my webpack project to jQuery 4.0.

    I also polyfilled globalThis using babel-plugin-transform-globalthis for older browsers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions