-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
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
Support esm on node with conditional exports #18498
Support esm on node with conditional exports #18498
Conversation
I've found that there's actually another possibility as well. If we add a {
"type": "module"
} then we could change the main {
"exports": {
"require": "./build/three.js",
"import": "./src/Three.js"
}
} I have tested this and it works, but has a horrible loading time (~300ms), whereas the prebuilt version loads pretty much instantaneously (~30ms). |
I feel this is a bit too redundant and confusing. With node now properly supporting modules I think we can look into deprecating the UMD / require build -- rollup, parcel, webpack, browsers, and some browser-based fiddle / pen editors support modules now. Adding Perhaps the UMD build variant can remain available for a bit and be removed alongside the |
I guess this is implied by the PR, but why does node.js not recognize |
This would indeed be the most elegant solution, but it is of course a breaking change: node versions that do not support es modules will no longer be able to const three = require('three'); My approach would be to at least have the UMD build still available somewhere. In that case it needs to have a const three = require('three/build/three.cjs'); @donmccurdy This is how esm works on Node: It can't use the |
I am opposed to single .mjs file. It reduces the loading time, but also makes it difficult for bundlers to perform tree-shaking (see #16059 (comment)). Here are the factors you have to take into consideration when solving this issue:
My idea to solve this problem
Since node recognizes But this solution is too aggressive and breaks the backward compatibility greatly, I'm not sure whether it can be accepted. |
Thanks for the explanation, @sebamarynissen. @gkjohnson @yushijinhun I'm having a hard time justifying giving up CommonJS/UMD support on the We'll probably eventually have to drop the |
@yushijinhun Is there any reason you don't want to use conditional exports here? Conditional exports will behave the same way:
The only drawback I see with this approach is that conditional exports is currently still experimental, so on Node 13.7 you will get a warning of it, even if you just const three = require('three'); A future problem with this approach might be that if bundlers would ever take into account conditional exports as well and have it take precedence over the |
@sebamarynissen As far as I know, no bundler is going to support conditional exports. So currently, we are safe. Your idea does have a better backward compatibility, but as is mentioned in Node.js documentation, dual CommonJS/ES Module package is really a "hazard". Dropping UMD support might be aggressive, but it reduces the complexity and avoids some potential problems. Anyway, it's up to the project director to decide which approach to take. |
One issue with the current implementation is that it potentially creates two separate cache entries (esm + cjs). If three.js is imported and required you will create two instances which can cause super hard to debug errors. More info about this hazard is in the NodeJS docs https://nodejs.org/dist/latest-v13.x/docs/api/esm.html#esm_dual_package_hazard The "wrapper" pattern is one that you could use in three that could avoid the roll-up step https://nodejs.org/dist/latest-v13.x/docs/api/esm.html#esm_approach_1_use_an_es_module_wrapper |
About examples (#17482 (comment))Another problem is that currently we cannot use bare module specifiers ( three.js/examples/jsm/controls/OrbitControls.js Lines 10 to 18 in 03cad1e
Using import-maps may solve this problem. |
@MylesBorins Perhaps that I can elaborate a bit on why I went with the ES module wrapper approach in chaijs/chai#1317, but not here. The main reason is that chai is not yet written using esm, but three.js is. As such it felt "natural" to not depend on the cjs build after all. But as you say, this indeed introduces the "dual package" hazard. Also, Chai only has a limited amount of what could be considered as named exports. Three.js has quite a lot of them which may make it harder to keep the esm wrapper file for node in sync with what three.js exports. Perhaps I can clarify a bit what this wrapper file approach would look like for three.js for people not familiar with it: // three.mjs
import three from '.';
const { BufferGeometry, Vector3, And, Everything, Three, Exports } = three;
export { BufferGeometry, Vector3, And, Everything, Three, Exports };
// package.json
{
"exports": {
"require": "./build/three.js",
"import": "./three.mjs"
}
} |
One thing to mention, which was figured out in today's Node.js modules call. With the following pattern there will be no warning when requireing the package (there will still be a warning for ESM) {
"exports": {
"import": "./three.mjs",
"default": "./build/three.js",
}
} This is because the default export is not part of the conditional exports feature and does not trigger the warning. With that being said I am unsure that is an accurate representation of what would theoretically be the default. |
Thanks, I will publish a new version tomorrow. A warning for ESM is fine because ESM itself is still experimental anyway. The warning when requiring was a bit cumbersome though. Glad to hear it can be solved! |
Currently, to fix it, i'm doing: import * as THREECJS from 'three'
import * as THREE from 'three/build/three.module'
Object.assign(THREE, THREECJS) This is the only way to run addons like ReactAreaLights in cjs, many others are broken as well (basically every add-on that relies on namespace mutation, like extending shaderlib etc). If we could remove cjs completely (or at least remove the entry in package.json) that would be such a relief. It would be no problem at all when examples import from |
Tested this out recently, and we could get around it by including an import map polyfill in each example page: https://github.com/KhronosGroup/KTX-Software/pull/172/files. I don't know when/if import maps will come to browsers natively, though. |
in node , i want to use gltfloader.js : but error : |
@cazen5050 Not sure if this really belongs here, but the reason is that Request is a browser API and hence not available in Node. You can polyfill it like this (though I did not test it). import fetch, { Headers, Request, Response } from 'node-fetch';
if (!globalThis.fetch) {
Object.assign(globalThis, {
fetch,
Headers,
Request,
Response,
});
} Also, apparently Node 18 will include this natively (see also https://www.stefanjudis.com/notes/global-fetch-landed-in-node-18/). |
What changes are included in this PR?
Since 13.6 node is able to load esm without the
--experimental-modules
flag, and since 13.7 node supports conditional exports unflagged as well.I modified the build config to support importing three.js from node as an ES module. As such code like
becomes possible on node. Without this only
is possible on node, which is a pity if you've written all your code to use named exports.
This approach implies that an additional build file is created called
three.mjs
. In theory we could use thethree.module.js
file as well, but there are some problems with this. Because there's nopackage.json
with"type": "module"
in the build folder, node will interpretthree.module.js
as a cjs module. There are a few imaginable solutions for this:three.module.js
tothree.module.mjs
and change"module": "build/three.module.js"
to"module": "build/three.module.mjs"
. This breaks all code explicitly depending on the file though.package.json
file with{ "type": "module" }
in the build folder. This however would cause node to interpret all.js
files in/build
as an ES module, which has the same drawbacks if someone on node explicitly depends onconst three = require('three/build/three.js');
Given that both solutions aren't ideal, I went with the option to simply create an entire new build, which is a duplicate though of
three.module.js
.Why does one need to import three.js on node?
Obviously three.js is meant to be run in a browser, but it may be useful to be able to run it on node as well for the sake of unit testing. As such one can run unit tests without a transpilation step, especially since mocha experimentally supports esm as well.