codymikol / karma-webpack

Karma webpack Middleware
MIT License
830 stars 221 forks source link

Webpack 5 & `webpackConfig.output.globalObject = "this"` results in webpackChunk error #497

Closed npetruzzelli closed 3 years ago

npetruzzelli commented 3 years ago

Expected Behavior

Successful build and test suite execution

Actual Behavior

Uncaught TypeError: Cannot read property 'webpackChunk' of undefined
  at C:/Users/%USERNAME%/AppData/Local/Temp/_karma_webpack_645953/runtime.js:100:64

  TypeError: Cannot read property 'webpackChunk' of undefined
      at C:/Users/%USERNAME%/AppData/Local/Temp/_karma_webpack_645953/runtime.js:100:64
      at C:/Users/%USERNAME%/AppData/Local/Temp/_karma_webpack_645953/runtime.js:130:12
      at C:/Users/%USERNAME%/AppData/Local/Temp/_karma_webpack_645953/runtime.js:137:12

The test suite isn't able to run at all.

NOTE: if globalObject is undefined, "window", or "self", then the build is successful. "this" is a valid, documented value, but results in an error.

This problem does not seem to happen when using karma-webpack with webpack 4.x.

Changing the globalObject option does not seem to change the resulting code when using webpack-cli to build a bundle file.

Code

Toggle Module Details **Babel Modules:** - `@babel/core` - `@babel/preset-env` **Webpack Modules** - `babel-loader` - `webpack` **Karma Modules** - `karma` - `karma-chrome-launcher` - `karma-ie-launcher` - `karma-jasmine` - `karma-webpack`
// ./package.json
{
  "private": true,
  "scripts": {
    "test": "karma start"
  },
  "devDependencies": {
    "@babel/core": "7.12.17",
    "@babel/preset-env": "7.12.17",
    "babel-loader": "8.2.2",
    "core-js": "3.9.0",
    "karma": "6.1.1",
    "karma-chrome-launcher": "3.1.0",
    "karma-ie-launcher": "1.0.0",
    "karma-jasmine": "4.0.1",
    "karma-webpack": "5.0.0",
    "webpack": "5.23.0"
  },
  "browserslist": [
    "last 1 chrome version",
    "last 1 edge version",
    "last 1 firefox version",
    "last 1 safari version",
    "ie 11"
  ]
}
// ./karma.conf.js
process.env.BABEL_ENV = 'test'
process.env.NODE_ENV = 'test'

const path = require('path')

module.exports = function(config) {
  config.set({
    plugins: [
      'karma-chrome-launcher',
      'karma-ie-launcher',
      'karma-jasmine',
      'karma-webpack',
    ],
    browsers: ['IE', 'Chrome'],
    frameworks: ['jasmine', 'webpack'],
    files: [{pattern: 'src/**/*.test.js', watched: false}],
    preprocessors:   {'src/**/*.test.js': ['webpack']},
    reporters: ['progress'],
    webpack: {
      mode: 'development',
      devtool: 'inline-source-map',
      output: {
        globalObject: 'this'
      },
      module: {
        rules: [
          {
            test: /\.(js|mjs|jsx|ts|tsx|cjs)$/,
            include: [path.resolve(__dirname, 'src')],
            loader: 'babel-loader',
            options: {
              // babel-loader specific options
              cacheDirectory: false,
              cacheCompression: false,

              // babel options
              compact: false,
              presets: [
                [
                  '@babel/preset-env',
                  {
                    useBuiltIns: 'entry',
                    corejs: '3.9',
                  }
                ]
              ]
            }
          }
        ]
      }
    }
  })
}
// ./src/foos.test.js
it('should pass', () => {
  expect(true).toBe(true)
})

How Do We Reproduce?

  1. Create the 3 files provided in the code section above into a directory
  2. Install the packages with yarn, though I expect the results to be the same with npm
  3. Run the script named test (yarn test --singleRun or npm test -- --singleRun
codymikol commented 3 years ago

In what scenario would you not want window to be the global object while using karma-webpack?

npetruzzelli commented 3 years ago

First I'd like to apologize, in getting more information to answer your question, I have learned that the problem is in Webpack, not karma-webpack.


Second, I'd like to answer your question anyway for the sake of anyone else who finds their way to this issue.

From Webpack's documentation:

output.globalObject

string = 'window'

When targeting a library, especially when libraryTarget is 'umd', this option indicates what global object will be used to mount the library. To make UMD build available on both browsers and Node.js, set output.globalObject option to 'this'. Defaults to self for Web-like targets.

For example:

webpack.config.js

module.exports = {
  // ...
  output: {
    library: 'myLib',
    libraryTarget: 'umd',
    filename: 'myLib.js',
    globalObject: 'this',
  },
};

Aside from this, I believe there there are 2 reasonable reasons:

  1. It worked with webpack 4 and the version of karma-webpack that supported it. There is no documentation in either project that suggests a specific value for this option no longer works, has been deprecated, or otherwise has been dropped. It is reasonable to assume that it should still work.
  2. The type error is horrible in that it does nothing to tell the developer what the problem was. I only found it by tearing apart very large karma, webpack, and babel configurations, option by option, until I broke it down to the minimum case I presented in the original post.

The fact that it doesn't work is surprising. Projects have every reason to believe that their configuration is valid when using this value for this option. Explicitly setting libraryTarget: "umd" doesn't help.


Third, I would like to share my findings.

The runtime code in question:

C:/Users/%USERNAME%/AppData/Local/Temp/_karma_webpack_645953/runtime.js ```js /******/ (function() { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({}); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(__webpack_module_cache__[moduleId]) { /******/ return __webpack_module_cache__[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = __webpack_modules__; /******/ /******/ // the startup function /******/ // It's empty as some runtime module handles the default behavior /******/ __webpack_require__.x = function() {}; /************************************************************************/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ !function() { /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } /******/ }(); /******/ /******/ /* webpack/runtime/jsonp chunk loading */ /******/ !function() { /******/ // no baseURI /******/ /******/ // object to store loaded and loading chunks /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched /******/ // Promise = chunk loading, 0 = chunk loaded /******/ var installedChunks = { /******/ "runtime": 0 /******/ }; /******/ /******/ var deferredModules = [ /******/ /******/ ]; /******/ // no chunk on demand loading /******/ /******/ // no prefetching /******/ /******/ // no preloaded /******/ /******/ // no HMR /******/ /******/ // no HMR manifest /******/ /******/ var checkDeferredModules = function() {}; /******/ /******/ // install a JSONP callback for chunk loading /******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) { /******/ var chunkIds = data[0]; /******/ var moreModules = data[1]; /******/ var runtime = data[2]; /******/ var executeModules = data[3]; /******/ // add "moreModules" to the modules object, /******/ // then flag all "chunkIds" as loaded and fire callback /******/ var moduleId, chunkId, i = 0, resolves = []; /******/ for(;i < chunkIds.length; i++) { /******/ chunkId = chunkIds[i]; /******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { /******/ resolves.push(installedChunks[chunkId][0]); /******/ } /******/ installedChunks[chunkId] = 0; /******/ } /******/ for(moduleId in moreModules) { /******/ if(__webpack_require__.o(moreModules, moduleId)) { /******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; /******/ } /******/ } /******/ if(runtime) runtime(__webpack_require__); /******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); /******/ while(resolves.length) { /******/ resolves.shift()(); /******/ } /******/ /******/ // add entry modules from loaded chunk to deferred list /******/ if(executeModules) deferredModules.push.apply(deferredModules, executeModules); /******/ /******/ // run deferred modules when all chunks ready /******/ return checkDeferredModules(); /******/ } /******/ /******/ var chunkLoadingGlobal = this["webpackChunk"] = this["webpackChunk"] || []; /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); /******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); /******/ /******/ function checkDeferredModulesImpl() { /******/ var result; /******/ for(var i = 0; i < deferredModules.length; i++) { /******/ var deferredModule = deferredModules[i]; /******/ var fulfilled = true; /******/ for(var j = 1; j < deferredModule.length; j++) { /******/ var depId = deferredModule[j]; /******/ if(installedChunks[depId] !== 0) fulfilled = false; /******/ } /******/ if(fulfilled) { /******/ deferredModules.splice(i--, 1); /******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]); /******/ } /******/ } /******/ if(deferredModules.length === 0) { /******/ __webpack_require__.x(); /******/ __webpack_require__.x = function() {}; /******/ } /******/ return result; /******/ } /******/ var startup = __webpack_require__.x; /******/ __webpack_require__.x = function() { /******/ // reset startup function so it can be called again when more startup code is added /******/ __webpack_require__.x = startup || (function() {}); /******/ return (checkDeferredModules = checkDeferredModulesImpl)(); /******/ }; /******/ }(); /******/ /************************************************************************/ /******/ /******/ // run startup /******/ var __webpack_exports__ = __webpack_require__.x(); /******/ /******/ return __webpack_exports__; /******/ })() ; ```

Which effectively breaks down to:

(function(){
  "use strict";
  !function(){ console.log(this) }()
})()
;

As MDN notes:

In strict mode, however, if the value of this is not set when entering an execution context, it remains as undefined

In Webpack v4, the bootstrap code does not include "use strict".

webpack/lib/MainTemplate.js#L154

                source.add("/******/ (function(modules) { // webpackBootstrap\n");

In Webpack 5, there is an allStrict variable which I would need to dig into to understand beyond reasonable assumptions.

webpack/lib/javascript/JavascriptModulesPlugin.js#L548-L569

        const allStrict = allModules.every(m => m.buildInfo.strict);

        let inlinedModules;
        if (bootstrap.allowInlineStartup) {
            inlinedModules = new Set(chunkGraph.getChunkEntryModulesIterable(chunk));
        }

        let source = new ConcatSource();
        let prefix;
        if (iife) {
            if (runtimeTemplate.supportsArrowFunction()) {
                source.add("/******/ (() => { // webpackBootstrap\n");
            } else {
                source.add("/******/ (function() { // webpackBootstrap\n");
            }
            prefix = "/******/ \t";
        } else {
            prefix = "/******/ ";
        }
        if (allStrict) {
            source.add(prefix + '"use strict";\n');
        }

So the problem lies in the webpack code setting strict mode, so this is no longer a reference to the window object. If there is a new option in Webpack 5 that allows us to influence allStrict then the answer may lie there. Otherwise this may be an issue that I'll need to ask the Webpack repository about.


As an aside, there is nothing explicitly telling Webpack users what all valid values are for output.globalObject. Since it looks like code in Webpack ends up being:

`${output.globalObject}[${globalPropertyName}] = ${javascriptCodeAsString}`

It is reasonable to assume the valid values are:

Other JavaScript environments may have other global objects, but that probably won't be relevant to many people who find their way to this issue.