Search code examples
javascripttypescriptwebpackts-loader

Exported TypeScript Class not included in WebPack bundle if not used directly


I'm converting a javascript project with Angular 1.x to WebPack and TypeScript (using ts-loader). I got it mostly working, but I'm running into trouble when ts-loader seems to be optimizing my scripts out of the bundle when the exports are not directly used.

Here's a sample project demonstrating the issue (npm install, webpack, then load index.html and watch the console).

https://github.com/bbottema/webpack-typescript

The logging from ClassA is showing up, but angular is reporting ClassB missing (provider). If you look in bundle.js you'll notice ClassB missing entirely. The difference is ClassA begin use directly after importing, and ClassB is only referenced by type for compilation.

Is it a bug, or is there a way to force ClassB to be included? Or am I going about it wrong? Angular 2 would probably solve this issue, but that's a step too large right now.

Relevant scripts from the project above:


package.json

{
    "devDependencies": {
        "typescript": "^1.7.5",
        "ts-loader": "^0.8.1"
    },
    "dependencies": {
        "angular": "1.4.9"
    }
}

webpack.config.js

var path = require('path');

module.exports = {
    entry: {
        app: './src/entry.ts'
    },
    output: {
        filename: './dist/bundle.js'
    },
    resolve: {
        root: [
            path.resolve('./src/my_modules'),
            path.resolve('node_modules')
        ],
        extensions: ['', '.ts', '.js']
    },
    module: {
        loaders: [{
            test: /\.tsx?$/,
            loader: 'ts-loader'
        }]
    }
};

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs"
  },
  "exclude": [
    "node_modules"
  ]
}

index.html

<!doctype html>
<html ng-app="myApp">
    <body>
        <script src="dist/bundle.js"></script>
    </body>
</html>

entry.js

declare var require: any;

'use strict';

import ClassA = require('ClassA');
import ClassB = require('ClassB');

var a:ClassA = new ClassA(); // direct use, this works

var angular = require('angular');

angular.module('myApp', []).
    // this compiles as it should, but in runtime the provider will not be packaged and angular will throw an error
    run(function(myProvider: ClassB) {
    }
);

ClassA.ts

// this line will be logged just fine
console.log('ClassA.ts: if you see this, then ClassA.ts was packaged properly');

class ClassA {
}

export = ClassA;

ClassB.ts

declare var require: any;

// this line is never logged
console.log('ClassB.ts: if you see this, then ClassB.ts was packaged properly');

class ClassB {
}

var angular = require(angular);

angular.module('myApp').service(new ClassB());

export = ClassB;

Solution

  • Turns out you have to signal WebPack to explicitly include a module by adding an extra require call without import statement.

    I'm not ready to mangle my .ts files by adding duplicate imports, so I made a generic solution for that using the preprocessor loader:

    {
        "line": false,
        "file": true,
        "callbacks": {
        "fileName": "all",
        "scope": "line",
        "callback": "(function fixTs(line, fileName, lineNumber) { return line.replace(/^(import.*(require\\(.*?\\)))/g, '$2;$1'); })"
        }]
    }
    

    As a proof of concept, this regex version is very limited it only support the following format:

    import ClassA = require('ClassA');
    // becomes
    require('ClassA');import ClassA = require('ClassA');
    

    But it works for me. Similarly, I'm adding the require shim:

    {
        "fileName": "all",
        "scope": "source",
        "callback": "(function fixTs(source, fileName) { return 'declare var require: any;' + source; })"
    }
    

    I made a sample project with this solution.