Exposing business classes from business library in Google Apps Script

Viewed 94

So, I am working on an MVVM-based core SDK for use any time I am developing some Google Apps Script based software, called OpenSourceSDK. It contain core business logic, including base classes to extend. For example, the file Models/BaseModel.gs in it is defined to be:

class BaseModel { 
  static FromObject(obj) {
    let model = new this()

    return model.unmarshal(obj)
  }

  /**
   * Creates a deep-copy of this, as plain Object
   */
  marshal() {
    return JSON.parse(JSON.stringify(this));
  }

  /**
   * Writes the target object into this and returns this
   * @param {*} obj
   */
  unmarshal(obj) {
    return Object.assign(this, obj);
  }

}

I follow the instructions to create this library, and make sure to import it into the project that's using it. When I attempt to use it, things go sideways: I am able to access the functions, but not the classes nor the constants!! I start typing in OpenSourceSDK.BaseModel, and IntelliSense is like "idk what to do". I sanity test via Logger.log(typeof OpenSourceSDK.BaseModel), only to get undefined.

This is unacceptable.

What is the most efficient way to make sure these classes (and constants) are shared from that library?

NOTE: I do not accept the following "solutions":

  • factory method per class. Why not? Because:
    • maintenance nightmare. Imagine making any change to the constructor signature of the class. You'd have to update the factory function on every change!
    • extra boilerplate that is wrapper around something that is so simple.
    • what happen if we want to extend a base class coming from the library?
  • going all function-based. That defeats the whole purpose of this project. There's a reason I am doing this MVVM, and hence OOP, style.
2 Answers

In ES6 let, const, and class do not add themselves to the global variable (unlike var and function).

My favorite way of exporting them on Apps Script is to assign all exported objects to globalThis:

class BaseModel { /* [...] */ }
class AnotherType { /* [...] */ }
const ExampleConstant = 420

Object.assign(globalThis, {
  BaseModel,
  AnotherType,
  ExampleConstant,
})

This allows you to choose what to "export" and follows a nice enough syntax.

Update

You can also mimic the individual export syntax with the following function:

function export_(exported) {
  const { name } = exported
  if (!name)
    throw new Error(`To export a function or a class, it needs to have a name.`)
  if (name in globalThis)
    throw new Error(`A feature with the same name already exists on globalThis.`)
  globalThis[name] = exported
  return exported
}

And you use it like this:

export_ (class Example {
  // [...]
})

Unfortunately the final underscore is necessary to not clash with the keyword and the parenthesis are not my cup of tea (specially the one at the end). But I agree that this would be more maintainable as you don't need to manually update the list.

References

I was able to get it resolved, but the solution is...hacky.

So, apparently, Google Apps Script exports only what is in globalThis of a project: just the functions and variables. No classes, no constants, ...

Probably has a lot to do with how ES6 works, with its globalThis behavior. One can see that in action, by creating a dummy function, a dummy variable, and a dummy class in their local developer console:

function aFunction() { return 42; }
class AClass {}
var a = 41;

When they log globalThis, they'll find some massive object with a, aFunction in it, but no class:

enter image description here

Also, it turns out that globalThis is immutable, so you can't hack the class in via some IIFE...

The only recourse is to go prepend var [ClassName] = in front of every class that is to be made public. That would assign the [ClassName] to a publicly exposed var.

I can't wait til Google Apps Script comes up with a better way to expose classes!!

Related