How to fix this ES6 module circular dependency?
Asked Answered
S

7

70

EDIT: for more background, also see the discussion on ES Discuss.


I have three modules A, B, and C. A and B import the default export from module C, and module C imports the default from both A and B. However, module C does not depend on the values imported from A and B during module evaluation, only at runtime at some point after all three modules have been evaluated. Modules A and B do depend on the value imported from C during their module evaluation.

The code looks something like this:

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

import A from 'A'
import B from '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}

I have the following entry point:

// --- Entrypoint

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

But, what actually happens is that module B is evaluated first, and it fails with this error in Chrome (using native ES6 classes, not transpiling):

Uncaught TypeError: Class extends value undefined is not a function or null

What that means is that the value of C in module B when module B is being evaluated is undefined because module C has not yet been evaluated.

You should be able to easily reproduce by making those four files, and running the entrypoint file.

My questions are (can I have two concrete questions?): Why is the load order that way? How can the circularly-dependent modules be written so that they will work so that the value of C when evaluating A and B will not be undefined?

(I would think that the ES6 Module environment may be able to intelligently discover that it will need to execute the body of module C before it can possibly execute the bodies of modules A and B.)

Stiltner answered 9/8, 2016 at 3:16 Comment(3)
Ah, wanted this as a canonical question for a long time, let's see when I have time to answer everythingRockyrococo
Joe, I see that you posted a solution at esdiscuss.org/topic/… but 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 this question?Mesa
@Mesa Hey, if you can reply in that thread, that would be great. I think to do that you just send an email with the same subject.Stiltner
S
45

The answer is to use "init functions". For reference, look at the two messages starting here: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem#content-21

The solution looks like this:

// --- Module A

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

initC();

console.log('Module A', C)

class A extends C {
    // ...
}

export {A as default}

-

// --- Module B

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

initC();

console.log('Module B', C)

class B extends C {
    // ...
}

export {B as default}

-

// --- Module C

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

var C;

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

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

-

// --- Entrypoint

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

Also see this thread for related info: https://github.com/meteor/meteor/issues/7621#issuecomment-238992688

It is important to note that exports are hoisted (it may be strange, you can ask in esdiscuss to learn more) just like var, but the hoisting happens across modules. Classes cannot be hoisted, but functions can be (just like they are in normal pre-ES6 scopes, but across modules because exports are live bindings that reach into other modules possibly before they are evaluated, almost as if there is a scope that encompasses all modules where identifiers can be accessed only through the use of import).

In this example, the entry point imports from module A, which imports from module C, which imports from module B. This means module B will be evaluated before module C, but due to the fact that the exported initC function from module C is hoisted, module B will be given a reference to this hoisted initC function, and therefore module B call call initC before module C is evaluated.

This causes the var C variable of module C to become defined prior to the class B extends C definition. Magic!

It is important to note that module C must use var C, not const or let, otherwise a temporal deadzone error should theoretically be thrown in a true ES6 environment. For example, if module C looked like

// --- Module C

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

let C;

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

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

then as soon as module B calls initC, an error will be thrown, and the module evaluation will fail.

var is hoisted within the scope of module C, so it is available for when initC is called. This is a great example of a reason why you'd actually want to use var instead of let or const in an ES6+ environment.

However, you can take note rollup doesn't handle this correctly https://github.com/rollup/rollup/issues/845, and a hack that looks like let C = C can be used in some environments like pointed out in the above link to the Meteor issue.

