GithubHelp home page GithubHelp logo

Comments (24)

mgol avatar mgol commented on June 11, 2024

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.

from jquery.

mgol avatar mgol commented on June 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...

from jquery.

GeoffreyBooth avatar GeoffreyBooth commented on June 11, 2024

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.

from jquery.

mgol avatar mgol commented on June 11, 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:

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

from jquery.

vankop avatar vankop commented on June 11, 2024

cc @alexander-akait

from jquery.

mgol avatar mgol commented on June 11, 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.

from jquery.

mgol avatar mgol commented on June 11, 2024

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?

from jquery.

mgol avatar mgol commented on June 11, 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 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 }.

from jquery.

mgol avatar mgol commented on June 11, 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:

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.

from jquery.

mgol avatar mgol commented on June 11, 2024

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.

from jquery.

mgol avatar mgol commented on June 11, 2024

cc @devongovett for the Parcel case.

from jquery.

GeoffreyBooth avatar GeoffreyBooth commented on June 11, 2024

Something worth considering is whether it might actually be okay to ship different builds to different targets for bundlers. For Node it’s an issue to ship a different instance for require as for import when each instance contains internal state, as within the same app some code might require the library while other code imports it; but is that really the case for bundlers? In a test app that both requires and imports jQuery, when built via Rollup or Parcel or Webpack, are two different instances of jQuery supplied in the build output? If not, then perhaps these tools can all simply get the ESM version, as it’s the more robust one (since it has the syntax to separately define a default export and named exports) and they can make require of that version work.

from jquery.

mgol avatar mgol commented on June 11, 2024

@GeoffreyBooth that's the issue - bundlers support passing an ESM version to require but then require( "jquery" ) returns { default: jQuery, $: jQuery, jQuery: jQuery } instead of jQuery and that's a huge breaking change that we cannot accept. Rollup adds some magic on top of this object as I posted above but that only allows to call the return value of require( "jquery" ) as a jQuery function but not use utils attached directly to jQuery.

To avoid that, we'd need to separate the import & require versions. And then we're hitting the double state issues while not being able to work around it as we do for Node.js since CommonJS support in these tools is not always enabled.

from jquery.

GeoffreyBooth avatar GeoffreyBooth commented on June 11, 2024

To avoid that, we'd need to separate the import & require versions.

But that's my point: if you separate them, does it fix the issue for bundlers? I would assume those tools only bundle one version or the other, not both?

If so, then the question becomes how to unbreak Node. But there's a node condition you could use.

from jquery.

mgol avatar mgol commented on June 11, 2024

But that's my point: if you separate them, does it fix the issue for bundlers? I would assume those tools only bundle one version or the other, not both?

I haven't tested this but I don't see how they could only bundle a single version in that case. I'm talking about projects using both import & require to fetch jQuery which is pretty common. Since the require( "jquery" ) call will then use a different entry from exports than an import ... from "jquery" call, both will need to be bundled. Bundlers cannot know those two versions are almost identical.

EDIT: just in case, I tested this with Rollup now and, just like I expected, if we have separate require & import conditions, Rollup will bundle jQuery twice.

from jquery.

cocco111 avatar cocco111 commented on June 11, 2024

Yes I can confirm words of @mgol . .module. and normal versions of jquery are nether same file nor same content (builder add code), so the bundler cannot understand they are the same.
Having 2 copies of jQuery is also a problem for plugins ecosystem: in the way they are builded, they add code to first or second jquery

from jquery.

GeoffreyBooth avatar GeoffreyBooth commented on June 11, 2024

bundlers support passing an ESM version to require but then require( "jquery" ) returns { default: jQuery, $: jQuery, jQuery: jQuery }

What if the CommonJS version that bundlers get is a wrapper file? Like module.exports = require('jquery').default || require('jquery').

from jquery.

mgol avatar mgol commented on June 11, 2024

