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
{{ message }}
Repository navigation
require( "jquery" ) returns a module object when used with Webpack & jQuery 4.0.0-beta #5416
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...
@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...
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.
@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:
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:
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
this is independent of whether we just expose the default export or more, I was wrong above ↩
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
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.
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:
importjQueryfrom"../dist/jquery.js";// "../dist/jquery.js" is a CommonJS fileconst$=jQuery;export{jQuery,$};exportdefaultjQuery;
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?
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:
importafrom"a";
in addition to the standard:
consta=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 }.
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:
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.
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.
Activity
mgol commentedon Feb 11, 2024
From @cocco111 in #5414 (comment), we have a test case:
mgol commentedon Feb 11, 2024
@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...GeoffreyBooth commentedon Feb 11, 2024
CommonJS doesn’t have a way to represent the “default plus named exports” feature of ES modules. The
__esModulepattern 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.mgol commentedon Feb 12, 2024
@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:
and:
At the same time, we cannot just serve two different versions of jQuery - one to
require, one toimportas 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
modulefield frompackage.json'sexportspointing to an ESM version of jQuery,require( "jquery" )will always return a module object instead ofjQuerywhich is the default export1. The only way to avoid it seems to rely on separateimport&requireconditions inexports. To avoid data duplication issues, in Node.js we already serve a tiny wrapper over CommonJS in theimportcondition:jquery/package.json
Lines 9 to 13 in bf11739
jquery/dist-module/jquery.node-module-wrapper.js
Lines 1 to 4 in bf11739
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 sayingimport-but-you-can-also-import-from-commonjs-herebut there's no such thing. Some bundlers, like Rollup, only set thedefault,module&importconditions by default, for example.Footnotes
this is independent of whether we just expose the default export or more, I was wrong above ↩
vankop commentedon Feb 12, 2024
cc @alexander-akait
[-]`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[/+]mgol commentedon Feb 12, 2024
We discussed it today at the meeting. It seems for Webpack it may be enough to drop the
modulecondition. That condition is already specified inside of thenodeone so any tool going there should support Node.jsimport/requiresemantics, in particular being able to import from a CommonJS module in an ESM one. Webpack supports that as expected.mgol commentedon Feb 12, 2024
That said, I think we have an issue with Rollup. Per https://www.npmjs.com/package/@rollup/plugin-node-resolve:
The
nodecondition is not supported by default. Also, Rollup doesn't supportrequireby default unless the@rollup/plugin-commonjsplugin is included.That poses a problem - if the CommonJS plugin is loaded, we cannot just point
import ... from "jquery"to a standalone ESM jQuery andrequire( "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:All good but then in a Rollup setup the
@rollup/plugin-commonjsplugin may not be enabled and then the above would crash with something like:If there was a condition like
import-but-this-tool-also-supports-requirethen we could differentiate between Rollup with the@rollup/plugin-commonjsplugin 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?
mgol commentedon Feb 13, 2024
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
aauthored in CommonJS exposing a single API viamodule.exports. The library has internal state.Bundlers enter the game and, despite the library not exposing anything ESM-related, they allow to do:
in addition to the standard:
Later, authors of
adecide 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 = atoexport default abut if you exposeexport default adirectly in a package, the CommonJS version exposed by Webpack becomesmodule.exports = { default: a }.mgol commentedon Feb 13, 2024
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: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:
Note that the ESM version:
import $ from "jquery"- doesn't have$.$or$.jQuery.mgol commentedon Feb 13, 2024
I tested Parcel with its experimental
exportssupport enabled (https://parceljs.org/features/dependency-resolution/#enabling-package-exports) and there's no magic behavior forrequire( "a" )like in Rollup, it just returns{__esModule: true, default: a}. However, it also doesn't enable thenodecondition 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
jQueryso Rollup'srequiremagic is not sufficient for us.14 remaining items
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
Core: Fix the exports setup to make bundlers work with ESM & CommonJS
mgol commentedon Mar 25, 2024
The fix will be available in jQuery
4.0.0-beta.2when that gets released.