How to solve this basic ES6-module circular dependency problem?

# /#!/JoePea (8 years ago)

I have the very basic problem detailed at stackoverflow.com/questions/38841469, but thought I'd bring it up here because it makes me wonder about the ES6 Module system.

Why is it that the body of module C is not evaluated before the bodies of modules A and B? I'm just wondering, because if that were the case, then it would work and the entrypoint would eventually be evaluated without errors.

A possibility could be that maybe it should work, and that my ES6 module environment simply doesn't handle it as it should? (But I'm not familiar enough with the spec, so that's why I'm not sure about that.)

These are the modules, posted here for convenience:

// --- Entrypoint

import A from './app/A'
console.log('Entrypoint', A)
// --- Module A

import C from './C'

console.log('Module A', C)

class A extends C {
    // ...
}

export {A as default}
// --- Module B

import C from './C'

console.log('Module B', C)

class B extends C {
    // ...
}

export {B as default}
// --- Module C

import A from './A'
import B from './B'

console.log('Module C', A, B)

class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}
# /#!/JoePea (8 years ago)

Oops, I forgot the export in module C:

// --- Module C

import A from './A'
import B from './B'

console.log('Module C', A, B)

class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

export {C as default} // export!
# /#!/JoePea (8 years ago)

For reference, I'm trying this in Meteor, in which case the first module to be evaluated is module B, but that means that C is undefined during evaluation of B, so we get an error.

# Jason Orendorff (8 years ago)

On Tue, Aug 9, 2016 at 3:03 PM, /#!/JoePea <joe at trusktr.io> wrote:

Why is it that the body of module C is not evaluated before the bodies of modules A and B?

The module system does execute all a module's dependencies before executing that module, except in cases like this where there are cycles in the dependency graph and that requirement is therefore impossible to satisfy for all modules simultaneously.

# /#!/JoePea (8 years ago)

True, and so that's why I'm wondering if the module system can see that it can satisfy all module requirements if it simply evaluates module C first, followed by A or B in any order. It is easy for us humans to see that. It would be nice for the module system to see that as well (I'm not sure if that is spec'd or not).

# Tab Atkins Jr. (8 years ago)

On Tue, Aug 9, 2016 at 4:00 PM, /#!/JoePea <joe at trusktr.io> wrote:

True, and so that's why I'm wondering if the module system can see that it can satisfy all module requirements if it simply evaluates module C first, followed by A or B in any order. It is easy for us humans to see that. It would be nice for the module system to see that as well (I'm not sure if that is spec'd or not).

That knowledge requires, at minimum, evaluating the rest of each module, beyond what is expressed in the import statements. That's assuming there's no dynamic trickery going on that would invalidate whatever assumptions it can draw from surface-level analysis.

Because of this, only the import statements are declaratively available to the module system to work with. Based on that, it definitely can't make any ordering assumptions; all it knows is that A imports C, B imports C, and C imports both A and B, making a circular import.

# John Lenz (8 years ago)

Without a way to load "later" (aka "soft") dependencies, ES6 module will continue to be more or less broken for circular dependencies.

# /#!/JoePea (8 years ago)

It seems that the environment I'm in (Meteor uses reify) tries to evaluate A and B first, so I thought I could take advantage of "live bindings" by changing my modules to the following:

// --- Entrypoint

import A from './app/A'
console.log('Entrypoint', A)
// --- Module A

import C from './C'

let A

export
function setUpA() {

    console.log('setUpA')
    console.log(C)

    A = class A extends C {
        // ...
    }

}

console.log('Module A', C, setUpA)

export {A as default}
// --- Module B

import C from './C'

let B

export
function setUpB() {

    console.log('setUpB', C)

    B = class B extends C {
        // ...
    }

}

console.log('Module B', C, setUpB)

export {B as default}
// --- Module C

import A, {setUpA} from './A'
import B, {setUpB} from './B'

let C = class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

setUpA()
console.log('Module C', A)

setUpB()
console.log('Module C', B)

export {C as default}

As you can see, modules A and B simply export the code that should be evaluated (note the live bindings). Then finally, the C module is evaluated last. At the end of the C module, you see that it calls setUpA and setUpB. When it fires setUpA, an error is thrown on the second console.log that C is undefined (or, specifically, C.default is undefined because the ES6 modules are compiled into CommonJS form).

