How to make a npm package wrapper

Hey guys, i the help of an npm package expert.

I have a regular javascript library that is normally loaded on the page via a script tag.

Is there a way to create some sort of npm wrapper package to the library can be uses in the react app that uses node modules and some sort of

import * from babylon-toolkit

Anyone wanna help and create a npm wrapper for the Unity Toolkit runtime library

OR

Some guides or tutorials would be great. The first option would be awesome :slight_smile:

Here are some links - cjs-to-es6 - npm
The article about conversion - Upgrade your Node app: From CommonJS to ES Modules | Medium

1 Like

Maybe @RaananW knows some good doc or example ?

1 Like

I can look for a nice tutorial, but without changing the architecture the simplest would be to publish a UMD package and hope the bundler consuming it supports commonjs :slight_smile: as I don’t know of any bundler that doesn’t support commonjs, you are good to go.

I found a very simple and straight forward example here - JavaScript Tutorial => Universal Module Definition (UMD) (riptutorial.com)

This is not the most modern way of solving things, but it will work with most (if not all) environments.

A better way would be to use es modules and export the public members you want to expose from your code, but that requires bigger changes in your code. This tutorial seems to be a very comprehensive guide - The JavaScript Modules Handbook – Complete Guide to ES Modules and Module Bundlers (freecodecamp.org) , but it is quite a read :slight_smile:

Start with UMD and see if you also need es modules support.

1 Like

Yo @RaananW … This is how all my classes in my library are defined:

module PROJECT {
    export class SampleClass  {
...
    }
}

not sure where “module” is coming from, but if you export like that you can simply take something like rollup, provide it with your entry file and compile it to an esm-compatible bundle. Webpack can do it as well for you. It really depends how you want users to consume your package, how you want to publish it (npm? CDN? download?), and what devs you want to support (old-school script-based UMD, or newer ESM).

I am sorry it feels like I am making it more complicated, but any “simple” answer here would result in consequences you might not have considered. This is why I initially suggested UMD - as you are currently just populating the global namespace, moving to UMD is pretty straight forward and doesn’t change too much the way people consume your package. The “import” syntax is anyhow parsed using a bundler, so you could also allow “import” support with UMD. but UMD has its bad sides as well (tree-shaking being one of them).

1 Like

I tried to use rollup, but dont think i am packaging right. Cuase i get an error saying its not a module when i try to use via npm in a react app.

@RaananW can you please help me make a rollup wrapper. I dont think I got the details right
because i cant use in a react app. How would you make the wrapper package to be consumed via npm in a react app ?

Here is what I have so far, but again, does not work

rollup.config.json

import { terser } from "rollup-plugin-terser";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import pkg from "./package.json";

export default {
  input: "src/index.ts",
  output: [
    {
      file: pkg.main,
      format: "cjs",
    },
    {
      file: pkg.module,
      format: "es",
    }
  ],
  plugins: [
    typescript(),
    resolve(),
    commonjs(),
    terser()
  ]
};

package.json

{
  "name": "babylon-toolkit",
  "version": "7.15.10",
  "description": "Babylon Toolkit Unity Exporter Runtime",
  "typings": "index.d.ts",
  "browser": "babylon.toolkit.js",
  "module": "dist/babylon.toolkit.esm.js",
  "main": "dist/babylon.toolkit.cjs.js",
  "scripts": {
    "build": "rollup -c"
  },
  "keywords": [],
  "author": "Mackey Kinard",
  "license": "MIT",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^25.0.7",
    "@rollup/plugin-node-resolve": "^15.2.3",
    "@rollup/plugin-typescript": "^11.1.6",
    "copyfiles": "^2.4.1",
    "rollup": "^2.79.1",
    "rollup-plugin-terser": "^7.0.2",
    "tslib": "^2.6.2",
    "typescript": "^5.3.3"
  },
  "dependencies": {}
}

tsconfig.json

{
    "compilerOptions": {
      "target": "es5",
      "module": "es2015",
      "outDir": "./dist",
      "strict": true,
      "declaration": false,
      "esModuleInterop": true
    },
    "include": ["src"],
    "exclude": ["node_modules", "dist"]
  }

index.d.ts

/// <reference path="babylon.toolkit.d.ts" />
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-unused-vars */
declare module "babylon-toolkit" {
    export = BABYLON.Toolkit;
}

and finally the entry point: index.js

const wnd:any = window;
if (wnd.BABYLON != undefined && wnd.BABYLON != null) {
    // console.warn("BABYLON TOOLKIT: BABYLON Namespace Detected");
} else {
    // console.error("BABYLON TOOLKIT: BABYLON Namespace Not Found");
}

Project file structure

And babylon.toolkit.js and babylon.toolkit.d.ts are the actual library files i am trying to wrap so they can be consumed in a react app

I think there should be a few other things that needs to be changed before you are able to deliver an es-version.

First, yu will need to use the es version of @babylonjs/core and not the global namespace (for the sake of consistency). If anyone is importing your lib, they will also expect you to import babylon correctly. So the entry file is wrong.

I would also not add anything ot the BABYLON namespace in general. Not sure if you are doing it already, but this can be confusing. Don’t work with namespaces. The declaration file should be defined differently, and can be a part of the bundling process.

What you are going through it a similar issue babylon had when we decided to publish es6 and umd. the decision was, eventually, to completely separate them because of a lot of incompatibilities. Declarations in one is totally different than the other. publish/build/bundling is also different. I am 100% sure it is achievable to deliver the two, but it’s not easy and doesn’t really scale.

If you want to share a project with me, or a branch with a few changes, I will be happy to comment on where I think things could be different.

Here is project:

JSTOES6_Project.zip (123.1 KB)

A GitHub link would be better :slight_smile:

You can’t wrap your library like that. All of my suggestions are valid to the project you shared with me. This way of wrapping a library in ESM is wrong - the entire lib should be built with esm in mind, otherwise - stick with UMD. Adding a UMD signature to your package should be simple - keep populating the global namespace, and return the object created to the commonjs signature.

Ok @RaananW … I started over making the package. First step, making my babylon.toolkit.js runtime library fully UMD compatible. I was already using gulp to make my package, i just needed to do the gulp insert.wrap stuff to wrap my lib with UMD signatures as you suggested:

gulpfile.js

// Note: Gulp@^4.0.0 - Required
var gulp = require("gulp");
var typescript = require("gulp-typescript");
var sourcemaps = require("gulp-sourcemaps");
var merge2 = require("merge2");
var concat = require("gulp-concat");
var uglify = require("gulp-uglify");
var insert = require("gulp-insert");

var tsConfig = {
    target: "es5",
    module: "umd",
    lib: [
        "es5",
        "es6",
        "dom",
        "esnext",
        "es2015.promise",
        "es2015.collection",
        "es2015.iterable",
        "es2015.symbol.wellknown",
        "es2020.bigint"
    ],
    moduleResolution: "node",
    declarationFiles: true,
    typescript: require("typescript"),
    experimentalDecorators: true
};

var tsProject = typescript.createProject(tsConfig);

// Note: Script Build Order
var srcfiles = [
    // DOM CONTROLS
    "./temp/dom/WindowManager.js",
    "./temp/dom/InputController.js",
    // BJS CONTROLS
    "./temp/core/babylon-manager.js",
    "./temp/core/babylon-system.js",
    "./temp/core/babylon-shader.js",
    "./temp/core/babylon-scripts.js",
    "./temp/core/babylon-parsing.js",
    "./temp/core/babylon-toolkit.js",
    "./temp/core/babylon-simplex.js",
    // PRO CONTROLS
    "./temp/pro/AnimationState.js",
    "./temp/pro/AudioSource.js",
    "./temp/pro/CharacterController.js",
    "./temp/pro/HavokVehicle.js",
    "./temp/pro/NavigationAgent.js",
    "./temp/pro/RaycastVehicle.js",
    "./temp/pro/RigidbodyPhysics.js",
    "./temp/pro/ShurikenParticles.js",
    "./temp/pro/TerrainGenerator.js",
    "./temp/pro/WebVideoPlayer.js"
];