One last important thing to note is the difference between export default C and export {C as default}. The first version does not export the C variable from module C as a live binding, but by value. So, when export default C is used, the value of var C is undefined and will be assigned onto a new variable var default that is hidden inside the ES6 module scope, and due to the fact that C is assigned onto default (as in var default = C by value, then whenever the default export of module C is accessed by another module (for example module B) the other module will be reaching into module C and accessing the value of the default variable which is always going to be undefined. So if module C uses export default C, then even if module B calls initC (which does change the values of module C's internal C variable), module B won't actually be accessing that internal C variable, it will be accessing the default variable, which is still undefined.

However, when module C uses the form export {C as default}, the ES6 module system uses the C variable as the default exported variable rather than making a new internal default variable. This means that the C variable is a live binding. Any time a module depending on module C is evaluated, it will be given the module C's internal C variable at that given moment, not by value, but almost like handing over the variable to the other module. So, when module B calls initC, module C's internal C variable gets modified, and module B is able to use it because it has a reference to the same variable (even if the local identifier is different)! Basically, any time during module evaluation, when a module will use the identifier that it imported from another module, the module system reaches into the other module and gets the value at that moment in time.

I bet most people won't know the difference between export default C and export {C as default}, and in many cases they won't need to, but it is important to know the difference when using "live bindings" across modules with "init functions" in order to solve circular dependencies, among other things where live bindings can be useful. Not to delve too far off topic, but if you have a singleton, alive bindings can be used as a way to make a module scope be the singleton object, and live bindings the way in which things from the singleton are accessed.

One way to describe what is happening with the live bindings is to write javascript that would behave similar to the above module example. Here's what modules B and C might look like in a way that describes the "live bindings":

// --- Module B

initC()

console.log('Module B', C)

class B extends C {
    // ...
}

// --- Module C

var C

function initC() {
    if (C) return

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC()

This shows effectively what is happening in in the ES6 module version: B is evaluated first, but var C and function initC are hoisted across the modules, so module B is able to call initC and then use C right away, before var C and function initC are encountered in the evaluated code.

Of course, it gets more complicated when modules use differing identifiers, for example if module B has import Blah from './c', then Blah will still be a live binding to the C variable of module C, but this is not very easy to describe using normal variable hoisting as in the previous example, and in fact Rollup isn't always handling it properly.

Suppose for example we have module B as the following and modules A and C are the same:

// --- Module B

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

initC();

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

export {B as default}

Then if we use plain JavaScript to describe only what happens with modules B and C, the result would be like this:

// --- Module B

initC()

console.log('Module B', Blah)

class B extends Blah {
    // ...
}

// --- Module C

var C
var Blah // needs to be added

function initC() {
    if (C) return

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
    Blah = C // needs to be added
}

initC()

Another thing to note is that module C also has the initC function call. This is just in case module C is ever evaluated first, it won't hurt to initialize it then.

And the last thing to note is that in these example, modules A and B depend on C at module evaluation time, not at runtime. When modules A and B are evaluated, then require for the C export to be defined. However, when module C is evaluated, it does not depend on A and B imports being defined. Module C will only need to use A and B at runtime in the future, after all modules are evaluated, for example when the entry point runs new A() which will run the C constructor. It is for this reason that module C does not need initA or initB functions.

It is possible that more than one module in a circular dependency need to depend on each other, and in this case a more complex "init function" solution is needed. For example, suppose module C wants to console.log(A) during module evaluation time before class C is defined:

// --- Module C

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

var C;

console.log(A)

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

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

Due to the fact that the entry point in the top example imports A, the C module will be evaluated before the A module. This means that console.log(A) statement at the top of module C will log undefined because class A hasn't been defined yet.

Finally, to make the new example work so that it logs class A instead of undefined, the whole example becomes even more complicated (I've left out module B and the entry point, since those don't change):

// --- Module A

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

initC();

console.log('Module A', C)

var A

export function initA() {
    if (A) return

    initC()

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

initA()

export {A as default} // IMPORTANT: not `export default A;` !!

-

// --- Module C

import A, {initA} from './a'
import B from './b'

initA()

var C;

console.log(A) // class A, not undefined!

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

    C = class C {
        constructor() {
            console.log(A)
            console.log(B)
        }
    }
}

initC();

export {C as default}; // IMPORTANT: not `export default C;` !!

Now, if module B wanted to use A during evaluation time, things would get even more complicated, but I leave that solution for you to imagine...

Stiltner answered 9/3, 2017 at 20:33 Comment(15)
Man, this is so confusing. What's the difference between the circular dependency being visible at module-evaluation-time versus runtime? Meaning, what is the practical advantage of this approach?Mesa
Well, if you want to export class A extends C, then C simply needs to be evaluated when class A is defined, because classes can't extend undefined. Try running class A extends undefined {} in your console.Stiltner
The C dependency is needed when the module is evaluated, otherwise A will be extending undefined and an error will be thrown. Dependencies at runtime means that the dependency isn't needed until some point in the future, for example, the user of module A calls new A at some point in the future, or maybe never calls it. If the user never calls new A, then the console.log statements will never run. So runtime dependencies are dependencies that are used at some point after modules are evaluated, and possibly they will never be used. Get what I mean?Stiltner
Another way to think about it is that "runtime" is when the entry point module is evaluated. At that point, the entry point code will run (all other modules will have already been evaluated). That's runtime. Plus, the entry point can delay logic to fire on user events, timeouts, or other code that fires in the future, long after modules have been evaluated.Stiltner
For example, if new A is fired an hour later, then all the modules have already been evaluated by that point, so the dependencies must exist (unless a module was written in some odd fashion requiring runtime code to call into the module to initialize exports).Stiltner
I meant "For example, if new A is fired an hour later for the first time,"Stiltner
Basically, A depends on C being NOT undefined by the time A is evaluated. But, module C does not depend on A when it is evaluated. At some point in the future, new A or new C may be called, which will trigger the C constructor, and only at that point will C need to use A.Stiltner
As far as I can tell this answer shows A depending on C at module-evaluation time and C depending on A at runtime. You seem to be implying that C depends on A at module-evaluation time, but how could that be seeing as the constructor is not evaluated at module-loading time?Mesa
You just reworded what I said in my previous comment: yep, module C relies on A at runtime, and yes, A relies on C at module-evaluation time. However, note that module B also relies on module C during module-evaluation time, but module B will be evaluated before module C! So, how can module B get C before C is evaluated? Answer: it calls the hoisted initC function.Stiltner
As far as I can tell, my solution supports precisely the same thing. A and B depend on C at module-evaluation time and C depends on both of them at runtime. The only difference is that my solution exposes "internal modules" that users shouldn't really see. Do you agree?Mesa
Have you tested your example at runtime? It doesn't log anything, because setting the prototype.constructor isn't updating the internal C class for when it is constructed. Fiddle that shows modifying prototype.constructor doesn't do anything: jsfiddle.net/yhcvem0q. You a.js and b.js are missing the A and B identifiers. If you could update it, that would be awesome! I think I see what it's doing, but want to test it.Stiltner
Good catch. Normally, replacing C.prototype.someMethod is enough but it turns out that constructors require a different mechanism. Please try my updated answer.Mesa
I've updated my answer with something a lot simpler. It's tested and works.Mesa
Is there any way to do this in TS and provide an accurate typing for C?Achromat
@Achromat var C: ReturnType<typeof initC> | undefined. Another way is to write an interface or declare class with the same structure, and use that, because TS is structural.Stiltner
C
8

I would recommend to use inversion of control. Make your C constructor pure by adding an A and a B parameter 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;

https://www.webpackbin.com/bins/-KlDeP9Rb60MehsCMa8u

Update, in response to this comment: How to fix this ES6 module circular dependency?

Alternatively, if you do not want the library consumer to know about various implementations, you can either export another function/class that hides those details:

// Module ConcreteCImplementation
import A from './A';
import B from './B';
import C from './C';
export default function () { return new C(A, B); }

or use this pattern:

// --- Module A

import C, { registerA } from "./C";

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

registerA(A);

// --- Module B

import C, { registerB } from "./C";

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

registerB(B);

// --- Module C

let A, B;

const inheritors = [];

export const registerInheritor = inheritor => inheritors.push(inheritor);

export const registerA = inheritor => {
  registerInheritor(inheritor);
  A = inheritor;
};

export const registerB = inheritor => {
  registerInheritor(inheritor);
  B = inheritor;
};

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

// --- Entrypoint

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

Update, in response to this comment: How to fix this ES6 module circular dependency?

To allow the end-user to import any subset of the classes, just make a lib.js file exporting the public facing api:

import A from "./A";
import B from "./B";
import C from "./C";
export { A, B, C };

or:

import A from "./A";
import B from "./B";
import C from "./ConcreteCImplementation";
export { A, B, C };

Then you can:

// --- Entrypoint

import { C } from "./lib";
const c = new C();
const output = ["Entrypoint", C, c];
console.log.apply(console, output);
document.getElementById("out").textContent = output.join();
Comate answered 28/5, 2017 at 11:5 Comment(15)
Thanks for the suggestion! One problem with this is that now you've moved dependency knowledge from library to end-user, and the end-user who may be using only C in this case (for whatever reason) would need to know about A and B, where before only the library author needed to know.Stiltner
Awesome that you signed up just to answer this. :)Stiltner
Then how about this? webpackbin.com/bins/-Kl_37vgaKD3saNUXqQoComate
Personally, I would prefer to keep the code referentially transparent (side-effect free) as far as reasonable, and export another function/class that hides that detail, as in the updated answer.Comate
That's a good idea, however in your example the entry point still needs to import A and B? Ideally, the end user only needs to import the class that is being used, so for example only A, only B, or only C, but not all three.Stiltner
To allow the end-user to import any subset of the classes, just make a lib.js file exporting the public facing api: with inheritor registration: webpackbin.com/bins/-Klni3WQy6CfG12EnBVO with referential transparency: webpackbin.com/bins/-KlniSKAxCTrkWX2Va-NComate
Feels like this answers three separate questions in one at this point ;)Comate
@Stiltner does this fulfill the requirements / needs / wants of you and your library end users? Or am I missing something?Comate
Interesting! Both of those methods would in fact work in my case. At the moment, end-usage is to simply import the desired class from the file where it is defined, as a default import. Doing it your way loses that direct import style, but nonetheless it works. Thanks! On a side note, that ConcreteCImplementation example should not work because the arrow function exported from ConcreteCImplementation can not be used as constructor.Stiltner
Ah, true, seems to be a bug with the transpiler. Can be fixed by making the arrow function a normal function: export default function () { return new C(A, B); }Comate
But, actually, I recommend checking out the stamp specification and stampit medium.com/javascript-scene/… github.com/stampit-org/stampit They allow you to create composable factory functions and objects from reusable, composable behaviors. They don't require using the new operator, allow you to use all the best parts of javascript, and do not enforce strict is-a inheritance hierarchies. Instead they allow composing has-a and can-do relations to create data types from simple building blocks.Comate
How could you do the first example if C was abstract (can can not be constructed)?Achromat
@Achromat what do you mean? Js doesnt have the concept of abstract classesComate
@Comate 🤦 Apologies, been living in TS land too long.Achromat
@Achromat perhaps with the new abstract new () => {} type in TS? Haven't tried it yet.Stiltner
M
4

All the previous answers are a bit complex. Shouldn't this be solved with "vanilla" imports?

You can just use a single master index, from which all symbols are imported. This is simple enough that JS can parse it and solve the circular import. There's a really nice blog post that describes this solution, but here it is according to the OP's question:

// --- Module A

import C from './index.js'
...

// --- Module B

import C from './index.js'
...

// --- Module C

import {A, B} from './index.js'
...

// --- index.js
import C from 'C'
import A from 'A'
import B from 'B'
export {A, B, C}

// --- Entrypoint

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

The order of evaluation is the order in index.js (C-A-B). Circular references in the body of declarations can be included this way. So, for example, if B and C inherit from A, but A's methods contain references to B or C (which would throw an error if importing normally), this will work.

Minium answered 15/12, 2020 at 11:59 Comment(6)
Yes, JS can resolve the circular imports natively just fine (regardless whether there's a master index module or not). What actually matters is the order of evaluation - can you please add the explanation how the master module solves that to your answer?Rockyrococo
Sure, I thought it was obvious that the order of evaluation is A-B-C.Minium
But only if you use index.js as the entry point :-) Also A-B-C is the wrong order for the OP, who needs the class C to be initialised before extending it in A and B.Rockyrococo
Thanks! Well I can hardly see the problem of having index.js as the entry point -- you might as well rename it to A if that's your preferred name (likewise with the order of evaluation). Frankly given how many extra concepts the alternatives introduce (compared to this: only imports/exports), I think that a tiny bit of refactoring/file renaming is a good trade-off :)Minium
The problem is that importing index.js is so important and forgetting to do it (or also adding extra dependencies that mess with the order) causes hard-to-debug issues. Apart from the fragility, I very much prefer this approach myself. +1!Rockyrococo
@Rockyrococo and Jotaf, Importing index.js means importing everything in the whole dependency graph even if not needed. Without build tools, importing a deep path directly is how one does manual "tree shaking", so that not every thing in a library will be imported and evaluated if only a single module of that library is needed (with its few dependencies). So the issue is more apparent when one wants to import particular files for efficiency. If a library author encourages end users to follow this good practice, then the order of imports becomes unpredictable.Stiltner
M
2

Throwing another contender into the mix: a blog post by Michel Weststrate

The internal module pattern to the rescue!

I have fought with this problem on multiple occasions across many projects A few examples include my work at Mendix , MobX, MobX-state-tree and several personal projects. At some point, a few years ago I even wrote a script to concatenate all source files and erase all import statements. A poor-mans module bundler just to get a grip on the module loading order.

However, after solving this problem a few times, a pattern appeared. One which gives full control on the module loading order, without needing to restructure the project or pulling weird hacks! This pattern works perfectly with all the tool-chains I’ve tried it on (Rollup, Webpack, Parcel, Node).

The crux of this pattern is to introduce an index.js and internal.js file. The rules of the game are as follows:

  1. The internal.js module both imports and exports everything from every local module in the project
  2. Every other module in the project only imports from the internal.js file, and never directly from other files in the project.
  3. The index.js file is the main entry point and imports and exports everything from internal.js that you want to expose to the outside world. Note that this step is only relevant if your are publishing a library that is consumed by others. So we skipped this step in our example.

Note that the above rules only apply to our local dependencies. External module imports are left as is. They are not involved in our circular dependency problems after all. If we apply this strategy to our demo application, our code will look like this:

// -- app.js --
import { AbstractNode } from './internal'

/* as is */

// -- internal.js --
export * from './AbstractNode'
export * from './Node'
export * from './Leaf'

// -- AbstractNode.js --
import { Node, Leaf } from './internal'

export class AbstractNode {
   /* as is */
}

// -- Node.js --
import { AbstractNode } from './internal'

export class Node extends AbstractNode {
   /* as is */
}

// -- Leaf.js --
import { AbstractNode } from './internal'

export class Leaf extends AbstractNode {
   /* as is */
}

When you apply this pattern for the first time, it might feel very contrived. But it has a few very important benefits!

  1. First of all, we solved our problem! As demonstrated here our app is happily running again.
  2. The reason that this solves our problem is: we now have full control over the module loading order. Whatever the import order in internal.js is, will be our module loading order. (You might want check the picture below, or re-read the module order explanation above to see why this is the case)
  3. We don’t need to apply refactorings we don’t want. Nor are we forced to use ugly tricks, like moving require statements to the bottom of the file. We don’t have to compromise the architecture, API or semantic structure of our code base.
  4. Bonus: import statements will become much smaller, as we will be importing stuff from less files. For example AbstractNode.js has only on import statement now, where it had two before.
  5. Bonus: with index.js, we have a single source of truth, giving fine grained control on what we expose to the outside world.
Mesa answered 19/7, 2023 at 3:54 Comment(0)
M
0

UPDATE: I'm going to leave this answer up for posterity, but nowadays I use this solution.


Here is a simple solution that worked for me. I initially tried trusktr's approach but it triggered weird eslint and IntelliJ IDEA warnings (they claimed the class was not declared when it was). The following solution is nice because it eliminates the dependency loops. No magic.

  1. Split the class with circular dependencies into two pieces: the code that triggers the loop and the code that does not.
  2. Place the code that does not trigger a loop into an "internal" module. In my case, I declared the superclass and stripped out any methods that referenced subclasses.
  3. Create a public-facing module.
  • import the internal module first.
  • import the modules that triggered the dependency loop.
  • Add back the methods that we stripped out in step 2.
  1. Have the user import the public-facing module.

OP's example is a little contrived because adding a constructor in step 3 is a lot harder than adding normal methods but the general concept remains the same.

internal/c.js

// Notice, we avoid importing any dependencies that could trigger loops.
// Importing external dependencies or internal dependencies that we know
// are safe is fine.

class C {
    // OP's class didn't have any methods that didn't trigger
    // a loop, but if it did, you'd declare them here.
}

export {C as default}

c.js

import C from './internal/c'
// NOTE: We must import './internal/c' first!
import A from 'A'
import B from 'B'

// See https://mcmap.net/q/261515/-why-is-it-impossible-to-change-constructor-function-from-prototype for why we can't replace
// "C.prototype.constructor" directly.
let temp = C.prototype;
C = function() {
  // this may run later, after all three modules are evaluated, or
  // possibly never.
  console.log(A)
  console.log(B)
}
C.prototype = temp;

// For normal methods, simply include:
// C.prototype.strippedMethod = function() {...}

export {C as default}

All other files remain unchanged.

Mesa answered 16/2, 2017 at 4:14 Comment(6)
In your example, C only depends on A and B at runtime, but not at module evaluation time. What if you want C to depend on A and B at evaluation time? That is the problem I had. For example, suppose we want class C extends (A.name === 'A' ? Foo : Bar) {}. That is purely hypothetical, imagine some build-step replaces A with a different definition based on who-knows-what. The main point is that class C can only be defined based on the value of A.Stiltner
The "init functions" in the esdiscuss thread are one way to solve that. Without those, then since A depends on C, and C depends on A, one module's evaluation will fail with either C or A being undefined, depending on the order the modules are evaluated.Stiltner
@Stiltner Can you please post a separate answer fleshing out what the "init functions" solution looks like in this case? I don't understand it.Mesa
Posted an answer, does that explain it better? Does it answer your question about CircularDep and NonCircularDep?Stiltner
@Stiltner You asked: What if you want C to depend on A and B at evaluation time? This answer shows A depending on C at module evaluation time, and C depending on A at runtime. Are you asking for A to depend on C, and C to depend on A both at module evaluation time? I don't think that is technically possible.Mesa
Should c.js exist? This looks like a anti pattern... a.js and b.js have internal/c.js twice in each of their own scopes.Respective
U
-1

There is another possible solution..

// --- Entrypoint

import A from './app/A'
setTimeout(() => console.log('Entrypoint', A), 0)

Yes it's a disgusting hack but it works

Until answered 3/8, 2017 at 10:56 Comment(0)
M
-1

You can Solve it with dynamically loading modules

I had same problem and i just import modules dynamically.

Replace on demand import:

import module from 'module-path';

with dynamically import:

let module;
import('module-path').then((res)=>{
    module = res;
});

In your example you should change c.js like this:

import C from './internal/c'
let A;
let B;
import('./a').then((res)=>{
    A = res;
});
import('./b').then((res)=>{
    B = res;
});

// See https://mcmap.net/q/261515/-why-is-it-impossible-to-change-constructor-function-from-prototype for why we can't replace "C.prototype.constructor"
let temp = C.prototype;
C = function() {
  // this may run later, after all three modules are evaluated, or
  // possibly never.
  console.log(A)
  console.log(B)
}
C.prototype = temp;

export {C as default}

For more information about dynamically import:

http://2ality.com/2017/01/import-operator.html

There is another way explain by leo, it just for ECMAScript 2019:

https://mcmap.net/q/261516/-circular-dependencies-in-es6-7

For analyzing circular dependency, Artur Hebda explain it here:

https://railsware.com/blog/2018/06/27/how-to-analyze-circular-dependencies-in-es6/

Monocarpic answered 18/1, 2019 at 19:40 Comment(1)
This is very problematic because the code importing c.js won't know at which point A and B will become available (since they are loaded asynchronously), so it's a game of russian roulette whether C will crash or not.Wolfgram

© 2022 - 2024 — McMap. All rights reserved.