grant / ts2gas

A function that transpiles TypeScript to Google Apps Script.
http://npmjs.com/ts2gas
MIT License
88 stars 11 forks source link

require and import #26

Open paxperscientiam opened 5 years ago

paxperscientiam commented 5 years ago

There's a note in the readme indicating that noLib is fixed at false. Does that mean ts2gas does not currently handle imports? Like, I'm trying to import a module, but nothing seems to happen.

Thanks.

grant commented 5 years ago

ES Modules aren't supported in this library. (for require and import) I think some developers have their own webpack setup for TypeScript compilation.

noLib just removes things like window, document from autocompletion.

angrykoala commented 5 years ago

import is not supported and it is commented out by ts2gas, however export is actually transpiled into exports.foo=foo, this completely breaks the compiled appscript, as foo is not part of the global scope and imports are simply commented out.

Is there a way to disable this behavior, so both import and export are simply commented out?

grant commented 5 years ago

@paxperscientiam @angrykoala Can you provide some test cases so we can test and fix this?

@PopGoesTheWza has written most of this library, so you may want to check up on the case with him/her.

angrykoala commented 5 years ago
export default const myFoo="foo";

my_foo.ts

import myFoo from './my_foo.ts';

console.log(myFoo)

main.ts

This should transpile to valid Appscript, preferably by overriding export/import (a lรก browserify) but at the very least should completely ignore export and import:

var myFoo="foo";

my_foo.gs

console.log(myFoo)

main.gs

I hope these examples help

PopGoesTheWza commented 5 years ago

@angrykoala please specify your build/publish workflow. Currently ts2gas is not designed to transpile more than one source .ts file at a time.