gulp.task("compile", () => {
    var tsResult = gulp.src(["./types/**/*.ts", "./src/**/*.ts"])
            .pipe(sourcemaps.init())
            .pipe(tsProject());

    return merge2([
        tsResult.dts
            .pipe(concat("babylon.toolkit.d.ts"))
            .pipe(gulp.dest("./dist/")),
        tsResult.js
            .pipe(sourcemaps.write("./", {
                    includeContent:false, 
                    sourceRoot: (filePath) => {
                        return ""; 
                    }
                }))
            .pipe(gulp.dest("./temp/"))
    ]);            
});

gulp.task("default", gulp.series("compile", () => {
    return merge2(gulp.src(srcfiles))
        .pipe(concat("babylon.toolkit.js"))
        .pipe(uglify())        
        .pipe(insert.wrap(`
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['babylonjs'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('babylonjs'));
    } else {
        // Browser globals (root is window)
        root.BABYLON = root.BABYLON || {};
        root.BABYLON.Toolkit = factory(root.BABYLON);
        root.BT = root.BABYLON.Toolkit;
        root.SM = root.BABYLON.Toolkit.SceneManager;
        root.WM = root.BABYLON.Toolkit.WindowManager;
        root.IC = root.BABYLON.Toolkit.InputController;
        root.UTIL = root.BABYLON.Toolkit.Utilities;
    }
}(typeof self !== 'undefined' ? self : this, function (BABYLON) {`, `    
    return BABYLON.Toolkit;
}));
        `))
        .pipe(gulp.dest("./dist/"));
}));

