three.js: ReactAreaLights do not seem to work in a module bundler

Description of the problem

basically, area lights do not work with npm/node module resolution, see:

simplified: https://codesandbox.io/s/three-fibre-userender-test-rohv5

raw threejs arealight demo: https://codesandbox.io/s/dreamy-platform-3jpim

why

all files under examples/jsm pull from ../../../build/three.module.js instead of three, which is arguably not correct. it will pull threejs two times in bundling systems that do not follow package.json:module (but “main” instead, which leads to build/three.js). it then creates two separate namespaces. uniforms.init() ends up writing to the wrong one.

solution

this is how a typical jsm file should look:

import {
	ClampToEdgeWrapping,
	DataTexture,
	FloatType,
	LinearFilter,
	NearestFilter,
	RGBAFormat,
	ShaderLib,
	UVMapping,
	UniformsLib
} from "three";

that’s how any other library works. i have never seen library that relies on a distro file - this has obvious pitfalls. i understand that this was made to make the html demos work, but imo this is a hack, and it now impacts production usage.

Three.js version
  • r107
Browser
  • Chrome
OS
  • macOS

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 50 (29 by maintainers)

Most upvoted comments

I’m sure this is what you meant but just to be clear the cjs examples should be able to be used universally when bundling three.js in an application with any of these tools.

Well, yes, if you tell a tool to import a cjs file then it should do that. It’s also fine for tools to import cjs using import * as - it seems like that’s violation of the spec, but it’s very convenient and common.

But if there is ambiguity, then a tool should always default to the es6 module when using import and always default to cjs when using require. For example, import * as THREE from 'three' is ambiguous since it’s not telling the bundler exactly what file to use. But it’s using import so it should always choose three.module.js if available. If you want cjs via an import statement then you should have to specify it using import * as THREE from "three/build/three".

If a bundler doesn’t do this then you have a situation where this:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

which is 100% correct es6 code, leads to wrong results. And that’s just crazy and frustrating.

The only thing I’d say is ‘wrong’ here is when a person or bundler uses cjs and es6 from the same npm package. Of course that’s still a relative statement and if it works in your project then great. But doing that is what’s causing all the confusion here. Since codesandbox is not a private app then it seems like they should be more responsible and at least warn people they are importing cjs even though an es6 module is available.

I’m using webpack with babel, react, three.js, and a variety of three.js example modules and I’m not seeing a any duplicate threejs bundles included. I set up two small example builds using rollup and parcel and both seemed to work just fine, as well.

if it would follow “main” or fetch three from source, you would have two threejs versions in the output and arealights would also not work.

Can you describe the setup you’re using such that the build process is using the build pointed to by package.json “main”? And by fetching from source do you mean pointing to the “src/Three.js” file? My experience is that the bundlers prioritize the “module” field over “main” so I wouldn’t expect that this would be a problem.

I expect that the solution won’t be to immediately change all the example scripts over to import from “three” because that would break quite a few things – maybe we can talk about solutions that afford both use cases instead?

Okay this sounds more like a philosophical discussion on the right / clean way to host and develop a javascript codebase independent of the fact that the current practice here is not breaking any real world projects with bundlers (aside from codesandbox). Am I understanding right?

Node has moved on and understands imports

I don’t think this is a fair claim quite yet. Modules have been behind an experimental flag for years, have recently had their implementation changed and might be released for real in October. If the node.js case is considered worth supporting at all then I think removing the cjs version outright might be a bit hasty. But it sounds like it’s coming sooner than I thought.

I think there have been enough demos though. All of the above highlight the problem. The sandbox picks cjs because it just works that way. The GH repo picks cjs because it can.

I agree that codesandbox illustrates the issue as we’ve discussed it but your github demo does not. Here’s why.

import * as THREE from "three/build/three"

This line subverts modules resolution which is the core of this discussion and therefore does not illustrate the problem. None of the three.js documention or standard use of the library support or advocate for the use of this import statement. Likewise someone could download their own version of three.js and import that as well as an example from this project and it would break in the same way.

None of the proposed solutions solve this case. That repo would break in the same way even after changing the example modules to resolve from three because the bundlers will resolve three/build/three to a different file from three. The same thing will happen if you change the main field in package.json. Do you understand my confusion? You’ve presented something as a repro case but none of the solutions you’re happy with actually solve it so it cannot really be considered a repo case of the issue.

