Skip to content

Imported variable bindings have difficult to debug names. #3957

Open

Description

@everett1992

Do you want to request a feature or report a bug?
Feature, improvement to debugging webpack-built projects.

What is the current behavior?
Webpack renames variable bindings in ES6 imports when it replaces them with __webpack_require__ calls. In this example the imported binding foo is compiled to __WEBPACK_IMPORTED_MODULE_0__foo__["a" /* foo */]. If you use dev tools debugger you cannot watch or executefoo because it is not defined.

input:

import {foo} from './foo';
console.log(foo());

output:

Object.defineProperty(exports, "__esModule", { value: true });                 
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(0);                                                                          
console.log(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__foo__["a" /* foo */])());

Debugging is difficult, you can't watch or reference the original binding. This is made worse by sourcemaps because you can't see new name in the original source. It's not immediately apparent why a line copied from the current context into the console results in not defined errors. Chrome even removes code that has a corresponding source map, so it becomes difficult to find the in-code name of an imported value.

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

What is the expected behavior?
Variable bindings should be preserved by name.

If this is a feature request, what is motivation or use case for changing the behavior?
Debugging a project built by webpack today is more difficult than it aught to be.

Activity

  1. sokra commented on Jan 14, 2017

    @sokra
    Member

    Variable bindings should be preserved by name.

    If you tell me how this is possible, I'll do it.

  2. everett1992 commented on Jan 15, 2017

    @everett1992
    Author

    I'm not familiar with webpack's internals, or intricacies of the ES6 spec, so I understand why this could be harder than an outside observer would think.

    Why can't the example in the issue be compiled to

    Object.defineProperty(exports, "__esModule", { value: true });                 
    /* harmony import */ var foo = __webpack_require__(0)["a" /* foo */];                                                                          
    console.log(__webpack_require__.i(foo)());

    I guess I see the problem, if the module ./foo were to redefine it's export foo the binding in index.js would be out of date.

    1. Could one use static-analysis to check for happy-case no export redefinition?
    2. Could ES6 proxies help here (Proxy support probably prevents this from being a viable choice)
  3. sokra commented on Jan 16, 2017

    @sokra
    Member

    I guess I see the problem, if the module ./foo were to redefine it's export foo the binding in index.js would be out of date.

    yes, that's the problem. ES import are live bindings. Actually the a property in __webpack_require__(0) is a getter that may return different values each time.

  4. Rincewind3d commented on Jan 25, 2017

    @Rincewind3d

    Yeah, this is a problem for me, too. Debugging is much harder now :-(

  5. olsonpm commented on Feb 15, 2017

    @olsonpm

    Regarding whether it's possible, babel transforms exported binding assignments to export assignments directly. e.g.

    // from
    export let a = 5;
    export function setAto10() {
      a = 10 // <------- from a simple variable assignment
    }
    
    // to
    exports.setAto10 = setAto10;
    var a = exports.a = 5;
    function setAto10() {
      exports.a = a = 10 // <------- to an exported variable assignment
    }

    This solves the issue and avoids the need to use a getter. I can't think of any reason why the approach wouldn't work with webpack however I've only been digging into this problem as of tonight.

  6. sokra commented on Feb 15, 2017

    @sokra
    Member

    @olsonpm This issue is about imported variables not exported.

    The only way which I see to fix it is:

    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo__ = __webpack_require__(0);
    with({
      get foo() { return __WEBPACK_IMPORTED_MODULE_0__foo__ ["a"]; }
    }) {
      // your code
      // debugger could access `foo`
    }

    But... no...

  7. sdwio commented on Mar 31, 2017

    @sdwio

    One small step could be to preserve the variable names in the

    __WEBPACK_IMPORTED_MODULE_0__foo__

    array. In the example it is not that obvious since the file and variable are named foo. If you have

    import {bar, qux, sol} from './foo';

    the three variables will end up as "a", "b", and "c". If they would be visible as

    __WEBPACK_IMPORTED_MODULE_0__foo__["bar"]

    etc. it would not be perfect but usable.
    Yet I have no idea if this might cause other complications.

  8. yang commented on Sep 29, 2017

    @yang

    For projects that don't care for the live binding requirement and do care about a sane interactive debugging experience, can there be an option to just treat imports like regular bindings? (I imagine there are many such projects.)

  9. valscion commented on Oct 1, 2017

    @valscion

    Hmmm... what if webpack just flipped the variable name to be first and then appends the __WEBPACK_IMPORTED_MODULE_X stuff to the end? That way at least you could try to find the variable with tab autocompletion?

    input:

    import {foo} from './foo';
    console.log(foo());

    output:

    Object.defineProperty(exports, "__esModule", { value: true });                 
    /* harmony import */ var __foo__WEBPACK_IMPORTED_MODULE_0 = __webpack_require__(0);                                                                          
    console.log(__webpack_require__.i(__foo__WEBPACK_IMPORTED_MODULE_0["a" /* foo */])());

    Then you could write __foo and locate the correct variable from the autocomplete result.

  10. thecotne commented on Nov 7, 2017

    @thecotne

    @sokra that is actually very good for development (i think you also need set function to preserve all behaviors ...)

    and for production you will stick with current version (so uglify can do his things)

    am i missing something?

  11. thecotne commented on Nov 7, 2017

    @thecotne

    i did some research and i am missing strict mode

    but you can go around this issue by something like this

    (() => {
      const scope = {
        get name() { return 'cotne' }
      }
      // strict mode does not apply here so we can use "with" statement 
      with (scope) {
        (() => {
          "use strict"
          // package code goes here where strict mode applies 
          // you can't use "with" statement here but outer one is fine
          console.log(name)
        })()
      }
    })()
  12. MagicDuck commented on Jan 6, 2018

    @MagicDuck
    Contributor

    @sokra: stupid question. Could we just add an option to webpack which will make it assume that the harmony bindings are not live when turned on. Maybe somebody could correct me, but I can't think of any code where live bindings would be really useful/be a good practice. At least in our code base, I believe this to be the case. To me, it contradicts the whole concept ES6 import/export declaration being static. Or would this introduce a problem with stuff from node_modules/? Although in that case we can have some sort of regex to determine which modules to do this for...

  13. gaearon commented on Jan 9, 2018

    @gaearon
    Contributor

    I don’t think suggesting to treat modules not per spec is a good idea. I wouldn’t like to see an option to disable live bindings, it’s only going to cause endless confusion.

    Rollup somehow is able to preserve names in live bindings (due to module concatenation). I doubt that would be fast enough for development, but thought it’s worth mentioning.

  14. MagicDuck commented on Jan 9, 2018

    @MagicDuck
    Contributor

    @gaearon and the weird import names are not causing endless confusion...? 😆

  15. gaearon commented on Jan 9, 2018

    @gaearon
    Contributor

    They are a debugging inconvenience, sure. (Potentially solvable by tooling.)

    Coming up with your own language semantics for an existing language feature to avoid an inconvenience is a much harder problem to unravel.

  16. 14 remaining items

  17. NikhilVerma commented on Nov 14, 2019

    @NikhilVerma

    Here's another suggestion then:

    Rather than going with a with block, we could wrap the whole file with a IIFE with named parameters. That would work with debugging the 99% case when exports are constant, while still allowing the actual code run as defined in ES6. Also, this would only apply if in dev mode.

    This:

    import {a, b} from 'foo'
    console.log(a);
    console.log(b);

    would then be transformed to this (I've omitted the hideous WEBPACK_IMPORTED_MODULE...)

    var __foo = __webpack_require__(0)
    ((a, b) => {
        console.log(__foo["a"]);
        console.log(__foo["b"]);
    })(__foo["a"], __foo["b"]);

    If this has performance implications it can only be enabled for development mode

  18. Gvozd commented on Nov 30, 2019

    @Gvozd

    @sokra , May support mutated imports, if register setter in consumer module, as the getter is now registered in the source module?
    Sоmething like this:

    consumer.js

    import {foo as bar} from './source';
    setInterval(() => console.log(bar), 1000);
    // -->
    
    // when changed(=, +=, ...) export `foo` in module `sourceModuleId`, then need call setters of all consumer modules
    __webpack_require__.registerSetter(__webpack_exports__, sourceModuleId, "foo", function(val) { bar = val; })
    let bar;
    setInterval(() => console.log(bar), 1000);//no changes. imported variable available as is

    source.js:

    export let foo = 123;
    setInterval(() => {
      foo = foo + 1;
    }, 1000);
    // ---->
    let foo;
    __webpack_require__.notify(sourceModuleId, 'foo', foo = 123);
    setInterval(() => {
      //exported variable available in scope as is
      __webpack_require__.notify(sourceModuleId, 'foo', foo = foo + 1);
    }, 1000);

    Of course, this solution can lead to performance problems, but this behavior has the right to live in development mode.

  19. Moghul commented on Dec 20, 2019

    @Moghul

    If you guys don't mind me asking, what is the debugging process supposed to look like with imports like this? How do you track down "unexpected behaviour" style bugs if you can't easily identify what you're looking at and what the values are? This is almost like debugging uglified/obfuscated code.

  20. NikhilVerma commented on Dec 20, 2019

    @NikhilVerma

    If you guys don't mind me asking, what is the debugging process supposed to look like with imports like this? How do you track down "unexpected behaviour" style bugs if you can't easily identify what you're looking at and what the values are? This is almost like debugging uglified/obfuscated code.

    It's not convenient at all. my strategies are:

    1. Disable source maps temporarily. Then you can see the real variable names and debug them properly.
    2. Memorise the webpack naming conventions. If you are at a breakpoint, and your import is similar to import UserModel from 'xxx'. Then the webpack bindings are usually called __WEBPACK_IMPORTED_MODULE_0__UserModel__. The devtools are usually friendly enough to autocomplete for you if you start typing __ and then go from there.
  21. Moghul commented on Dec 22, 2019

    @Moghul

    Ok so what you're saying is "use commonjs".

  22. Yehonal commented on Jun 12, 2020

    @Yehonal

    I think it's an issue with all kind of transpilers: microsoft/TypeScript#9627 (comment)

  23. staeke commented on Jun 19, 2020

    @staeke

    For webpack 4, use this plugin. This will simplify names if you're using eval and allow straight import vars to be used with source maps when debugging (if they're not reassigned, which probably counts for 99.9% of the cases).

    So this code:

    import a, {b, c as d} from 'foo'
    a()

    becomes

    var __foo = webpack_require('...'); let {default: a, b, c: d} = __foo; if (typeof d === "undefined") d = _foo
    _foo.a()

    https://gist.github.com/staeke/f197ff144e379aa884f91b95003f4ee1

  24. epikhighs commented on Dec 15, 2020

    @epikhighs

    I couldn't get transform-es2015-modules-commonjs-simple to work anymore, but setting modules: "cjs" in babel config's @babel/preset-env seems to work for me. https://babeljs.io/docs/en/babel-preset-env#modules

    {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": "cjs",
          }
        ],
      ]
    }
    
  25. Megabyteceer commented on Aug 27, 2021

    @Megabyteceer

    I think in development build imports should work in next way:
    At top level of the module declare empty variable with readable name of imported variable. At very beginning this variable will be empty because imported module could has no export defined yet.

    At entrance to code block, where imported variable is used - assign value to such a top level variable from module exports. So it is will become visible in debugger at the moment we need it. And now generated code can access to imported variables by its real name without go to imported module exports each time.

  26. Trias commented on Jan 28, 2022

    @Trias

    Based on @staeke 's comment i've created a webpack plugin which will add debug names to generated files (with @staeke permission).

    See https://github.com/Trias/debug-import-vars-webpack-plugin

    Happy debugging!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions