Home > Back-end >  Universal typescript npm module
Universal typescript npm module

Time:02-08

I am modifying an existing library so it can be imported in typescript. I've boiled it down to a Minimal working example

The requirements

  • To remain backwards compatible, the library needs to be importable with a simple <script> tag
  • To simplify future usage, the library needs to be importable with a single typescript import statement (no <script> tags in HTML should be needed)
  • I have used rollup, but solutions using any other way to achieve this result (gulp, webpack, etc.) are welcome too.

What I've got so far

Library

File structure:

│   package.json
│   rollup.config.js
│   tsconfig.json
│   yarn-error.log
│   yarn.lock
│
└───src
        lib.ts
        options.ts

package.json:

{
  "name": "library",
  "version": "1.0.8",
  "description": "example library",
  "main": "dist/lib.umd.js",
  "module":"dist/lib.esm.js",
  "types":"dist/types/lib.d.ts",
  "license": "MIT",
  "private": true,
  "devDependencies": {
    "@rollup/plugin-typescript": "^8.3.0",
    "rollup": "^2.67.0",
    "tslib": "^2.3.1",
    "typescript": "^4.5.5"
  },
  "exports": {
    "import": "./dist/lib.esm.js",
    "require": "./dist/lib.umd.js"
  },
  "scripts": {
    "build:types": "tsc -d --emitDeclarationOnly",
    "build:js": "rollup -c rollup.config.js",
    "build:minjs:umd": "terser dist/index.umd.js --compress --mangle > dist/index.umd.min.js",
    "build:minjs:esm": "terser dist/index.esm.js --compress --mangle > dist/index.esm.min.js",
    "build:minjs": "npm run build:minjs:esm -s && npm run build:minjs:umd -s",
    "build": "npm run build:js -s && npm run build:minjs -s && npm run build:types -s",
    "prepublishOnly": "npm run lint -s && npm test -s && npm run build",
    "semantic-release": "semantic-release"
  },
  "type":"module"
}

rollup.config.js:

import typescript from '@rollup/plugin-typescript';

export default {
  input: 'src/lib.ts',
  output: [
    {
      file: 'dist/lib.esm.js',
      format: 'es',
    },
    {
      file: 'dist/lib.umd.js',
      format: 'umd',
      name: 'Lib',
    },
  ],
  plugins: [typescript({tsconfig:'./tsconfig.json'})],
};

tsconfig.json:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "esnext",
    "moduleResolution": "node",
    "declaration": true,
    "outDir": "./types",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
}

src/lib.ts:

import Options from "./options";

export default class Lib{
    constructor(options:Options){
        console.log("It works!");
        console.log(options.message);
    }
}

options.ts:

export default interface Options {
    message: string;
}

I am compiling all of this using yarn build:js.

Using the library

With <script> tags

When I copy the resulting lib.umd.js to a folder and create a index.html:

<script src=lib.umd.js></script>
<script>
    var a = new Lib({message:"message here"});
</script>

It works. So far so good.

In another typescript project

Next, I created a simple typescript project that utilizes my library.

File structure:

│   gulpfile.js
│   package.json
│   tsconfig.json
│   yarn.lock
│
├───dist
│       index.html
│
└───Scripts
        Index.ts

In package.json I include my library as a dependency, along with jQuery to hopefully rule out improper configuration of this project:

{
    "private": true,
    "version": "1.0.0",
    "scripts": {
        "preinstall": "npx use-yarn",
        "gulp": "node_modules/.bin/gulp"
    },
    "name": "ts-import",
    "devDependencies": {
        "@types/jquery": "^3.5.13",
        "gulp": "^4.0.2",
        "gulp-browserify": "^0.5.1",
        "gulp-clean": "^0.4.0",
        "gulp-concat": "^2.6.1",
        "gulp-sourcemaps": "^3.0.0",
        "gulp-typescript": "^6.0.0-alpha.1",
        "gulp-uglify": "^3.0.2",
        "typescript": "^4.5.5",
        "vinyl-source-stream": "^2.0.0"
    },
    "dependencies": {
        "jquery": "*",
        "library": "file:../../library"
    }
}