With my (little and very personal) use of clasp/ts2gas, I hardly had to mess with export and `import:

Unless @grant decides a different design approach, in which import should be handled (I am not sure yet of a satisfying option...)

angrykoala commented 5 years ago

the problem I have is that export is being transpiled instead of ignored, so I need to manually remove the generated code. Simply ignoring it (like it happens with import) would be enough and will work on transpiling one file at a time

PopGoesTheWza commented 5 years ago

@paxperscientiam please bear with my slowmindedness...

Checking at the result of ts2gastest suite:

v--TS--v
// next statement will be ignored
export { foo, bar }
  from "file";
// next statement will be preserved
/** Supported languages for localized data */
export enum Languages {
  /** English (USA) */
  eng_us = 'eng_us',
  /** French (France) */
  fre_fr = 'fre_fr',
}
export class Client {
  /** URL to the login endpoint */
  private readonly signinUrl: string;
}
export function foo() {}
export default Client;
export
  { ZipCodeValidator };
// now resume with next statement
โ€“โ€“โ€“
// Compiled using ts2gas 1.6.0 (TypeScript 3.3.3333)
var exports = exports || {};
var module = module || { exports: exports };
//export { foo, bar }\n  from "file";
// next statement will be preserved
/** Supported languages for localized data */
var Languages;
(function (Languages) {
    /** English (USA) */
    Languages["eng_us"] = "eng_us";
    /** French (France) */
    Languages["fre_fr"] = "fre_fr";
})(Languages = Languages || (Languages = {}));
var Client = /** @class */ (function () {
    function Client() {
    }
    return Client;
}());
exports.Client = Client;
function foo() { }
exports.foo = foo;
// now resume with next statement
 ^--GS--^

Most export statements are removed or ignored during transpiling. Classes, functions and variables end up producing the following:

exports.Client = Client;
exports.foo = foo;

Are those the lines you wish would not be produced? What is the impact of those lines being in your gas?

PopGoesTheWza commented 5 years ago

incidentally, I now wonder why export enum Languages ... produces an incorrect (Languages = Languages || (Languages = {})) The correct resulting javascript should read (Languages = exports.Languages || (exports.Languages = {}))

@grant I'll investigate this sometime this week.

angrykoala commented 5 years ago

@PopGoesTheWza Yep, those are the things that I would like to ignore (thanks, sorry for not explaining myself, it was quite some time ago since I had this problem), unless import actually access the "export" global variable" it is impossible for the code to reach these variables

thanks

PopGoesTheWza commented 5 years ago

@angrykoala I failed to see the use of export (for classes, functions, enums and variables) unless when producing a gas library. Does this match your use case?

If you are solely producing "alongside .gs scripts", they all exist in the same global namespace: Code.gs

foo()

lib.gs

function foo() {
  return 'zug zug';
}

Is there something obvious I am overlooking?

angrykoala commented 5 years ago

lets assume 2 files in ts:

export default class Foo{ }
import Foo from 'foo'
new Foo()

It will transpile to:

exports.Foo=class Foo ....
new Foo()

As you can see, the Foo in the second file won't be found because Foo is not in the global scope, but in the object exports.

PopGoesTheWza commented 5 years ago

@angrykoala are you using clasp to publish your two files? If you have src1.ts and src2.ts, doesn't it ends up as src1.gs and src2.gs in your gas document?

angrykoala commented 5 years ago

Yes, that's correct,but the class Foo won't be found in src2.gs because it is not declared in the global scope of src1.gs, but as part of the exports object

PopGoesTheWza commented 5 years ago

@angrykoala odd... for this does not match my (limited) experience. Maybe it is related to my project settings, but when pushing src1.ts and src2.ts, I get in my gas project src1.gs and src2.gs which share the same global namespace... hence src1.gs call functions from src2.gs as if they were alongside in a single file. To illustrate, you may give a look at https://github.com/PopGoesTheWza/swgoh-tb-sheets which has some limited import to access a gas library (using lib/index.d.ts for type definitions) and all other scripts sharing the same global namespace (hence no need for import/export @grant could you share some of your wisdow here?

angrykoala commented 5 years ago

As I see, you are defining namespaces instead of directly exporting a class from the fule, maybe that's the reason the namespace is available across multiple files?

PopGoesTheWza commented 5 years ago

@angrykoala I added namespaces later on, in order to organise sets of function of classes in a semantically meaningful... manner. It also serves the purpose of disallowing direct use from Google Sheets formula, but this is another topic. Also note that the same namespace definition can appear in several files, hence src1.ts and src.2.ts can both add stuff to a single namespace. Still, any function (or any kind of statement in fact) starts in the same global namespace (which is in fact key for defining Google Sheets custom functions.

angrykoala commented 5 years ago

If I define 2 classes in separate files, without export nor import everything works fine (are global variables), the problem comes when I try to use export/import (mainly for testing purposes using ts) that export is being transpiled into export.Foo

While export is indeed a global variable, Foo is not, is a parameter of the global object export, so calling Foo directly from a separate file won't work. At least, that was what is happening to me when exporting classes using clasp

PopGoesTheWza commented 5 years ago

@angrykoala Probably the confusion is that, in the context of node or browser environnemment, import/export not only define a namespace to hold statements, but mostly the dynamic for "loading" resource. With the current gas engine, all .gs files exist in the very same global namespace (gas library being sort of an exception and instantiated in their own namespace) and thus import doesn't make sense. So, if we limit ourself to "regular" .ts files, namespace is probably the best option to organise your code.

PopGoesTheWza commented 5 years ago

@angrykoala here is the result of transpiling your test case:

First export

export default class Foo {}

The Foo class is defined within a closure which is available as Foo from the global namespace

// Compiled using ts2gas 1.6.2 (TypeScript 3.3.3333)
var exports = exports || {};
var module = module || { exports: exports };
var Foo = /** @class */ (function () {
    function Foo() {
    }
    return Foo;
}());

Then import

import Foo from 'foo'
new Foo()

The import statement is commented out. the Foo class is accessed directly from the global namespace

// Compiled using ts2gas 1.6.2 (TypeScript 3.3.3333)
var exports = exports || {};
var module = module || { exports: exports };
//import Foo from 'foo'
new Foo();

Please comment.

angrykoala commented 5 years ago

Hi @PopGoesTheWza Sorry for taking too long, I've been busy and It's been a long time since I've been working on the appscript project. I think I found the case where I'm still having problems, if you don't mind I'll copy part of the original source code It's troubling me. This problem was found using clasp and ts2gas 1.6.0

Typescript original snippet

import { ICONS } from "../../constants";
// SOME CODE
doSomething(ICONS.email);

....

}

_thread_summarysection.ts

export const CACHE_ENABLED = true;

export const ICONS = {
    email: `foo.png`,
};

constants.ts

After compiling and executing I get the following error:

ReferenceError: "ICONS" is not defined. [line: 28, function: ThreadSummarySection]

Compiled GS

//import { ICONS } from "../../constants";

doSomething(ICONS.email);

thread_summary_section.gs

// Compiled using ts2gas 1.6.0 (TypeScript 3.2.4)
var exports = exports || {};
var module = module || { exports: exports };

exports.CACHE_ENABLED = true;
exports.ICONS = {
    email: "foo.png",
};

constants.gs

Maybe this case is only happening when trying to export without default?

I hope this helps

PopGoesTheWza commented 5 years ago

@grant do you have an opinion about if/how such export/import should be addressed?

(I personally use namespace instead)

JeanRemiDelteil commented 5 years ago

@PopGoesTheWza as @grant already mentioned there are Webpack setup for Google Apps Script. And it's a much better solution as it completely solve the issue with the module by preserving the private environment of a module.

Right now if I want to write TS code that works when compiled to gas (with CLASP), my editor just display errors everywhere because of no import / exports... Not really usefull :/

PopGoesTheWza commented 5 years ago

@JeanRemiDelteil I am dubious about adding webpack to my build process. I manage proper code isolation across many source files by using namespace with no problem:

namespace Package {
  export function foo() {}
}

Package.foo();

const nameIWantForMyImports = Package.foo;
foo();

Namespaces can be defined across multiple files (i.e. the "one file per class" way.) They also can be nested (i.e. Module.Submodule.Class)

PopGoesTheWza commented 5 years ago

One very ugly and kludgy solution, if one has to use export/import

import { ICONS } from './package';

// BEGIN: typescript kludge
type __ICONS = typeof ICONS;  // sad but I couldn't avoid adding this type definition
declare namespace exports {  // because this is the namespace where transpiled exports go
  const ICONS: __ICONS;  //  Yeah! now TypeScript is aware of my transpiled exports
}

exports.ICONS.email;  // this code is valid in both TypeScript and GAS
grant commented 5 years ago

@PopGoesTheWza I don't know how this should be addressed. Please suggest a solution.

PopGoesTheWza commented 5 years ago

@grant @angrykoala @JeanRemiDelteil

With ts2gas currently build around ts.transpileModule, there is no hope in having it to modify the output script in a satisfying way.

Going for another approach (TypeScript compiler service if I remember correctly) would be great but is a task I can barely fathom at the moment.

So if we look at we have now:

To hint at what I would suggest, I opened a @google/clasp PR with edits to the TypeScript.md

N.B. I am only a small contributor, with no intent to step up for involvement (enough on my plate thanks you ;) ) so I let you all decide.

angrykoala commented 5 years ago

My current workaround (which falls a bit into the "third party packager") but is less ugly than the one mentioned above (imo) it to remove with a regex all the export and export default statements from the ts files before compiling. It is currently working for me using clasp. I would like to have that as a embedded option either in clasp or here, but I understand it may fall out of the scope of both projects and it is quite hacky. Just in case this is useful for anyone with the same problem:

https://gist.github.com/angrykoala/f5456c1f7a00d286b155e67774948855

In order to make this work, just add bin (or the target folder) to the gitignore and set as the compilation target (if using clasp, adding !bin/** to the .claspignore does the trick) and execute it before ts2gs or clasp.

My package.json scripts using clasp (preprocessing.js is the file in the gist above):

  "scripts": {
    "test": "nyc mocha -r ts-node/register tests/*.test.ts tests/*/*.test.ts",
    "deploy": "npm run test && node tasks/preprocessing.js && clasp push",
  },
PopGoesTheWza commented 5 years ago

Under @google/clasp is now documented possible workaround: https://github.com/google/clasp/blob/master/docs/typescript.md

PopGoesTheWza commented 5 years ago

@angrykoala @paxperscientiam could you please comment latest updates and state if your issue has been properly addressed?

angrykoala commented 5 years ago

I haven't had time to test the namespace workaround @PopGoesTheWza

JeanRemiDelteil commented 5 years ago

@PopGoesTheWza I tested the namespace workaround. Even if it feels like a hack, it works.

However I had to export/import the namespace because my editor was complaining that it could not find it otherwise:

I would change the demo like following:

// module.ts
export namespace Module {
  export function foo() {}
  function bar() {}  // this function can only be addressed from within the `Module` namespace
}
import {Module} from "./module.ts"

// anyFiles.ts
Module.foo();  // address a namespace's exported content directly

const nameIWantForMyImports = Module.foo;  // to simulate `import` with renaming
nameIWantForMyImports();
PopGoesTheWza commented 5 years ago

@JeanRemiDelteil may I ask about your editor and settings? The namespace workaround should not required that you export the namespace less that you import it. Could the errors in your editor be related with your tsconfig.json or something?

JeanRemiDelteil commented 5 years ago

I'm using Webstorm, and for my test project I don't have a tsconfig.json as the editor has TS working by default (it's running the v3.3.3333 of TS)

If it's just a configuration issue, that should not matter.

JeanRemiDelteil commented 5 years ago

Even with the config file recommended in the clasp/ts doc it gives this error: TS2304: cannot find name 'Module' when I don't export/import

PopGoesTheWza commented 5 years ago

@JeanRemiDelteil could you please check with this minimal project?

https://github.com/PopGoesTheWza/gas-ts-namespace

and its associated gas project

https://script.google.com/d/1cfHm55LxxYOQ7uYT8OYCefhZIcQYjQKMD2_NCA335Gnt0W1EV_URzduC/edit?usp=sharing

The only issue I could experiencing is hoisting (using a namespace/module resource before it gets defined)

PS: totally off topic, but you have a very french sounding name

JeanRemiDelteil commented 5 years ago

@PopGoesTheWza I found why I need to import: In your minimal project, add a sub/ folder in ./src/ add a new .ts file, and write a function that user Module.hello();

For me it worked correctly until I use files in another folder.

PopGoesTheWza commented 5 years ago

@JeanRemiDelteil I updated gas-ts-namespace with a module in a subfolder. For some unknown reason, typescript+vsc was initially confused, but after closing and reopening the project everything was fine in the editor.

Note: for simplicity, this project has matching namespace and file name (i.e. Module in Module.ts) but they are totally unrelated and do not need to match.

PopGoesTheWza commented 5 years ago

Request For Comments:

I have put up a repository which illustrate and document what I believe is the best alternative to the lack of support for module. https://github.com/PopGoesTheWza/ts-gas-project-starter

I am very interested in your comments, critics and feedback

mrts commented 4 years ago

As of today, import seems to be mostly working with clasp and it claims to use

// Compiled using ts2gas 3.6.2 (TypeScript 3.9.6)

Heartfelt thanks for this ๐ŸŽ‰ !


However, when I include Moment by adding moment.js and moment.d.ts to lib and import it to any module in src with

import moment from '../lib/moment.js'

then I get the following error in Google Sheets:

ReferenceError: moment is not defined
    at [unknown function](lib/moment.d:5:18)

lib/moment.d.gs looks as follows after going through ts2gas:

// Compiled using ts2gas 3.6.2 (TypeScript 3.9.6)
var exports = exports || {};
var module = module || { exports: exports };
"use strict";
module.exports = moment;
//# sourceMappingURL=module.js.map

lib/moment.gs itself remains more or less pristine moment.js after going through ts2gas and it handles module export with

;(function(global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? (module.exports = factory())
    : typeof define === 'function' && define.amd
    ? define(factory)
    : (global.moment = factory())
})(this, function() { ...

So it seems global.moment = factory() in moment.gs does not actually create the global moment symbol and therefore module.exports = moment fails in moment.d.gs.

What is the right way to work around this error?

paxperscientiam commented 4 years ago

Hey @mrts , i haven't used this google stuff in a bit, but I've run into something similar. You might have luck by explicitly attaching moment to globalThis

PopGoesTheWza commented 3 years ago

@mrts check the clasp repos. Some docs was added recently which involved moment.js if I remember correctly

mrts commented 3 years ago

Thanks, I was able to get rid of moment.js meanwhile, so all is well at the moment.

guilhermetod commented 3 years ago

Just in case someone is trying to avoid the problematic syntax.


// .eslintrc.js
module.exports = {
  // ...
  rules: {
    // ...
    'no-restricted-syntax': [
      'error',
      {
        selector: 'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator:not([init.type=ArrowFunctionExpression])',
        message: 'Don\'t use exported variable declarations unless it\'s a function (https://github.com/grant/ts2gas/issues/26)',
      },
    ],
  }
}
`
mrts commented 2 years ago

