Cloud Run functions × TypeScript 開発をしよう！【ビルド：Parcel を使ったTypeScriptのビルド】

# はじめに
この記事では以下のことについて解説しています。
- TSのビルドツールの選定
- Cloud Run functions関数を Parcel を使ってビルドする手法
- Parcelでのモジュール解決方法
- Parcel の注意点 / 問題点
---

# TypsScriptのビルドツールを選ぶ上で考えること
本番環境ではTypeScriptをそのまま動かすことはしません。JavaScriptに変換（トランスパイル）して使います。
この変換するプロセスを「ビルド」と言いますが、TS → JSのビルドプロセスは以下のように進みます。

1. 型チェック
2. TypeScript → JavaScript変換
3. ECMAScript構文変換
4. Polyfill（古いブラウザでサポートされていない機能を使えるようにするためのコード変換）
5. バンドル（一つのファイルにまとめる, minifyする）

![mermaid-diagram-2024-12-08-211821.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/dab4846a-9bac-1b96-ab7c-768371726f10.png)

巷には大量のTSビルドツールが溢れていますが、上記のプロセスを踏んでいるかつ`xxx.config.json`的なものを一生懸命書かなくてもサクッと使える **Parcel** を選択したパターンを紹介したいと思います。

Parcelは上記で述べたビルドプロセスに加えて、
- Tree shaking（実行されないコードの削除）
- minify

もやってくれる上に、「Zero Config」つまりほとんど自前で設定する必要がないという特徴があります。規模の大きなプロジェクトとかだと物足りないかもしれませんが、今回の開発要件（小規模かつ一人で開発している）だと十分です。

# Parcel で Node.js関数（TS）をビルドする
まず Parcel を `devDependencies` にインストールしましょう。
```bash
npm install --save-dev parcel
```
以下のコマンドを `package.json` に追加しましょう。

```js: package.json
  "scripts": {
    "build": "npx parcel build ./src/index.ts"
  }
```

`npm run build` を実行してみましょう。

```bash:
$ npm run build                                            

> advent-calendar-2024@1.0.0 build
> npx parcel build ./src/index.ts

✨ Built in 228ms

dist/index.js    211 B    39ms
```

`tsconfig.json` のファイルを読み込んでよしなにファイル出力してくれました。

```bash:ビルド後のディレクトリ構成
├── .parcel-cache
├── dist
│   └── index.js
│   └── index.js.map
...
```

`.parcel-cache` は適宜 `.gitignore` などに入れておきましょう。

しかし **Parcel は何も設定していないとTSの型チェックを行ってくれません。**
試しに以下のように書き換えてあえて型エラーを起こしてみましょう。

```ts: stringの型に数値を入れてみる
import * as ff from '@google-cloud/functions-framework';
import type { HttpFunction } from "@google-cloud/functions-framework";

export const helloGET: HttpFunction =  (req: ff.Request, res: ff.Response) => {
    let a: string = "aaa";
    a = 1;
    res.send(`Hello World!`);
};
```

`npm run build` を入力してみます。

```bash
$ npm run build                                            

> advent-calendar-2024@1.0.0 build
> npx parcel build ./src/index.ts

✨ Built in 228ms

dist/index.js    211 B    39ms
```

通ってしまいました。出力後のファイルを見ても何にも改善されていません。

```js:
const $c3f6c693698dc7cd$export$900f632196ed0f96 = (req, res)=>{
    let a = "aaa";
    a = 1;
    res.send(`Hello World!`);
};


export {$c3f6c693698dc7cd$export$900f632196ed0f96 as helloGET};
```

公式のページではこのことについて「Parcelはデフォでは型チェック行わへんで〜〜別途コマンド叩いて確認してや〜〜」と述べています。

https://parceljs.org/languages/typescript/#type-checking

一応Experimentalではありますが、型チェック用のプラグイン `@parcel/validator-typescript` が用意されています。このプラグインを使うという情報を設定ファイルを作って伝えましょう。~~Zero Configとは。~~

```bash: .parcelrcを追加する
├── .parcelrc
├── src
...
```

```js: .parcelrc
{
  "extends": "@parcel/config-default",
  "validators": {
    "*.{ts,tsx}": ["@parcel/validator-typescript"]
  }
}
```

もう一度 `npm run build` を実行してみましょう。

```bash
$ npm run build                             

> advent-calendar-2024@1.0.0 build
> npx parcel build ./src/index.ts

🚨 Build failed.

@parcel/validator-typescript: Type 'number' is not assignable to type 'string'.

  /Users/usr0302216/local/advent-calendar-2024/src/index.ts:5:3
    4 |   let a: string = "aaa";
  > 5 |   a = 1;
  >   |   ^^ Type 'number' is not assignable to type 'string'.
    6 |   res.send(`Hello World!`);
    7 | });

```
しっかり型チェックが走るようになりましたね。
「Experimental」と書いてあるように、まだ実験段階の機能のため不安定な挙動を起こすことがあるようです。ビルド前に `tsc --noEmit` を叩いて型チェックを挟むのが安全かもしれませんね。

