Ben Collins
Ben Collins

Reputation: 20686

Exporting Typescript function in Webpack bundle built with gulp, empty object in browser

I'm trying to build a js bundle from typescript with exports that I can call directly from an inline script, but I don't seem to be able to get it to work.

I have a Typescript source file like this:

export namespace Init {
    export function index() {
        // do some things
    }
}

export function blahblah() { console.log('blahblah') }

This sits alongside some other sources in the same folder src/ts/*.ts. My webpack.config.js looks like this:

'use strict';

module.exports = {
    context: __dirname,
    output: {
        filename: 'bundle.js',
        library: 'bitthicket',
    },
    devtool: 'sourcemap',
    resolve: {
        extensions: [ '.ts', '.tsx', '.js' ]
    },
    module: {
        rules: [
            { test: /\.tsx?$/, exclude: /node_modules/, loader: 'ts-loader' }
        ]
    }
}

And finally, my gulpfile.js looks like this:

var gulp = require('gulp')
var gutil = require('gulp-util')
var plumber = require('gulp-plumber')
var watch = require('gulp-watch')
var sass = require('gulp-sass')
var ts = require('gulp-typescript')
var clean = require('gulp-clean')
var webpack = require('webpack')
var webpack_config = require('./webpack.config.js')
var webpack_stream = require('webpack-stream')


let _sass = () => 
    gulp.src('src/css/**/*.scss')
        .pipe(plumber())
        .pipe(sass())
        .pipe(gulp.dest('static/css'))
        .on('end', () => gutil.log('_sass: done'))

let _webpack = () => 
    gulp.src('src/ts/**/*.ts')
        .pipe(plumber())
        .pipe(webpack_stream(webpack_config, webpack))
        .pipe(gulp.dest('static/js'))
        .on('end', () => gutil.log('_webpack: done'))

gulp.task('build:sass', _sass)
gulp.task('clean:sass', () => gulp.src('static/css/**/*').pipe(clean()))
gulp.task('watch:sass', () => watch('src/css/**/*.scss', () => _sass()))

gulp.task('build:webpack', _webpack)
gulp.task('clean:ts', () => gulp.src('static/js/**/*').pipe(clean()))
gulp.task('watch:webpack', () => watch('src/ts/**/*.ts', () => _webpack()))

gulp.task('watch', ['watch:sass', 'watch:webpack'])
gulp.task('clean', ['clean:sass', 'clean:ts'])
gulp.task('build', ['build:sass', 'build:webpack'])
gulp.task('default', ['clean', 'build'])

In an html file:

<script src="bundle.js" async></script>
<script>window.onload = function() { console.log(bitthicket); }</script>

What's happening, as I understand it, is that webpack is getting the piped list of sources as entry files, as though they were placed on the commandline like webpack {src list} bundle.js. But I've set my library output option, and the bitthicket variable does get set, but it's an empty object. What happened to Init.index?

Edit: I added the export keyword to the Init namespace, as well as a plain function to init.ts to see what would happen - but the resulting object still doesn't have those functions.

Here's the resulting webpack bootstrapper argument:

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

exports.__esModule = true;
var _ = __webpack_require__(2);
var Init;
(function (Init) {
    function index() {
        Nav.attachNavbarScrollWatcher();
        document.addEventListener('scroll', function (event) {
            var articles = document.querySelectorAll('.featured column.summary article');
            var visibleArticle = _.find(articles, function (el) { return Util.isElementInViewport(el); });
            console.log("{0} just became visible", visibleArticle);
        });
    }
    Init.index = index;
})(Init = exports.Init || (exports.Init = {}));
function blahblah() {
    console.log('blahblah');
}
exports.blahblah = blahblah;


/***/ }),

As you can see, exports.blahblah and exports.Init.index are assigned in this block - but the object I get back in my browser looks like this:

enter image description here

Upvotes: 3

Views: 1911

Answers (1)

Ben Collins
Ben Collins

Reputation: 20686

The real answer to my question is

Do you even module, bro?

I just flat-out misunderstood the relationship between what typescript calls namespaces and modules, and what webpack thinks modules are.

After this exercise in futility, I'm not even sure what the point of Typescript namespaces are anymore, unless you have a multi-stage build process where you use tsc and tsconfig.json to create multiple transpiled bundles that will later be re-bundled by some other loader like Webpack.

Anyway, to the point:

Typescript "namespaces" mean nothing at all to Webpack (more specifically, ts-loader: see TypeStrong/ts-loader#193). Webpack is not able to walk namespace dependencies, and so you wind up with undefined symbols in the bundle, which means the webpackBootstrap function also had no idea how to load things in the right order. The entry point in my bundle was kinda like this:

/* 0 */
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__(1);  // <-- actual entry point, exports are lost
__webpack_require__(2);
exports = __webpack_require__(4); // <-- not entry point, nothing is exported here anyway
/***/ }),
...

Webpack (and ts-loader) require a form of modules that it understands in order to make the bootstrapper load things correctly.

I'm still using a Typescript namespace for structure, but I'm exporting it and using import to indicate dependencies on other Typescript modules:

import * as _ from 'lodash'
import * as Nav from './nav'
import * as Util from './util' 

export namespace Init {
    // ...
}

The main thing is the expression of the dependencies as es2015-style import/exports.

Upvotes: 3

Related Questions