jcgzzc
jcgzzc

Reputation: 121

Using Webpack when extending a browser-native class

I am using Webpack with React and Typescript and I'm trying to create a wrapper class for WebSocket, a browser native class.

The class is in a file webSocketConnection.ts and looks something like this:

export default class WebSocketConnection extends WebSocket {
    constructor(url: string, protocols?: string | string[]) {
        super(url, protocols);
    }
}

A separate file imports and uses it

import WebSocketConnection from './webSocketConnection';

export function Connect() {
    return new WebSocketConnection("<<someUrl>>");
}

It builds fine, but then on running the site I get NodeInvocationException: Prerendering failed because of error: ReferenceError: WebSocket is not defined.

From my understanding, this is a server side error due to node not finding the WebSocket object, even though it works fine on the client. This works perfectly fine when just using new Websocket("<<someUrl>>").

My expectation is that this could be solved by excluding that specific file from being bundled, or from the server seeing it.

My webpack.config.js is

const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const merge = require('webpack-merge');

module.exports = (env) => {
    const isDevBuild = !(env && env.prod);

    // Configuration in common to both client-side and server-side bundles
    const sharedConfig = () => ({
        stats: { modules: false },
        resolve: {
            extensions: ['.js', '.jsx', '.ts', '.tsx'],
            alias: {
                ["~"]: path.resolve(__dirname, "ClientApp"),
            }
        },
        output: {
            filename: '[name].js',
            publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
        },
        module: {
            rules: [
                { test: /\.tsx?$/, include: /ClientApp/, use: 'awesome-typescript-loader?silent=true' },
                { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
            ]
        },
        plugins: [new CheckerPlugin()]
    });

    // Configuration for client-side bundle suitable for running in browsers
    const clientBundleOutputDir = './wwwroot/dist';
    const clientBundleConfig = merge(sharedConfig(), {
        entry: { 'main-client': './ClientApp/boot-client.tsx' },
        module: {
            rules: [
                {
                    test: /\.css$/,
                    oneOf: [
                        {
                            resourceQuery: /raw/,
                            use: ['style-loader', 'css-loader']
                        },
                        {
                            use: ExtractTextPlugin.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' })
                        }
                    ]
                },
                {
                    test: /\.less$/,
                    use: ExtractTextPlugin.extract(['css-loader', 'less-loader'])
                },
                {
                    test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
                    use: [{
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'fonts/'
                        }
                    }]
                }
            ]
        },
        output: { path: path.join(__dirname, clientBundleOutputDir) },
        plugins: [
            new ExtractTextPlugin('site.css'),
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require('./wwwroot/dist/vendor-manifest.json')
            })
        ].concat(isDevBuild ? [
            // Plugins that apply in development builds only
            new webpack.SourceMapDevToolPlugin({
                filename: '[file].map', // Remove this line if you prefer inline source maps
                moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
            })
        ] : [
                // Plugins that apply in production builds only
                new webpack.optimize.UglifyJsPlugin()
            ])
    });

    // Configuration for server-side (prerendering) bundle suitable for running in Node
    const serverBundleConfig = merge(sharedConfig(), {
        resolve: { mainFields: ['main'] },
        module: {
            rules: [
                { test: /\.css$/, loader: 'ignore-loader' },
                { test: /\.less$/, loader: 'ignore-loader' }
            ]
        },
        entry: { 'main-server': './ClientApp/boot-server.tsx' },
        plugins: [
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require('./ClientApp/dist/vendor-manifest.json'),
                sourceType: 'commonjs2',
                name: './vendor'
            })
        ],
        output: {
            libraryTarget: 'commonjs',
            path: path.join(__dirname, './ClientApp/dist')
        },
        target: 'node',
        devtool: 'inline-source-map'
    });

    return [clientBundleConfig, serverBundleConfig];
};

UPDATE 2 36pm The result after transpiling is as so:

var WebSocketConnection = (function (_super) {
    __extends(WebSocketConnection, _super);
    function WebSocketConnection(url, protocols) {
        return _super.call(this, url, protocols) || this;
    }
    return WebSocketConnection;
}(WebSocket));

