Search code examples
javascriptwebpackrequirejsamdshim

Webpack 2: shimming like RequireJS for jQWidgets?


I’m migrating from a RequireJS project to Webpack. The latter is new to me, I’m using this as a learning exercise. In RequireJS I could register stuff like this:

shim: {
    'jqxcore': {
        exports: "$",
        deps: ["jquery"]
    },
    'jqxtree': {
        exports: "$",
        deps: ["jquery", "jqxcore"]
    },
    'jqxbutton': {
        exports: "$",
        deps: ["jquery", "jqxcore"]
    },
    'jqxsplitter': {
        exports: "$",
        deps: ["jquery", "jqxcore"]
    },
    'jqxmenu': {
        exports: "$",
        deps: ["jquery", "jqxcore"]
    }
}

and then just require “jqxsplitter” for example like so:

import "jqxsplitter"

and stuff would be correctly registered and loaded. Now I was looking at a couple of guides/tutorials/takes I found on migrating from RequireJS to Webpack, such as this one and this one. So following those insights I’m trying something like this in my webpack.config.js:

"use strict";

// Required to form a complete output path
const path = require("path");

// Plagin for cleaning up the output folder (bundle) before creating a new one
const CleanWebpackPlugin = require("clean-webpack-plugin");

const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");

// Path to the output folder
const bundleFolder = "./wwwroot/";

// Path to the app source code
const appFolder = "./app/";

module.exports = {
    // Application entry point
    entry: {
        main: appFolder + "index.ts",
        vendor: [
            "knockout",
            "jquery",
            "jqxcore"
        ],
        jqxsplitter: "jqxsplitter"
    },

    // Output file
    output: {
        filename: "[name].js",
        chunkFilename: "[name].js",
        path: path.resolve(bundleFolder)
    },
    module: {
        rules: [{
            test: /\.tsx?$/,
            loader: "ts-loader",
            exclude: /node_modules/
        }, {
            test: /\.html?$/,
            loader: "html-loader" //TODO: file-loader?
        }],
        loaders: [{
            test: /jqxcore/,
            loader: "imports?jquery!exports?$"
        }, {
            test: /jqxsplitter/,
            loader: "imports?jquery,jqxcore!exports?$"
        }]
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js"],
        alias: {
            "jqxcore": "jqwidgets-framework/jqwidgets/jqxcore",
            "jqxsplitter": "jqwidgets-framework/jqwidgets/jqxsplitter"
        }
    },
    plugins: [
        new CleanWebpackPlugin([bundleFolder]),
        new HtmlWebpackPlugin({
            filename: "index.html",
            template: appFolder + "index.html",
            chunks: ["main", "vendor"]
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: "vendor",
            filename: "vendors.js",
            minChunks: Infinity
        })
    ],
    devtool: "source-map"
};

the relevant part (I assume) being

module: {
    loaders: [{
        test: /jqxcore/,
        loader: "imports?jquery!exports?$"
    }, {
        test: /jqxsplitter/,
        loader: "imports?jquery,jqxcore!exports?$"
    }]
},

It’s pretty clear how the syntax of “imports/exports” is supposed to be the equivalent of RequireJS’ “deps” and “exports”. However when I do this in my index.ts file (app root):

import "jqwidgets-framework/jqwidgets/jqxsplitter";

I get the “jqxBaseFramework is undefined” error when running my app. I’ve found references to this error on the forums of jQWidgets, but none of the answers seem to REALLY tackle the issue or include things like the AOT compilation, which doesn’t apply to my situation because I’m not using Angular.

I've posted this same question on the jQWidges forums, but so far no actual answer (going on two weeks now), only a single generic answer saying I should load jqxcore.js before jqxwhateverplugin.js. Well yes, obviously, that's what I'm trying to accomplish using the shimming after all.

Any ideas?


Solution

  • Well I ended up deep diving and figuring it out for myself. Here's the solution should anyone find themselves in the same or a similar boat.

    If you beautify the jQWidgets script files jqxcore.js, you'll see it creates a what would normally be a global variable called "jqxBaseFramework", which will of course never be exposed globally, only within its own module. And there lies the problem.

    The solution is to use this configuration:

    module: {
        rules: [{
            test: /jqxcore/,
            use: "exports-loader?jqxBaseFramework"
        }, {
            test: /jqxknockout/,
            use: ["imports-loader?jqxBaseFramework=jqxcore,ko=knockout", "exports-loader?jqxBaseFramework"]
        }, {
            test: /jqxsplitter/,
            use: "imports-loader?jqxBaseFramework=jqxknockout"
        }]
    },
    resolve: {
        ...
        alias: {
            "knockout": "knockout/build/output/knockout-latest",
            "jqxcore": "jqwidgets-framework/jqwidgets/jqxcore",
            "jqxknockout": "jqwidgets-framework/jqwidgets/jqxknockout",
            "jqxsplitter": "jqwidgets-framework/jqwidgets/jqxsplitter"
        }
    },
    

    I guess once it clicks, this all makes sense. The jqxcore module will now export its jqxBaseFramework variable with the same name.

    I added in knockout support while at it. jqxknockout expects two global variables to work normally: ko (knockout) and jqxBaseFramework. So now we tell webpack that whenever jqxknockout is loaded, it should load the jqxcore module and assign its export to a module-local variable called "jqxBaseFramework" and load the knockout module and assign its export to a module-local variable called "ko". This effectively equates to prepending the following code to the jqxknockout.js script:

    var jqxBaseFramework = require("jqxcore");
    var ko = require("knockout");
    

    The script can now execute again because those two variables are found. I added the export loader to export the same, but now processed/augmented jqxBaseFramework variable from jqxknockout.

    jqxSplitter normally only needs jqxCore to work, but I want to use it with knockout, always. So instead of importing jqxBaseFramework from jqxCore for jqxSplitter, I'm getting it from jqxKnockout, so all the pieces are in place. So now when I add this code to whatever file I'm in:

    import "jqwidgets-framework/jqwidgets/jqxsplitter";
    

    Webpack will require jqxknockout and its export for it, being jqxBaseFramework, which in turn will require jqxcore and knockout et voilà, the whole thing is wired up beautifully.

    Hope this helps someone!