Skip to content

Instantly share code, notes, and snippets.

@gund
Last active July 4, 2018 18:27
Show Gist options
  • Save gund/fa244bfef5024ebe2aba72d31d565cb0 to your computer and use it in GitHub Desktop.
Save gund/fa244bfef5024ebe2aba72d31d565cb0 to your computer and use it in GitHub Desktop.
Webpack 2 beta circular dependencies trouble

So consider we have Logger class. And also we have a LoggerInContext class which extends Logger. And Logger has factory method to create new LoggerInContext instances.

// logger.ts

import { LoggerInContext } from './logger-in-context';

export class Logger {
  // Some methods like log()...
  
  getInContext(context) {
    return new LoggerInContext(context);
  }
}
// logger-in-context.ts

import { Logger } from './logger';

export class LoggerInContext extends Logger {
  constructor(public context) {
    super();
  }
  
  // Here we can override some behavior
}

So as we will try to import logger.ts into our application Webpack will then run into file logger-in-context.ts but at that time Logger will be undefined so our extend will fail and stack trace will look something like this (TS compiler is doing his job here as well):

Uncaught TypeError: Cannot read property 'prototype' of undefined
    at __extendsFn (index.js:15)
    at eval (logger-in-context.ts:4)
    at eval (logger-in-context.ts:50)
    at Object../src/app/services/logger/logger-in-context.ts (index.js:718)
    at __webpack_require__ (index.js:48)
    at eval (logger.service.ts:5)
    at Object../src/app/services/logger/logger.service.ts (index.js:726)
    at __webpack_require__ (index.js:48)
    at eval (index.ts:5)
    at Object../src/app/services/logger/index.ts (index.js:702)
    at __webpack_require__ (index.js:48)
    at eval (auth-interceptor.service.ts:7)
    at Object../src/app/services/sso/auth-interceptor/auth-interceptor.service.ts (index.js:742)
    at __webpack_require__ (index.js:48)
    at eval (index.ts:5)

And in generated code error comes from here (simplified a bit):

var logger_1 = __webpack_require__("./src/logger.ts");
// ...
__extends(LoggerInContext, logger_1.Logger); // <--- This is yet undefined
@gund
Copy link
Author

gund commented Dec 25, 2016

In current example it is still possible to workaround this issue.
Consider we importing Logger in our app.ts file like so:

// app.ts
import { Logger } from './logger';

// Do something with Logger

And to fix it we can import logger-in-context.ts file just before logger.ts:

// app.ts
import './logger-in-context'; // <--- This will fix the issue
import { Logger } from './logger';

// Do something with Logger

But this workaround is only useful in this particular example because we are not doing anything with LoggerInContext in the logger.ts file at runtime (like we do in logger-in-context.ts by using it with extend keyword) - just returning a new instance only when particular method will be called.

@trusktr
Copy link

trusktr commented Dec 26, 2016

Here's a more generic fix that works in general for any almost all circular dependencies (maybe all of them?)

// logger.ts
// Other files depend on Logger at module evaluation time. For this dependency,
// wrap the dependency in a setup function like so:

import { LoggerInContext } from './logger-in-context';

var Logger; // not `let`, as Webpack has a bug, it must be `var` (`let` should work, but not sure if Webpack fixed it yet)

export function setupLogger() {
  // if this function was already called once, then Logger is defined.
  if (Logger) return

  Logger = class Logger {
    // Some methods like log()...

    getInContext(context) {
      return new LoggerInContext(context);
    }
  }
}

// Always call the setup function in the samemodule, in case this module
// evaluates first.
setupLogger()

export default Logger
// logger-in-context.ts
// This file depends on the other dependency at module evaluation time. But the
// other file does not depend on this file at module evaluation time, only at
// runtime during a getInContext() call. So this file calls the setup function.

import { Logger, setupLogger } from './logger';

// call the setup function *before* using the needed dependency. The setup
// function was hoisted by the module engine, so even if the other file was not
// yet evaluated, the function already exists!
setupLogger()


// Now Logger is defined.

export class LoggerInContext extends Logger {
  constructor(public context) {
    super();
  }

  // Here we can override some behavior
}

For your simple case, @gund's solution would work. For the general case, and even with complicated dependencies, this new solution should always work with cases where the needed dependency is required at module evaluation time while the other file does not require a dependency at module evaluation time. Something to consider: if both files depend on each other at module evaluation time, then you'll need a more complicated solution, and possibly it merits a code redesign.

@trusktr
Copy link

trusktr commented Dec 26, 2016

See this discussion for more info on how we came up with the technique: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem

@trusktr
Copy link

trusktr commented Dec 26, 2016

In the previous example, I meant export {Logger}, not export default Logger.

@Jessidhia
Copy link

Jessidhia commented Dec 27, 2016

@trusktr IIUC, export {Logger} should avoid your issue with let. You can even do export { Logger as default } if you want to make it the default export.

The problem with export default Logger is that, as export default is exporting an expression result (the current value of Logger), and export are evaluated before other code, Logger is being read before it is defined (i.e. while it is in TDZ). That actually probably should be an error even in native ES modules.

@Choleriker
Copy link

Guys, there is another simple solution for this. I have 2 modules which are somehow deep in the structure using each other. I ran into the same problem with circular dependencies with webpack and angular 2. I simply changed the way of how the one module is declared:

....

@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        require('../navigation/navigation.module')
    ],
    declarations: COMPONENTS,
    exports: COMPONENTS
})
class DppIncludeModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: DppIncludeModule
        };
    }
}

export = DppIncludeModule;

When I now using the imports statement on ngModule attribute I simply use:

@NgModule({
    imports: [
        CommonModule,
        ServicesModule.forRoot(),
        NouisliderModule,
        FormsModule,
        ChartModule,
        DppAccordeonModule,
        PipesModule,
        require('../../../unbranded/include/include.module')
    ],
....

With this all problems are away.

@gund
Copy link
Author

gund commented Aug 16, 2017

@Choleriker I believe your solution will most likely break Angular's AOT compatibility

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