Simplest possible TypeScript project setup

Hi all,

I’m trying to transition into using Babylon with TypeScript on a project. I’ve previously used it with JavaScript, but I’d like the stronger typing.

I took a look at this tutorial page:

https://doc.babylonjs.com/setup/frameworkPackages/es6Support

… and it looked almost like what I want, but I have no interest in using webpack etc. I just want a simple starting point for a plain old TypeScript project that uses Babylon.

Nonetheless, I thought that tutorial might make a good starting point! So I created a new directory, in which:

$ npm install -D typescript
$ npm install @babylonjs/core @babylonjs/materials
$ mkdir src

I then copied the example HTML and tsconfig.json files from the tutorial page into the directory, and copied the example TypeScript file from the tutorial page into src.

Let’s see what happens then we run tsc:

$ tsc
node_modules/@babylonjs/core/Engines/engine.d.ts:1216:29 - error TS2552: Cannot find name 'ReadonlySet'. Did you mean 'Readonly'?

1216 type GPUSupportedFeatures = ReadonlySet<string>;
                                 ~~~~~~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:1218:29 - error TS2552: Cannot find name 'ReadonlySet'. Did you mean 'Readonly'?

1218 type WGSLLanguageFeatures = ReadonlySet<string>;
                                 ~~~~~~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2497:5 - error TS1169: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.

2497     [Symbol.iterator](): IterableIterator<XRInputSource>;
         ~~~~~~~~~~~~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2497:6 - error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.

2497     [Symbol.iterator](): IterableIterator<XRInputSource>;
          ~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2497:26 - error TS2304: Cannot find name 'IterableIterator'.

2497     [Symbol.iterator](): IterableIterator<XRInputSource>;
                              ~~~~~~~~~~~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2502:16 - error TS2304: Cannot find name 'IterableIterator'.

2502     entries(): IterableIterator<[number, XRInputSource]>;
                    ~~~~~~~~~~~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2503:13 - error TS2304: Cannot find name 'IterableIterator'.

2503     keys(): IterableIterator<number>;
                 ~~~~~~~~~~~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2504:15 - error TS2304: Cannot find name 'IterableIterator'.

2504     values(): IterableIterator<XRInputSource>;
                   ~~~~~~~~~~~~~~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2766:20 - error TS2583: Cannot find name 'Set'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

2766 type XRAnchorSet = Set<XRAnchor>;
                        ~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2851:19 - error TS2583: Cannot find name 'Set'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

2851 type XRPlaneSet = Set<XRPlane>;
                       ~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2917:26 - error TS2583: Cannot find name 'Map'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