@GeoffreyBooth are you proposing changes to the logic in bundlers or to jQuery? If the latter, could you elaborate your idea? I don’t see how wrapping a CommonJS file in another CommonJS one in the jQuery repository is going to help here.

from jquery.

timmywil avatar timmywil commented on June 11, 2024

CommonJS version that bundlers get

I think the issue is it's hard (impossible?) to differentiate between bundlers and not bundlers. And even then, what if the user does const { jQuery } = require('jquery')

from jquery.

mgol avatar mgol commented on June 11, 2024

Even if there was a condition only true for bundlers, it still wouldn’t help. What we need instead is a way to detect environments that support both import & require. This condition would be true for Node, Webpack & Rollup with the CommonJS plugin and it’d be false for Rollup without the CommonJS plugin.

from jquery.

devongovett avatar devongovett commented on June 11, 2024

Maybe what could work would be to make two versions:

  1. For node, make CommonJS the main version, and have a small wrapper that re-exports it as an ES module. This works because node allows importing CJS from ESM, but not the other way around.
import jQuery from './jquery.node.cjs';
export default jQuery;
  1. For bundlers, make the ESM version the main one, and have a small wrapper that re-exports it as a CJS module. Bundlers allow requiring an ES module from CJS unlike node, and you can use this to remove the interop default object.
module.exports = require('./jquery.browser.esm.js').default;

But maybe I missed something.

from jquery.

mgol avatar mgol commented on June 11, 2024

@devongovett thanks for your input; what you wrote seems to work in Webpack, Rollup & Parcel! This may also be what @GeoffreyBooth meant by his last message that I misunderstood. We'd essentially reverse the flow for non-Node environments, assuming universal ESM support & non-universal CommonJS one there.

There's no condition true for all bundlers and only then but I'd consider applying Devon's suggestion for everything outside of the node section. The only question is - is there any important environment supporting CommonJS that is not Node.js and not a bundler? And that doesn't allow synchronously requiring an ESM file? If not, then we could just do:

  "exports": {
    ".": {
      "node": {
        "import": "./dist-module/jquery.node-module-wrapper.js",
        "require": "./dist/jquery.js"
      },
      "script": "./dist/jquery.min.js",
      "require": "./dist/jquery.bundler-require-wrapper.js",
      "default": "./dist-module/jquery.module.js"
    },
    ...
  }

where dist/jquery.bundler-require-wrapper.js is:

"use strict";

const jQueryModule = require( "../dist-module/jquery.module.js" );
module.exports = jQueryModule.default || jQueryModule;

The fallback to the full module may not even be needed, this is mostly to play safe in case a tool chooses a different way to transpile the default ESM export. Maybe it's not necessary.

from jquery.

mgol avatar mgol commented on June 11, 2024

Side-note: I removed the production & development conditions from my above proposal. They are reported by Webpack & Parcel but not by Rollup. The way we added them initially doesn't seem correct to me now, they should rather be additional conditions to consider for each existing condition. For example, my above proposal would look like the following with production & development considered:

  "exports": {
    ".": {
      "node": {
        "import": {
          "production": "./dist-module/jquery.node-module-wrapper.min.js",
          "default": "./dist-module/jquery.node-module-wrapper.js"
        },
        "require": {
          "production": "./dist/jquery.min.js",
          "default": "./dist/jquery.js"
        }
      },
      "script": "./dist/jquery.min.js",
      "require": {
        "production": "./dist/jquery.bundler-require-wrapper.min.js",
        "default": "./dist/jquery.bundler-require-wrapper.js"
      },
      "default": {
        "production": "./dist-module/jquery.module.min.js",
        "default": "./dist-module/jquery.module.js"
      }
    },
    ...
  }

That's pretty verbose and I don't think we really need it. This is perhaps more useful when a library has logic differences between development & production builds, other than just minifying the output.

from jquery.

mgol avatar mgol commented on June 11, 2024

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

from jquery.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.