Share three bundle across multiple packages

I have a large three js project that I am starting to break into constituent packages. This is to modularise the system so that specialist developers can work in a sandbox like environment and not need to know how the whole engine works, and also to have app state (any stored data generated during the user session) serparated from the core engine.

I’ve run into an issue that the three js version in each bundle isn’t shared across the separate packages. This will obviously add d/l overhard that I’d rather avoid, but worse, if I test instanceof (using typescript and definitely typed three) in one package, testing an object created in another package, it now fails where it would have previously succeeded. (e.g. object.material instance of THREE.Material).

So I think I need the separate packages to share the same three code bundle. I’m building using a monorepo and lerna. Are there any bundler wizards out there who can advise?

At the moment ViteJS is the best option, check this Installation section to get started … As for the versioning, to keep a consistent version across your packages, be specific when installing ThreeJS npm install three@<version>, (latest to this date npm install three@0.153.0
).

To update an existing project, manually edit your package.json

"dependencies": {
     //...
     "three": "0.153.0",
     //...
}

Then run npm install

The same goes for typescript, npm i -D @types/three@0.153.0, and it has to be the same version as the installed ThreeJS.

Thanks @Fennec I am using vite and have consistent versioning across the packages… but the package needs to share the same instance of three.

Consider this structure:

       [package_1]
      /
[core]
      \
       [package_2]

Both package_1 and package_2 require core, and both need to share the same instance/bundle of three for instanceof to work. Is there some way to serve three from [core]? Can you point me to some example of vite config for this or similar?

The answer isn’t straightforward and depends on the specific project. Are package_1 and package_2 related in terms of logic and objects? If so, where do they differ?

A possible solution that I may suggest is creating a separate Class or library that encapsulate all the ThreeJS logic required by both packages. It can also serve as a repository for the ThreeJS objects (renderer, scene, camera, materials …). For example you can expose a utility method like myThreeInstance.isMaterial(myMaterial) to check if an object’s material is an instance of THREE.Material.

I recommend reading this excellent article about “How to write clear and maintainable Three.js code”. Although it may not directly address your specific question, it provides valuable insights.

If you’re developing “packages” in the sense of separate JavaScript modules that can be used by the same or different applications, you may want to consider “peer dependencies”. The idea is that each package depends on three.js, but does not bundle it, and defers to the application to provide a compatible version of three.js.

In general when building a package that should be used with three.js, peer dependencies are a good default. See:

Note that I include a loosely-versioned dependency on “three” in peerDependencies but not in dependencies. Then devDependencies installs a specific version of three.js for use while developing that specific package.

2 Likes

Thank you both for your time and considered answers.

I’ve added peer dependencies as it makes it more explicit that it is expecting Three, but it didn’t solve the issue. I also tried exporting Three from core and it didn’t solve it either. I think I was actually already loading just a single instance of Three (because in hindsight I hadn’t included it as a dependancy in the subsequent packages) and my issue was caused by something else.

Having looked around (cyclic dependencies · Issue #6241 · mrdoob/three.js · GitHub) it feels like using instanceof isn’t exactly recommended and the Three way seems to make use of node.type instead. Switching to this method fixes solves the problem for now.

Rather then go down the rabbit hole I will just use ‘node.type’ in place of instanceof and add peerDependencies.

Thanks both!

Sounds like you have a working solution anyway, but as an aside here – if “three” is in peerDependencies and not dependencies, and your bundler is still packing all of three.js into the subpackage, then something is broken. Most bundlers should be able to handle this, perhaps it needs to be enabled with an option on yours (sometimes called “externals” or “globals”).

Thanks again, and sorry I wasn’t clear; three isn’t being bundled by the subpackage. I think the issue is caused elsewhere.

I see the warning: ‘WARNING: Multiple instances of Three. js being imported.’ three times in the console, this could be a symtom of the same problem, but I’m not sure the cause.

After adding breakpoints it looks like I hit this on the very first import * as THREE from 'three'. _THREE_ already seems set on the Window when I reach the first three import in the package called core.

Possible red herring as it only appears once in the network tab. (Unlike this issue: "Multiple instances of Three.js being imported" from CDN · Issue #21167 · mrdoob/three.js · GitHub).

Fixed properly with a combination of the two suggestions… using peerDependencies and exporting three from the core package.

1 Like