Now my babylon.toolkit.js is properly UMD wrapped looks like this:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['babylonjs'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('babylonjs'));
    } else {
        // Browser globals (root is window)
        root.BABYLON = root.BABYLON || {};
        root.BABYLON.Toolkit = factory(root.BABYLON);
        root.BT = root.BABYLON.Toolkit;
        root.SM = root.BABYLON.Toolkit.SceneManager;
        root.WM = root.BABYLON.Toolkit.WindowManager;
        root.IC = root.BABYLON.Toolkit.InputController;
        root.UTIL = root.BABYLON.Toolkit.Utilities;
    }
}(typeof self !== 'undefined' ? self : this, function (BABYLON) {!function(i){var e;function t(){}e=i.Toolkit||(i.Toolkit={}),t.IsWindows=function(){return"undefined"!=typeof 

  .....

So far the UMD rebuild is working great in browsers, i am working on a npm packaging project for my toolkit… Thanks so much for the help so far :slight_smile:

Yo @RaananW … I am making a reference to BABYLON.GLTF2 in my project. Like:

BABYLON.GLTF2.GLTFLoader
BABYLON.GLTF2.IGLTFLoaderExtension
BABYLON.GLTF2.Loader.IScene
BABYLON.GLTF2.Loader.INode
BABYLON.GLTF2.IMaterial
BABYLON.GLTF2.Loader.IAnimation
BABYLON.GLTF2.ArrayItem
BABYLON.GLTF2.INode
BABYLON.GLTF2.IMesh
BABYLON.GLTF2.IMeshPrimitive

In my library project, I would normally load babylon.GLTF2FileLoader.js with script tags

but when now using in react application . I think i am loading babylonjs, the gltf2 loader and my babylon-toolkit

import * as BABYLON from 'babylonjs'
import 'babylonjs-loaders';
import 'babylon-toolkit

But I keep getting error it cant find GLTFLoader

Cannot read properties of undefined (reading 'GLTFLoader')
TypeError: Cannot read properties of undefined (reading 'GLTFLoader')
    at Object.<anonymous> (http://localhost:3000/static/js/bundle.js:5299:32)

Any ideas ?

A few suggestions, but these are things you will have to research yourself and decide what and how to do them:

  1. you are using outdated tools and outdated methods. Leave gulp - it was deprecated 4 years ago and is no longer maintained. Moved to a more modern framework that supports more modern technologies.
  2. Stop using the global namespace. Babylon uses it only for the sake of backwards compatibility. Don’t relay on global namespace changes. Import everything you need and use the imported classes/objects directlyy.
  3. Don’t use the UMD babylon version. use the es6 version.

There are many other changes I would suggest, but this is a start. And quite a lot of work TBH.

Yo @RaananW … For sure, I could update from gulp 4.0

But I like the old-school namespace approach of BABYLON.Toolkit.ScriptComponent and PROJECT.MySampleScript. That I originally developed to give me that Unity Style Game Mechanic approach to my BJS development.

I would not be able to maintain the parallel I have to the the Unity API Script Components and mechanics I use for the toolkit using ES6 style imports of individual classes.

This is the style of script component I am looking for:

module PROJECT {
    export class MySampleScript extends BABYLON.Toolkit.ScriptComponent {
     
        protected awake(): void {
            /* Init component function */
        }

        protected start(): void {
            /* Start component function */
        }

        protected update(): void {
            /* Update render loop function */
        }
    }
}

I was hoping I could just wrap my babylon.toolkit.js runtime in a node npm package so if your are using a node based react web application for your BJS development you could do:

npm install babylonjs babylonjs-loaders babylon-toolkit

And use like so in react:

import * as BABYLON from 'babylonjs';
import 'babylonjs-loaders';
import 'babylon-toolkit';

But instead anybody wanting to use my toolkit in their react web application is just going to have
to load dynamically at runtime:

await BABYLON.Tools.LoadScriptAsync("cdn/or/scripts/babylon.toolkit.js");

That sucks for me :frowning:

I just don’t really understand why the namespacing is needed, but maybe it is a matter of taste.
You can wrap anything and publish it to npm, but there will be some backlash for sure, as people will expect a certain way to consume your package. No side-effects, no unnamed imports (or - as little as possible), no global namespace alterations.

import { WhatYouNeed, ClassToExtend } from "babylonjs-toolkit";

export class NewClass extends ClassToExtend {

/// implementation

}

clean, efficient, understandable.

My issue with keeping it the same is mainly, i would have to refactor my entire babylon.toolkit.js library… Like 6+ years of BABYLON.Classes and BABYLON.Toolkit.Classes syntax every where.

Would be a massive undertaking right now

but probably worth doing for future users of the toolkit

Yo @RaananW … I refactored my whole toolkit to use its own root namespace of TOOLKIT. I no longer piggy back off the BABYLON namespace (was also causing export issues with the es6 version). But I love namespaces. It keeps all my toolkit classes neatly tucked up under the TOOLKIT namespace. Now use of the toolkit can name their classes however they like, but the core toolkit classes are all under the TOOLKIT namespace. Examples:

TOOLKIT.ScriptComponent
TOOLKIT.CharacterController
TOOLKIT.AnimationState

But I did also update to webpack and create a true UMD/ES6 package that can be used in both browsers and es6 web applications. The key was in the the proper export setup:

export default TOOLKIT

But you are right, webpack is pretty awesome at what it does. This is working perfect so far:

const path = require('path');

module.exports = {
  entry: './src/index.ts',
  output: {
    filename: 'babylon.toolkit.js',
    path: path.resolve(__dirname, 'dist'),
    library: {
      name: 'TOOLKIT',    // The name of the global variable
      type: 'umd',        // The UMD format
      export: 'default',  // Export as a default plain object, not as a module
    },
    globalObject: 'this', // Ensures 'window' is used as the global object in browsers
  },
  resolve: {
    extensions: ['.ts', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  externals: {
    'babylonjs': 'BABYLON',
  },
};

I can now use my toolkit natively in node via imports and NOT have to inject the javascript at runtime using BABYLON.Tools.LoadScriptAsync. I can use natural es6 conventions now, like:

import * as BABYLON from '@babylonjs/core/Legacy/legacy';
import '@babylonjs/gui';
import '@babylonjs/loaders';
import '@babylonjs/materials';
import TOOLKIT from 'babylon-toolkit';

Note: Although i am having an issue with missing BABYLON.GLTF2.GLTFLoader in the es6 version. But i bet that is just me not reference something the correct way in es6 that what I was doing before.

1 Like