Skip to content

Using Import Maps

Description

Since Threejs r130, when manually importing modules into your scripts, you may encounter the error

Uncaught TypeError: Failed to resolve module specifier "three".
Relative references must start with either "/", "./", or "../".

Your script, or any of its dependencies likely contains a line resembling,

import * as THREE from 'three'

Check the error information in your browsers' developer tools console to find out which script and which line number.

Relative Reference Error

The part of the line that is erroring, contains the word three. This is a named module specifier. If you are using a build tool, such a Webpack, Vite, Rollup, ESBuild, Parcel, etc., then the build tool will resolve this and the generated scripts given to your browser will be bundled and contain the correct references to the Threejs library and other imports. But if you execute this line straight in your browser, it will not understand it. The browser is expecting a relative reference. I.e., something starting with "/", "./", or "../".

So in the case where you aren't using a build tool, but you are importing individual module scripts into your browser either locally hosted, or from a CDN, then you can help your browser to resolve named module specifiers when it encounters them by adding import map information to the HTML describing where it can find the scripts.

"An import map is a JSON object that allows developers to control how the browser resolves module specifiers when importing JavaScript modules. It provides a mapping between the text used as the module specifier in an import statement or import() operator, and the corresponding value that will replace the text when resolving the specifier", from MDN Importmap

Import maps and self-hosted modules

While this website is primarily focused on developing using TypeScript and using a build tool such as Vite and Webpack, many of the pages also contain working examples written in plain JavaScript and are using import maps. You can view the JavaScript source by pressing the <> in any of the working examples.

Some Examples :

If you press the <> on any of the working examples, you will see an importmap resembling,

<script type="importmap">
    {
        "imports": {
            "three": "/build/three.module.js"
        }
    }
</script>

This tells the browser where it can find the three.module.js in case it encounters the module specifier three in any of the import statements.

In the above example links, they all also import other module files, that reference the Threejs library using the named module specifier three.

E.g., the OrbitControls.js itself, contains the instruction,

import {
    EventDispatcher,
    MOUSE,
    Quaternion,
    Spherical,
    TOUCH,
    Vector2,
    Vector3,
    Plane,
    Ray,
    MathUtils,
} from 'three'

Note that if you are using a build tool, such as Vite, Webpack, et al., which is demonstrated in this course, you don't need to use import maps since the build tool generates the scripts with many features, including the correct references to the Threejs library.

Now, my web server for this website allows me to import modules individually in scripts since I have also copied them to web accessible folders. E.g., the three.module.js file is saved under the relative folder named /build/. My web server is also hosting the Threejs addons modules under the /jsm/ folder. You can name your folder structure any way which works for you.

In the first examples above, I imported three.module.js using the named module specifier three, but the OrbitControls and Stats were loaded using relative references. See the / in the URLs below. My web server hosts these files at these relative URLs.

 <script type="module">
    import * as THREE from 'three'
    import { OrbitControls } from '/jsm/controls/OrbitControls.js'
    import Stats from '/jsm/libs/stats.module.js'

    // ... etc

Since there are many ways to use import maps, I could make all the imports use the three module specifier instead.

E.g.,

 <script type="module">
    import * as THREE from 'three'
    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
    import Stats from 'three/examples/jsm/libs/stats.module.js'

    // ... etc

And then the import maps to match my web server configuration would be,

<script type="importmap">
    {
        "imports": {
            "three": "/build/three.module.js",
            "three/examples/": "/"
        }
    }
</script>

And this would also work, although unnecessary to duplicate the jsm/ part.

<script type="importmap">
    {
        "imports": {
            "three": "/build/three.module.js",
            "three/examples/jsm/": "/jsm/"
        }
    }
</script>

Basically in the above examples, the string three/examples/ at the beginning of the import URLs is replaced with /.

My web server returns the files at the relative URLs. E.g., the OrbitControls.js will be downloaded from https://sbcode.net/jsm/controls/OrbitControls.js

The above approach of using named module specifiers for many imports under the same folder structure is demonstrated in the Follow Cam example. See the source code by pressing <> in the working example.

Import maps using the addons alias.

The official Threejs examples do something similar to the above method. Although the module files are hosted using GitHub pages and redirected to the relative ./jsm/ sub folder using the addons alias.

E.g., the Official Orbit Controls example

<script type="importmap">
    {
        "imports": {
            "three": "../build/three.module.js",
            "three/addons/": "./jsm/"
        }
    }
</script>

<script type="module">
    import * as THREE from 'three';

    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    // etc

The string three/addons/ is replaced with ./jsm/ which when run from the Threejs examples website points to https://raw.githubusercontent.com/mrdoob/three.js/master/examples/jsm/controls/OrbitControls.js

Import maps from a CDN

Now, you don't have to host the Threejs library and its addons modules relative to your own website. You can use any number of CDNs.

I often use unpkg.com

E.g.,

<script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.156.0/build/three.module.js",
            "three/addons/": "https://unpkg.com/three@0.156.0/examples/jsm/"
        }
    }
</script>

<script type="module">
    import * as THREE from 'three';
    import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
    import Stats from 'three/addons/libs/stats.module.js';

    // ... etc

Now it is also important to note that addons is an alias set in the import map. In the above example, it replaces the three/addons/ string in the import URLs with the URL https://unpkg.com/three@0.156.0/examples/jsm/

Import maps without aliases.

You don't need to use any aliases if you would rather not.

E.g., removing the addons alias, and just importing using the default folder structure (/examples/jsm/) as found in versions Three r156 and earlier.

<script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.156.0/build/three.module.js",
            "three/": "https://unpkg.com/three@0.156.0/"
        }
    }
</script>

<script type="module">
    import * as THREE from 'three';
    import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
    import Stats from 'three/examples/jsm/libs/stats.module.js';

    // ... etc

And this would also work,

<script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.156.0/build/three.module.js"
        }
    }
</script>

<script type="module">
    import * as THREE from 'three';
    import {OrbitControls} from 'https://unpkg.com/three@0.156.0/examples/jsm/controls/OrbitControls.js';
    import Stats from 'https://unpkg.com/three@0.156.0/examples/jsm/libs/stats.module.js';

    // ... etc

Import maps with custom aliases.

Here is an example of using some custom aliases. Name them what ever you want.

Working Example : Importmaps with Larry, Curly, Moe

<script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.156.0/build/three.module.js",
            "larry": "https://unpkg.com/three@0.156.0/examples/jsm/controls/OrbitControls.js",
            "curly": "https://unpkg.com/three@0.156.0/examples/jsm/libs/stats.module.js",
            "moe": "https://cdn.skypack.dev/dat.gui"
        }
    }
</script>

<script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'larry';
    import Stats from 'curly';
    import { GUI } from 'moe';

    // ... etc

Import maps with Emojis. 😜

And finally below, is an example of using Emojis.

Working Example : Importmaps with Emojis

<script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.156.0/build/three.module.js",
            "😄": "https://unpkg.com/three@0.156.0/examples/jsm/controls/OrbitControls.js",
            "😍": "https://unpkg.com/three@0.156.0/examples/jsm/libs/stats.module.js",
            "💗": "https://cdn.skypack.dev/dat.gui"
        }
    }
</script>

<script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from '😄';
    import Stats from '😍';
    import { GUI } from '💗';

    // ... etc

MDN Importmap

Comments