-
Notifications
You must be signed in to change notification settings - Fork 20.6k
Build: Add exports
to package.json, export slim & esm builds
#5255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
dce6641
to
b538327
Compare
package.json
Outdated
"type": "commonjs", | ||
"exports": { | ||
".": { | ||
"node": "./dist/jquery.js", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up just defining a single entry point for Node.js. This is because our compiled ESM version still only has a default export with the value of module.exports
from the regular file - which is just what Node.js does natively if you import
a CommonJS module from an ESM one. Using a single file allows to avoid the dual package hazard altogether.
https://nodejs.org/api/esm.html#import-statements
When importing CommonJS modules, the module.exports object is provided as the default export. Named exports may be available, provided by static analysis as a convenience for better ecosystem compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "node"
field should only be used when an export is only usable by Node.js, for example if it contains compiled native extensions. In general we encourage packages to define only require
and default
whenever possible, to maximize compatibility. In particular, we wouldn’t want ESM Node users getting a CommonJS file defined under "node"
.
My previous comment #5122 (comment) is still valid; see https://github.com/GeoffreyBooth/js-mjs-mime-type-test. Even in 2023 it’s unsafe to assume that web servers and CDNs will serve .mjs
files correctly. Your default
export should be a .js
file.
Since the most important files, under src
, are written in ESM with .js
extensions, I would treat .js
as meaning “ESM” throughout the codebase for consistency. Therefore the root package.json
would get "type": "module"
and all the CommonJS files scattered around would get .cjs
extensions. Then this block becomes:
"exports": {
".": {
"require": "./dist/jquery.cjs",
"default": "./dist/jquery.js"
And likewise for "./slim"
and so on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Responding to each aspect:
The node
field in exports
If I only define require
and default
I will hit dual package hazard. Per Node.js packages docs, to avoid it one can isolate the state in a CommonJS module and require it in an ESM. This is Node-specific (not suitable for pure ESM envs) so I’d have to add the node
entry anyway.
This is what I actually initially aimed at, but then I realized that in our case the ESM module would just have a default export matching module.exports
from the CommonJS version, so the ESM wrapper would be useless; Node is properly serving our CommonJS module to ESM consumers fine by default.
The node
field is Node-specific in our case, in that it makes Node always serve the CommonJS version to avoid dual package hazard.
.mjs
As for .mjs
, why is serving the CommonJS version with the .cjs
extension less risky?
Also, right now we produce the same files for browser & Node. The CommonJS version is also the default script version for the browser, it having a .cjs
extension would be very unintuitive. I’m also sure it’d break lots of jQuery integrations. On the other hand, both ESM & CommnJS files having the same .js
extension won’t work for Node… It seems that avoiding .mjs
would require keeping the CommonJS file duplicated, with both the .js
and .cjs
extension. 😕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe the solution to avoid .mjs
and .cjs
is to keep them in separate dist
& dist-esm
directories and add proper package.json
with the type
fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dual package hazard is only an issue if a package contains internal state. Does jQuery
contain any state? Do people register plugins on the jQuery
global perhaps, and therefore would be surprised if they existed on jQuery
in one module environment but not the other?
Assuming that that is an issue, then you need to choose between these options:
- Ship the CommonJS version to all Node users, regardless of whether the Node application is written in CommonJS or ESM. This might lead to bundlers including all of jQuery rather than tree-splitting the ESM version, and it might annoy the developer who’s writing ESM to see CommonJS in the stack trace. Neither of these are showstoppers, but they’re worth considering.
- Ship only the ESM version, period, joining the list of packages that have dropped CommonJS support in their latest major versions. Obviously this would greatly annoy any Node developers writing CommonJS applications.
- Ship both versions where Node ESM users get the ESM version and Node CommonJS users get the CommonJS version. You would probably have to add a warning in the docs telling people not to add custom properties to the
jQuery
object or other exports, that they expect to see across module systems.
If the hazard is a real concern, then sure, maybe the first option is the least bad one.
As for
.mjs
, why is serving the CommonJS version with the.cjs
extension less risky?
Because web servers and CDNs aren’t serving the CommonJS version; that version is only for Node or bundlers, who would be loading it from files (and which all have support for the .cjs
extension; all Node versions treat unknown extensions as CommonJS, so it works back to the beginning). Whereas the version for use in browsers needs to be served with the correct MIME type, and per https://github.com/GeoffreyBooth/js-mjs-mime-type-test even in 2023 many big open source webservers like Nginx and Apache don’t serve .mjs
with the correct MIME type by default. Or put another way, there’s no benefit to .mjs
but there’s a big downside to this potential incompatibility.
right now we produce the same files for browser & Node
Aren’t you producing both a browser ESM version and a browser AMD version? So it’s the browser AMD version that is identical to the Node CommonJS version?
Yes, you could just output it twice, like jquery.amd.js
and jquery.cjs
, and the contents of the two versions happen to be identical. That wouldn’t be terrible. It probably would be preferable for them to not be identical, as the .cjs
version doesn’t need some of the AMD stuff like checking if require
is defined; it can just assume that it’s in a CommonJS environment.
You could also do something like put this combined AMD/CommonJS file in a subfolder like dist/browser-amd-and-node-commonjs/jquery.js
and add dist/browser-amd-and-node-commonjs/package.json
with "type": "commonjs"
. Then it’s a little more explicit that this file is being reused. Personally I would just do the first option, outputting it twice, so that you have the opportunity to optimize each one a bit rather than having checks that are only needed since you’re reusing the same file in two very different environments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@GeoffreyBooth I wonder - do we need to provide a default export at all? The CommonJS version would have one but if we keep different exported tokens between CommonJS & ESM versions anyway, can we just go all the way and only export named $
& jQuery
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to provide a default export at all?
For backward compatibility, I would assume you need to provide it; aren't people writing import jQuery from 'jquery'
today? And you're forced to support that if you support Node via the CommonJS wrapper approach; so then not allowing that syntax in pure ESM environments would seem likely to cause confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, I forgot about a lot of the code relying on bundlers that would auto-switch to the ESM version & break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@GeoffreyBooth I'm working on updates right now. I'm missing why we need the production
section for the node
condition. I understand why it's needed outside of Node.js and so that we need the production
, development
& module
fields there but why in Node?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason to include it for Node is that so if someone wants to use the minified build in Node, for example for server side rendering they want to match client behavior, there's a way to do so. It's arguably optional though.
var jQueryOrJQueryFactory = typeof window !== "undefined" && window.document ? | ||
jQueryFactory( window, true ) : | ||
function( w ) { | ||
if ( !w.document ) { | ||
throw new Error( "jQuery requires a window with a document" ); | ||
} | ||
return jQueryFactory( w ); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking whether it'd be better to extract this factory logic to a separate entry point: jquery/factory
, and have the default one just assume the global window
. This would simplify these headers.
The current setup doesn't let you freely use the factory in all scenarios anyway - if you load any files depending on jQuery via CommonJS - which is common, through UMD - then that file depends on "jquery"
resolving to an actual jQuery instance, not a factory. In effect, you need to set the global window
anyway and the factory remains useless.
What is useful about this wrapper is the window
encapsulation - if such a global is present, browser globals are all taken from window
instead of global variables. This is what unlocks libraries like JSDOM. When I mentioned to factory to Domenic some time ago, he didn't even know about the factory way of loading jQuery. 🙃
The problem with an additional entry point is we'd also want to expose the factory in slim version - which would expand from the current two entry points: jquery
& jquery/slim
to 4: jquery
, jquery/slim
, jquery/factory
, jquery/slim-factory
.
// This accentuates the need for the creation of a real `window`. | ||
// e.g. var jQuery = require("jquery")(window); | ||
// See ticket trac-14549 for more info. | ||
var jQueryOrJQueryFactory = typeof window !== "undefined" && window.document ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no point in falling back to this
as we do in the main wrapper. There, it made sense as this
has some meaning in CommonJS. In ESM, it's just undefined
at the top level.
src/package.json
Outdated
@@ -0,0 +1,3 @@ | |||
{ | |||
"type": "module" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows src/
files to retain the .js
extension, despite being ECMAScript modules. Node.js recognizes they are ESM in such a case.
@@ -0,0 +1,14 @@ | |||
import { JSDOM } from "jsdom"; | |||
|
|||
import { ensureJQuery } from "./lib/ensure_jquery.mjs"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that all of these use the .mjs
extension. This could be avoided by adding a simple package.json
:
{
"type": "module"
}
to the test/node_smoke_tests/module/
folder. However, some of node smoke test runs actually test exports
from within the package:
https://nodejs.org/api/packages.html#self-referencing-a-package-using-its-name
That way, we can easily test whether what we define via exports
really works.
The problem is, this feature requires the closest package.json
to the current module so a root one in our case. This means we cannot add such a simple package.json
to test/node_smoke_tests/module/
.
These are just tests run in Node.js, nothing to be consumed externally, so I think it should be fine.
package.json
Outdated
"test:browser": "grunt && grunt karma:main", | ||
"test:esmodules": "grunt && grunt karma:esmodules", | ||
"test:amd": "grunt && grunt karma:amd", | ||
"test:no-deprecated": "grunt test:prepare && grunt custom:-deprecated && grunt karma:main", | ||
"test:selector-native": "grunt test:prepare && grunt custom:-selector && grunt karma:main", | ||
"test:slim": "grunt test:prepare && grunt custom:slim && grunt karma:main", | ||
"test": "npm run test:slim && npm run test:no-deprecated && npm run test:selector-native && grunt && grunt test:slow && grunt karma:main && grunt karma:esmodules && grunt karma:amd", | ||
"test:node_smoke_tests:slim": "grunt node_smoke_tests:commonjs:./dist/jquery.slim.js && grunt node_smoke_tests:commonjs:jquery/slim", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This first run node smoke tests by running require( "FULL/PATH/TO/dist/jquery.slim.js" )
and then by running require( "jquery/slim" )
; the latter part goes through exports
defined in this package.json
, which is a nice test that this setup works.
The same is done for other test:node_smoke_tests:*
scripts.
@@ -0,0 +1,15 @@ | |||
"use strict"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous node smoke tests have been moved to the commonjs
folder (with some modifications), their ESM versions are in the module
folder.
import { getJQueryModuleSpecifier } from "./lib/jquery-module-specifier.mjs"; | ||
|
||
const jQueryModuleSpecifier = getJQueryModuleSpecifier(); | ||
const { default: jQueryFactory } = await import( jQueryModuleSpecifier ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
top-level await is available unflagged in Node.js since 14.8.0
:
https://nodejs.org/en/blog/release/v14.8.0
We now test on Node.js 10, 16, 18 & 20 and 10 is on that list only because our Jenkins runs it. But node smoke tests are skipped in Node 10 now.
@aduh95 @GeoffreyBooth would you be able to review this version? You previously reviewed my POC at #5122. |
A review from @guybedford would also help if possible. |
Fixes gh-4592 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is jQuery itself stateful so that the dual package hazard will be a real issue? The default export seems fine as they are very much designed for the jQuery pattern. So long as the ESM version does export default jQuery
.
Yes, people register jQuery plugins that attach themselves as properties to the |
The |
`grunt` already generates all the files, we don't need to do it by hand.
This required some files to be renamed from `*.js` to `*.cjs`.
1. Don't build at the beginning of test:node_smoke_tests 2. Use `npm run test:browserless` at the beginning of `npm test` 3. Some renaming & reordering
Those tasks are already run individually for each variant via `build-all-variants`.
Technically speaking, we're just using `.js` & `.cjs` for now but specifying all three is more future-proof.
Summary of the changes: * expand `node_smoke_tests` to test the full & slim builds * run `compare_size` on all built minified files; don't run it anymore on unminified files where they don't provide lots of value The main goal of this change is to make it easier to compare sizes of both the full & slim builds between the `3.x-stable` & `main` branches. Ref jquerygh-5255 (partially cherry-picked from commit 8be4c0e)
Summary of the changes: * expand `node_smoke_tests` to test the full & slim builds * run `compare_size` on all built minified files; don't run it anymore on unminified files where they don't provide lots of value The main goal of this change is to make it easier to compare sizes of both the full & slim builds between the `3.x-stable` & `main` branches. Closes gh-5291 Ref gh-5255 (partially cherry-picked from commit 8be4c0e)
The Migrate setup is simpler than Core as it doesn't have the slim or factory versions, but the core ideas are similar. Ref jquery/jquery#5255 Ref jquery/jquery#5429
The Migrate setup is simpler than Core as it doesn't have the slim or factory versions, but the core ideas are similar. Ref jquery/jquery#5255 Ref jquery/jquery#5429
The Migrate setup is simpler than Core as it doesn't have the slim or factory versions, but the core ideas are similar. Fixes jquerygh-524 Ref jquery/jquery#5255 Ref jquery/jquery#5429
The Migrate setup is simpler than Core as it doesn't have the slim or factory versions, but the core ideas are similar. Fixes jquerygh-524 Ref jquery/jquery#5255 Ref jquery/jquery#5429
The Migrate setup is simpler than Core as it doesn't have the slim or factory versions, but the core ideas are similar. Fixes gh-524 Closes gh-566 Ref jquery/jquery#5255 Ref jquery/jquery#5429
Summary
Summary of the changes:
--esm
option togrunt custom
, generating jQuery as an ECMAScript moduleexports
field inpackage.json
node_smoke_tests
to test the slim & ESM builds and their various combinationsI published a test version of jQuery with these changes using
jquery-release
at:https://www.npmjs.com/package/@mgol/jquery-exports-test-do-not-use-v7
I also pushed the
jquery-dist
repo at4.0.0
with these changes to my fork:https://github.com/mgol/jquery-dist/tree/exports-test
I also added a test repo using this package to make sure it works:
https://github.com/mgol/jquery-exports-test
Fixes gh-4592
Checklist