3

I am aware that similar questions have been asked before, but methodology changes quickly so I'm seeking to understand current best practices. (In fact, as recently as 2 days ago, Chad Killingsworth added a comment to an accepted answer from three years ago that @expose annotation is now deprecated.)

I'm using the module pattern. Working JSFIDDLE of the below code:

/** @const */
var MATHCALCS = (function () {
    'use strict';

    var MY = {};

    /**
     * @constructor
     * @param {!Object} obj
     * @expose
     */
    MY.ModuleStruct = function (obj) {
        /** @expose */
        this.color = (obj.color !== undefined) ? obj.color : null;
        /** @expose */
        this.size = (obj.size !== undefined) ? obj.size : null;
    };

    /**
     * @expose
     */
    MY.ModuleStruct.prototype.clone = function () {
        return new MY.ModuleStruct({
            "color": this.color,
                "size": this.size
        });
    };


    MY.moduleProperty = 1;

    /**
     * @type {function(!Array<number>)}
     * @expose
     */
    MY.moduleMethod = function (a) {
        var i, x = 0;
        for (i = 0; i < a.length; i += 1) {
            x = x + a[i];
        }
        return x;
    };

    return MY;

}());

window["MATHCALCS"] = MATHCALCS;*

Currently, using @expose annotation, above can be minified with Closure in advance mode and the following calls work (minified example):

// call a public method
alert(MATHCALCS.moduleMethod([1, 2, 3]));

// allocate a new structure
var ms = new MATHCALCS.ModuleStruct({
    "color": "red",
        "size": "small"
});
alert(ms.color + '\t' + ms.size);

// clone a second instance
var ms2 = ms.clone();
alert(ms2.color + '\t' + ms2.size);
alert(ms !== ms2); // cloned objs are not equal

// and directly update the properties of the object
ms2.color = "white";
ms2.size = "large";
alert(ms2.color + '\t' + ms2.size);

If possible, without changing away from the module pattern, I would like to update code (approx 10,000 lines) to use @export annotation. However, when I replace @expose with @export Closure raises this error:

ERROR - @export only applies to symbols/properties defined in the global scope.

Q: Is it possible, and if so, how should the above code be annotated to work with ADVANCED_OPTIMIZATIONS?

I am aware that I can possibly use this type of notation:

MY["ModuleStruct"] = MY.ModuleStruct;
MY["ModuleStruct"]["prototype"]["clone"] = MY.ModuleStruct.prototype.clone;

but exporting object properties this way will become tedious. Further JSLint complains about weird assignments so I would rather use JSDocs annotation.

Community
  • 1
  • 1
Karl
  • 1,814
  • 1
  • 25
  • 37
  • `@export` is the correct method. It looks like we'll need to do some work on it to support this type of use case. – Chad Killingsworth Apr 20 '15 at 15:11
  • 1
    Issue added https://github.com/google/closure-compiler/issues/912 – Chad Killingsworth Apr 20 '15 at 15:21
  • @ChadKillingsworth, thanks for raising the issue ticket. It would be great if support for the `@expose` notation is not removed until `@export` supports this pattern for `@expose` is currently working fine for this case. – Karl Apr 21 '15 at 16:06
  • 1
    `@expose` support won't be removed from the compiler for quite some time. However, warnings will now be issued about it's use. I'm going to be working on improvements for `@export` soon. – Chad Killingsworth Apr 21 '15 at 17:53

1 Answers1

0

Until the issue raised by @ChadKillingsworth is resolved, here's a solution which will enable you to use @export with only minor modifications to your code:

/** @const */
var MATHCALCS = {};

goog.scope(function () {
    'use strict';

    var MY = MATHCALCS;

    /**
     * @constructor
     * @param {!Object} obj
     * @export
     */
    MY.ModuleStruct = function (obj) {
        this.color = (obj.color !== undefined) ? obj.color : null;
        this.size = (obj.size !== undefined) ? obj.size : null;
    };

    /**
     * @export
     */
    MY.ModuleStruct.prototype.clone = function () {
        return new MY.ModuleStruct({
            "color": this.color,
                "size": this.size
        });
    };


    MY.moduleProperty = 1;

    /**
     * @type {function(!Array<number>)}
     * @export
     */
    MY.moduleMethod = function (a) {
        var i, x = 0;
        for (i = 0; i < a.length; i += 1) {
            x = x + a[i];
        }
        return x;
    };

});

The changes are:

  • Change the @expose tags to @export.
  • Create an empty MATHCALCS object outside the module wrapper function, and make the MY alias point to it.
  • Instead of executing the module wrapper function immediately (IIFE), pass it to goog.scope(). This enables aliasing within scope functions, allowing the compiler to work out that the exported symbols are being defined on the global MATHCALCS object. This prevents the compiler from raising the error ("@export only applies to symbols/properties defined in the global scope").
  • Remove the following items, which are not needed:
    • The @export tags on this.color and this.size
    • return MY;
    • window["MATHCALCS"] = MATHCALCS;

When compiled with this command:

java -jar compiler.jar \
    --js closure/goog/base.js \
    --js mathcalcs.js \
    --js_output_file mathcalcs.min.js \
    --compilation_level ADVANCED_OPTIMIZATIONS \
    --generate_exports \
    --formatting PRETTY_PRINT \
    --output_wrapper '(function() {%output%}).call(window);'

you'll get:

(function() {var f = this;
function g(a, d) {
  var b = a.split("."), c = f;
  b[0] in c || !c.execScript || c.execScript("var " + b[0]);
  for (var e;b.length && (e = b.shift());) {
    b.length || void 0 === d ? c[e] ? c = c[e] : c = c[e] = {} : c[e] = d;
  }
}
;function h(a) {
  this.color = void 0 !== a.color ? a.color : null;
  this.size = void 0 !== a.size ? a.size : null;
}
g("MATHCALCS.ModuleStruct", h);
h.prototype.clone = function() {
  return new h({color:this.color, size:this.size});
};
h.prototype.clone = h.prototype.clone;
g("MATHCALCS.moduleMethod", function(a) {
  var d, b = 0;
  for (d = 0;d < a.length;d += 1) {
    b += a[d];
  }
  return b;
});
}).call(window);

The g() function is the compiled version of goog.exportSymbol() – see the @export docs for more details.

Note: if you want to run the code uncompiled, you'll need to load the Closure Library, or define goog.scope() yourself:

var goog = {};
goog.scope = function(fn) {
    fn();
};

Here's a fork of your JSFiddle with all these changes.

TachyonVortex
  • 8,242
  • 3
  • 48
  • 63