2917 interface XRHand extends Map<XRHandJoint, XRJointSpace> {
                              ~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:2950:31 - error TS2583: Cannot find name 'Map'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

2950 abstract class XRHand extends Map<XRHandJoint, XRJointSpace> implements XRHand {}
                                   ~~~

node_modules/@babylonjs/core/Engines/engine.d.ts:3467:18 - error TS2583: Cannot find name 'Set'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

3467 type XRMeshSet = Set<XRMesh>;
                      ~~~

node_modules/@babylonjs/core/Lights/light.d.ts:240:33 - error TS2583: Cannot find name 'Map'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

240     _shadowGenerators: Nullable<Map<Nullable<Camera>, IShadowGenerator>>;
                                    ~~~

node_modules/@babylonjs/core/Lights/light.d.ts:325:37 - error TS2583: Cannot find name 'Map'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

325     getShadowGenerators(): Nullable<Map<Nullable<Camera>, IShadowGenerator>>;
                                        ~~~

node_modules/@babylonjs/core/Misc/coroutine.d.ts:6:38 - error TS2583: Cannot find name 'Iterator'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

6 type CoroutineBase<TStep, TReturn> = Iterator<TStep, TReturn, void> & IterableIterator<TStep>;
                                       ~~~~~~~~

node_modules/@babylonjs/core/Misc/coroutine.d.ts:6:71 - error TS2304: Cannot find name 'IterableIterator'.

6 type CoroutineBase<TStep, TReturn> = Iterator<TStep, TReturn, void> & IterableIterator<TStep>;
                                                                        ~~~~~~~~~~~~~~~~

node_modules/@babylonjs/core/Misc/coroutine.d.ts:12:32 - error TS2304: Cannot find name 'IteratorResult'.

12 export type CoroutineStep<T> = IteratorResult<void, T>;
                                  ~~~~~~~~~~~~~~

node_modules/@babylonjs/core/Misc/PerformanceViewer/performanceViewerCollector.d.ts:47:45 - error TS2583: Cannot find name 'Map'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

47     readonly metadataObservable: Observable<Map<string, IPerfMetadata>>;
                                               ~~~


Found 19 errors in 4 files.

Errors  Files
    13  node_modules/@babylonjs/core/Engines/engine.d.ts:1216
     2  node_modules/@babylonjs/core/Lights/light.d.ts:240
     3  node_modules/@babylonjs/core/Misc/coroutine.d.ts:6
     1  node_modules/@babylonjs/core/Misc/PerformanceViewer/performanceViewerCollector.d.ts:47

Wow, that’s … not good. 19 errors? Looks like it may be a problem with the TypeScript compiler setup? I wonder what happens if I delete the tsconfig.json file and run tsc again after generating a “clean” tsconfig.json?

$ rm tsconfig.json 
$ tsc --init

Created a new tsconfig.json with:                                                                                       
                                                                                                                     TS 
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig
$ tsc
$

Great, compiled with no errors! After changing the script path in the HTML file from the example page, I load the webpage and get … nothing. The JavaScript log says:

ReferenceError: Can't find variable: exports

Hmm. Maybe if I change the script type in the HTML file to “module”? Nope, that didn’t work.

Can anyone tell me how to get this working? I have no interest in additional junk like webpack, I’d just like to know the correct TypeScript compiler settings / HTML file additions to make this simple example work as expected.

Thanks! :slight_smile:

Changing tsconfig.json to:

{
    "compilerOptions": {
        "module": "system",
        "target": "es2015",
        "moduleResolution": "node",
    }
}

as per saurabhstc’s post seems to fix the “ReferenceError: Can’t find variable: exports” problem, but I now get:

ReferenceError: Can't find variable: System

in the JavaScript console, so I guess I’m not including some required script or other in advance of my own? I tried putting

<script src="https://cdn.babylonjs.com/babylon.js"></script>

in the HTML header, but no luck so far.

Bad news first: It slightly sucks.

The relevant background info is that there are Javascript and Typescript. Typescript needs to be compiled down to Javascript before it can run in the browser. That’s what the Typescript compiler can do.

The other relevant bit is modules and dependencies. These days, ES modules are the standard way of dealing with them, and npm is the standard package manager. Frustratingly, this part has lots of quirks and weirdness.

From what I know, and from what I can find online, Typescript will not deal with compiling libraries from node_modules. But okay, no problem, after all they should already be fully functional Javascript. And we just need to import them as ES modules, which the browser understands.

So we write
import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera";
No dice. The browser has no idea where @babylonjs/core could be. So we whip out import maps and tell it “look in the node_modules folder”.

<script type="importmap">
  {
    "imports": {
      "@babylonjs/core/": "./node_modules/@babylonjs/core/",
      "@babylonjs/loaders/": "./node_modules/@babylonjs/loaders/",
      "@babylonjs/materials/": "./node_modules/@babylonjs/materials/"
    }
  }
</script>

Lovely, this should work.
Nope, the browser is still horribly confused.
import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera";
does not include a file extension. So while Typescript automatically figures out what to look at, the browser does not.

Okay, we begrudgingly accept that and write
import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera.js";

And with a bit of fiddling, and using the latest standards to simplify it as much as possible, we get this barebones setup
a.zip (2.9 KB)

I think it works. I don’t know if the sphere should be black, but I’ll take it.

And finally, my future recommendation: Once you feel like you’ve learned enough, and want to go a step further, I do recommend looking at bundlers. They solve the problem of “taking your code, and giving you something that you can actually put on a HTTP server”. And you get a few other niceties, like “hot reloading”, “stuff just working, even when a library has a terribly cursed setup”, …

As for bundlers, the currently nicest one is Vite. It’s relatively simple to set up, and has sensible defaults, unlike some others.

1 Like

Thank you, @stefnotch!

That was possibly the most useful response to a tech question I’ve ever received. Kudos to you!

I’ll definitely look into bundlers moving forward, but I was trying my best to keep things as simple as possible to start with …

1 Like

Thank you! The compliment is very appreciated, it’s rare to get such a nice compliment.

And I’m glad that I could dig up a decent solution for you.

Quick question:

You added the “loaders” path to the imports section of the HTML; I can’t seem to get the loaders to work.

HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Local Development</title>

    <style>
      html,
      body {
        width: 100%;
        height: 100%;
        padding: 0;
        margin: 0;
        overflow: hidden;
      }

      #renderCanvas {
        width: 100%;
        height: 100%;
        display: block;
        font-size: 0;
      }
    </style>

    <script type="importmap">
      {
        "imports": {
          "@babylonjs/core/": "./node_modules/@babylonjs/core/",
          "@babylonjs/loaders/": "./node_modules/@babylonjs/loaders/",
          "@babylonjs/materials/": "./node_modules/@babylonjs/materials/"
        }
      }
    </script>
  </head>

  <body>
    <canvas id="renderCanvas" touch-action="none"></canvas>

    <script type="module" src="build/index.js"></script>
  </body>
</html>

TypeScript:

