Skip to content

require( "jquery" ) returns a module object when used with Webpack & jQuery 4.0.0-beta #5416

Closed

Description

@mgol

The fact that we started exporting more than a single thing from jQuery - apart from the default export we have $ & jQuery named tokens pointing to the same thing - providing an ESM version of jQuery via the module field in package.json’s exports makes Webpack treat require( "jquery" ) as a way to export the full module instead of just the jQuery object...

Originally posted by @mgol in #5414 (comment)

Issue originally reported by @cocco111 in #5414

Activity

  1. added this to the 4.0.0 milestone on Feb 11, 2024
  2. added
    Discuss in MeetingReserved for Issues and PRs that anyone would like to discuss in the weekly meeting.
    on Feb 11, 2024
  3. mgol commented on Feb 11, 2024

    @mgol
    MemberAuthor

    From @cocco111 in #5414 (comment), we have a test case:

    https://github.com/cocco111/jQuery4Test
    If you prefer you can simply open /dist/index.html in a browser and see result in console.

  4. mgol commented on Feb 11, 2024

    @mgol
    MemberAuthor

    @GeoffreyBooth considering you were a strong advocate of us starting to expose named exports in addition to the default one - do you perhaps know how to fix this issue without stopping to expose those named tokens? Making require( "jquery" ) not return jQuery but instead a transpiled ESM module object when bundled with Webpack is going to break way too much code...

  5. GeoffreyBooth commented on Feb 11, 2024

    @GeoffreyBooth

    CommonJS doesn’t have a way to represent the “default plus named exports” feature of ES modules. The __esModule pattern that Babel and TypeScript use is meant to provide a workaround, for tools that understand the hint to treat the CommonJS export differently. You should probably ask a Webpack maintainer, though, as they would be the experts to advise how to output for maximum compatibility with their tool.

  6. mgol commented on Feb 12, 2024

    @mgol
    MemberAuthor

    @sokra @vankop I'd appreciate some Webpack expertise from you here to help with setting up jQuery. We need the following to still work in jQuery 4.x, whether standalone or when bundled with Webpack or others:

    const jQuery = require( "jquery" );
    jQuery( "<div>" ).appendTo( "body" );

    and:

    import jQuery from "jquery";
    jQuery( "<div>" ).appendTo( "body" );

    At the same time, we cannot just serve two different versions of jQuery - one to require, one to import as there's lots of internal state that needs to be shared. Is this possible at all to achieve?

    If I understand correctly, if we have Webpack pick up the module field from package.json's exports pointing to an ESM version of jQuery, require( "jquery" ) will always return a module object instead of jQuery which is the default export1. The only way to avoid it seems to rely on separate import & require conditions in exports. To avoid data duplication issues, in Node.js we already serve a tiny wrapper over CommonJS in the import condition:

    jquery/package.json

    Lines 9 to 13 in bf11739

    "node": {
    "module": "./dist-module/jquery.module.js",
    "import": "./dist-module/jquery.node-module-wrapper.js",
    "require": "./dist/jquery.js"
    },

    import jQuery from "../dist/jquery.js";
    const $ = jQuery;
    export { jQuery, $ };
    export default jQuery;

    but this assumes the ability to import from CommonJS files in ESM - which is true in Node.js but not necessarily in other tooling reading from exports. What we'd really need is a condition saying import-but-you-can-also-import-from-commonjs-here but there's no such thing. Some bundlers, like Rollup, only set the default, module & import conditions by default, for example.

    Footnotes

    1. this is independent of whether we just expose the default export or more, I was wrong above ↩

  7. vankop commented on Feb 12, 2024

    @vankop
  8. changed the title [-]`require( "jquery )` returns a module object when used with Webpack & jQuery 4.0.0-beta[/-] [+]`require( "jquery" )` returns a module object when used with Webpack & jQuery 4.0.0-beta[/+] on Feb 12, 2024
  9. mgol commented on Feb 12, 2024

    @mgol
    MemberAuthor

    We discussed it today at the meeting. It seems for Webpack it may be enough to drop the module condition. That condition is already specified inside of the node one so any tool going there should support Node.js import/require semantics, in particular being able to import from a CommonJS module in an ESM one. Webpack supports that as expected.

  10. mgol commented on Feb 12, 2024

    @mgol
    MemberAuthor

    That said, I think we have an issue with Rollup. Per https://www.npmjs.com/package/@rollup/plugin-node-resolve:

    Additional conditions of the package.json exports field to match when resolving modules. By default, this plugin looks for the ['default', 'module', 'import'] conditions when resolving imports.

    When using @rollup/plugin-commonjs v16 or higher, this plugin will use the ['default', 'module', 'require'] conditions when resolving require statements.

    The node condition is not supported by default. Also, Rollup doesn't support require by default unless the @rollup/plugin-commonjs plugin is included.

    That poses a problem - if the CommonJS plugin is loaded, we cannot just point import ... from "jquery" to a standalone ESM jQuery and require( "jquery" ) to a standalone CommonJS jQuery as that would result in two jQuery instances. jQuery has internal state so we need a single instance. A common way to resolve it is to avoid exposing a pure ESM jQuery and instead use a tiny wrapper over the CommonJS version, like we do for Node.js:

    import jQuery from "../dist/jquery.js"; // "../dist/jquery.js" is a CommonJS file
    const $ = jQuery;
    export { jQuery, $ };
    export default jQuery;

    All good but then in a Rollup setup the @rollup/plugin-commonjs plugin may not be enabled and then the above would crash with something like:

    [!] RollupError: "default" is not exported by "dist/jquery.js", imported by "src/app.js".
    

    If there was a condition like import-but-this-tool-also-supports-require then we could differentiate between Rollup with the @rollup/plugin-commonjs plugin enabled or not - but we don't have anything like that.

    @lukastaegert would you be able to help here or point us to someone who could help with making jQuery work with Rollup?

  11. mgol commented on Feb 13, 2024

    @mgol
    MemberAuthor

    I'm surprised this is so hard to get right. At its core, the situation is quite simple and looks common.

    We have a package a authored in CommonJS exposing a single API via module.exports. The library has internal state.

    Bundlers enter the game and, despite the library not exposing anything ESM-related, they allow to do:

    import a from "a";

    in addition to the standard:

    const a = require( "a" );

    Later, authors of a decide to migrate it to ESM. There doesn't seem to be an easy way to achieve that in a way that preserves both above use cases when used with bundlers while not duplicating the library state.

    The crux of the issue is that bundlers internally convert module.exports = a to export default a but if you expose export default a directly in a package, the CommonJS version exposed by Webpack becomes module.exports = { default: a }.

  12. mgol commented on Feb 13, 2024

    @mgol
    MemberAuthor

    I've checked some scenarios with Rollup and Rollup seems to be doing something interesting. As long as the default export is a function, require( "a" ) is a wrapper function:

    var a = function a () {
    	if (this instanceof a) {
    		return Reflect.construct(f, arguments, this.constructor);
    	}
    	return f.apply(this, arguments);
    };

    and calling it defers to the original function. However, extra APIs attached to that function are not proxied. In the jQuery case, this means the following:

    const $ = require( "jquery" );
    $.$( "<div>" );      // a jQuery object
    $.jQuery( "<div>" ); // a jQuery object
    $( "<div>" );        // a jQuery object <-- this works!
    $.$.noop             // a noop function
    $.jQuery.noop        // a noop function
    $.noop               // undefined <-- this doesn't work!

    Note that the ESM version: import $ from "jquery" - doesn't have $.$ or $.jQuery.

  13. mgol commented on Feb 13, 2024

    @mgol
    MemberAuthor

    I tested Parcel with its experimental exports support enabled (https://parceljs.org/features/dependency-resolution/#enabling-package-exports) and there's no magic behavior for require( "a" ) like in Rollup, it just returns {__esModule: true, default: a}. However, it also doesn't enable the node condition by default as Webpack seems to do.

    In practical terms, it's not much different from Rollup for us since we have utils attached directly to jQuery so Rollup's require magic is not sufficient for us.

  14. 14 remaining items

  15. added 3 commits that reference this issue on Mar 2, 2024
    8c5c625
    d3a28fa
    7145c00
  16. removed
    Discuss in MeetingReserved for Issues and PRs that anyone would like to discuss in the weekly meeting.
    on Mar 4, 2024
  17. added 4 commits that reference this issue on Mar 8, 2024
    8bb95a4
    c94ff35
    21f821d
    85859c2
  18. added and removed on Mar 11, 2024
  19. added a commit that references this issue on Mar 11, 2024
    77add9a
  20. added a commit that references this issue on Mar 11, 2024
    60f11b5
  21. mgol commented on Mar 25, 2024

    @mgol
    MemberAuthor

    The fix will be available in jQuery 4.0.0-beta.2 when that gets released.

  22. locked as resolved and limited conversation to collaborators on Sep 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions