Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

jQuery 4 exports explainer

Michał Gołębiowski-Owczarek edited this pageMar 11, 2024 ·2 revisions

Summary

NOTE: this doc explains a future 4.0 design as implemented in PRhttps://github.com/jquery/jquery/pull/5429. This is work in progress, further changes may happen before the final release; this doc will be updated in such a case.

jQuery 4.0 will ship withexports in itspackage.json.exports allow to expose multiple entry points, hide implementation details, serve different files toimport vs.require and many more.

This doc explains all the choices behind the jQuery 4.0exports definition.

Theexports API

Theexports syntax is pretty complex and we won't cover it all here. To learn more aboutexports, readhttps://nodejs.org/api/packages.html#package-entry-points. For jQuery purposes, the most important rules are:

  1. The top level contains entry points. The following definition:
    "exports": {".":"main.js","./foo":"bar.js"}
    in a library namedlib would mean importinglib returns the contents ofmain.js, while importinglib/foo - the context ofbar.js.
  2. The value for each entry point is an object in which keys are possibleconditions and leaves are all the possible resolved path values. For example, in the following case:
    "exports": {".": {"a": {"b": {"c":"c.js"      }    },"d":"d.js","default":"default.js"  }}
    if the environment reports conditionsa,b &c,c.js will be returned. Otherwise, if it reports thed condition, we'll getd.js. Otherwise, we'll receivedefault.js.All conditions on the path to a specific leaf need to be reported to get that leaf.
  3. Conditions are evaluated from top to bottom. If they are multiple reported condition paths, the most top one will be the chosen one.
  4. There are three most important conditions reported by Node.js:default is always reported,import is reported when the entry point is fetched via the ESMimport andrequire if it's fetched via a CommonJSrequire.
  5. If the value for an entry point is a string instead of an object, the provided path will always be the chosen one regardless of reported conditions.
  6. Wildcards are reported in entry point definitions as well as in paths.* in an entry point matches a substring directly mapped to* on the right side. For example:
    "exports": {"a/*.js":"b/*.js"}
    means importinglib/a/foo/bar.js will provideb/foo/bar.js from thelib package.

Requirements

  1. There need to be four entry points:jquery,jquery/slim,jquery/factory &jquery/factory-slim. The first two need to point to the full/slim version of jQuery respectively, the last two to their factory versions - for example, the following:
    import{jQueryFactory}from"jquery/factory";const$=jQueryFactory(window);
    will make$ point to jQuery, wherewindow is a browser-compatiblewindow implementation.
  2. For compatibility reasons, both:
    import$from"jquery";
    in ESM files and:
    const$=require("jquery");
    in CommonJS files need to continue working, with$ pointing to jQuery.
  3. Because of interop issues between ESM & CommonJS when default ESM exports are used, named$ &jQuery exports need to be exposed via ESM in addition to the default one. They all need to point to the same jQuery:
    import$default,{jQuery,$}from"jquery";console.assert($default===jQuery);console.assert($===jQuery);
  4. Regardless of whether Node.js or a popular bundler is used to run/build jQuery, if a single project fetches jQuery both viaimport andrequire, they need to point to the same copy of jQuery.
  5. require should work in an environment supporting only CommonJS;import needs to work in an environment supporting only ESM.
  6. Thesrc directory needs to be fully exposed for more advanced usage; only ESM is supported there.

Implementation

As we have four entry points plus all the files insrc, we are starting with:

"exports": {".": {},"./slim": {},"./factory": {},"./factory-slim": {},"./src/*.js":"./src/*.js"  },"main":"dist/jquery.js",

We are keepingmain for backwards compatibility with tools not supportingexports. It is not that rare; for example, even the newest versions of TypeScript ignoreexports ifmoduleResolution is set tonode10 or its legacynode alias.

The line"./src/*.js": "./src/*.js" exposes all JS files insrc as-is. We don't guarantee any stability here.

The first four exports form two groups:"." will look almost identical to"./slim" and"./factory" to"./factory-slim". We will only discuss non-slim versions.

The main entry point:.

The most obvious setup would look like the following:

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

Since some older environments may only recognize thedefault condition, we need to add one pointing to CommonJS - but then we don't need therequire condition as thedefault one can be reused:

".": {"import":"./dist-module/jquery.module.js","default":"./dist/jquery.js"}

However, this means projects fetching jQuery via both the ESMimport and the CommonJSrequire would get two different jQuery copies, each with their own data storage.

Node.js packages docs have a section dedicated to this issue:Dual CommonJS/ES module packages. In Node.js, ESM files can synchronously import from CommonJS ones - but CommonJS onescannot synchronously require from ESM ones. This is because ES modules are inherently async. This means we can create a small ESM wrapper file calledjquery.node-module-wrapper.js with the following contents:

