nakama icon indicating copy to clipboard operation
nakama copied to clipboard

[Feature] Example or template using Webpack as bundling tool for Typescript runtime code

Open jason-otherwhere opened this issue 4 years ago • 7 comments

Your Environment

  • Nakama: 3.1.2

Adding this request per forum discussion.

The Typescript compiler can bundle JS code for you, but not if you use Node modules such as lodash. Webpack can bundle all dependencies together to allow leveraging the huge number of Node libraries out there.

How to get Webpack to do that shouldn't be too hard (in theory! I'm still working that out), but some official support would be ideal.

jason-otherwhere avatar Mar 12 '21 00:03 jason-otherwhere

Thanks @jason-otherwhere. By the way, if you've done some thinking on this and want to PR a webpack integration as either a proof of concept or production-ready effort you are definitely encouraged to.

lugehorsam avatar Mar 12 '21 00:03 lugehorsam

This is the closest I can get (seems to be bundling "lodash" library with the code), but the server fails to start with:

"caller": "server/runtime_javascript.go:1809",
"msg": "InitModule function not found. Function must be defined at top level.",
"module": "index.js"

At first, Webpack was emitting 0 bytes since the InitModule function was assigned to a const but never used, so it culled the whole thing as dead code. Which is good, but not this time. 😄

Next, I made InitModule a global function not assigned to anything, and it built! 😲 But when I deployed, the server failed to start with the error above. I'm not sure how to resolve that. 😭

function InitModule( ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer )
{
    logger.info( "Hello World!" );
}

For what it's worth, the approach I took is to let Typescript compile to ES6 (into a build/es6/ folder), then have Webpack and Babel do the rest to convert it into ES5 in a bundle. That seems to have sidestepped the Typescript limitations I had before.

Here's my current webpack.config.js config:

const path = require("path");

module.exports = {
    entry: "./build/es6/main.js",
    target: "es5",
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: ["@babel/preset-env"]
                    }
                },
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: ['.js'],
    },
    output: {
        path: path.resolve(__dirname, "build"),
        filename: "index.js",
    },
};

And tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ES6",
    "outDir": "./build/es6/",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "src/**/*"
  ]
}

And package.json (probably has some cruft, it's evolving as I learn):

{
  "name": "game-server",
  "version": "1.0.0",
  "main": "main.ts",
  "dependencies": {
    "graphql": "14.7.0",
    "nakama-runtime": "git+https://github.com/heroiclabs/nakama-common.git"
  },
  "devDependencies": {
    "@babel/cli": "7.13.10",
    "@babel/core": "7.13.10",
    "@babel/preset-env": "7.13.10",
    "@types/lodash": "4.14.168",
    "@types/node": "14.14.33",
    "babel-loader": "8.2.2",
    "babel-plugin-lodash": "3.3.4",
    "lodash": "4.17.21",
    "typescript": "4.2.3",
    "webpack": "5.25.0",
    "webpack-cli": "4.5.0"
  },
  "scripts": {
    "build": "rm -rf ./build/es6/* && tsc && webpack --mode=production",
  },
  "engines": {
    "node": "14.11"
  }
}

jason-otherwhere avatar Mar 13 '21 18:03 jason-otherwhere

@jason-otherwhere I'm a year late to the party but I recently got a webpack+typescript project building and integrating with Nakama. The core issue you're most likely running into is that webpack wraps everything into an anonymous 'webpackBootstrap' function which prevents your InitModule from appearing at the top level of your generated Javascript file.

Alter your webpack config to output a module like so: output: { module: true, library: { type: "module" } }, experiments: { outputModule: true },

Your generated file will then look like this, with the InitModule at the top level - outside of any webpackBootstrap function. var __webpack_exports__ = {}; /*!**********************!*\ !*** ./src/index.ts ***! \**********************/ var InitModule = function (ctx, logger, nk, initializer) { logger.info("Hello World!"); };

Deploying should then successfully output a Hello World message as long as your config file is pointing the javascript runtime to this file

matthewvroman avatar Jul 21 '22 04:07 matthewvroman

Amazing. Thanks guys, tagging @tomglenn and @sesposito for next steps on getting this documented.

lugehorsam avatar Jul 21 '22 14:07 lugehorsam

So unfortunately my above answer only works when you don't import any additional files. Once you add a single import webpack goes back to adding an IIFE wrapper around the InitModule.

We can somewhat circumvent this and get the InitModule to execute by exporting it as a var instead of a module within Webpacks settings: library: { name: 'InitModule', type: 'var', export: 'InitModule' }

and define our InitModule like this in Javascript: export const InitModule = function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer)

which gives us an InitModule var at the top level of our Javascript file that looks like this: InitModule = __webpack_exports__.InitModule;

This will execute the InitModule function and we can see output logs from the server via the logger argument, but any future calls that try to find the InitModule via getInitModuleFn fails https://github.com/heroiclabs/nakama/blob/c3d3cc69ae69cf7f92469a63c8a574d2629af5a5/server/runtime_javascript_init.go#L975

This is the same for any hooks passed into the InitModule https://github.com/heroiclabs/nakama/blob/c3d3cc69ae69cf7f92469a63c8a574d2629af5a5/server/runtime_javascript_init.go#L1002

Unfortunately the solution to get all this working doesn't appear very clear cut. My assumption is that the next step is to write a Webpack plugin that outputs the InitModule and any of its hooks as top level functions with the expected names and signatures that then passthrough to the wrapped Webpack versions. This seems reasonable enough for the InitModule but I'm not immediately sure how to automatically identify and export the hook functions as well without requiring some sort of decorator that must be added

matthewvroman avatar Jul 30 '22 19:07 matthewvroman

Thanks for digging into this @matthewvroman. Given the complexity around setup and issues you've identified I think for now we will continue to support Rollup as our bundler of choice in the documentation as the process is much simpler for the end user. I also believe that while WebPack is a powerful tool, it's purpose extends far beyond transpilation and bundling and is perhaps not as well suited to being used in this context.

With that said, if you do manage to find a clean solution to the above problems I would be happy to re-evaluate this.

tomglenn avatar Aug 05 '22 09:08 tomglenn

@tomglenn I totally agree. I spent more time digging into how to accomplish what I stated above but it started to feel like I was trying to reverse all the things that webpack is normally used for. I switched my project to rollup and have had a much easier time getting that up and running.

If anything it might make sense to clarify some documentation around how Goja handles the Javascript runtime and explicitly warn against bundlers like webpack. The Compatibility section mentions there is no support for libraries requiring Node but it's easy to forget that bundlers such as Webpack rely on this as well- even if the code you're writing doesn't utilize Node.

matthewvroman avatar Aug 05 '22 21:08 matthewvroman