Jonas Strandstedt Jonas Strandstedt - 1 month ago 16
TypeScript Question

Should I exclude dependencies for modular Typescript project?

I am currently trying to create a NPM package for a Typescript project (I am building using gulp and browserify). The problem is that the package consumer is currently not using modules so I am trying to package a standalone bundle using Browserify.

First and foremost, could it be an issue to bundle all the dependencies? As far as I can tell the bundled js file simply wraps my dependencies (three and hammerjs) into the global namespace. My package consumer has another component that is including hammerjs (almost the same version) so I suspect that the component included last will define what hammerjs package is available for my application? How do other NPM packages that work in a standalone way deal with this?

I found that Browserify could simply exclude its dependencies by setting bundleExternal to false or by excluding dependencies one by one and then include those libraries in the browser. This does not work and I get a "Cannot find module 'hammerjs'" error in the console. I found how to use the exclude in browserify? and how to browserify all the dependencies separately which also worked but from what I can tell this would be the same as simply bundling them in the place since I cannot simply include the hammer.min.js file from their website?

TL;DR



What is the correct way to bundle a modular Typescript NPM package and deal with dependencies for use in an application that does not support modules?

Answer

I'm still not quite sure the best way to deal with dependencies but the route I chose to go with was to create a mylib.js and a mylib.min.js that are distributed in the npm package and not containing any dependencies. Alongside the distributed js files I included the library in modular form that can be consumed with for example browserify. The problem I has with browserify was that the output where I tried to exclude libraries was still dependent on some form of require, when I tried webpack it worked out of the box.

I included the complete build scripts for reference.

File structure

├───dist                // Output folder for all my distributable standalone js files
│   ├───mylib.d.ts      // Manually writtes declaration file
│   ├───myLib.js        // Distributable without dependencies
│   └───myLib.min.js    // Compressed distributable without dependencies
├───lib
│   ├───myLib.js        // Compiled src
│   └───myLib.d.ts      // Compiled d.ts 
├───src                 // Folder containing my Typescript src
├───tests               // Output folder for my tests
└───testSrc             // Src folder for my test code
    ├───test.html
    └───unittests

package.json

{
  "name": "mylib",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "build": "gulp compile && gulp webpack",
    "prepublish": "gulp prepublish"
  },
  "main": "lib/mylib.js",
  "typings": "lib/mylib",
  "dependencies": {
    "@types/es6-promise": "0.0.32",
    "@types/hammerjs": "2.0.33",
    "@types/three": "0.0.24",
    "es6-promise": "4.0.5",
    "hammerjs": "2.0.8",
    "three": "0.82.1"
  },
  "devDependencies": {
    "@types/jasmine": "2.5.37",
    "gulp": "3.9.1",
    "gulp-cli": "1.2.2",
    "gulp-concat": "2.6.0",
    "gulp-copy": "0.0.2",
    "gulp-jasmine": "2.4.2",
    "gulp-preprocess": "2.0.0",
    "gulp-typescript": "3.1.2",
    "jasmine": "2.5.2",
    "ts-loader": "1.0.0",
    "typescript": "2.0.6",
    "webpack-stream": "3.2.0"
  }
}

Gulpfile.js

var gulp = require('gulp');
var ts = require('gulp-typescript');

// Create projects from tsconfig.json
var tsProject = ts.createProject('tsconfig.json');
var mainTestTsProject = ts.createProject('testSrc/tsconfig.json');
var jasmineTsProject = ts.createProject('testSrc/unittests/tsconfig.json');

// External build libraries
var jasmine = require("gulp-jasmine");
var concat = require("gulp-concat");
var copy = require("gulp-copy");
var preprocess = require("gulp-preprocess");
var webpack = require("webpack-stream");

// Compile the modular library
gulp.task('compile', function () {
    return tsProject.src()
        .pipe(tsProject())
        .pipe(gulp.dest("lib"));
});

// Pack and distribute
gulp.task('webpack', function (callback) {
    var config = require("./webpack.config.js");
    return tsProject.src()
        .pipe(webpack(config))
        .pipe(gulp.dest('./dist'))
});

// Pre-process the test.html
gulp.task('preprocessMainHtml', function () {
    return gulp.src('./testSrc/*.html')
        .pipe(preprocess({ context: { CURRENT_TIMESTAMP: Date.now() } }))
        .pipe(gulp.dest('./tests/'));
});

// Copy output libraries for testing
gulp.task('copyLibs', function () {
    return gulp.src(['./dist/*.js', './dist/*.map'])
        .pipe(copy('./tests', { prefix: 1 }));
});

// Compile the test-html main javascript file
gulp.task('compileMainTest', ['copyLibs', 'preprocessMainHtml'], function (callback) {
    return mainTestTsProject.src()
        .pipe(mainTestTsProject())
        .pipe(concat('mylib-test.js'))
        .pipe(gulp.dest("tests"));
});

gulp.task('prepublish', ['compile', 'webpack']);

gulp.task('test', ['compile'], function () {
    return jasmineTsProject.src()
        .pipe(jasmineTsProject())
        .pipe(gulp.dest("tests/unittests"))
        .pipe(jasmine());
});

gulp.task("default", ['webpack', 'compileMainTest']);

webpack.config.js

module.exports = {
    resolve: {
        extensions: ['', '.js', '.ts', '.tsx']
    },
    module: {
        loaders: [
            { test: /\.tsx?$/, loader: 'ts' },
        ]
    },
    externals: {
        "hammerjs": "Hammer",
        "three": "THREE"
    },
    entry: {
        "MyLib": ['./src/mylib.ts'],
    },
    output: {
        path: __dirname + '/dist',
        filename: 'mylib.js',
        libraryTarget: "umd",
        library: 'MyLib'
    },
    devtool: 'source-map',
    debug: true
}

Manually written declaration file

// Manually maintained declaration file

// External references
/// <reference types="hammerjs" />
/// <reference types="three" />

// This namespace should map to what is exported in the Gruntfile.js
export as namespace MyLib;

export declare class MyMainClass {
    constructor(a: string);
}

Js src in test.html

<!-- All mylib.js dependencies -->
<script src="../node_modules/three/build/three.js?_=<!-- @echo CURRENT_TIMESTAMP -->"></script>
<script src="../node_modules/hammerjs/hammer.js?_=<!-- @echo CURRENT_TIMESTAMP -->"></script>

<!-- mylib.js library -->
<script src="./mylib.js?_=<!-- @echo CURRENT_TIMESTAMP -->"></script>

<!-- mylib.js test source -->
<script src="./mylib-test.js?_=<!-- @echo CURRENT_TIMESTAMP -->"></script>