/*
Setup:
$ npm install -D typescript
$ npm install @babylonjs/core @babylonjs/materials @babylonjs/loaders
*/

import { Vector3 } from "@babylonjs/core/Maths/math.vector.js";
import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera.js";
import { HemisphericLight } from "@babylonjs/core/Lights/hemisphericLight.js";
import { Scene } from "@babylonjs/core/scene.js";
import { Engine } from "@babylonjs/core/Engines/engine.js";
import { AbstractMesh } from "@babylonjs/core";
import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader.js";

// Neither of these seem to help.
//import "@babylonjs/loaders";
//import "@babylonjs/loaders/OBJ/objFileLoader";

function assert (condition: unknown, msg?: string): asserts condition {
  if (!condition) throw new Error(msg);
}

async function loadMeshes(scene: Scene, path: string): Promise<AbstractMesh[]|null> {
  console.log(`Loading ${path}`);
  let promise = SceneLoader.ImportMeshAsync(null, path, undefined, scene)
  .then( (result: any) => {
    console.log(`Finished loading ${path}`);
    return result.meshes;
  })
  .catch( () => {
    console.log(`Error loading ${path}`);
    return null;
  });
  return promise;
}

const canvas = document.getElementById("renderCanvas");
assert(canvas instanceof HTMLCanvasElement);

const engine = new Engine(canvas);
var scene = new Scene(engine);
var camera = new FreeCamera("camera1", new Vector3(0, 5, -10), scene);

camera.setTarget(Vector3.Zero());
camera.attachControl(canvas, true);

var light = new HemisphericLight("light1", new Vector3(0, 1, 0), scene);
light.intensity = 0.7;

engine.runRenderLoop(() => {
  scene.render();
});

// Attempt to load an .obj file ...
loadMeshes(scene, 'test.obj');

.obj file (“test.obj”, a simple cube):

v 0.000000 2.000000 2.000000
v 0.000000 0.000000 2.000000
v 2.000000 0.000000 2.000000
v 2.000000 2.000000 2.000000
v 0.000000 2.000000 0.000000
v 0.000000 0.000000 0.000000
v 2.000000 0.000000 0.000000
v 2.000000 2.000000 0.000000
f 1 2 3 4
f 8 7 6 5
f 4 3 7 8
f 5 1 4 8
f 5 6 2 1
f 2 6 7 3

The compiled script runs fine, but produces an error for the .obj load:

[Log] BJS - [09:00:20]: Babylon.js v7.0.0 - WebGL2 - Parallel shader compilation (logger.js, line 52)
[Log] Loading test.obj (index.js, line 20)
[Warning] BJS - [09:00:20]: Unable to find a plugin to load .obj files. Trying to use .babylon default plugin. To load from a specific filetype (eg. gltf) see: https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes (logger.js, line 52)
[Log] Error loading test.obj (index.js, line 27)

I’m using the same tsconfig.json as @stefnotch provided earlier. All source files attached to this post, hopefully.

This code works fine when I’ve used it before in plain JavaScript, so my guess is I have another path issue somewhere and some handler code inside Babylon can’t find the appropriate code module to load/use?

Thanks!

loader_problems.zip (3.7 KB)

Okay, responding to my own question here.

If I add either:

import "@babylonjs/loaders/index.js";

or

import "@babylonjs/loaders/OBJ/index.js";

… the code seems to work (I think the latter is more specific if you’re only using .obj files, to keep data transfer as low as possible).

This is not, however, what the es6 docs say about the matter:

Loaders

In Babylon.js the loaders you can install from @babylonjs/loaders are actually plugins of the main SceneLoader module. In order to use for instance the obj loader in your app, you simply need to import it for side effects only: import "@babylonjs/loaders/OBJ"; . It would be exactly the same for glTF: import "@babylonjs/loaders/glTF"; .

Looks like we need the explicit inclusion of the index.js file(s) when using “vanilla” TypeScript for this.

This is really frustrating, but I hope the resultant discussion here is of value to anyone searching the intawebz about these issues.

1 Like

cc @RaananW

@jgrime Here’s a repo where I tried to create the simplest TypeScript project setup:

regnaio/bjs-ts-browser

Source code is from Babylon.js ES6 support with Tree Shaking. Also, to avoid needing .js file extensions in imports, this link also mentions using:

{
    "compilerOptions": {
        "module": "esNext",
        "target": "es5",
        "moduleResolution": "node",
    }
}

Ok!

I’m back.

Trying to understand exactly how my input can move the topic further (and I do notice it was already marked as resolved).

Module resolution is different in different scenarios/configurations, and also depends on your environment, and build tools.

I love pure esm, and will always push towards that, which means - yes, adding a .js, and importing from index.js instead. But the legacy users (and the default behavior ATM) does not require it.
If there is anything specific I can help with and I missed it in the conversation, please let me know :slight_smile:

1 Like