Search code examples
typescriptgoogle-apps-scriptclasp

Importing moment.js in transpiled Apps Script


I have the necessity of using moment.js inside a locally developed Apps Script project, which uses google\clasp to convert from TypeScript.

I have already tried adding the js library inside the project as I do when writing normally in .gs, but using clasp doesn't seem to permit me to use that way.

I have already tried, as I read a lot in other Stack Overflow answers, to use eval like this without success:

eval(UrlFetchApp.fetch('library.min.js url').getContentText());

In all the cases, after I pushed to Apps Script and run the function in the Apps Script Editor, I receive

ReferenceError: "moment" is not defined. (row 6, file "Test")

My TS file: enter image description here


Solution

  • One can use external libraries like Moment.js via eval & UrlFetchApp in the "standard" Apps Script projects because the variables exports and module are not defined, so the library installs itself into the global context

    Indeed, we can verify the result by inspecting this in the Apps Script Editor:

    Code.gs

    var momentURL = "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.4/moment.min.js";
    function main() {
      eval(UrlFetchApp.fetch(momentURL).getContentText());
      Logger.log(this["moment"]);
    }
    

    Executing main yields

    function e() {
        return Qe.apply(null, arguments);
    }
    

    For transpiled TypeScript, because exports and module are globally defined, the initialization of eval'd libraries assumes that it has a more recent runtime / package management system than that provided by Google Apps Script.

    Code.ts

    import * as moment from "moment";
    
    const momentURL = "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.4/moment.min.js";
    function main() {
        eval(UrlFetchApp.fetch(momentURL).getContentText());
        Logger.log(`this['moment']: ${this["moment"]}`);
        Logger.log(`this.module: ${this.module}`);
        for (let key in this.module)
            Logger.log(`this.module[${key}]: ${this.module[key]}`);
    }
    

    $ clasp push -->

    Code.gs

    // Compiled using ts2gas 1.6.0 (TypeScript 3.2.2)
    var exports = exports || {};
    var module = module || { exports: exports };
    //import * as moment from "moment";
    var momentURL = "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.4/moment.min.js";
    function main() {
        eval(UrlFetchApp.fetch(momentURL).getContentText());
        Logger.log("this['moment']: " + this["moment"]);
        Logger.log("this.module: " + this.module);
        for (var key in this.module)
            Logger.log("this.module[" + key + "]: " + this.module[key]);
    }
    

    Which yields a log of

    this['moment']: undefined
    this.module: [object Object]
    this.module[exports]: 
    function e() {
        return Qe.apply(null, arguments);
    }
    

    Solutions

    So the eval is successful, but binds to module.exports and not moment. You can (in the transpiled Apps Script) refer to module.exports instead of moment:

    Logger.log(module.exports().format("YYYY")); // 2019
    

    Probably you will need to use a different method than clasp, since it seems ts2gas (as of v1.6.0) does not support import / export transpiling. This observed behavior wherein module.exports in transpiled TS is the eval'd import seems heinously fragile, and will certainly not be easy to work around when writing the actual TypeScript.

    Related: