ほりひログ

所属組織の製品 (Azure とか) に関連する内容が多めだけど、個人の見解であって、所属組織を代表する公式情報ではないです。

Azure Functions で Top-Level await は使えるのか? New Programming model 編

Microsoft Azure Tech Advent Calendar 2022 の 3 つ目?の 23 日目。

はじめに

Azure Functions の Node.js ランタイムで新しいプログラミング モデル*1の開発が進んでいるのは以前のエントリーの通り。
uncaughtexception.hatenablog.com
Azure ではまだ使えない。リリースが待ち遠しい。

そのエントリーの中でコソッと書いたこれ。

その後ちゃんと検証せずに放置していたので改めて見てみた。

いきなり結論

使える。
# 既視感。

まぁ、モデルを変えたタイミングでトレンドからあえて外れる、ってことはないだろうから、これは予想通り。

ちゃんと解説

# 中身よりもサンプルを試したい人はコチラ

前回も書いた通り、Top-level await は ES modules の仕様*2
なので、関数コードが ES modules として扱われる必要がある。

新しいプログラミング モデルでは、ユーザーがデプロイした関数コードのパッケージに含まれる package.jsonmain フィールドに書いたファイルがエントリー ポイントになるので、このファイルが import 文 or import 関数で呼ばれる条件がわかればいい。

特に仕様書的なのはないので、ソースコードを探索。

ユーザーがデプロイした関数コードを読む個所は /src/loadScriptFile.ts の 8 行目あたりから。

14 行目で eval('import(fileUrl.href)') というのが見えるので import 関数を使って Dynamic Import している。 このブロックに入るには 10 行目で 関数 isESModuletrue を返せばいい。

じゃあ、その関数が true を返す条件はなんなのか、関数の中身を見てみる。

同じファイルの 24 行目から始まる関数 isESModuletrue を返す条件は以下の 2 つ。

  1. 指定されたファイルの拡張子が .mjs
  2. package.jsontype フィールドの値が module
    ただし、ファイルの拡張子が .cjs 以外*3

このうち 1. の拡張子 .mjs については、現状のプログラミング モデルでもできた方法。
2. については、プログラミング モデルの刷新で package.json を扱うことになったのでできるようになったこと。
npm で公開されているような通常の Node.js のモジュールと同じような扱いができるようになった。

TypeScript は?

TypeScript で実装した場合は、TypeScript コンパイラ tsc によって、デプロイ パッケージの時点で JavaScript のコードになっている。

現状のモデルでは package.jsontype フィールドが使えないので、拡張子 .mjs 一択。
それには、.mts という拡張子でコードを書くか*4.ts でコードを書いて .js として出力されたファイルを .mjs に変換する処理を自前で書く必要があった。

一方で新しいモデルでは package.json が使えるようになったので、.ts で書いて .js で出力されたとしても👆の 2. で対応可能。

ただしどちらを使っても tscコンパイル結果を CommonJS ではなく ESModule として出力する必要がある。
なので、tsconfig.jsonESModule として出力する設定をする。ここも前回と変わらない。

試した感じ、今なら以下の moduletargetmoduleResolution の書き方でいけそう。

{
  "compilerOptions": {
    "module": "ES2022",
    "target": "ES2017",
    "moduleResolution": "node",
    :
}

サンプル

流石に焼き増しが過ぎるエントリーので、一応サンプルも。

github.com

おなじみの Puppeteer で指定のページを PDF として取れる API を新しいモデルで実装(サンプルとしてわかりやすいので)。
.devcontainer も載せたので、Dev Container か GitHub Codespaces で試せるはず。

せっかくなので今回から使える package.json"type": "module" を書く方法で ES module として認識させている。
https://github.com/horihiro/20221222-func-nodejs-prototype-toplevelawait/blob/main/package.json#L5

Top-level await はここ。
https://github.com/horihiro/20221222-func-nodejs-prototype-toplevelawait/blob/main/src/index.ts#L4

このコードの場合、Chromium を起動する launch メソッドに必要な await を関数の外で使えるので、アプリ起動のタイミングで Chromium を一度立ち上げておける。
関数呼び出し時は、起動時に立ち上げたインスタンスを使いまわせる。

比較として、現状のモデル & Top-level await 無しのサンプルはコチラ

あまり大したことないように見えるけど、アプリの中で 1 回だけ実行したいDB 接続の初期化処理などは、最近 async で定義されているものが多い。 それを関数の外で使いたいのは割とあるシーンじゃないだろうか。

でも結局

まだ Azure 上では使えない。

Python の新しいモデルは Public Preview なので、Node.js もはよ。

*1:コードネーム的なのはないんだろか

*2:https://github.com/tc39/proposal-top-level-await

*3:.js を想定していると思うが、ここまで他の拡張子のファイルが到達しないかは未確認

*4:このやり方、今回初めて知った。前回もこれでいけたのかも。