# Parcelのモジュール解決方法
「Zero Config」ではありますが、何も設定しなくていい反面何かカスタマイズしようとすると超めんどくさかったりします。
例えば [`tsconfig.json` の章](https://qiita.com/KyongminS/items/a3c3fb6cf88df6098c9d)で紹介した以下の設定。

```js:tsconfig.json（最初）
{
  "compilerOptions": {
    "target": "es2022",
    "module": "node16",
    "moduleResolution": "node16",
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    },
    "rootDirs": ["./src"],
    "outDir": "./dist",
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
  },
  "include": ["src/**/*"],
  "exclude": ["dist", "node_modules"]
}
```

Parcel を使うと強制定期に**package.jsonの存在するディレクトリが`baseUrl`かつ`rootUrl`になります。ついでに`paths`のエイリアスも~(チルダ)になります。そして Parcel の設定に`tsconfig.json`を合わせなければいけません。**

https://parceljs.org/features/dependency-resolution/#tsconfig

https://parceljs.org/features/dependency-resolution/#tilde-specifiers


```js: Parcel 用にカスタマイズ
{
  "compilerOptions": {
    "target": "es2022",
    "module": "node16",
    "moduleResolution": "node16",
    "baseUrl": ".",
    "paths": {
      "~/*": ["./*"]
    },
    "rootDirs": ["."],
    "outDir": "./dist",
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
  },
  "include": ["src/**/*"],
  "exclude": ["dist", "node_modules"]
}
```

例えば以下のディレクトリ構成でファイルを作成したとします。

```bash:サンプル
├── src
│   ├── hoge
│   │   └── hoge.ts
│   └── index.ts
├── package.json
└── tsconfig.json
```


```ts:src/hoge/hoge.ts
export const hoge = () => "hogehoge"
```

この`src/hoge/hoge.ts`を`src/index.ts`で呼び出すとなると、以下のようになります。

```ts:src/index.ts
import * as ff from '@google-cloud/functions-framework';
import type { HttpFunction } from "@google-cloud/functions-framework";

import { hoge } from '~/src/hoge/hoge.js';

export const helloGET: HttpFunction =  (req: ff.Request, res: ff.Response) => {
  const hogehoge = hoge();
  console.log(hogehoge)
  res.send(`Hello World!`);
};
```

```bash:build
$ npm run build

> advent-calendar-2024@1.0.0 build
> npx parcel build ./src/index.ts --no-cache

✨ Built in 466ms

dist/index.js    347 B    32ms
```

出力されたファイルを見ると、パス解決がされたバンドルファイルが出力されました。

```js:dist/index.js
const $a15d677254c32d38$export$bdaf53e7a5e88bfe = ()=>"hogehoge";


const $c3f6c693698dc7cd$export$900f632196ed0f96 = (req, res)=>{
    const hogehoge = (0, $a15d677254c32d38$export$bdaf53e7a5e88bfe)();
    console.log(hogehoge);
    res.send(`Hello World!`);
};


export {$c3f6c693698dc7cd$export$900f632196ed0f96 as helloGET};
//# sourceMappingURL=index.js.map

```

# Parcel のイケていないところ
僕的に Parcel を使っていて思った痒いところは、
- せっかく書いた `tsconfig.json` の設定をあんまり使ってくれない
    - `paths`, `baseUrl` が　`Parcel` 側で決まっている
    - なんなら `Parcel` 側に合わせて書き換えろと言われる始末
    - 結果 `~/src` って書かないといけないが、なんかダサい
- ビルド後のファイルの minifyが 甘い
- ビルド時の型チェックが実験的な機能でしかできない

とその他、言ったところでしょうか。微妙にやりたいことができない割に実現しようと思うと結構めんどくさいなといった印象です。こだわらなければ楽でそれなりにいいツールだと思います。~~色々と細かい設定をしたいはずのビルドが「Zero Config」でできるはずがないんですよね~~

### 補足
`moduleResolution` に `node16` を使用しているためモジュールインポートの際に拡張子を記載しています。今ではこの拡張子も記載する記法がスタンダードになっているみたいですね。

# おわりに
今回は Parcel を利用したTSのビルド方法を紹介しました。

Parcelを使ったビルド環境を構築したことがあったので紹介してみたのですが、記事書いていて「あれ？Parcel微妙じゃね？」ってなったのでもう一つビルド系の記事書きます。

# おまけ：トラブルシューティング
## @parcel/core: Failed to resolve '@google-cloud/functions-framework' from <br/> @parcel/resolver-default: External dependency "@google-cloud/functions-framework" is not declared in package.json.
### 原因
ビルド時に`@google-cloud/functions-framework`が見つからねーよって言っています。
`devDependencies` にfunctions-frameworkをインスコしている可能性が高いです。本番用にビルドするため、本番用のライブラリを読みこむ `dependencies` に置かないといけません。


### 解決法
functions-frameworkを以下のコマンドで devDependencies → dependencies に移しましょう。
```
npm install --save-prod @google-cloud/functions-framework
```