Using Three.js With Typescript & ES Modules (Without a NodeJS Server)
I was recently experimenting with three.js (once again), and decided it was a good time to start using Typescript to organise my projects better. After all, I’m always greeted with uncertainty on how to structure my JS prototypes, so I thought this would solve a few decisions for me.
The three.js docs suggest you use ES modules for your app code, and don’t have any special instructions for Typescript projects (apart from the tsconfig.json
configuration).
My initial attempt was this:
import * as THREE from 'three';
class App {
constructor()
...
init() {
const scene = new THREE.Scene();
}
}
Running the Typescript compiler produces a very similar looking JS file:
import * as THREE from 'three';
class App {
constructor()
...
init() {
const scene = new THREE.Scene();
}
}
The import
statement at the top doesn’t know how to find the three module under node_modules
, so this won’t work.
My second attempt was to define the module relative to the current path (assuming the typescript file is at the project root).
import * as THREE from './node_modules/three/build/three.module.js'
This gets copied as-is to the JS output, and resolves the ES module correctly. It’s also surprising to me that the Typescript compiler doesn’t complain about including a JS module, but perhaps it pulls the TS definitions automagically.
So far, this looked like a promising solution.
But what happens if your project structure is different, and your Typescript source is in a different folder than the output files?
Hidden costs of relative module imports
In theory, if your folder structure looks something like this:
src/a/app.ts
then you could use the above import method to point to the node_modules directory:
import * as THREE from '../../node_modules/three/build/three.module.js'
Apart from the obvious inconvenience of typing the correct number of dots, this quickly breaks down if your input (TS files) and output (JS files) have different tree depths.
Typescript doesn’t seem to be doing any conversion between module import paths, so at this point I couldn’t figure out any trickery to ensure the paths work both on the Typescript & Javascript side.
Of course, I’ve been also doing some research while trying my luck, to see what other people did to get their Typescript projects running with the three.js modules.
Common solutions on the web
Most three.js + typescript solutions on the web employ one of these two strategies:
- A NodeJS server, that serves the three.js module file at the “correct” (i.e. non-relative) path
- A project using some sort of module bundler
I’m not a big fan of using NodeJS for everything, because:
- I don’t want to write server code, however minimal it is
- I don’t want to diffuse responsibility in multiple parts of my codebase (e.g. a ‘server’ portion, an ‘app’ portion etc.), especially as I’m not a wev dev. Having fewer moving parts almost always makes things simpler.
- It still kind of depends on hardcoding paths for your app
So I went the webpack way.
Bundling the three.js app with Webpack
Up to this point, I was trying to avoid having to learn another tool for my ultra-simple web projects, but here we are.
The way I saw it, is that going through that initial configuration pain would keep me happy in the future, not having to fiddle with my import paths.
In effect, the webpack tool will take something like
import * as THREE from 'three'
and convert it to
import * as THREE from '../node_modules/three/build/three.module.js'
Not only that, but it will bundle your app with the entire three.js library, so you only have to include a single JS file at the end.
My webpack.config.js
is super simple, and I’m sure there are vastly better setups for this, but here goes:
const path = require('path');
module.exports = {
mode: "development",
devtool: 'inline-source-map',
entry: './src/app.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx','.ts','.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
};
Here’s a Github repo that implements the same idea as a boilerplate project: https://github.com/pinqy520/three-typescript-starter
Fin.
I now have a simple & robust(-ish) build system in place, while avoiding having to spin off a server as part of my prototyping work.
Hope the same seems helpful to anyone that is looking for the fastest way to get from code to pixels 😉!