ES6 modules + other THREE libraries

So, I’ve already searched through such topic and I’m putting my hands on ES6 modules.
As far as I understand, THREE will deprecate non-ES6 modules approaches in the near future (?), so that’s a good reason to embrace this model soon. Although I really miss the days where you simply had to add “somelib.min.js” straightforward without additional steps.

I’m wrapping my head around webpack and stuff to pack everything together, following this boilerplate (https://github.com/paulmg/ThreeJS-Webpack-ES6-Boilerplate).

My first question is, what’s the best approach when you have to combine THREE core functionalities with specific loaders (e.g. glTF with Draco) and other THREE-based libraries? (E.g.: lets suppose THREE mesh UI - https://github.com/felixmariotto/three-mesh-ui) into a single, minified bundle.js ?

My second question is there a way to test out without webpack?
For instance I’m currently testing js modules straight from npm like this (it’s working):

import * as THREE from "./node_modules/three/build/three.module.js";
import { OrbitControls } from "./node_modules/three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "./node_modules/three/examples/jsm/loaders/GLTFLoader.js";
...

but if I add (another npm package downloaded in node_modules):

import { ThreeMeshUI } from './node_modules/three-mesh-ui/src/three-mesh-ui.js';

it throws Uncaught TypeError: Failed to resolve module specifier "three/src/core/Object3D.js". Relative references must start with either "/", "./", or "../". (I suppose it’s looking into the wrong path)

Do I have to forcefully use webpack (or similar tools) in order to pack together additional libs?
If so, I’d like to configure a setup where I produce a bundle with all that stuff but excluding my app, in order to decouple main application and build different apps on top of that bundle.js (hope I’m clear enough)

What is your approach in these cases?
Are there some best practices?

At then end of 2020, the examples/js directory will be removed so classes like OrbitControls or GLTFLoader will only be available as ES6 modules.

Notice that you don’t need a build tool or npm to use modules. You can import ES6 modules directly in HTML pages like so:

<script type="module">

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.118.3/build/three.module.js';
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.118.3/examples/jsm/OrbitControls.js';

</script>

I would also say that importing modules like so is the preferred approach for testing or learning.

What is your motivation behind this? TBH, sounds a it uncommon to do this separation.

I’m not familiar with Webpack but some beginners found the following start project helpful. It’s a minimal three.js project setup using ES6 modules and rollup: https://github.com/Mugen87/three-jsm

2 Likes

Yes I know. In fact for testing I was able to import from both npm packages and local folder.
A minified/bundled js would come handy when probably going to production (but one step at the time)

My problem right now is that if I import external THREE-based libraries like ThreeMeshUI

import { ThreeMeshUI } from './node_modules/three-mesh-ui/src/three-mesh-ui.js';

it cannot resolve or find THREE components. So my question is how to test it without using webpack/rollup, etc…?

For instance when creating a library (eg. “MYLIB”) based on THREE. So basically a bundle that packs several features you can load to build your own app on top of that, like:

<script src="build/mylib-bundle.js"></script>
<script>
// note: this code will vary (different apps with different requirments)

MYLIB.config(....);
// do stuff using MYLIB available methods/APIs
MYLIB.run();
</script>

Hope is more clear

This is a problem I’ve struggled with too. I’m not sure how to resolve it. ThreeMeshUI imports like this internally:

import { Object3D } from 'three/src/core/Object3D.js';

Arguably it should be:

import { Object3D } from 'three/build/three.module.js';

However, that wouldn’t help matters here.

I don’t know of any way to use this library (or any library that uses NPM style three.js imports) without using a build tool.

I’ve also struggled with this from the point of view of the person authoring the extension. How can we write an extension that imports three.js components in such a way that a user can use it with or without a build tool?

you can’t use additional libs unless they don’t have dependencies, plain and simple. it is still questionable if the browser esm spec will ever turn out to be a standard that people choose to employ, but right now it’s arguably not so useful because it cuts you off from javascripts eco system, which is npm. the reason you can use three/jsm at all is because three breaks node resolution.

just use tools that are easier instead. webpack is hard to set up. forget boilerplate. but something like parcel for instance can get you going in a second. there’s also codesandbox, which allows you to open fully working projects in the browser. you can download them and run them locally.

1 Like

Agreed with @drcmda — if you’re pulling libraries from npm, with very few exceptions you’ll need to use a bundler. three.js itself is an exception — because it has no dependencies — but packages depending on three.js are not. There are some bundlers that may be easier to set up than Webpack: https://www.snowpack.dev/ and https://parceljs.org/ come to mind. I think you’ll find it worthwhile to experiment and find a bundler that suits your needs.

1 Like

Damn, I was hoping someone had a magic solution to this that I couldn’t see.
Do you think import maps would work in the future?

And while we’re on the topic, am I right in thinking the way ThreeMeshUI currently imports from three/src is not best practice? Rather than this:

import { Object3D } from 'three/src/core/Object3D.js';

It should be:

import { Object3D } from 'three';

Otherwise people will end up with two copies of three.js in their bundle. /ping @felixmariotto

Do you think import maps would work in the future?

Maybe, but you need to include dependencies of dependencies of dependencies in your import maps. Which automated tooling could solve, but then you’re back to a bundler-shaped tool. :slight_smile:

1 Like

Good catch… yeah, that’s like to cause problems if (a) the user doesn’t do the same, or (b) the user includes anything from the examples/jsm/* folder. Perhaps it’s meant to enable tree-shaking, but the better solution there is to continue pushing toward a tree-shakeable ‘three’ package.

2 Likes

@looeee hanks for the heads up :blush:
As @donmccurdy pointed out, it was an (unsuccessful) attempt to enable tree-shaking, because ThreeMeshUI doesn’t use the renderer but still get the full three.js lib included in its bundle.

Anyway I’ve just reverted this to :

import { Object3D } from 'three';

@phxbf It’s unlikely the cause of your problem importing ThreeMeshUI though, as others wrote before you must use a bundler ( not necessarily Webpack ) to import it from npm. The other solution is to import three-mesh-ui/dist/three-mesh-ui.js, which can even be done with a script tag like so :

<script src='https://unpkg.com/three-mesh-ui/dist/three-mesh-ui.js'></script>

But beware that the latter option includes the whole three.js lib, so the package is fat ( ~650kb )

Thank you all for hints and suggestions.

So far I tried this (very rough) parcel setup using a single js file (“mybundle.js”) alongside node_modules/ folder:

import * as THREE from "three";

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import * as ThreeMeshUI from 'three-mesh-ui';
/* other imports here...*/

window.THREE = THREE;
window.OrbitControls = OrbitControls;
window.ThreeMeshUI = ThreeMeshUI;
/*...*/

then I performed:

parcel build mybundle.js -d dist/

that builds mybundle.js in dist output folder, containing all THREE related stuff, including in this case also functionalities from the ThreeMeshUI library. In the testing html page then I just added:

<script type="text/javascript" src="dist/mybundle.js"></script>

Is that correct?
From a quick test seems everything is found from THREE and ThreeMeshUI.
Are there cleaner approaches?

1 Like

I think it looks fine but bundling is an occult science so I will let better informed people tell you if it’s optimal.

it should be easier, and i don’t think it’s correct that you change stuff within /dist, it should be automatic. best you follow the instructions: https://parceljs.org/

never used parcel before but just tried it, i got it up and running quickly: https://github.com/drcmda/threeparcel

that’s a full dev environment with hot module reload, you can install dependencies (npm install three) so no more copying files around, and it’s production ready with minification and such. i think i’ll use parcel more often now. :slight_smile:

the commands are

parcel index.html which starts the dev server, you can now edit and save and it will automatically refresh.
parcel build index.html which creates a /dist folder.

you usually add these commands to your package.json (which you get once you type npm init inside your empty folder.

PS, the window.THREE = THREE; stuff is also curious. in a module environment you don’t need globals. if something relies on a global like that it points to a mistake.

One note on parcel is that you have to tell it what to do with static files like models: https://github.com/donmccurdy/three-gltf-viewer/blob/6ffca05fe6032c436606351c083e4cf7255b9994/package.json#L57-L63 … this uses the parcel-plugin-static-files-copy plugin.

1 Like

One thing I don’t like about Parcel is that it tries to control the whole app structure including assets when all I want is to bundle JS. I found myself fighting against it, which I think is what @phxbf is experiencing.

Recently I’ve started to use ESBuild which is more similar to Rollup but with the simplicity of Parcel. It’s a very new project but it seems to work well already, and it’s extremely fast.

1 Like

One thing I don’t like about Parcel is that it tries to control the whole app structure including assets when all I want is to bundle JS.

Yeah, if you only want to bundle JS then there are simpler options. I would use Rollup or Microbundle when writing a library, for instance. But if the thing I’m building is actually a web app, I tend to find that a bundler designed for building web apps has advantages (live reload, built-in web server, better sourcemap support, etc.). Definitely some personal preference in that.

You can use a public folder like this https://github.com/parcel-bundler/parcel/issues/1080#issuecomment-633119160 but generally it is better to always import, moving away from the sentiment that only JS should be imported. This allows the bundler to handle assets in a safe and managed way, gives you cache control and build time errors should a file not be present.

Like don said, there are tools for building libraries that you want to publish, rollup and I guess esbuild, and there are tools for building frontend applications that have different features and requirements. All frontend tools come with a dev server and hot module reload. Imagine if you had to create a dist and refresh the browser for each change while coding/debugging.

I’ve received some personal feedback from beginners (in JS and three.js) that Parcel is somewhat overwhelming. A simple setup based on rollup and serve was preferred by most of these users.

2 Likes

Thanks, I just used “dist” as sample output folder in my project to test out parcel with a sample web page. I’ll have a look to all options offered by parcel.
So far it does the job (bundling together a custom build of THREE including external libraries)

I’m in this scenario with a test page loading the generated (and minified) mybundle.js:

<script type="text/javascript" src="dist/mybundle.js"></script>
<script>
let v = new THREE.Vector3(0,0,1); // sample call
// do stuff using functions from mybundle.js
</script>

if I remove the window.THREE = THREE; from the parcel setup, when opening the test page it throws Uncaught ReferenceError: THREE is not defined - whenever you call any method/function from THREE (see second <script>).

I will also test other proposed solutions like rollup and esbuild. So far anyway parcel seems to do the job for my specific scenario.