Serverless + Webpack + TypeScript + tslint メモ

# :pray: 神 :pray: 

[Getting Started With Serverless and TypeScript](https://gregshackles.com/getting-started-with-serverless-and-typescript/)

# 用意するもの

- Node.js
    - あんまり古くないやつ

# Hello World まで

## repo を作る

```shell-session
$ mkdir oreno-sls-ts
$ cd oreno-sls-ts
$ git init
```

## 依存物をインストール

```shell-session
$ npm init
$ npm install --save-dev serverless serverless-webpack ts-loader tslint typescript webpack
```

今回はあえて `serverless` をグローバルに入れないパティーンでやってみる。 `package.json` で `serverless` のバージョンを管理できるメリットがある（`serverless.yml` の中にバージョン固定する設定もできるけど）。

## 雛形を作り、いらないものを一旦捨てる

```shell-session
$ node_modules/.bin/serverless create --template aws-nodejs
$ node_modules/.bin/tslint --init
$ rm handler.js serverless.yml # このあと手動でつくる
```

## いろいろ作る

```text:.gitignore
node_modules
.serverless
.built
```

```yaml:serverless.yml
service: oreno-sls-ts
provider:
  name: aws
  runtime: nodejs4.3
plugins:
- serverless-webpack
functions:
  hello:
    handler: index.hello
    events:
    - http:
        path: hello
        method: get
```

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs"
  },
  "exclude": [
    "node_modules"
  ]
}
```

```js:webpack.config.js
var path = require('path');

module.exports = {
  entry:  './index.ts',
  target: 'node',

  module: {
    loaders: [
      {
        test:   /\.ts(x?)$/,
        loader: 'ts-loader',
      },
    ],
  },

  resolve: {
    extensions: [
      '.ts',
      '.js',
      '.tsx',
      '.jsx',
      '',
    ],
  },

  output: {
    libraryTarget: 'commonjs',
    path:          path.join(__dirname, '.built'),
    filename:      'index.js',
  },
};
```

```ts:index.ts
export * from './functions/hello';
```

```ts:functions/hello.ts
export function hello(event, context, callback): void {
  const response = {
    statusCode: 200,

    headers: {
    },

    body: JSON.stringify({
      "message": "Hello TypeScript!!!!"
    })
  };

  callback(null, response);
};
```

## ローカルで動作確認

```shell-session
$ node_modules/.bin/serverless webpack serve
```

うまく動いていれば http://localhost:8000/hello で動作確認できるはず。

## デプロヰ

```shell-session
$ node_modules/.bin/serverless deploy                                                                                                                              
```

```shell-session
$ curl https://********.execute-api.us-east-1.amazonaws.com/dev/hello | jq
```

うまくいけば `{"message": "Hello TypeScript!!!!"}` が表示されるはず。


# せっかくの TypeScript なので

## 型定義を使ってみる

Lambda Function に渡される [`context` オブジェクト](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-context.html)を型定義してみます。 `event` はめちゃくちゃ長くてアレだったので、ちゃんと書くときには定義します…

追記: `context` の型定義は自分でしなくても `@types/aws-lambda` パッケージを使えます。

```ts:types.d.ts
export interface LambdaExecutionContext {
  callbackWaitsForEmptyEventLoop: boolean
  logGroupName:                   string
  logStreamName:                  string
  functionName:                   string
  memoryLimitInMB:                string
  functionVersion:                string
  invokeid:                       string
  awsRequestId:                   string
  invokedFunctionArn:             string
}
```

さっきの `hello` function を少し変更してこれを使ってみます。

```ts:functions/hello.ts
import {LambdaExecutionContext} from '../types';

export function hello(event, context: LambdaExecutionContext, callback): void {
  const response = {
    statusCode: 200,

    headers: {
    },

    body: JSON.stringify({
      "message": `Hello!!!! I am a function named "${context.functionName}"`
    })
  };

  callback(null, response);
};
```

さてさて型定義されているということは。 Visual Studio Code なら補完が効きますし、

![hello_ts_-_serverless-ts.png](https://qiita-image-store.s3.amazonaws.com/0/78376/55ff5ee8-cb8b-a7eb-0b59-537c8ed602a4.png)

ありえないプロパティを指定するとちゃんとエラーを指摘してくれます。

![hello_ts_-_serverless-ts.png](https://qiita-image-store.s3.amazonaws.com/0/78376/c81ed0b0-5406-f05c-4566-cad3eef492a2.png)

すてき！！！！！

## AWS SDK で `async/await` してみる

パッケージを追加する。

```shell-session
$ npm install --save-dev aws-sdk @types/aws-sdk json-loader
```

`@types/aws-sdk` は [DefinitelyTyped にある AWS SDK 用の型定義](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/aws-sdk)。 TypeScript の型定義管理はちょうざっくり変遷を辿ると 

1. `tsd` っていう型定義管理ツールがあるぞ
2. `tsd` はオワコン。 `typings` の時代だぜ
3. 型定義インストール専用のツールなんてもういらん。 `npm` で `@types/foo` って入れればおｋ

…という流れらしく、従って `npm` で入れる。

`json-loader` は AWS SDK の中で `require('foo.json')` してるの（たぶん）を Webpack で処理するのに必要。

ちょっと昔は AWS SDK と Webpack の組み合わせに問題があり、みんな困ってたけど[直った模様](https://github.com/aws/aws-sdk-js/issues/603#issuecomment-245804312)。マジ感謝。

```ts:index.ts
export * from "./functions/hello";
export * from "./functions/listfunctions";
```

```ts:functions/listfunctions.ts
import * as AWS from "aws-sdk";
import {LambdaExecutionContext} from "../types";

const lambda = new AWS.Lambda();

export async function listfunctions(event, context: LambdaExecutionContext, callback): Promise<void> {
  const functions  = [];
  let   nextMarker = null;

  do {
    const response: AWS.Lambda.ListFunctionsResponse = await lambda.listFunctions({Marker: nextMarker}).promise();

    functions.push(...response.Functions);
    nextMarker = response.NextMarker;
  } while (nextMarker);

  callback(null, {
    statusCode: 200,
    headers:    {},
    body:       JSON.stringify({functions}),
  });
};
```

を追加します。 AWS の API によくある `NextMarker` での判断を含んだ非同期 N 回ループがこんなに簡単に書けちゃいます。最高。

他のファイルも微変更が必要なのですが、後述の repo に全部あげておくのでそちらを参照してください。

再度デプロイして…

```shell-session
$ node_modules/.bin/serverless deploy                                                                                                                              
```

実行してみます。

```shell-session
$ curl https://********.execute-api.us-east-1.amazonaws.com/dev/listfunctions | jq
```

うまくいけばデプロイされたこれ自体を含む Lambda Function の一覧が取れるはずですね！

# ここまでやったのがこちら

- ~~[y13i/serverless-ts-example](https://github.com/y13i/serverless-ts-example)~~
- [y13i/serverless-ts-boilerplate](https://github.com/y13i/serverless-ts-boilerplate)

どうぞご笑覧ください。その後、諸々を見直してこの記事の内容とだいぶ違いがあったりします、ご注意を。