importjQueryfrom"../dist/jquery.js";export{jQuery,jQueryas$};exportdefaultjQuery;

However, we cannot depend that all tools supporting ESM recognize such imports from CommonJS. We need to constrain this workaround to Node.js only. Thankfully, Node.js reports thenode condition.

Our updatedexports definition:

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

We handled Node.js. Bundlers don't usually report thenode condition. However, they usually report themodule one, regardless of whether the ESMimport or the CommonJSrequire is used to fetch the library.

Note: this is because bundlers have more relaxed rules as opposed to Node.js and allow not only ESM files fetching CommonJS ones viaimport but also allow CommonJS files to synchronously fetch ESM files viarequire. It's possible as bundlers... well, bundle: they can merge multiple input files into one bundle with synchronous access between parts.

It seems we could just duplicate thenode section, changing the key fromnode tomodule. However, some bundlers have pure ESM modes where CommonJS is not recognized at all; one such example is Rollup. We need to serve a pure ESM version to them.

To solve this issue, we use a similar solution as for Node.js but reversed - i.e., we ship a pure ESM version but the CommonJS one, contained in thejquery.bundler-require-wrapper.js file, is just re-exporting the ESM one:

const{ jQuery}=require("../dist-module/jquery.module.js");module.exports=jQuery;

For tools supporting only ESM or only CommonJS, we leave the top-levelimport &default entries.

The final section for entry point. looks like the following:

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

The factory entry point:./factory

For the factory entry point, we could follow the same strategy as for the main. one. That would require two extra wrapper files (four if you count the slim versions). However, we can simplify it a bit, leveraging the fact that factory entry points are new, they are not meant to be usable from browser script tags and we have more freedom when it comes to their APIs.

To avoid wrapper files:

  1. We don't use the default export in ESM. We use one named one:jQueryFactory.
  2. In CommonJS, we use a similar API:module.exports = { jQueryFactory }.

With these assumptions, we can just serve the CommonJS version to Node and the ESM one to bundlers without differentiating on whetherimport orrequire was used to fetch the library. The final version:

"./factory": {"node":"./dist/jquery.factory.js","module":"./dist-module/jquery.factory.module.js","import":"./dist-module/jquery.factory.module.js","default":"./dist/jquery.factory.js"}

Final version

The final full version ofexports &main:

"exports": {".": {"node": {"import":"./dist-module/jquery.node-module-wrapper.js","default":"./dist/jquery.js"    },"module": {"import":"./dist-module/jquery.module.js","default":"./dist/jquery.bundler-require-wrapper.js"    },"import":"./dist-module/jquery.module.js","default":"./dist/jquery.js"  },"./slim": {"node": {"import":"./dist-module/jquery.node-module-wrapper.slim.js","default":"./dist/jquery.slim.js"    },"module": {"import":"./dist-module/jquery.slim.module.js","default":"./dist/jquery.bundler-require-wrapper.slim.js"    },"import":"./dist-module/jquery.slim.module.js","default":"./dist/jquery.slim.js"  },"./factory": {"node":"./dist/jquery.factory.js","module":"./dist-module/jquery.factory.module.js","import":"./dist-module/jquery.factory.module.js","default":"./dist/jquery.factory.js"  },"./factory-slim": {"node":"./dist/jquery.factory.slim.js","module":"./dist-module/jquery.factory.slim.module.js","import":"./dist-module/jquery.factory.slim.module.js","default":"./dist/jquery.factory.slim.js"  },"./src/*.js":"./src/*.js"},"main":"dist/jquery.js",

FAQ

Why are you not handling thedevelopment &production conditions?

Some tools, likeWebpack orParcel support thedevelopment &production conditions. They can be especially useful if a library ships different development code, e.g. adding some debugging features. Some other tools, likeRollup, does not support these conditions.

In jQuery, the only difference between the development & production builds are that the latter is minified. Most workflows caring about the file size already include the minification step that also minifies vendors, though, so pointing to the minified jQuery version wouldn't help much. On the other hand, handling these conditions would greatly enlarge an already hugeexports definition - we'd have to split almost every current path to itsdevelopment &production versions. All the wrapper files would need to be doubled as well. For example, the section for just the. entry point would look like the following:

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

For now, we decided to avoid this complexity.

Why are you not handling the X condition?

The only entry points Node.js supports arenode,node-addons,import,require, anddefault. However,Node docs themselves mention a few other conditions:types,browser,development &production. We don't provide types and we ship the same API to Node.js & the browser - while Node doesn't have a browser-compatible globalwindow, one can simulate it via jsdom. We've already discussed thedevelopment &production conditions.

In general, adding support for a new condition can be done in a minor release; it's not a breaking change. For this first release, we're trying to do only as much as it's needed to fulfill our requirements.

Clone this wiki locally

[8]ページ先頭

©2009-2025 Movatter.jp