As of today, import seems to be mostly working with clasp and it claims to use // Compiled using ts2gas 3.6.2 (TypeScript 3.9.6)

Something changed recently and broke the quite well-functioning import system. I hadn't used clasp for a while and when I installed clasp into my new computer, cloned my script repository, made a minor change to a TypeScript file and did a clasp push, then opened the Google Spreadsheet that uses the scripts, I suddenly started getting ReferenceError: MyImportedModule is not defined errors.

Also, for some reason the ts2gas version is no longer reported in the script header, the script name is used instead (incorrectly I presume, this looks like a bug to me): // Compiled using myscriptname 0.1.0 (TypeScript 4.5.2)

I had to use npm list to figure out the versions:

$ npm list -g ts2gas
/usr/lib
โ””โ”€โ”ฌ @google/clasp@2.4.1
  โ””โ”€โ”€ ts2gas@4.2.0

@PopGoesTheWza, any ideas what might have caused this?

DarekDarecki commented 2 years ago

@mrts I have the same issue, have you found the reason by any chance?

mrts commented 2 years ago

@DarekDarecki, no, I retreated and removed the nice import system... built a poor man's Webpack instead:

  1. cd src && cat *.ts > Bundle.ts.
  2. Manually remove imports and exports from Bundle.ts.
  3. Modify your .claspignore so that only appsscript.json and src/Bundle.ts are included.
  4. clasp push.

What a great loss!

PopGoesTheWza commented 2 years ago

@mrts have you tried using namespaces?

To clarify, either go bundling (webpack and all that) or go namespace (like https://github.com/PopGoesTheWza/ts-gas-project-starter)

All else is bound to fail

balloman commented 2 years ago

I'm glad to know I'm not the only one who is experiencing this issue. Almost 3000 lines of apps script code is suddenly broken by ts2gas and I've been pulling my hair out trying to figure out why. Any reason why this broke all of a sudden? How can we revert @PopGoesTheWza ?

balloman commented 2 years ago

Functions from imported files that used to be transpiled like this stringifyData(travelData.travelMap, "travelData"); all of a sudden transpile like this: (0, Util_1.stringifyData)(travelData.travelMap, "travelData");. and obviously this completely breaks any function that is used from another class

PopGoesTheWza commented 2 years ago

Ok. I think I now do see your issue. Updating clasp/ts2gas/typescript changed your transpiled code in a no longer functioning state.

First, I should restate here: Google App Script do NOT support modules. Never use import/export (except for namespaces)

ts2gas handling of import/export of module is in fact extremely limited, deceitful and a bad idea to start with.

Now the no easy way to possibly restore your project should be to downgrade clasp/ts2gas/typescript to previous versions.

Any other solution will likely require some amount of refactoring in order to either rely on some bundler or use namespaces.