tsconfig.json:

{
    "compilerOptions": {
        "noEmitOnError": true,
        "noImplicitAny": true,
        "sourceMap": true,
        "target": "es5",
        "moduleResolution": "node",
        "outDir":"./js"
    },
    "compileOnSave": true,
    "exclude": [
        "**/node_modules/**"
    ],
    "include":[
        "./Scripts"
    ]
}

I'm using gulp to compile ts and resolve imports - gulpfile.js:

const gulp = require('gulp');
const {series} = require('gulp');
const clean = require('gulp-clean');
const ts = require('gulp-typescript');
const sourcemaps = require('gulp-sourcemaps');
const browserify = require('gulp-browserify');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify');

function cleanAll(cb) {
  return gulp.src([
      "./tmp/",
      "./dist/js"
    ], { read: false, allowEmpty: true })
    .pipe(clean());
}

function transpileTS() {
  const tsproject = ts.createProject('./tsconfig.json');
  return tsproject
      .src()
      .pipe(sourcemaps.init())
      .pipe(tsproject()).js
      .pipe(sourcemaps.write('./sourcemaps'))
      .pipe(gulp.dest('./tmp/js'));
}

function minJS() {
  return gulp
      .src('./tmp/js/Index.js')
      .pipe(sourcemaps.init({ loadMaps: true }))
      .pipe(browserify())
    //   .pipe(uglify())
      .pipe(concat('index.min.js'))
      .pipe(sourcemaps.write('./sourcemaps'))
      .pipe(gulp.dest('./dist/js'))
}

exports.default = series( cleanAll, transpileTS, minJS );

I'm importing and using my library like this - Scripts/Index.ts:

import * as $ from "jquery";
import Lib from "library";

$(()=>{
    console.log("creating lib instance.");
    new Lib({message:"example message here"});
});

But when I launch this in a browser - dist/index.html:

<script src="js/Index.min.js"></script>

I get an error:

Uncaught TypeError: library_1.default is not a constructor

Indeed, library_1.default doesn't exist, library_1 does though:

> library_1
< class Lib {
        constructor(options) {
            console.log("It works!");
            console.log(options.message);
        }
    }

How do I fix this? I suspect the error is somewhere in the library, but I have no idea where.

CodePudding user response:

I've figured it out. In my lib.ts I had to change the way I export my class:

import Options from "./options";

class Lib{
    constructor(options:Options){
        console.log("It works!");
        console.log(options.message);
    }
}

export default Lib;

Next, I changed rollup.config.js (added exports:'default') to keep the exported class global:

...
{
      file: 'dist/lib.umd.js',
      format: 'umd',
      name: 'Lib',
      exports:'default'
},
...

I also had to change the library's package.json - I pointed main to my es module as it looks like browserify just ignores the module directive.
I also ended up removing the exports section as some of our other projects (not using browserify) kept using the require file, not the import one:

{
  "name": "library",
  "version": "1.0.16",
  "description": "example library",
  "main": "dist/lib.esm.js",
  "module":"dist/lib.esm.js",
  "types":"dist/types/lib.d.ts",
  "license": "MIT",
  "private": true,
  "devDependencies": {
    "@rollup/plugin-typescript": "^8.3.0",
    "rollup": "^2.67.0",
    "tslib": "^2.3.1",
    "typescript": "^4.5.5"
  },
  "exports": {
    "import": "./dist/lib.esm.js",
    "require": "./dist/lib.umd.js"
  },
  "scripts": {
    "build": "rollup -c rollup.config.js"
  }
}

Finally, I had to use babelify in my consumer project to avoid ParseError: 'import' and 'export' may appear only with 'sourceType: module' while running browserify.

The full source is available here

  •  Tags:  
  • Related