Upvotes: 1

Views: 171

Answers (1)

jcgzzc
jcgzzc

Reputation: 121

Update 6:42 PM: After further testing, the original answer did build correctly but did not run correctly. Despite explicitly setting the prototype to WebSocket, it still called WebSocketMock during super().

A second method did work, only to find you can't extend WebSocket at all in Chrome, because you'll always get the error Failed to construct 'WebSocket': Please use the 'new' operator, this DOM object constructor cannot be called as a function.

In case somebody else needs to extend a browser-native class that can be extended, this is how it was successfully accomplished:

///Inside of file webSocketConnection.ts
export interface WebSocketConnection extends WebSocket {
    //Custom properties here
}

let classVar: any;

if (typeof(WebSocket) !== 'undefined') {
    classVar= class WebSocketConnection extends WebSocket {
        constructor(url: string, protocols?: string | string[]) {
            super(url, protocols);
        }
    }
}

export default function(url: string, protocols?: string | string[]): WebSocketConnection {
    return new classVar(url, protocols) as WebSocketConnection;
}

--

///Inside of a second file
import createWebSocket, { WebSocketConnection } from './webSocketConnection';

function DoSomething() {
    //Note no "new" keyword used, because this function isn't actually a constructor
    let socket: WebSocketConnection = createWebSocket("<<someUrl>>");
}

For completion's sake, the non-TypeScript solution would look something like this:

///Inside of file webSocketConnection.js
let classVar;

if (typeof(WebSocket) !== 'undefined') {
    classVar = class WebSocketConnection extends WebSocket {
        constructor(url, protocols) {
            super(url, protocols);
        }
    }
}

export default function(url, protocols) {
    return new classVar(url, protocols);
}

--

///Inside of a second file
import createWebSocket from './webSocketConnection';

function DoSomething() {
    //Note no "new" keyword used, because this function isn't actually a constructor
    let socket = createWebSocket("<<someUrl>>");
}

Original Answer -- Did not work, but left here as it may provide insight to someone

OP here, the solution that worked meant creating a mock class WebSocketMock that had all the same properties as WebSocket, but not implemented, and have WebSocketConnection extend WebSocketMock. Afterwards, I would update the prototype of WebSocketConnection to be WebSocket, if it existed. This if statement was true in the browser, but false in node.

TypeScript solution:

/* Mock class = WebSocketMock; new empty class that looks similar to original class
 * Original class = WebSocket; browser-only class we want to extend
 * New class = WebSocketConnection; class that extends original class
 */

/* Creating a blank interface, with the same name as the mock class,
 * that extends the original interface we're trying to mock
 * allows the mock class to have all the properties of the original class
 * without having to actually implement blank versions of them
 */
interface WebSocketMock extends WebSocket {
}

/* The mock class must have the same constructor as the original class
 * so that the new class can use super() with the right signature
 */
class WebSocketMock {
    constructor(url: string, protocols?: string | string[]) {
    }
}

// New class extends the mock class
export default class WebSocketConnection extends WebSocketMock {
    constructor(url: string, protocols?: string | string[]) {
        super(url, protocols);
    }

    //Other properties and code will be added here
}

/* Updates the prototype of the new class to use the original class
 * when the original class exists. Of course, if you try to use the new
 * class in an environment (read: browser) that doesn't have the original
 * class, everything would break, as it's just an empty "shim"
 */
if (typeof (WebSocket) !== 'undefined')
    Object.setPrototypeOf(WebSocketConnection, WebSocket);

Without typescript, it would likely look something like this (I don't have a TypeScript-free environment that uses Webpack for me to test with)

class WebSocketMock {
    constructor(url, protocols) {
    }
}

export default class WebSocketConnection extends WebSocketMock {
    constructor(url, protocols) {
        super(url, protocols);
    }

    //Other properties and code will be added here
}

if (typeof (WebSocket) !== 'undefined')
    Object.setPrototypeOf(Object.getPrototypeOf(WebSocketConnection), WebSocket);

Upvotes: 1

Related Questions