I’m looking for a repro of a bundler setup that illustrates importing from three and an example file but results in two versions of three.js included in the page. It was claimed previously that many projects may be inadvertently including two versions of three.js in the page if they use examples. How can this happen on accident? What would the bundler config look like?

If we’re actually just talking about the codesandbox case then that’s fine and @mrdoob’s proposed solution will solve it just fine.

A small zip project demonstrating the loading problem when using DracoLoader, for example, would be fantastic. Without something like this I’m not sure how any of us can be expected to sympathetically discuss this.

I think there have been enough demos though. All of the above highlight the problem. The sandbox picks cjs because it just works that way. The GH repo picks cjs because it can. The reason why anyone would want to use cjs doesn’t matter. Now two three versions are loaded by the browser, which must naturally clash because three mutates its own namespace.

IMO we don’t have to test that against each and every thing, dracoloader etc, b/c when three adds any new information to its module-namespace it just won’t be accessible in the cjs namespace. This is a clear violation of npm/node resolution, and i’m quite sure that among the millions of libs on npm three is probably the only one doing this.

But thankfully cjs is an obsolete concept, so it’s not that hard to fix. Node has moved on and understands imports, all bundlers do, require could be marked obsolete here. And as we see from all these examples, in a way it already is. Removing it at least from package.json would be a good compromise, as it doesn’t upset anything else (one could hope).

The real fix

… would be to change everything in three/examples to import {...} from 'three'. This would break the html demos. But as i have said before, you’re trying to cater to a standard that does not exist. Standard modules lack resolution, so they are a 100% reliant on build tools.

We are hacking around that by priming everything to a fixed module, thereby throwing npm under the bus. But standard modules are still useless, because you would not be able to use this in the real world. Every 3rd party lib you want to use has to be transpiled (resolved). Just like you had to transpile jsm to “three.module” to make it work.

I think the es spec should not be used until it’s completely ratified and esp until they have solved the many issues that still plague it.

i think at least module and main could point to the same thing. jsnext was obsolete, and if it’s gone old bundlers would probably go towards main i guess. but any of these variations will work, main only, main and module or all three - pointing to build/three.module

yes, that would work. 👍 now resolution and three/examples point to the same file. you could still distribute the commonjs file so people can use it, but if you don’t declare it in package json, we’re good.

well, not a 100%, because the commonjs module in build would still be incompatible, but cjs is dying and that’s a good compromise i think.

@mrdoob if commonjs is unsupported i would indeed remove it. there would only be one export: build/three.module.js and everything starts working again. i have since bumped into more and more of the same problem in similar circumstances. the dracoloader is broken too for instance - all due to module mismatch.

Are you saying that bundlers are not able to tell that the three/build/three.module.js that three resolves to and the …/…/build/three.module.js that OrbitControls.js import from are the same file?

Exactly. If the bundler picks CJS, which it can, then you have two separate namespaces. There is no resolution happening if three/examples targets a specific module (build/three.module), while the bundle goes through standard package.json resolution and picks up commonjs, it has no chance of resolving the two properly. This is why i said the root issue is with three/examples. That’s what resolution is there for, so that everything is on the same page.

The bundler isn’t the issue. The issue is commonjs. If for whatever reason you ever pick commonjs, three and three/examples are not compatible any longer. This will hold true in any dev environment and all bundlers.

@looeee you keep saying this is a bundler issue, this is false. if three exposes two modules, bundlers are allowed to pick cjs. if that would violate a spec (which it doesn’t), why do you expose cjs at all?

Your new example is incorrect since you are mixing cjs and es6 modules from a single npm package.

// This is a cjs module
import * as THREE from "three/build/three"
// This is an es6 module
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"

Just change that to import * as THREE from "three", as it should be, and it will work fine.

It seems like webpack is smart enough to handle cjs imports even though it’s against the spec. Great. But if you really want to use the cjs version of three then you’ll need to specifically request the cjs versions of the examples too, which you’ll be able to do once #16920 lands.

To be clear, any bundler that encounters 'import * as X from 'package' in an npm package should use the ‘module’ field if present.

If they do otherwise, as codesandbox is doing, then they are incorrect and should expect problems like this one.