I thought that if C was a live binding, then it should be ready by the time the setUpA function is called. Should this in fact be the case?

# /#!/JoePea (8 years ago)

I can get the whole thing to work if I pass the C dependency into the setUpA and setUpB functions as follows, but oddly A is undefined in the Entrypoint module at the console.log statement, which makes it seem to me like live bindings aren't working the I was expecting.

// --- Entrypoint

import A from './app/A'
console.log('Entrypoint', A) // HERE, output: "Entrypoint undefined"
// --- Module A

import C from './C'

let A

export
function setUpA(C) {

    console.log('setUpA')
    console.log(C)

    A = class A extends C {
        // ...
    }

}

console.log('Module A', C, setUpA)

export {A as default}
// --- Module B

import C from './C'

let B

export
function setUpB(C) {

    console.log('setUpB', C)

    B = class B extends C {
        // ...
    }

}

console.log('Module B', C, setUpB)

export {B as default}
// --- Module C

import A, {setUpA} from './A'
import B, {setUpB} from './B'

let C = class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

setUpA(C)
console.log('Module C', A)

setUpB(C)
console.log('Module C', B)

export {C as default}
# /#!/JoePea (8 years ago)

When I try this same code with Webpack, I get the exact same results: the console.log statements in the exact same order, where the last output shows that A in the entry point is undefined).

Am I misunderstanding something about live bindings? Is there some guaranteed order in which these modules should be evaluated?

The reason why I'm after a solution for the circular dependency is because in my real-world case I need to use instanceof A and intanceof B within the C superclass defined in module C. This is a case of the Fragile Base Class Problem where a class should usually not have knowledge of it's subclasses, but the base class in my case is intended to be internal only, not a part of the public API that end users will extend from.

# Isiah Meadows (8 years ago)

First of all, I'll point out that even if it's an internal API, you should just initialize them immediately. You already have an otherwise fully initialized C, so you should just add them whenever it comes. You shouldn't need a setUpA export, especially called by one of its dependencies. Just declare and initialize that crap when it's being declared.

/* index.js */
import A from './app/A'
console.log('Entrypoint', A)
/* app/A.js */
import C from './C'

export default class A eclxtends C {
    // ...
}

// set up A here
console.log('Module A')
/* app/B.js */
import C from './C'

export default class B extends A {
    // ...
}

// set up B here
console.log('Module B')
/* app/C.js */
import A from './A'
import B from './B'

export default class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

// set up C
console.log('Module C')

What's your full output, anyways? That would help me best explain what's going on, though.

# /#!/JoePea (8 years ago)

In your module B, class B should extends C instead of A, so both classes A and B extend from C.

I made a reproduction that you can run (assuming you have Meteor installed). See the following with instructions: meteor meteor/issues/7621#issuecomment-238923360

But, anyways, the example you just gave is almost identical to my [original example](esdiscuss.org/topic/how-to-solve-this- basic-es6-module-circular-dependency-problem#content-0) (except for my B class extends from the C class).

I can make a reproduction of that too if you want, but what happens is that the environment will try to execute module A before executing module C, at which point C is undefined inside of module A. Basically, the result would be the same as writing:

class A extends undefined {} // throws an Error

I noticed that both Meteor and Webpack will try and execute modules A and B first, then finally C, so I thought I could export the setup functions andrun them after defining the C class so that even if modules A and B run first the actual class definitions would run after the C class definition. You can see that I'm trying to take advantage of "live bindings" in order for this to work (but it didn't, hence I have to pass C into the setup functions).

I have a feeling that both Meteor's and Webpack's implementations aren't up-to-spec as far as live bindings with circular dependencies, but I could be wrong.

You shouldn't need a setUpA export, especially called by one of its

dependencies. Just declare and initialize that crap when it's being declared.

That's what I thought at first too, but it's not the case, and I'm trying to find a solution.

# /#!/JoePea (8 years ago)

Isaiah, also note that

export default class Foo {}

