webpack: Optional chain is incorrectly removed when used on import

Bug report

What is the current behavior?

Webpack incorrectly compiles x?.value to (something).value when x is something that’s been imported.

If the current behavior is a bug, please provide the steps to reproduce.

webpack.config.js:

module.exports = {
    entry: "./index.js",
    target: "web",
};

index.js:

import x from './x.js';
console.log(`optional value is: ${x?.value}`);

x.js:

export default undefined;

Produces dist/main.js:

(()=>{"use strict";console.log(`optional value is: ${(void 0).value}`)})();

which leads to TypeError: Cannot read property 'value' of undefined at runtime.

What is the expected behavior?

The ? should be retained or converted to a ternary operator.

When import x from './x.js'; is removed and replaced with const x = (() => void console)(); (something complicated enough that webpack doesn’t completely constant-fold it), the output is:

(()=>{const o=void console;console.log(`optional value is: ${o?.value}`)})();

which is as expected (the ? is retained).

Other relevant information: webpack version: latest, 5.27.2 Node.js version: v15.9.0 Operating System: macOS 11.2.2

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 29
  • Comments: 26 (10 by maintainers)

Commits related to this issue

Most upvoted comments

seems to be included in 5.71.0 now 🥳

image

A simple workaround we’ve found is referencing the imported module as another variable. For example:

import x from './x.js';
const xRef = x;
console.log(`optional value is: ${xRef?.value}`);

Not ideal, but it works until this can be fixed.

5.71.0 now works even without environment: { optionalChaining: true }. Thanks!

No, this issue about supporting a.js

export * as c from './c.js'

index.js

import * as a from './a.js'
a.c?.b // or
a?.c?.b

// right now will produce
a.c.b
a.c.b

to produce optional chain in output we need outputOptions.environment.supportOptionalChaining

Snowpack users are likely to run into this issue, and we came up with a similar solution to this one to configure Snowpack’s Webpack plugin.

[
            "@snowpack/plugin-webpack",
            {
                extendConfig: (config) => {
                    const targetPlugin = config.module.rules.reduce(
                        (acc, rule) => acc ?? rule.use.find((plugin) => plugin.loader.includes("babel-loader")),
                        undefined
                    );
                    if (!targetPlugin) {
                        throw new Error("Could not find babel-loader plugin!");
                    }
                    targetPlugin.options.plugins = [require.resolve("@babel/plugin-proposal-optional-chaining")];

                    return config;
                },
            },
        ],

I didn’t realise the include option existed though - that might let us simplify this a bit.

I also ran into this issue. To avoid this issue cropping back up before a fix is implemented, I ended up forcing optional chaining to be transformed by babel before webpack has a chance to remove it. Obviously this removes the actual optional chaining operators in the bundled code, but at least it’s functionally equivalent.

babel.config.js:

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        // force optional chaining transform
        include: ['proposal-optional-chaining'],
        // omitting the rest of my preset-env config for brevity
      }
    ],
  ]
};

@alexander-akait @sokra Any suggested workarounds for this issue?