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

/#!/JoePea joe at trusktr.io
Sun Aug 14 04:09:51 UTC 2016


Hi Logan,

> 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:

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

```js
// --- Module A

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

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

class A extends C {
    // ...
}

export {A as default}
```

```js
// --- Module B

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

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

class B extends C {
    // ...
}

export {B as default}
```

```js
// --- 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}
```

*/#!/*JoePea

On Thu, Aug 11, 2016 at 10:26 AM, Logan Smyth <loganfsmyth at gmail.com> wrote:

> 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?
>
> On Thu, Aug 11, 2016 at 12:23 AM, /#!/JoePea <joe at trusktr.io> wrote:
>
>> 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):
>>
>> ```js
>> // --- Entrypoint
>> import A from './A'
>> console.log('Entrypoint', new A)
>> ```
>>
>> ```js
>> // --- 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}
>> ```
>>
>> ```js
>> // --- 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}
>> ```
>>
>> ```js
>> // --- 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](https://github
>> .com/meteor/meteor/issues/7621#issuecomment-238992688) pointed out by
>> Ben Newman. And it fails in Rollup because Rollup [isn't really creating
>> live bindings](https://github.com/rollup/rollup/issues/845), 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.
>>
>>
>>
>> */#!/*JoePea
>>
>> On Wed, Aug 10, 2016 at 5:04 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>
>>> I found a solution that works in environments compiled by Babel, using
>>> the [workaround suggested by Ben Newman](https://github.com/met
>>> eor/meteor/issues/7621#issuecomment-238992688):
>>>
>>> ```js
>>> // --- 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}
>>> ```
>>>
>>> ```js
>>> // --- 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}
>>> ```
>>>
>>> ```js
>>> // --- 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}
>>> ```
>>>
>>> ```js
>>> // --- 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](http://goo.gl/PXXBK
>>> I), and may not work in other ES6 module implementations.
>>>
>>> Is there some solution that will theoretically work in any ES6 module
>>> environment?
>>>
>>> */#!/*JoePea
>>>
>>
>>
>> _______________________________________________
>> es-discuss mailing list
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160813/6f0c8d63/attachment-0001.html>


More information about the es-discuss mailing list