does not create a live binding that can be modified at a later point in time, which is the feature that my setUpA and setUpB functions are theoretically relying on (and which I believe the Meteor and Webpack environments don't handle properly if I understand live bindings correctly).

# Isiah Meadows (8 years ago)

What do you get out of Rollup.js (which actually implements the ES6 spec with bindings, unlike Webpack, etc., which handle CommonJS modules)? You should be able to run the bundled repro in Node, or you could inspect the bundle yourself just as easily to see the execution order.

# /#!/JoePea (8 years ago)

Oh! And although I think that my setUpA and setUpB technique should work due to the fact that Webpack and Meteor load the modules in the exact same order where the C module is executed last, this may in fact fail in some other ES6 environment that happens to execute the C module first, in which case setUpA and setUpB will be undefined when C is evaluated.

So, I don't know if my solution is good. I am wondering if there's something in the spec that guarantees that the C module evaluates last?

# Isiah Meadows (8 years ago)

To clarify, Webpack and friends translate ES6 modules to CommonJS modules pretty naïvely, so the bindings won't get translated correctly (CommonJS modules export values, not read-only bindings). Rollup actually handles the exports correctly as static bindings instead. So that's why I recommended testing it in Rollup.

# Bradley Meck (8 years ago)

Please note that in tc39.github.io/ecma262/#sec-moduleevaluation , Modules evaluate their children prior to evaluating themselves (15.2.1.16.5.6.c) , C should never be evaluate before A or B in this dep graph.

# /#!/JoePea (8 years ago)

Isiah, here's the rollup.js result using my setup functions technique. When I paste the result in my console it complains that A is undefined inside the setUpA function, which seems odd. Here's the result of my original code (similar to your example), and as you can see it will evaluate B first in which case C will be undefined and throw an error.

Bradley, true, but C is also child of A, so it can also make sense to evaluate C before A. They are children of each other. In that case, what is the correct order of evaluation?

# /#!/JoePea (8 years ago)

Aha, when using my setup functions, rollup.js tries to evaluate C first, in which case the hoisted functions are available, but A is not declared yet. I don't think that this would happen in real ES6 modules (no hoisting like that).

# Bradley Meck (8 years ago)

---------- Forwarded message ---------- From: Bradley Meck <bradley.meck at gmail.com>

Date: Wed, Aug 10, 2016 at 3:05 PM Subject: Re: How to solve this basic ES6-module circular dependency problem? To: /#!/JoePea <joe at trusktr.io>

Bradley, true, but C is also child of A, so it can also make sense to

evaluate C before A. They are children of each other. In that case, what is the correct order of evaluation?

The graph is traversed in order of declaration in source text, depth first. trusktr/meteor/blob/issue-7621/meteor-app/client/main. js#L23 will be evaluated only after all of trusktr meteor/blob/issue-7621/meteor-app/client/main.js#L22 has been evaluated.

  • A will never load via before C, because C loads A first. (confusing wording since we have circular stuff here)
  • A attempting to load C when C has already begun evaluating results in the short circuit at 15.2.1.16.5.4.
# /#!/JoePea (8 years ago)

Bradley, thanks! Based on what you've pointed out, if the entrypoint imports only B from module B, then that should cause the import lookup to go to module C, which sees the import for A and causes the lookup to go to module A, and finally depth first executes A, then C, then finally B. This is in fact what I'm seeing in rollup.js.

Users of my API (names are more meaningful in reality, but I have three modules defining classes in exactly the same way) will only ever import A or B directly, but never C, so I should be able to use that fact to formulate a solution. I'm not sure what the solution is yet though.

# /#!/JoePea (8 years ago)

I found a solution that works in environments compiled by Babel, using the workaround suggested by Ben Newman:

// --- Module A

import C from './C'

let A = A // @benjamn's workaround applied

export
function setUpA(C) {

    A = class A extends C {
        // ...
    }

}

export {A as default}
// --- Module B

import C from './C'

let B = B // @benjamn's workaround applied

export
function setUpB(C) {

    B = class B extends C {
        // ...
    }

}

export {B as default}
// --- Module C

import A, {setUpA} from './A'
import B, {setUpB} from './B'

let C = class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

setUpA(C)
setUpB(C)

export {C as default}
// --- Entrypoint

import A from './A'
console.log('Entrypoint', new A) // runs the console.logs in the C
constructor.

Although that works in my environment which is compiled from ES6 modules to CommonJS by Babel, it doesn't work in Rollup.js, and may not work in other ES6 module implementations.

Is there some solution that will theoretically work in any ES6 module environment?

# Logan Smyth (8 years ago)

You could also swap that around to simplify the logic in the individual classes, e.g.

// --- Module A

import C, {initC} from './c';

initC();

console.log('Module A', C)

class A extends C {
    // ...
}

export {A as default}

then export that function to force-initialize the C variable.

// --- Module C

import A from './a'
import B from './b'

var C;

export function initC(){
    if (C) return;

    C = class C {
        constructor() {
            // this may run later, after all three modules are evaluated, or
            // possibly never.
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default};

So all the individual classes have to do is call the initC function to ensure C has been properly initialized, then go from there. Note the usage of var here is also critical since let would throw a TDZ error.

# /#!/JoePea (8 years ago)

Alright, so I believe I have found the solution. It is not possible to guarantee a certain module evaluation order, but using some clever (but tedious) conditional checking I believe the problem is solved (with two caveats listed after):

// --- Entrypoint
import A from './A'
console.log('Entrypoint', new A)
// --- Module A

import C from './C'
import {setUpB} from './B'

let A

export
function setUpA(C) {

    if (!A) {
        A = class A extends C {
            // ...
        }
    }

}

if (setUpA && C) setUpA(C)
if (setUpB && C) setUpB(C)

export {A as default}
// --- Module B

import C from './C'
import {setUpA} from './A'

let B

export
function setUpB(C) {

    if (!B) {
        B = class B extends C {
            // ...
        }
    }

}

if (setUpA && C) setUpA(C)
if (setUpB && C) setUpB(C)

export {B as default}
// --- Module C

import A, {setUpA} from './A'
import B, {setUpB} from './B'

class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

if (setUpA && C) setUpA(C)
if (setUpB && C) setUpB(C)

export {C as default}

The caveat is that this fails in both Babel environments and in Rollup. For it to work in Babel environments, let A and let B have to be changed to let A = A and let B = B, as per the fault in Babel's ES2015-to-CommonJS implementation pointed out by Ben Newman. And it fails in Rollup because Rollup isn't really creating live bindings, which is fine in most cases, but doesn't work with these circular dependencies. The Rollup output does not create the C reference before it is ever used in the first pair of conditional checks, unlike what (I think) would happen with real live bindings (please correct me if wrong). To understand what I mean in the case of Rollup, just run if (FOO) console.log(FOO) in your console, and you'll get an error because FOO is not defined. Had the bindings been live, then FOO would be defined.

# Logan Smyth (8 years ago)

Keep in mind let A = A; is a TDZ error in any real ES6 environment.

The example I posted works properly with Babel's live-binding implementation and should require less repetition. What were your thoughts on it?

# /#!/JoePea (8 years ago)

Are bindings live across all modules at the same time, as if the identifier were defined in an outer scope common to all the modules that import it?

# Logan Smyth (8 years ago)

Actively thinking about "live bindings" may honestly confuse the way you think about imports. When you do

export var foo = "value";

then

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

Every time you access foo in the second module, you are literally reaching into the first module to grab the current value of foo. You can think of it like this, in CommonJS terms:

var foo = "value";

Object.defineProperty(exports, 'foo', { get(){ return foo; } }

and

var fooMod = require('./foo');
console.log(fooMod.foo);

so accessing the 'foo' export calls a getter method to access the current value of foo, so any change to the value inside the first module will immediately change the next time you trigger the getter.

# /#!/JoePea (8 years ago)

Every time you access foo in the second module, you are literally

reaching into the first module to grab the current value of foo.

Exactly! So, that means that in theory, I should be able to remove the C parameters from my setUpA and setUpB functions in my last example, so it becomes the following (but it doesn't work in some Babel environments (Meteor, Webpack) or Rollup).

// --- Entrypoint
import A from './A'
console.log('Entrypoint', new A)
// --- Module A

import C from './C'
import {setUpB} from './B'

let A

export
function setUpA() {

    if (!A) {
        A = class A extends C {
            // ...
        }
    }

}

if (setUpA && C) setUpA()
if (setUpB && C) setUpB()

export {A as default}
// --- Module B

import C from './C'
import {setUpA} from './A'

let B

export
function setUpB() {

    if (!B) {
        B = class B extends C {
            // ...
        }
    }

}

if (setUpA && C) setUpA()
if (setUpB && C) setUpB()

export {B as default}
// --- Module C

import A, {setUpA} from './A'
import B, {setUpB} from './B'

class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.
        console.log(A)
        console.log(B)
    }
}

if (setUpA && C) setUpA()
if (setUpB && C) setUpB()

export {C as default}

I found that with Ben Newman's deferred imports idea, the problem literally goes away:

// --- Entrypoint
import A from './A'
console.log('Entrypoint', new A)
// --- Module A

import C from './C'

class A extends C {
    // ...
}

export {A as default}
// --- Module B

import C from './C'

class B extends C {
    // ...
}

export {B as default}
// --- Module C

class C {
    constructor() {
        // this may run later, after all three modules are evaluated, or
        // possibly never.

        // reach into the A and B modules
        import A from './A'
        import B from './B'

        console.log(A)
        console.log(B)
    }
}

export {C as default}
# /#!/JoePea (8 years ago)

The example I posted works properly with Babel's live-binding

implementation and should require less repetition. What were your thoughts on it?

Your initC solution is working in Meteor (Babel + Reify) and Webpack+Babel, but the initC logic seems to run twice, as if there are two C variables instead of one. The following code is based on yours, and the console.log('initC!!!') statement unexpectedly executes twice, and you'll see output like this:

module B
initC!!!
module C
initC!!!
module A
function A() { ... }
function B() { ... }
Entrypoint A {}

I'm also not sure how initC can be defined when it is called in the B module, which is evaluated before C and A. The evaluation order is B, C, A (depth first). Does the initC function get hoisted into a scope common with all three modules? That is the only way that would seem to explain it, but that seems to go against the intuition I had that each module had it's own module scope (as if it were wrapped inside a function() {}, and therefore I thought the initC function would be hoisted within the C module, and that with B being evaluated first I thought an "undefined" error would be thrown when it tried to execute initC (but that is not happening!). How is it that initC can be available to the B module before C is evaluated?

This is the code I have:

// --- Entrypoint
import A from './A'
console.log('Entrypoint', new A)
// --- Module A

import C, {initC} from './C'

console.log('module A')
initC()

class A extends C {
    // ...
}

export {A as default}
// --- Module B

import C, {initC} from './C'

console.log('module B')
initC()

class B extends C {
    // ...
}

export {B as default}
// --- Module C

import A from './A'
import B from './B'

console.log('module C')
let C

export function initC(){
    if (C) return

    console.log('initC!!!')

    C = class C {
        constructor() {
            // this may run later, after all three modules are evaluated, or
            // possibly never.
            console.log(A)
            console.log(B)
        }
    }
}

initC()

export {C as default}
# Logan Smyth (8 years ago)

Your initC solution is working in Meteor (Babel + Reify) and

Webpack+Babel, but the initC logic seems to run twice, as if there are two C variables instead of one. The following code is based on yours, and the console.log('initC!!!') statement unexpectedly executes twice, and you'll see output like this:

Damn, that's a Babel bug with the block scoping logic. That said, as in my example, that needs to be var C; anyway, let would throw (in an environment with working TDZ anyway). Changing it to var also stops the duplicate printing.

I'm also not sure how initC can be defined when it is called in the B

module, which is evaluated before C and A. The evaluation order is B, C, A (depth first). Does the initC function get hoisted into a scope common with all three modules? That is the only way that would seem to explain it, but that seems to go against the intuition I had that each module had it's own module scope (as if it were wrapped inside a function() {}, and therefore I thought the initC function would be hoisted within the C module, and that with B being evaluated first I thought an "undefined" error would be thrown when it tried to execute initC (but that is not happening!). How is it that initC can be available to the B module before C is evaluated?

There are two separate pieces to executing a module, Instantiation, and Evaluation, which are what comes into play here. When you tell a JS environment to execute a file, it will instantiate every ES6 module in dependency graph before beginning to execute any of the modules. Babel does its best to simulate this behavior, though it's not perfect at it. One of the things that happens during module instantiation is that hoisted declarations are initialized, which means you can import and call a function declaration from any module, even if that module hasn't started the Evaluation phase yet, the same way it'd work with other cases of hoisting, where execution hasn't reached the function declaration, but it is available early.

This behavior is why you can't use let C there, because when B is being evaluated, the let C line won't have run because the Evaluation phase of C hasn't started yet. You can however access the initC function because it is a function declaration. As long as there are no TDZ errors in what you're doing, that function can do whatever it would like, assuming it doesn't depend on other stuff that would require Evaluation to have finished in C, the same as what happens with hoisting normally. That means for instance you couldn't do

import B from './B';
var SOME_CONSTANT = "hello";

export function initC(){
    return SOME_CONSTANT;
}

because calling initC here would return undefined if called from inside a dependency cycle from B.

# /#!/JoePea (8 years ago)

Damn, that's a Babel bug with the block scoping logic. That said, as in

my example, that needs to be var C; anyway, let would throw (in an environment with working TDZ anyway). Changing it to var also stops the duplicate printing.

Should I open an issue there? And I think you're right about it should throw, because it is strange that the function can be executed before the module is ever evaluated (that seems like it should be impossible), and therefore a TDZ error would happen because the let line wasn't theoretically evaluated yet.

I don't see how it is possible with var. How is it that vars or functions can be hoisted out of the module? Is that part of spec? If so, then that is different hoisting from function-based hoisting of pre-ES6.

I was under the impression that the modules were like a function, and hoisting would only happen inside the module. In that case, the initC function could not possibly be available until the C module itself was evaluated, so I was expecting for there to be an undefined error when initC was called before the C module was evaluated.

You say that

which means you can import and call a function declaration from any

module, even if that module hasn't started the Evaluation phase yet, the same way it'd work with other cases of hoisting, where execution hasn't reached the function declaration, but it is available early.

Which makes sense based on what I see happening, but it seems strange because it means that the scope of the module (as far as hoisting is concerned) is not the module itself, but rather some outer scope that wraps all the modules that import a given symbol.

You said,

the same way it'd work with other cases of hoisting, where execution

hasn't reached the function declaration, but it is available early.

Pre-ES6 "hoisting" in javascript happens on boundaries set by functions, and I thought that modules would be similar to function bodies, and therefore I thought that hoisting would be limited to within a module and did not expect hoisting to go beyond the module boundary to some scope that encompasses multiple modules. That to me is a different type of "hoisting" than what I know from pre-ES6 JavaScript's function-scope hoisting so it isn't necessarily the "same way it'd work with other cases of hoisting"; there is definitely some ES6-module-specific stuff happening that is a little different from pre-ES6 function-scope hoisting. (sidenote: I've asked the awesome Axel of 2ality to add these useful details to his articles.)

If I use the var method as you proposed (which is working in my Babel env), should I expect that method to always work in any theoretical 100%-implemented-to-spec ES6 environment, not just in Babel?

If so, then this may be one of the rare cases of "when we'd want to actually use var instead of let" besides for cases when we want pre-ES6 hoisting which I think should be generally avoided in order to make code less error-prone and easier to understand. This behavior would have been nearly-impossible to know about without the knowledge gained from this conversation (or from reading the spec in depth which can be difficult).

# Logan Smyth (8 years ago)

Should I open an issue there? And I think you're right about it should

throw, because it is strange that the function can be executed before the module is ever evaluated (that seems like it should be impossible), and therefore a TDZ error would happen because the let line wasn't theoretically evaluated yet.

If you'd like, go for it. To note, Babel 6 doesn't implement TDZ at all at the moment anyway.

I don't see how it is possible with var. How is it that vars or functions can be hoisted out of the module? Is that part of spec? If so, then that is different hoisting from function-based hoisting of pre-ES6.

They are not hoisted out of the module. It seems like you may be misunderstanding how modules are linked together.

Pre-ES6 "hoisting" in javascript happens on boundaries set by functions, and I thought that modules would be similar to function bodies, and therefore I thought that hoisting would be limited to within a module and did not expect hoisting to go beyond the module boundary to some scope that encompasses multiple modules. That to me is a different type of "hoisting" than what I know from pre-ES6 JavaScript's function-scope hoisting so it isn't necessarily the "same way it'd work with other cases of hoisting"; there is definitely some ES6-module-specific stuff happening that is a little different from pre-ES6 function-scope hoisting. (sidenote: I've asked the awesome Axel of 2ality to add these useful details to his articles.)

Hoisting of var and functions behaves the same way in ES6 and in ES5 and you are correct there is no "beyond the module boundary". Let's clarify hoisting in a function.

function fn() {
    console.log(inner());

    function inner(){ return "hello world"; }
}
fn();

in this context, when an engine executes fn(), before any execution has happened inside the fn, the engine does the following

  1. Create the conceptual function "scope", which exists immediately as soon as the function is called.
  2. Look for all function declarations inside fn and create their variables and assign their values to point to function objects. This is "function hoisting".
  3. Look for all var declarations inside fn and create variables with the value undefined. This is "var hoisting".
  4. Look for all let/const declarations, and create uninitialized variables (these will throw when accessed)
  5. A bunch of other stuff I'm skipping
  6. Execute the function body itself.

A very similar process happens for modules. You can think of it like steps 1-4 running, then before executing the module body (step 6), we recursively do this same process on every imported module. So by the time any module gets to step 6, every imported module, and every module those modules depend on, will have executed step 1-4, and have scopes that have been created, and variables that have been declared (possibly with a value, or possibly left uninitialized).

So by the time you get to step 6, there isn't a "beyond the module boundary", when you access the imported variables in your module, the JS engine will reach across the module boundary for you, to get the current value of the variable in the imported module. This behavior of reaching across module scopes is what module syntax allows, and it is what enables live binding.

Because of this live behavior, if you imported something that was defined with let, like your let C; example, it would cause a TDZ error because the variable was still "uninitialized", whereas if you make it a var, it will be initialized to undefined.

If I use the var method as you proposed (which is working in my Babel

env), should I expect that method to always work in any theoretical 100%-implemented-to-spec ES6 environment, not just in Babel?

Correct.

# J Decker (8 years ago)

Just out of curiosity; And because this is a singular issue using babel... why are you bothering to use import when nothing supports it or apparently plans to?

Why Was import designed in such a way to not be polyfillable? But instead requires a magical syntax to invoke?

Why structured reuse of modules such a bastard child?

# /#!/JoePea (8 years ago)

Thanks for that explanation Logan, that was very helpful!

I understand correctly, function scopes are instantiated only right before a function will run, while the module scopes are all instantiated first before any modules are evaluated, not right before the module code will be evaluated. That's where my assumption was wrong, as I was thinking that the hoisting would happen right before module evaluation (similar to functions), and therefore it would have been impossible for module B to access initC in your example, but now I get it. Thanks a ton!

# /#!/JoePea (8 years ago)

Hello Logan, I wonder if I can borrow your expertise for just a moment:

I ran into another problem. Suppose I have the following code which is very similar to the code I had before (using the var trick along with initC function), but now Module C imports someFunction from a newly added module.

The problem is that module B will be evaluated before both Module C and the utilities module. When initC is called inside of the B module, the initC function will try and call someFunction but there will be an error because the utilities module was not yet evaluated, so someFunction is undefined.

What would you recommend as a solution?

Here's the code:

// --- Entrypoint
import A from './A'
console.log('Entrypoint', new A)
// --- Module A

import C, {initC} from './C'

console.log('module A')
initC()

class A extends C {
    // ...
}

export {A as default}
// --- Module B

import C, {initC} from './C'

console.log('module B')
initC()

class B extends C {
    // ...
}

export {B as default}
// --- Module C

import A from './A'
import B from './B'
import {someFunction} from './utilities'

console.log('module C')

var C

initC()

export function initC(){
    if (C) return

    console.log('initC!!!')
    someFunction()

    C = class C {
        constructor() {
            // this may run later, after all three modules are evaluated, or
            // possibly never.
            console.log(A)
            console.log(B)
        }
    }
}

export {C as default}
// utilities.js

var someFunction = () => {...}

export {
    someFunction,
}

I think a possible (though strange) solution is to change the import order in Module C, f.e.:

import A from './A'
import B from './B'
import {someFunction} from './utilities'

I haven't actually tested that example code, but I believe I'm having a problem like this with Webpack+Babel here: trusktr/infamous/tree/circular-dep-bug

In this case, there is a circular dependency between src/motor/Scene and src/motor/Sizeable (which I've already planned to eliminate, but am still curious about how I would solve it without eliminating the circular dep). The modules import the defaults of each other, and I'm using the var Sizeable/initSizeable trick. However, there's a third module, Utility.js from which the Sizeable module needs to import something to use inside of the initSizeable function.

You can experience the error by simply opening motor-scratch.html in your browser, in which case you'll get an error like

Sizeable.js:328: Uncaught TypeError: Cannot read property '
makeLowercaseSetterAliases' of undefined

where makeLowercaseSetterAliases is akin to the someFunction of my earlier example. The odd thing is that everything works fine in Meteor's Reify environment, and I'm only getting this error when using the bundle made by Webpack+Babel.

If I re-order the dependencies in src/motor/Sizeable so that they look like this:

import { makeLowercaseSetterAliases } from './Utility'
import XYZValues from './XYZValues'
import Motor from './Motor'
import Scene from './Scene'

then the error goes away, but I get another error:

node.js:129: Uncaught TypeError: Cannot read property 'prototype' of
undefined

I have a feeling that moving the import to the beginning solved the problem, and that now I have another problem...

If you'd like to test changes (f.e. moving the import statement), you can simply:

npm install
npm run watch

which will automatically compile the global.js file after making changes to anything in src, and you can reload the page in your browser.

Any and all input you may have on this would be greatly appreciated!

Thanks a ton!

All the best,

# /#!/JoePea (8 years ago)

Yep, I believe that the first error was solved by moving the import statement to the top!

As for the second error, I think (but not yet sure) that the problem might be due to the circular dependency `src/motor-html/node ->

src/motor/Transformable -> src/motor/Sizeable -> src/motor/Scene ->

src/motor/ImperativeBase -> src/motor/Node -> src/motor-html/node`.

The problem will be solved after I re-organize some things, but if you have any suggestions it would be greatly appreciated!

I'm still not sure why it works in Reify and not in Webpack.

# /#!/JoePea (8 years ago)

I eliminated the circular dep, and all is well now. I suppose I learned something that is really good to know if ever dealing with circular deps:

Always import circular deps after non-circular deps.

f.e.

this is bad:

import CircularDep from 'somewhere'
import NonCircularDep from 'another-place'

and this is good:

import NonCircularDep from 'another-place'
import CircularDep from 'somewhere'

This ensures that the non-circular deps are evaluated and ready before hoisted circular-dep init functions are called (i.e. when using the var/initDep trick).

# cowwoc (8 years ago)

I don't understand what|CircularDep|and|NonCircularDep|refer to. To me, all the modules in the question contain some form of circular dependencies. Can you please post an answer in terms of|A|,|B|,|C|as defined in stackoverflow.com/q/38841469/14731 ?

Thank you, Gili

# /#!/JoePea (8 years ago)
# Mikael Sand (8 years ago)

How about applying Inversion of Control by implementing Dependency Injection using Constructor injection? Just make your C constructor take two parameters A and B like this:

// --- Module A

import C from './C';

export default class A extends C {
     // ...
}

// --- Module B

import C from './C';

export default class B extends C {
     // ...
}

// --- Module C

export default class C {
     constructor(A, B) {
         // this may run later, after all three modules are evaluated, or
         // possibly never.
         console.log(A);
         console.log(B);
     }
}

// --- Entrypoint

import A from './A';
import B from './B';
import C from './C';
const c = new C(A, B);
console.log('Entrypoint', C, c);
document.getElementById('out').textContent = 'Entrypoint ' + C + ' ' + c;

www.webpackbin.com/bins/-KlDeP9Rb60MehsCMa8u

en.wikipedia.org/wiki/Dependency_injection#Constructor_injection

en.wikipedia.org/wiki/Inversion_of_control#Implementation_techniques

# Mikael Sand (8 years ago)

Constructor injection should work as expected in any environment compatible and compliant with the ES6 and module specs. Or any functional environment with a module system.

Imho, it is the simplest and easiest to understand solution as well, it only removes code and adds two parameters. Thus expressing exactly what part is dependent on A and B in a parameterized reusable way, without any static cyclic dependencies. And without any complexity in the module initialization / resolution / evaluation.

Just reading the code once allows me to think about the entire flow, without keeping more than three concept in mind at once. Essentially allowing me to predict the entire execution of the program, without even running it.