ES6 and closure modules

266 views
Skip to first unread message

co...@colinalworth.com

unread,
Oct 22, 2020, 3:40:26 PM10/22/20
to Closure Compiler Discuss
I'm exploring using ES6 modules in a project which already uses J2CL (which only emits goog.module declarations, and assumes that all of its dependencies are also goog.module), but for the purposes of this question, I'll keep things to just plain Closure-flavored modules and JS. In short, this Closure JS follows this pattern in each module (greatly simplified):

goog.module("my.project.ProjectClass")

// declare that we depend on the class, for jsdoc and such
let DependencyClass = goog.forwardDeclare("some.dependency.DependencyClass");
class ProjectClass {
  constructor() {
    // actually get a reference to the class so we can use it, assign to top-level var
    // note that this line itself cannot be top level to allow circular dependencies
    DependencyClass = goog.module.get("some.dependency.DependencyClass");

    // use the various dependencies that are now wired up
    this.foo = new DependencyClass();
  }

  /**
   * Read-only getter for foo
   * @return {DependencyClass}
   */
  get foo() {
    return this.foo;
  }
 
}

exports = ProjectClass;


On the other hand, we have some dependency made up of closure-compatible ES6 modules, and I would like to use them directly in this project. The first step is to make this file reachable by closure modules, something like this at the end:

//... imports, decl of DependencyClass, etc
goog.declareModuleId('some.dependency.DependencyClass');
export {DependencyClass};

Now, it seems as though this should be enough to let it be referenced by name from inside of a closure module, but instead the goog.forwardDeclare line results in an error:
ERROR - [JSC_FORWARD_DECLARE_FOR_ES6_SHOULD_BE_CONST] goog.forwardDeclare alias for ES6 module should be const.

Is there a way that this module can be referenced _and_ the type can be referenced?

--

Our quick and dirty workaround is to provide a "shim" which bridges the gap between this es6-closure "hybrid" module (which cannot be used in forwardDeclare like this) and a "real" closure module. First, the declareModuleId needs to be different - I renamed it above to have the suffix .es6. Then, a new closure module is created:

// Declare a "full" closure module, so we can reference it with requireType+module.get
goog.module('some.dependency.DependencyClass');

// Depend on the "hybrid" module
const hybrid = goog.require('some.dependency.DependencyClass.es6');

// Re-export the module that was imported
exports = hybrid;


With this extra file in the project, it seems possible to use the forwardDeclare/module.get pattern, though a new file is needed for each es6 module that will be depended on in this way, so it isn't ideal.

More j2cl discussion on this at https://github.com/google/j2cl/issues/113.

Laura Harker

unread,
Oct 23, 2020, 12:25:09 PM10/23/20
to Closure Compiler Discuss
First - I don't know why forwardDeclare is required to be const for ES modules but not goog.modules. Not sure if there's some different technical restriction due to how ES modules are rewritten, sorry.

Second - why do you need to need to reassign to the top-level DependencyClass variable?

If you only reference DependencyClass as a value within the constructor, you can just initialize a new variable via goog.module.get in every constructor call. In the compiled code all these extra goog.module.gets will be optimized away so this won't be inefficient (although if you're testing uncompiled code, I guess this could be a performance issue? not familiar with the performance of goog.module.get uncopmiled).

The `const DependencyClass = goog.forwardDeclare('...` would only be referencable in type annotations but I think that should be fine for you?

So you'd get something like this:

goog.module("my.project.ProjectClass")

// declare that we depend on the class, for jsdoc and such
const DependencyClass = goog.forwardDeclare("some.dependency.DependencyClass");
class ProjectClass {
  constructor() {
    // actually get a reference to the class so we can use it
    // note that this line itself cannot be top level to allow circular dependencies
    const DependencyClass = goog.module.get("some.dependency.DependencyClass").DependencyClass;

    // use the various dependencies that are now wired up
    this.foo = new DependencyClass();
  }

  /**
   * Read-only getter for foo
   * @return {DependencyClass.DependencyClass}
   */
  get foo() {
    return this.foo;
  }
 
}

exports = ProjectClass;

co...@colinalworth.com

unread,
Dec 3, 2020, 3:45:59 PM12/3/20
to Closure Compiler Discuss
Sorry for the delay in replying, the new groups seems to have eaten my last reply and I didn't take the time to do it again yet. This time around it is a little more brief, but I hope still clear enough.

First, thank you for taking a look at this and offering your feedback, including fixing a few typos as I translated from my various experiments into something I could share here.

In short yes, we need the DependencyClass in plenty of places - I was over-simplifying how j2cl generated the type. All of the dependencies are loaded via goog.module.get(...) in  one static method, which is called when the class is first initialized (using java semantics - so creating an instance, invoking a static method, or accessing a non-constant static field) - it isn't enough to just re-declare in once place, unless we went ahead and re-declared each dependency anywhere that used them. Re-declaring does apparently work, but is pretty noisy.

I did briefly experiment with using a different alias for the externally referenced class ("DependencyClass_fwd" vs "DependencyClass") based on where it was declared - As I recall this had an error before, but is working correctly now? This way it could be used from within any function. Example:
// declare that we depend on the class, for jsdoc and such
const DependencyClass_fwd = goog.forwardDeclare("some.dependency.DependencyClass");
let DependencyClass;

class ProjectClass {
  constructor() {
    // actually get a reference to the class so we can use it, assign to top-level var
    // note that this line itself cannot be top level to allow circular dependencies
    DependencyClass = goog.module.get("some.dependency.DependencyClass").DependencyClass;

    // use the various dependencies that are now wired up
    this.foo = new DependencyClass();
  }
  /**
   * Read-only getter for foo
   * @return {DependencyClass_fwd.DependencyClass}

   */
  get foo() {
    return this.foo;
  }


That does seem to work, but also feels a bit gross, double-declaring anything that might be referenced in this way. Maybe it is sufficient, but would be a size regression that probably wouldn't pass muster with the j2cl team at Google.

Goktug Gokdogan

unread,
Dec 3, 2020, 10:30:24 PM12/3/20
to Closure Compiler Discuss
Laura, Colin is referring to J2CL code generation pattern to make cyclic class dependencies work; which essentially needs goog.forwardDeclare/goog.module.get pairs, hence the lefthand side cannot be a const. And not using top level variables and requiring to re-access the type every time it is referenced would be an impractical solution.

I'm not sure why this is only enforced for ES6 modules but if it needs to be; then I think an exception could be added to J2clSuppressWarningsGuard.

Goktug Gokdogan

unread,
Dec 3, 2020, 10:30:33 PM12/3/20
to closure-comp...@googlegroups.com
Laura, Colin is referring the J2CL pattern to make cyclic class dependencies work; which essentially needs goog.forwardDeclare/goog.module.get pairs, hence the lefthand side cannot be a const. And not using top level variables and requiring to re-access the type every time it is referenced would be an impractical solution.

I'm not sure why this is only enforced for ES6 modules but if it needs to be; then I think an exception could be added to J2clSuppressWarningsGuard. 

--

---
You received this message because you are subscribed to a topic in the Google Groups "Closure Compiler Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/closure-compiler-discuss/dBgj7B3GqhE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to closure-compiler-d...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/closure-compiler-discuss/4de64e8d-44ce-4789-a4d8-57cf7288db18n%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages