Skip to content

Commit

Permalink
Add TypeScript support
Browse files Browse the repository at this point in the history
  • Loading branch information
grant committed Jul 18, 2018
1 parent aeed9ec commit 1bfa2a1
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 24 deletions.
40 changes: 26 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

> Develop [Apps Script](https://developers.google.com/apps-script/) projects locally using clasp (*C*ommand *L*ine *A*pps *S*cript *P*rojects).
<!-- GIF bash prompt: PS1='\[\033[38;5;9m\]❤ \[$(tput sgr0)\]' -->
<!-- Width: 888px -->
![clasp](https://user-images.githubusercontent.com/744973/35164939-43fd32ae-fd01-11e7-8916-acd70fff3383.gif)

**To get started, try out the [codelab](https://g.co/codelabs/clasp)!**
Expand All @@ -27,6 +29,16 @@
- `slides.js`
- `sheets.js`

**[🔷 Write Apps Script in TypeScript](docs/typescript):** Write your Apps Script projects using TypeScript features:
- Arrow functions
- Optional structural typing
- Classes
- Type inference
- Interfaces
- And more...

Read the [TypeScript guide](docs/typescript) to get started.

## Install

First download `clasp`:
Expand All @@ -47,20 +59,20 @@ sudo npm i -g grpc @google/clasp --unsafe-perm
```sh
clasp
```
- `clasp login [--no-localhost]`
- `clasp logout`
- `clasp create [scriptTitle] [scriptParentId]`
- `clasp clone <scriptId>`
- `clasp pull`
- `clasp push [--watch]`
- `clasp open [scriptId]`
- `clasp deployments`
- `clasp deploy [version] [description]`
- `clasp redeploy <deploymentId> <version> <description>`
- `clasp version [description]`
- `clasp versions`
- `clasp list`
- `clasp logs [--json] [--open]`
- [`clasp login [--no-localhost]`](#login)
- [`clasp logout`](#logout)
- [`clasp create [scriptTitle] [scriptParentId]`](#create)
- [`clasp clone <scriptId>`](#clone)
- [`clasp pull`](#pull)
- [`clasp push [--watch]`](#push)
- [`clasp open [scriptId]`](#open)
- [`clasp deployments`](#deployments)
- [`clasp deploy [version] [description]`](#deploy)
- [`clasp redeploy <deploymentId> <version> <description>`](#redeploy)
- [`clasp version [description]`](#version)
- [`clasp versions`](#versions)
- [`clasp list`](#list)
- [`clasp logs [--json] [--open]`](#logs)

## How To...

Expand Down
191 changes: 191 additions & 0 deletions docs/typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# TypeScript

[TypeScript](https://www.typescriptlang.org/) is a typed superset of JavaScript that can compile to plain Apps Script.
Using TypeScript with your `clasp` project can allow you to use TypeScript features such as:
- Arrow functions
- Optional structural typing
- Classes
- Type inference
- Interfaces
- And more...

Clasp `1.5.0` allows **new** and **existing** Apps Script projects to use TypeScript.

> Note: Once you use TypeScript, you cannot develop on script.google.com (the [transpiled](https://en.wikipedia.org/wiki/Source-to-source_compiler) code).
## Quickstart

This quickstart guide describes how to create a TypeScript project from scratch.

### Prerequisites

1. Ensure you have upgrade to clasp >= 1.5.0
- `clasp -v`
1. Install TypeScript definitions for Apps Script in your project's folder.
- `npm i -S @types/google-apps-script`

### Create the TypeScript Project

Create a clasp project in an empty directory (or use an existing project):

```sh
clasp create
```

If you haven't already, run `npm i -S @types/google-apps-script` to allow your code editor to autocomplete TypeScript.

Create a TypeScript file called `hello.ts` with the following contents:

```ts
const greeter = (person: string) => {
return `Hello, ${person}!`;
}

function testGreeter() {
const user = 'Grant';
Logger.log(greeter(user));
}
```

> Note: This is a valid TypeScript file (but an invalid Apps Script file). That's OK.
### Push the project to the Apps Script server

Push the TypeScript file to the Apps Script server:

```sh
clasp push
```

> Note: clasp automatically transpiles `.ts` files to valid Apps Script files upon `clasp push`.
### Verify the project works on script.google.com

Open the Apps Script project on script.google.com:

```sh
clasp open
```

Notice how a transpiled version of your project was pushed to the Apps Script server.

Run `testGreeter()` and press `View` > `Logs` to view the logs to see the result.

## TypeScript Examples

This section lists TypeScript examples derived from [this guide](https://angular-2-training-book.rangle.io/handout/features/):

These features allow you to write Apps Script concisely with intelligent IDE errors and autocompletions.

```ts
// Optional Types
let x: string = 'string';
let y: string = 'string2';
const doc: GoogleAppsScript.Document.Document = DocumentApp.create('Hello, world!');
var foo = (a, b) => a + b;

const add = function(x: number, y: number): number {
return x + y;
}
add(4_232, 1e3);

// Classes
class Hamburger {
constructor() {
// This is the constructor.
}
listToppings() {
// This is a method.
}
}

// Template strings
var name = 'Sam';
var age = 42;
console.log(`hello my name is ${name}, and I am ${age} years old`);

// Rest arguments
const add = (a, b) => a + b;
let args = [3, 5];
add(...args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`

// Spread operator (array)
let cde = ['c', 'd', 'e'];
let scale = ['a', 'b', ...cde, 'f', 'g']; // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

// Spread operator (map)
let mapABC = { a: 5, b: 6, c: 3};
let mapABCD = { ...mapABC, d: 7}; // { a: 5, b: 6, c: 3, d: 7 }

// Destructure map
let jane = { firstName: 'Jane', lastName: 'Doe'};
let john = { firstName: 'John', lastName: 'Doe', middleName: 'Smith' }
function sayName({firstName, lastName, middleName = 'N/A'}) {
console.log(`Hello ${firstName} ${middleName} ${lastName}`)
}
sayName(jane) // -> Hello Jane N/A Doe
sayName(john) // -> Helo John Smith Doe

// Export (The export keyword is ignored)
export const pi = 3.141592;

// Google Apps Script Services
var doc = DocumentApp.create('Hello, world!');
doc.getBody().appendParagraph('This document was created by Google Apps Script.');

// Decorators
function Override(label: string) {
return function (target: any, key: string) {
Object.defineProperty(target, key, {
configurable: false,
get: () => label
});
}
}
class Test {
@Override('test') // invokes Override, which returns the decorator
name: string = 'pat';
}
let t = new Test();
console.log(t.name); // 'test'
```

After installing `@types/google-apps-script`, editors like Visual Studio Code autocomplete types:

```ts
var doc = DocumentApp.create('Hello, world!');
doc.getBody().appendParagraph('This document was created by Google Apps Script.');
Logger.log(doc.getUrl());
```

In this case, we could write the fully qualified type:

```ts
const doc: GoogleAppsScript.Document.Document = DocumentApp.create('Hello, world!');
```

Or inferred type:

```ts
const doc = DocumentApp.create('Hello, world!');
```

In most cases, the inferred type is sufficient for Apps Script autocompletion.

## How it works

`clasp push` transpiles ES6+ into ES3 (using [`ts2gas`](https://github.com/grant/ts2gas)) before pushing files to the Apps Script server.

## Gotchas

### Advanced Services

Enableing advanced services modifies the `appsscript.json` file on script.google.com. After enabling an advanced service in the UI, copy the `appsscript.json` file from script.google.com into your editor to use the advanced services in your project.

### TypeScript Support

Currently, `clasp` supports [`[email protected]`](https://www.npmjs.com/package/typescript/v/2.9.2). If there is a feature in a newer TypeScript version that you'd like to support, or some experimental flag you'd like enabled, please file a bug.

## Further Reading

- Consider using a linter like [`tslint`](https://github.com/palantir/tslint) to increase the quality of your TypeScript projects.
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"recursive-readdir": "^2.2.2",
"split-lines": "^1.1.0",
"string.prototype.padend": "^3.0.0",
"ts2gas": "^1.0.0",
"url": "^0.11.0",
"watch": "^1.0.2"
},
Expand Down
3 changes: 2 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as del from 'del';
import * as pluralize from 'pluralize';
import { watchTree } from 'watch';
import { drive, loadAPICredentials, logger, script, discovery } from './auth';
import { discovery, drive, loadAPICredentials, logger, script } from './auth';
import { fetchProject, getProjectFiles, hasProject, pushFiles } from './files';
import {
DOT,
Expand Down Expand Up @@ -501,6 +501,7 @@ export const status = async (cmd: { json: boolean }) => {
} else {
console.log(LOG.STATUS_PUSH);
filesToPush.forEach((file) => console.log(`└─ ${file}`));
console.log(); // Separate Ignored files list.
console.log(LOG.STATUS_IGNORE);
untrackedFiles.forEach((file) => console.log(`└─ ${file}`));
}
Expand Down
50 changes: 45 additions & 5 deletions src/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
logError,
spinner,
} from './utils';
const ts2gas = require('ts2gas');
const path = require('path');
const readMultipleFiles = require('read-multiple-files');

Expand Down Expand Up @@ -96,13 +97,24 @@ export function getProjectFiles(rootDir: string, callback: FilesCallback): void
}
}
});
if (abortPush) return callback(new Error(), null, null);

if(abortPush) return callback(new Error(), null, null);

// Loop through every file.
const files = filePaths.map((name, i) => {
// File name
let nameWithoutExt = name.slice(0, -path.extname(name).length);
// Replace OS specific path separator to common '/' char
nameWithoutExt = nameWithoutExt.replace(/\\/g, '/');
let type = getAPIFileType(name);

// File source
let source = contents[i];
if (type === 'TS') {
// Transpile TypeScript to Google Apps Script
// @see github.com/grant/ts2gas
source = ts2gas(source);
type = 'SERVER_JS';
}

// Formats rootDir/appsscript.json to appsscript.json.
// Preserves subdirectory names in rootDir
Expand All @@ -114,12 +126,40 @@ export function getProjectFiles(rootDir: string, callback: FilesCallback): void
nameWithoutExt.length,
);
}
if (getAPIFileType(name) && !anymatch(ignorePatterns, name)) {

/**
* If the file is valid, add it to our file list.
* We generally want to allow for all file types, including files in node_modules/.
* However, node_modules/@types/ files should be ignored.
*/
const isValidFileName = (name: string) => {
let valid = true; // Valid by default, until proven otherwise.
// Has a type or is appsscript.json
let isValidJSONIfJSON = true;
if (type === 'JSON') {
isValidJSONIfJSON = (name === 'appsscript.json');
} else {
// Must be SERVER_JS or HTML.
// https://developers.google.com/apps-script/api/reference/rest/v1/File
valid = (type === 'SERVER_JS' || type === 'HTML');
}
// Prevent node_modules/@types/
if (name.includes('node_modules/@types')) {
return false;
}
const validType = type && isValidJSONIfJSON;
const notIgnored = !anymatch(ignorePatterns, name);
valid = !!(valid && validType && notIgnored);
return valid;
};

// If the file is valid, return the file in a format suited for the Apps Script API.
if (isValidFileName(name)) {
nonIgnoredFilePaths.push(name);
const file: AppsScriptFile = {
name: formattedName, // the file base name
type: getAPIFileType(name), // the file extension
source: contents[i], //the file contents
type, // the file extension
source, //the file contents
};
return file;
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ export const LOG = {
FINDING_SCRIPTS_DNE: 'No script files found.',
OPEN_PROJECT: (scriptId: string) => `Opening script: ${scriptId}`,
PULLING: 'Pulling files...',
STATUS_PUSH: 'The following files will be pushed by clasp push:',
STATUS_IGNORE: 'Untracked files:',
STATUS_PUSH: 'Not ignored files:',
STATUS_IGNORE: 'Ignored files:',
PUSH_SUCCESS: (numFiles: number) => `Pushed ${numFiles} ${pluralize('files', numFiles)}.`,
PUSH_FAILURE: 'Push failed. Errors:',
PUSH_WATCH: 'Watching for changed files...\n',
Expand Down

0 comments on commit 1bfa2a1

Please sign in to comment.