はじめに
この記事では以下のことについて解説しています。
- TSのビルドツールの選定
- Cloud Run functions関数を Parcel を使ってビルドする手法
- Parcelでのモジュール解決方法
- Parcel の注意点 / 問題点
TypsScriptのビルドツールを選ぶ上で考えること
本番環境ではTypeScriptをそのまま動かすことはしません。JavaScriptに変換(トランスパイル)して使います。
この変換するプロセスを「ビルド」と言いますが、TS → JSのビルドプロセスは以下のように進みます。
- 型チェック
- TypeScript → JavaScript変換
- ECMAScript構文変換
- Polyfill(古いブラウザでサポートされていない機能を使えるようにするためのコード変換)
- バンドル(一つのファイルにまとめる, minifyする)
巷には大量のTSビルドツールが溢れていますが、上記のプロセスを踏んでいるかつxxx.config.json
的なものを一生懸命書かなくてもサクッと使える Parcel を選択したパターンを紹介したいと思います。
Parcelは上記で述べたビルドプロセスに加えて、
- Tree shaking(実行されないコードの削除)
- minify
もやってくれる上に、「Zero Config」つまりほとんど自前で設定する必要がないという特徴があります。規模の大きなプロジェクトとかだと物足りないかもしれませんが、今回の開発要件(小規模かつ一人で開発している)だと十分です。
Parcel で Node.js関数(TS)をビルドする
まず Parcel を devDependencies
にインストールしましょう。
npm install --save-dev parcel
以下のコマンドを package.json
に追加しましょう。
"scripts": {
"build": "npx parcel build ./src/index.ts"
}
npm run build
を実行してみましょう。
$ 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
のファイルを読み込んでよしなにファイル出力してくれました。
├── .parcel-cache
├── dist
│ └── index.js
│ └── index.js.map
...
.parcel-cache
は適宜 .gitignore
などに入れておきましょう。
しかし Parcel は何も設定していないとTSの型チェックを行ってくれません。
試しに以下のように書き換えてあえて型エラーを起こしてみましょう。
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
を入力してみます。
$ 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
通ってしまいました。出力後のファイルを見ても何にも改善されていません。
const $c3f6c693698dc7cd$export$900f632196ed0f96 = (req, res)=>{
let a = "aaa";
a = 1;
res.send(`Hello World!`);
};
export {$c3f6c693698dc7cd$export$900f632196ed0f96 as helloGET};
公式のページではこのことについて「Parcelはデフォでは型チェック行わへんで〜〜別途コマンド叩いて確認してや〜〜」と述べています。
一応Experimentalではありますが、型チェック用のプラグイン @parcel/validator-typescript
が用意されています。このプラグインを使うという情報を設定ファイルを作って伝えましょう。Zero Configとは。
├── .parcelrc
├── src
...
{
"extends": "@parcel/config-default",
"validators": {
"*.{ts,tsx}": ["@parcel/validator-typescript"]
}
}
もう一度 npm run build
を実行してみましょう。
$ 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
の章で紹介した以下の設定。
{
"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
を合わせなければいけません。
{
"compilerOptions": {
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
"baseUrl": ".",
"paths": {
"~/*": ["./*"]
},
"rootDirs": ["."],
"outDir": "./dist",
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}
例えば以下のディレクトリ構成でファイルを作成したとします。
├── src
│ ├── hoge
│ │ └── hoge.ts
│ └── index.ts
├── package.json
└── tsconfig.json
export const hoge = () => "hogehoge"
このsrc/hoge/hoge.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!`);
};
$ 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
出力されたファイルを見ると、パス解決がされたバンドルファイルが出力されました。
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
@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