Showing Posts From
技術
Nx で Firebase Functions を扱う方法
目次 Nx って何? Nx は Angular や React を使った企業での Web 開発で利用されることを想定した monorepo ツールです。 公式では Angluar と React をサポートしています。React が使われている静的サイトジェネレータの Gatsby も。 サーバーサイドでは Express, Nest.js, Next.js がサポートされています。 プラグイン機構になっており、他にもサードパーティが提供するプラグインで色々サポートすることができる仕組みになっています。 Vue.js のプラグイン もあります。同じところが Nuxt.js のプラグイン も提供しています。 あるシステムを開発するとして、ユーザー用 Web アプリ 管理者用 Web アプリ バックエンドのサーバーアプリといった具合に幾つかのプロジェクトで構成されていることが多いかと思います。それぞれリポジトリ分けてると面倒だなぁ このデータ型定義、あっちのアプリでもこっちのアプリでも同じの使ってるなぁ、1箇所にまとめたいなぁ ユーザー用と管理者用でどっちでも利用する画面コンポーネントがあるんだけどなぁ、1箇所にまとめたいなぁ ESLint の設定やら TypeScript の設定やら、同じような設定をあっちにもこっちにも、共通定義を拡張する形にできないかなぁといった、「共通化したい」要求が出てきます。 それを叶えるために monorepo 構成にするわけですが、Nx は 特に Angular や React などの Web アプリ開発に特化した monorepo 管理ツールになります[^1]。 [^1]: 他に有名な monorepo ツールに Lerna があります。Nx のブログに Why you should switch from Lerna to Nx という記事があります。オープンソースでライブラリ開発なら Lerna だけど、企業でアプリ開発するなら Nx だという主張で理由が述べられています。 英語ですが動画付きでチュートリアルもあります。どんな感じのものか掴めるかと思います。 全ての npm パッケージはプロジェクトルートの node_modules にインストールされ、一括管理されます。 動機 勤め先の creato(クリート) では React も Vue.js も利用していますが、第一選択肢は Angular です。 Nx は親和性が高く利用したいのですが、Nest.js より Firebase を使うプロジェクトの方が多いです。 ところが Nx は Firebase(というか Functions)にデフォルトでは対応していません。 Nx で Firebase Functions を扱う方法が Allow development of Firebase Cloud Functions で議論されています。 実際に試してみたので、ここで日本語で手順をまとめて共有したいと思います。 前提 Node.js が動作する環境を用意してください。 Git も必要です。 Unix コマンドを使います。Linux や macOS は良いですが、Windows でコマンドプロンプトや PowerShell を使う場合は、記載されているコマンドで動作しないものがある(rm など)ため、各環境でのコマンドに置き換えてください。 実際の作業は Docker コンテナを作ってやったのですが、ここでは理解を簡単にするために Docker を使わない前提で説明します。 前提知識や用意するものを増やしたくないので。 yarn は利用せずに Node.js 付属の npm を利用して説明します。 自分が試した環境は以下です。Node.js 14.16.0 Nx 11.4.0 および 11.6.1Nx ワークスペース作成 git の user.email と user.name は設定しておいてください。 ワークスペース作成の最後で email と name が取得できずにエラーが発生します。 Nx の monorepo ワークスペースを作成します。 npx create-nx-workspace@latestどのタイプのワークスペースを作成するか聞かれます。 ここでは Functions 以外に何を利用するかは特に想定していないので empty を選択しておきます。 "Use Nx Cloud?" は "NO" で。 empty を選択しておいて後から Angular でも React でも、必要なアプリケーションは追加可能です。 empty 以外を選択した場合はあくまでお手軽セットアップができるだけです[^2]。 [^2]: Angular を試してみましたが、empty にしておいて後から追加した方がオプションで指定できるものが多いので好ましいかもと思いました。 ワークスペース名はリポジトリ名となるものを指定すれば良いかと。 > NX NOTE Nx CLI is not installed globally. This means that you might have to use "yarn nx" or "npx nx" to execute commands in the workspace. Run "yarn global add nx" or "npm install -g nx" to be able to execute command directly.と表示されます。 もし nx コマンドの実行時に npx nx と打つのが面倒なら nx パッケージをグローバルインストールしてください。 npm i -g nxグローバルを汚したくないなら[^3]、npx nx でローカル node_modules 内の nx を実行できるので必須ではないです。 ここではコマンドを簡略化して記載するために、グローバルインストールされている前提で説明します。 グローバルインストールしない場合は、nx の前に npx を付けて実行してください。 [^3]: 複数の Nx ワークスペースがある場合、ワークスペースごとに利用している Nx のバージョンが異なることは容易に考えられます。グローバルとローカルの nx バージョンが合わないことで要らないトラブルを招くより、ローカルの nx を実行した方が良さそうに思います。自分は Docker で環境を分離するので、Docker コンテナ内でグローバルインストールしますが。 Firebase プロジェクト作成 先に Firebase コンソールでプロジェクトを作成しておきます。 Firestore を利用するなら、利用開始をしておきます(リージョンは後から変更できないので注意。asia-northeast1 が東京、asia-northeast2 が大阪。asia-northeast3 はソウル、日本ではないので注意。) firebase-tools をグローバルインストールしておきます。 npm i -g firebase-toolsFirebase にログインして、プロジェクトの初期化を行います。 firebase login firebase initここでは Functions を利用するように答える想定で説明します。 他は利用したいものを指定してください。 Functions は TypeScript 利用、ESLint 利用を選択。 install dependencies は n を選択 (後で Nx ワークスペース機能を利用するので、ここでインストールする必要がないためです)。 .gitignore 最初から .gitignore が存在していることで、firebase init した際に .gitignore が生成されません。 普通に作成した際に生成される .gitignore の内容で、Nx が生成した .gitignore に足らなくて必要そうな firebase 関係のものを追加しておきましょう。 また firebase ディレクトリも .gitignore に追加しておきます。 この中にユーザー認証情報やエミュレータの実行ファイルなどが格納されます。 これらは git 管理しなくても、他の開発者が firebase login や firebase emulators:start した際に、必要なものが作成されるようです(全部 ignore して良いか自信ないので、firebase ディレクトリ以下で必要なものがあるなら、もう少し細かく指定してください)。 以下を .gitignore に追加しておきます。 # # Firebase #firebase# logs firebase-debug.log* firebase-debug.*.log*# Firebase cache .firebase/Node アプリケーション追加 Functions アプリケーションを Node.js アプリケーションとして追加します。 コンソールで以下を実行します。 npm i -D @nrwl/node nx g @nrwl/node:application functions最初の npm コマンドは Node アプリケーションの作成機能を Nx に追加します。 このように作成できるアプリケーションの種類をプラグインとして追加していく仕組みになっています。 (Nx 11.4.0 の時点では nx add @nrwl/node で追加しましたが、11.6.1 では nx add コマンドは削除されていました。) 次の nx コマンドで Node アプリケーションを追加しています(nx g は nx generate の省略形)。 apps/functions が生成されます。 (アプリケーションは apps の下に、ライブラリは libs の下に配置されます) ESLint の tsconfig や ESLint の設定は、親にある共通設定が利用されるようになっています。 Functions でだけ適用したいものがあれば追加できるけれど、ここではこのままで。 プログラムの中身は firebase init が生成するものもほぼ空っぽだけれど、一応入れ替えておきます。 ここに Functions の関数を実装していくことになります。 cp functions/src/index.ts apps/functions/src/main.tsFirebase 関係のパッケージ追加 firebase init が生成した package.json に含まれているパッケージのうち、足らないものを Nx ワークスペースにインストールします。 npm i firebase-admin firebase-functions npm i -D firebase-functions-testデプロイ用 package.json 生成ツール追加 tools/scripts/build-firebase-functions-package-json.ts を作成します。内容は以下です。 途中にある engines: {node: '14' } のところは、Firebase Functions で利用する Node バージョンに置き換えてください。 この記事執筆時点(2021/04)では 14 はまだ Public Preview です。 import * as depcheck from 'depcheck'; import * as fs from 'fs'; import * as path from 'path';import * as packageJson from '../../package.json';const PACKAGE_JSON_TEMPLATE = { engines: { node: '14' }, main: 'main.js', };async function main(): Promise<void> { const args = process.argv.slice(2); if (!args?.length || !args[0]) { throw new Error('Application name must be provided.'); } const APPLICATION_NAME = args[0]; console.log(`Application name: ${APPLICATION_NAME}`); /***************************************************************************** * package.json * - Filter unused dependencies. * - Write custom package.json to the dist directory. ****************************************************************************/ const ROOT_PATH = path.resolve(__dirname + '/../..'); const DIST_PROJECT_PATH = `${ROOT_PATH}/dist/apps/${APPLICATION_NAME}`; console.log('Creating cloud functions package.json file...'); // Get unused dependencies const { dependencies: unusedDependencies } = await depcheck(DIST_PROJECT_PATH, { package: { dependencies: packageJson.dependencies, }, }); // Filter dependencies const requiredDependencies = Object.entries(packageJson.dependencies as { [key: string]: string }) ?.filter(([key, _value]) => !unusedDependencies?.includes(key)) ?.reduce<{ [key: string]: string }>((previousValue, [key, value]) => { previousValue[key] = value; return previousValue; }, {}); console.log(`Unused dependencies count: ${unusedDependencies?.length}`); console.log(`Required dependencies count: ${Object.values(requiredDependencies)?.length}`); // Write custom package.json to the dist directory await fs.promises.mkdir(path.dirname(DIST_PROJECT_PATH), { recursive: true }); await fs.promises.writeFile( `${DIST_PROJECT_PATH}/package.json`, JSON.stringify( { ...PACKAGE_JSON_TEMPLATE, dependencies: requiredDependencies, }, undefined, 2 ) ); console.log(`Written successfully: ${DIST_PROJECT_PATH}/package.json`); }main() .then(() => { // Nothing to do }) .catch(error => { console.error(error); });要するに、ワークスペースルートにある package.json を元に、Functions のソースコードが依存していないものを除外する Node.js ランタイムのバージョン指定を追加したデプロイ用の package.json を生成するスクリプト。 依存関係を調べるのに depcheck パッケージを利用するのでインストール。 npm i -D depcheckこのスクリプトのディレクトリに tsconfig や ESLint 関係のファイルを追加しておきます。 ルートにあるものを参照するだけでいいので、apps/functions と同じでいいです(ちょうど階層的にも ../.. がルート)。 一応 Jest でテストも作成可能なように jest.config.js もコピーしておきます。 cp apps/functions/*.json tools/scripts cp apps/functions/.eslintrc.json tools/scripts cp apps/functions/jest.config.js tools/scriptsただ、このスクリプトでは json ファイルを import していて、このままだと resolveJsonModule オプションを付けることを考えろというエラーが出ます。 また Node.js は import を使うとエラーになる[^4]ので、TypeScript から JavaScript へ変換後の形式は commonjs にしておきます。 tools/scripts/tsconfig.json に以下を追加。 [^4]: 一応 Node 12 から ES Modules はサポートされていますが、実験的機能のため --experimental-modules オプションを指定する必要があったり、拡張子を mjs にする必要があったり。TypeScript が commonjs にトランスパイルしてくれるのに、それらを使う意味がありません。 { // 略 "compilerOptions": { "resolveJsonModule": true, "module": "commonjs" },なおツールの実行時には ts-node に --project ./tools/scripts/tsconfig.json オプションを指定し、利用する tsconfig を明示しないと自動認識はしてくれませんでした。 workspace.json 編集 Nx ワークスペース作成時に empty を指定してれば、プロジェクトルートに workspace.json があるはずです。 なんでこんなことを書いているかというと、angular を選択したときは angular.json という名前で作成されたからです。 workspace.json(または angular.json)を編集して、build, serve, deploy などのコマンド実行時の処理を変更、追加します。 Nx で Node アプリケーションとして追加した時点で、workspace.json にアプリ設定が追加されていますが、それをさらに編集します。 コメントは説明のために記載しているもので、実際のファイルには記載しません。 "functions": { // 略 "architect": { // build の項目を build-node に変更。 // build 時に他にも実行したことがあるので、サブコマンド扱いにする。 "build-node": { // 略 }, // build の項目を新しく追加 // build-node を実行して TypeScript ビルドした上で、package.json の用意も行う。 "build": { "builder": "@nrwl/workspace:run-commands", "options": { "commands": [ { "command": "nx run functions:build-node" }, { "command": "ts-node --project ./tools/scripts/tsconfig.json tools/scripts/build-firebase-functions-package-json.ts functions" }, { "command": "cd dist/apps/functions && npm install --package-lock-only" } ], "parallel": false }, "configurations": { "production": { "prod": true } } }, // ビルドした上で、Firebase Emulator を使って実行するように書き換え "serve": { "builder": "@nrwl/workspace:run-commands", "options": { "command": "nx run functions:build && firebase emulators:start --only functions --inspect-functions" } }, // ビルドした上で functions:shell を利用できる設定を追加 "shell": { "builder": "@nrwl/workspace:run-commands", "options": { "command": "nx run functions:build && firebase functions:shell --inspect-functions" } }, // shell と同じ "start": { "builder": "@nrwl/workspace:run-commands", "options": { "command": "nx run functions:shell" } }, // デプロイ設定を追加 "deploy": { "builder": "@nrwl/workspace:run-commands", "options": { "command": "firebase deploy --only functions" } }, // lint と test はそのまま // 略 } }大事なのはビルド設定だけです。 あとはビルドと firebase コマンドの組み合わせだったり、firebase コマンドを実行するだけだったり。 それらを追加する必要性を感じないけれど、firebase init が生成した package.json の scripts にあるものを移植した感じです。 いらなくなった functions ディレクトリ削除 参考にするために残していましたが、もう移植し終わったので firebase init が生成した functions ディレクトリを削除します。 rm -rf functionsデプロイ設定 Functions のデプロイ時のビルド設定を行います。 ソースコードの場所の階層がプロジェクトルートに対して ./functions から ./apps/functions に変わっています。 このままだとデプロイ時にエラーが発生するので、明示的に source を指定する必要があります[^5]。 [^5]: Error: An unexpected error has occurred. と表示され、firebase-debug.log を見ると TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined というエラーが出てました。エラーが分かりにくいけど、そういう理由でした。 firebase.json を編集します。 "functions": { "source": "dist/apps/functions", "predeploy": [ "nx lint functions", "nx build functions" ] },なお nx コマンドがサブコマンドとして認識できる lint や build, test, serve, e2e などに関しては、nx run functions:lint を nx lint functions のように書くことができます。 まとめ Angular や React を使った企業での Web 開発で利用されることを想定した monorepo ツールである Nx で、残念ながら企業での Angular や React を使った Web 開発でよく利用される Firebase に対応したプラグインが提供されていなかったため[^6]、手作業で対応する方法を解説しました。 [^6]: 正確には Google Cloud Functions Generator が見つかったのですが、Functions の関数1つにつき1つのアプリ扱いされるもので、およそ企業での Web 開発に向くものではありませんでした。 元々は GitHub の Issue で議論されていた内容 です。やっている内容をプラグイン化すれば、次回からは楽をできるし、他の人も助かると思うのですが誰も面倒でやってないという状況ですね。 自分もやる気がないです。実際に手を動かして設定することで Nx の仕組みや設定方法がよく分かる(次回も手作業すると思い出す) この記事のようにやり方がまとまっていれば、作業が酷く面倒というほどでもない どうせプロジェクトセットアップの最初にやるだけ プラグイン化するには、様々な状況下を想定して、プラグインが追加する設定が他に干渉をしないかを考える必要がある(手作業なら作業者が考えて回避したり調整できる) 機械学習のプロジェクトにアサインされてしまったので、暫く Web 開発することはなさそうあたりが理由です。 最初の理由が大きいですね。こういったツールは便利ですが、トラブった時に対処できるようになっておきたいので、中身の仕組みをある程度把握しておきたいと考えてしまいます。 最後のは完全に自分個人の事情ですが、専門が特にないエンジニアなので、必ずしも Web 開発してるってわけではないんです。 作るモチベーションがあっても、メンテするモチベーションはないなぁと。 この記事を読んでプラグイン化することに興味を持った方は、チャレンジしてみてはどうででしょうか?
Pugの文法まとめ
Pug の文法について網羅的に説明された日本語のサイトが見つからなかったので、自分でまとめてみました。 英語でよければ公式にしっかりしたドキュメントがあります。 この記事はその内容の中でも特に文法に絞って、重要な順番で解説しています。 できるだけリファレンスを網羅している長い記事ですが、大事な順に書いているので、単に HTML を簡潔に書くためだけなら、最初の方の「HTML を簡潔に書くための基本文法」だけ読んでおけば大丈夫です。 目次 Pug って何? HTML を簡潔に書くことができるものです。HTML に変換されます。昔は Jade と呼ばれていましたが名前が変更されました。 単に HTML を簡潔に書けるだけでなく、テンプレートエンジン的な機能も持っています。他のファイルを include する レイアウトを他のファイルに書いて extends して内容を埋め込む 変数を定義する ループしたり条件分岐したり Markdown ファイルを HTML に変換して埋め込んだり、Sass を CSS に変換して埋め込んだりといったことができます。 HTML を簡潔に書くための基本文法 まずはテンプレートエンジン的な機能は置いておいて、HTML を簡潔に書く記法について解説します。 タグ 以下の HTML が Pug でどうなるか見てみましょう。 <main> <article> <header> <h1>記事タイトル</h1> </header> <section> <p>記事サンプルです。</p> </section> </article> </main>Pug は以下のようになります。 main article header h1 記事タイトル section p 記事サンプルです。タグを閉じるのではなく、インデント(行の先頭から続く空白の数)でタグの入れ子構造を表します。 doctype は以下のように記述します。 doctype html html head body↓ HTML に変換すると... <!DOCTYPE html> <html> <head></head> <body></body> </html>属性 以下のように属性があるタグをどう書くかの基本を解説します。 基本 <a id="google-link" class="button small" href="//google.com">Google</a>タグの属性は括弧の中に入れて表します。(文字列を囲うのはシングルクォーテーション、ダブルクォーテーションのどちらも使えます) a(id='google-link' class='button small' href='//google.com') Google属性と属性の間にコンマを入れても構いません。 a(id='google-link', class='button small', href='//google.com') Google属性を複数行に分けて書くこともできます。 a( id='google-link' class='button small' href='//google.com' ) Googleid の # 記法 id は特別に # を使った記法が使えます。 a#google-link(class='button small' href='//google.com') Googleなお、div タグの場合はタグ名を省略できます。 #link a#google-link(class='button small' href='//google.com') Google↓ <div id="link"> <a id="google-link" class="button small" href="//google.com">Google</a> </div>class の . 記法 クラスにも特別に . を使った記法が用意されています。複数つなげて指定できます。 a#google-link.button.small(href='//google.com') Googleこちらも id と同様に div を省略できます。 .link a#google-link.button.small(href='//google.com') Google↓ <div class="link"> <a id="google-link" class="button small" href="//google.com">Google</a> </div>style 属性 タグに直接スタイルを指定するときは以下のように書けます。 a(style={color: 'red', background: 'green'})Boolean 属性 真偽値を表す属性は true か false を受け付けます。値を指定しなければ true だとみなされます。 input(type='checkbox' checked)input(type='checkbox' checked=true)input(type='checkbox' checked=false)false を指定した場合、Pug はその属性を出力しません。 true の場合、Pug はその文書の doctype が HTML かどうかで適切に出力します。 出力の詳しい動作については後述する Boolean 属性の出力 を参照。 文字列(Plain Text) タグの後に続けて中身の文字列を記述することができます。 p 記事サンプルです。複数行に分けて書きたい場合は、パイプ( | ) を利用した記法があります。 p | これはサンプルです。 | Pugのパイプを使って複数行に分けて記述しています。これは以下の HTML になります。パイプを使って複数行に分けて書いたものは、まとめてタグの中身になります。 <p>これはサンプルです。Pugのパイプを使って複数行に分けて記述しています。</p>パイプを使うと、文章中にインラインタグを埋め込むのは以下のように書けます(後述の もうちょっと高度な話 で説明する #[] を使って埋め込むこともできます。)。 p | 文章の中に span.keyword インラインタグ | を埋め込むことができます。 br | 改行タグも埋め込めます。↓ <p> 文章の中に<span class="keyword">インラインタグ</span>を埋め込むことができます。<br /> 改行タグも埋め込めます。 </p>コメント HTML として出力されるコメントは以下のように書きます。 // これはHTMLに出力されるコメントです。 p foo以下のように HTML のコメントとして出力されます。 <!-- これはHTMLに出力されるコメントです。 --> <p>foo</p>HTML に出力して欲しくないコメントは以下のようにハイフンを追加して書きます。 //- これはHTMLに出力されないコメントです。 p foo複数行にわたるブロックコメントも記載できます。 body //- これはHTMLには出力されないコメントです。 複数行に渡るブロックコメントです。 // これはHTMLに出力されるコメントです。 複数行に渡るブロックコメントです。テンプレート構文 単に HTML を Pug でシンプルに記述したいだけであれば、覚えることは今まで説明しただけで事足ります。 他のテンプレート構文と組み合わせて使うなら、これから説明する内容は覚えなくてもいい・・・というより、混ぜると訳がわからなくなるので Pug のテンプレート構文は使うべきではないでしょう。 他のテンプレート構文と組み合わせて使うというのは、例えば Laravel のテンプレートエンジン Blade と組み合わせる とか、React で利用する とか Vue.js で利用する とか、Angular で利用する とかを指しています [^2]。この場合は、後述の もうちょっと高度な話 のうち、HTML 記述に関係するところには目を通しておくと良いかと思います。 [^2]: React をテンプレート構文と呼ぶのは違和感ありますが Pug の公式では変数の定義に var キーワードを使って説明されています。古いブラウザ(IE10 以下とか)でも動くようにという配慮からだと思いますが、もう var は使わず let や const を使うべきかと。少なくとも Node.js 上で Pug を HTML に変換するのであればブラウザの対応は関係ありません。ここでは let や const で説明します。 JavaScript コード ここで説明するのは HTML に含めるブラウザで動作させる JavaScript のことではなく、Pug が変換時に実行する JavaScript のことです。 Pug では HTML への変換時に JavaScript を実行する機能が用意されています。とはいえ、Node.js パッケージを読み込んで利用するような高度なことはできません。 Unbuffered Code 頭に -(ハイフン)を付ける記述方法で、HTML タグとは無関係に JavaScript コードを実行します。 Pug では HTML として出力されるものを buffered, 出力されないものを unbuffered と呼んでいます。 最もよく利用するのは変数の定義でしょう。 - const list = ["Uno", "Dos", "Tres", "Cuatro", "Cinco", "Seis"]このように - (ハイフン)を頭に付けると JavaScript コードと認識されます。 以下のように複数行に分けて記述することもできます。 - const list = ["Uno", "Dos", "Tres", "Cuatro", "Cinco", "Seis"]Buffered Code タグの中身に JavaScript コードの実行結果を埋め込みます。 //- イコール(=)を使ってタグの中身に JavaScript の結果を埋め込める p = 'This code is' + ' <escaped>' //- 1行で書くこともできる p= 'This code is' + ' <escaped>'タグの内容は HTML エスケープされます。 <p>This code is <escaped>!</p> <p>This code is <escaped>!</p>エスケープして欲しくない場合は = ではなく != を使います。次の「変数の埋め込み」で説明します。 変数の埋め込み エスケープする場合 #{変数名} で変数の内容を文字列に埋め込むことができます。また先ほど説明した Buffered Code を使って h1= 変数名 とか p= 変数名 の形でタグの中に変数の内容を埋め込めます。内容が埋め込まれる前に HTML はエスケープされます。 - const title = "On Dogs: Man's Best Friend"; - const author = "enlore"; - const theGreat = "<span>escape!</span>";h1= title p Written with love by #{author} p This will be safe: #{theGreat}出力される HTML は以下になります。 <h1>On Dogs: Man's Best Friend</h1> <p>Written with love by enlore</p> <p>This will be safe: <span>escape!</span></p>変数の埋め込みと紹介していますが、もちろん JavaScript のコードを実行して結果を埋め込むこともできます。 h1= title.toUpperCase() p Written with love by #{author.toUpperCase()}#{ 自体を文字列に含めたい場合は、\#{ のようにスラッシュでエスケープするか、#{} の中に入れて JavaScript の文字列として評価させる方法が取れます。 p Escaping works with \#{interpolation} p Interpolation works with #{'#{interpolation}'} too!以下の HTML に変換されます。 <p>Escaping works with #{interpolation}</p> <p>Interpolation works with #{interpolation} too!</p>エスケープしない場合 埋め込む文字列をエスケープしたくない場合は != や !{} を利用します。 当然ですがユーザーの入力をそのまま埋め込むのは危険です。 - const title = "On Dogs: <span class=\"subtitle\">Man's Best Friend</span>" - const theGreat = "<span>escape!</span>"; title!= title p This will be unsafe: !{theGreat}以下のように変数内の HTML タグがそのまま出力されます。 <h1>On Dogs: <span class="subtitle">Man's Best Friend</span></h1> <p>This will be safe: <span>escape!</span></p>条件分岐 if まずは if 文から。 - const user = {description: 'foo bar baz'} - const authorised = false #user if user.description h2.green Description p.description= user.description else if authorised h2.blue Description p.description. User has no description, why not add one... else h2.red Description p.description User has no descriptionunless if の逆の「もし〜でなければ」を表す unless が用意されています。 unless user.isAnonymous p You're logged in as #{user.name}これは if を使って以下を書いた場合と同じになります。 if !user.isAnonymous p You're logged in as #{user.name}case JavaScript では switch 文に相当するものですね。 - const friends = 10 case friends when 0 p you have no friends when 1 p you have a friend default p you have #{friends} friendsbreak は書かなくても勝手に下まで実行していったりしません。もし複数の条件で同じ内容を実行したい場合は以下のように記述できます。 - const friends = 0 case friends when 0 when 1 p you have very few friends default p you have #{friends} friendsある条件のときには何もしないという場合は、明示的に break を記述する必要があります。 - const friends = 0 case friends when 0 - break when 1 p you have very few friends default p you have #{friends} friends繰り返し処理 each 集合の中を順に処理する each が用意されています。ほとんどの場合、繰り返し処理で使うのはこれです。 - const values = [1, 2, 3, 4, 5] ul each val in values li= val↓ <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul>values の部分に JavaScript のコードを直接書いても構いません。 ul each val in [1, 2, 3, 4, 5] li= valul each val in values.length ? values : ['There are no values'] li= val値だけでなくインデックスも受け取れます。 ul each val, index in ['zero', 'one', 'two'] li= index + ': ' + valオブジェクトをループさせることもできます。(注: each の値に指定する変数は 値, キー の順番) - const dict = {1: 'one', 2: 'two', 3: 'three'} ul each val, key in dict li= key + ': ' + valwhile 条件に一致する間ループを回す while 文も利用できます。 - let n = 0; ul while n < 4 li= n++↓ <ul> <li>0</li> <li>1</li> <li>2</li> <li>3</li> </ul>他のファイルの埋め込み (inlucde) 他のファイルの内容を取り込みます。 例えば index.pug が以下の内容とします。 //- index.pug html //- ここで includes/_head.pug を読み込み include includes/_head.pug body h1 My Site p Welcome to my super lame site. //- ここで includes/_foot.pug を読み込み include includes/_foot.pugincludes/_head.pug は以下の内容とします。 //- includes/_head.pug head title My Site script(src='/javascripts/jquery.js') script(src='/javascripts/app.js')includes/_foot.pug は以下の内容とします。 //- includes/_foot.pug footer#footer p Copyright (c) foobarindex.pug を HTML に変換すると、inculdes/_head.pug と includes/_foot.pug の内容が埋め込まれて以下のようになります。 <!DOCTYPE html> <html> <head> <title>My Site</title> <script src="/javascripts/jquery.js"></script> <script src="/javascripts/app.js"></script> </head> <body> <h1>My Site</h1> <p>Welcome to my super lame site.</p> <footer id="footer"> <p>Copyright (c) foobar</p> </footer> </body> </html>平文テキストの include Pug ではないファイルを取り込むこともできます。例えば CSS ファイルや JavaScript ファイルですね。 //- index.pug doctype html html head style include style.css body h1 My Site p Welcome to my super lame site. script include script.js/* style.css */ h1 { color: red; }// script.js console.log('You are awesome');index.pug は以下の HTML に変換されます。 <!DOCTYPE html> <html> <head> <style> /* style.css */ h1 { color: red; } </style> </head> <body> <h1>My Site</h1> <p>Welcome to my super lame site.</p> <script> // script.js console.log('You are awesome'); </script> </body> </html>Markdown ファイルの埋め込み (Filter) Pug には 後述する Filter という機能 があります。これを使うと Markdown や SASS などの変換処理を行った上で埋め込むことができます。 //- index.pug doctype html html head title An Article body include:markdown-it article.md# article.mdThis is an article written in markdown.index.pug を変換すると以下の HTML になります。 <!DOCTYPE html> <html> <head> <title>An Article</title> </head> <body> <h1>article.md</h1> <p>This is an article written in markdown.</p> </body> </html>共通レイアウトの定義 (extends) include では例えばヘッダやフッタなどの複数ページで利用する共通部品を分離することができました。 そうではなく、全体で共通に利用するレイアウトを用意して、その中にページごとに異なる部分を埋め込みたい時に extends が利用できます。 基本 レイアウトファイルを以下のように作成します。 //- _layout.pug//- 外から block で変数(title)を渡せるようにする block variableshtml head //- title はページごとに異なるので外から与えられたものを埋め込む title My Site - #{title} //- ページによって利用する js ファイルが異なるので block で渡せるようにする block scripts //- もし block scripts が渡されなければ、デフォルト値として以下を利用 script(src='/jquery.js') body //- ページのコンテンツは block で渡せるようにする block content //- ページによってはフッタが異なるので block で渡してカスタマイズできるようにする block foot //- もし block foot が渡されなければ、デフォルト値として以下を利用 #footer p some footer contentこれを利用するページは以下のように extends を使います。 //- page-a.pug extends _layout.pug//- タイトルを変数で渡す block variables - const title = 'page A'//- jquery 以外に pets.js というページ独自の js ファイルも利用するのでデフォルト値を上書き block scripts script(src='/jquery.js') script(src='/pets.js')//- ページのコンテンツを渡す block content h1= title - var pets = ['cat', 'dog'] each petName in pets p= petName//- フッタはデフォルトのものを利用(block foot は渡さない)↓ page-a.pug を HTML に変換すると <html> <head> <title>My Site - page A</title> <script src="/jquery.js"></script> <script src="/pets.js"></script> </head> <body> <h1>page A</h1> <p>cat</p> <p>dog</p> <div id="footer"> <p>some footer content</p> </div> </body> </html>append / prepend (デフォルト値の前後に追加) 今までの説明では block に指定するとデフォルト値を上書き(置き換え)していました。 そうではなく、デフォルト値にさらに追加するための方法が用意されています。 例えば先の例で page-a.pug は jquery.js の他に pet.js も利用するために //- jquery 以外に pets.js というページ独自の js ファイルも利用するのでデフォルト値を上書き block scripts script(src='/jquery.js') script(src='/pets.js')と記述していました。 これは append を利用して以下のように記述できます。 //- デフォルト値である jquery.js の後に pets.js というページ独自の js ファイルの利用を追加 block append scripts script(src='/pets.js')append でなく prepend を指定するとデフォルト値の前に追加されます。 レイアウトを継承してサブレイアウトを定義 サイト全体で利用する _layout.pug を作ったわけですが、幾つかのページではさらにコンテンツ部分の構造が同じなので共通化したいとします。 以下のように _layout.pug を継承した _sub-layout.pug を作成します。 //- _sub-layout.pug extends _layout.pug//- コンテンツ部分の構造をさらに共通化 block content .sidebar block sidebar p nothing .primary block primary p nothingこれで sidebar と primary というブロックが content の内部に作られました。 これを以下のように利用できます。 //- page-b.pug extends _sub-layout.pug block variables - const title = 'page B' block sidebar p Sidebar block primary p Primary↓ <html> <head> <title>My Site - page B</title> <script src="/jquery.js"></script> </head> <body> <div class="sidebar"> <p>Sidebar</p> </div> <div class="primary"> <p>Primary</p> </div> <div id="footer"> <p>some footer content</p> </div> </body> </html>extends の継承については公式が注意書きをしているので、多用したり複雑な使い方をするのであれば extends 利用のよくある間違い には目を通しておいた方がいいでしょう。 Mixin 同じ部品を色々な箇所で利用したい場合、別ファイルにして include を利用する方法を既に説明しました。 でも1つのファイルでしか使わないので別ファイルにするほどでも・・・という場合もあります。 また小さい部品を幾つか集めて1つのファイルで管理したいということもあるでしょう。 Mixin はプログラム言語で言うところの関数みたいなものです。 あるいは独自タグを定義するような、SPA フレームワークでのコンポーネント的なものとも言えるかもしれません。 //- Mixinの宣言 mixin list ul li foo li bar li baz//- Mixinを利用するときは頭に + を付ける +list +list↓ <ul> <li>foo</li> <li>bar</li> <li>baz</li> </ul> <ul> <li>foo</li> <li>bar</li> <li>baz</li> </ul>Mixin に引数を渡す 引数を渡すこともできます。 mixin pet(name) li.pet= nameul +pet('cat') +pet('dog') +pet('pig')引数のデフォルト値もサポートしています。 mixin article(title='Default Title') .article .article-wrapper h1= title//- 引数省略してデフォルト値を利用 +article()+article('Hello world')可変長引数もサポートしています。 //- 最初の引数は id で、その後は items に配列として格納 mixin list(id, ...items) ul(id=id) //- items を順にループ each item in items li= item//- 最初の 'my-list' は id、残りは items +list('my-list', 1, 2, 3, 4)↓ <ul id="my-list"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul>Mxin にブロックを渡す block を渡すこともできます。 mixin article(title) .article .article-wrapper h1= title if block //- ブロックが渡されてたらそれを埋め込む。 block else //- ブロックが渡されてなかったら以下 p No content provided//- ブロックを渡さないで呼び出し +article('Hello world')//- ブロックを渡して呼び出し +article('Hello world') p This is my p Amazing article↓ <div class="article"> <div class="article-wrapper"> <h1>Hello world</h1> <p>No content provided</p> </div> </div> <div class="article"> <div class="article-wrapper"> <h1>Hello world</h1> <p>This is my</p> <p>Amazing article</p> </div> </div>1つ目は "No content provided" が、2つ目は渡したブロックが埋め込まれていますね。 Mixin に属性を渡す Mixin では渡された属性を attributes で参照できます。 mixin link(href, name) //- 属性として渡される class を、a タグの class に設定(!= を使ってるのでエスケープはしない) a(class!=attributes.class href=href)= name//- 属性として class="btn" を渡している +link('/foo', 'foo')(class="btn")class!=attributes.class としてエスケープしないようにしているのは、Mixin を呼び出す際の class="btn" で既にエスケープされてるので、もう一度エスケープするのが無駄だからです。 渡された属性をそのまま全部展開したい場合は &attributes が利用できます。 mixin link(href, name) a(href=href)&attributes(attributes)= name+link('/foo', 'foo')(class="btn")渡された属性 class="btn" がそのまま link Mxin 内の属性として展開されます。 <a class="btn" href="/foo">foo</a>引数なしの Mixin に属性を渡す場合、+link(class="btn") と書いてもそれが引数でなく属性だと認識されます。 ただ Pug 公式では +link()(class="btn") のように引数がないことを明示する記述をお勧めしています。 Filter Pug に渡す前に変換処理を行う仕組みです。例えばマークダウンを HTML に変換するとか、SCSS を CSS に変換して埋め込むとかができます。 Filter は頭に : を付けて呼び出します。括弧の中にはフィルタに渡すオプションを指定できます。 :markdown-it(linkify langPrefix='highlight-') # Markdown Markdown document with http://links.com and ```js var codeBlocks; ```↓ <h1>Markdown</h1> <p>Markdown document with <a href="http://links.com">http://links.com</a> and</p> <pre><code class="highlight-js">var codeBlocks; </code></pre>この解説は文法の説明に絞っており、Pug の実行方法には触れません^3が、markdown-it フィルタを利用するにはそのためのパッケージのインストールが必要です。 全ての JSTransformer モジュールがフィルターとして利用可能です。 注意点として、フィルタに Pug の処理中に JavaScript で生成した動的なものを渡すことはできません。 ちょっとわかりにくいので以下の例で説明します。 - const mds = [ '# タイトル1', '# タイトル2', ]each md in mds :markdown-it(linkify langPrefix='highlight-') md上記で md の部分は変数の内容を渡したいのですが、このままでは単に "md" という文字列としてしか認識してくれません。 :markdown-it(linkify langPrefix='highlight-') #{md}のように #{md} で渡しても、フィルタの中身は単なる文字列としてしか認識されず、"#{md}" という文字列が渡されるだけです。 なおフィルタのオプションにも変数を渡したり JavaScript で動的に生成したものを渡したりできません。 JavaScirpt で生成したものをフィルタに渡す機能が一切提供されていないんですね。 高速に動作させるためとのことですが、Pug を静的サイトジェネレータとして利用しようとすると、これが大きな制限になります。 もうちょっと高度な話 タグ 文字列中へのタグの埋め込み 長い文字列の中にインラインタグを埋め込むときに #[] でタグを埋め込む構文を覚えておくと楽に書けます。 これを知らなくても |(パイプ)を使って書けるので基本としては紹介しませんでした。 p. This is a very long and boring paragraph that spans multiple lines. Suddenly there is a #[strong strongly worded phrase] that cannot be #[em ignored]. p. And here's an example of an interpolated tag with an attribute: #[q(lang="es") ¡Hola Mundo!]以下の HTML に変換されます。 <p> This is a very long and boring paragraph that spans multiple lines. Suddenly there is a <strong>strongly worded phrase</strong> that cannot be <em>ignored</em>. </p> <p> And here's an example of an interpolated tag with an attribute: <q lang="es">¡Hola Mundo!</q> </p>入れ子を1行で表現 a imgを以下のように1行で記述する方法が用意されています。 a: imgcase を短く書きたい場合にも使えます。 - const friends = 1 case friends when 0: p you have no friends when 1: p you have a friend default: p you have #{friends} friends閉じタグのいらないタグ img のような閉じタグのいらないタグを Pug は自動的に判別してそのように変換してくれます。 a imgこれは以下の HTML になります。 <a><img /></a>HTML 5 に規定されているタグなら勝手に判別してくれますが、コンポーネントなどの独自タグの場合はそうはいきません。以下のように閉じタグがないことを後ろにスラッシュを付けて伝える必要があります。 foo(bar='baz')/以下の HTML に変換されます。 <foo bar="baz" />属性 エスケープの抑止 Pug はデフォルトで全ての属性の値をエスケープします。エスケープして欲しくない場合は = ではなく != を使います。 div(escaped="<code>") div(unescaped!="<code>")上記が下記の HTML に変換されます。 "<" が < に、 ">" が > にエスケープされるかどうかが異なっています。 <div escaped="<code>"></div> <div unescaped="<code>"></div>特殊文字が含まれる場合 SPA フレームワークと組み合わせて利用すると、属性に [] とか () などの特殊文字が使われることがあります。そう Angular のことですね。そういう特殊文字が含まれる場合は '' や "" で囲ってやってください。 button(class='button' '(click)'='play()')Boolean 属性の出力 doctype html を指定していない場合、Pug は文書が HTML かどうか分からない(例えば XML かもしれない)ので、値として属性名を出力します。 input(type='checkbox' checked)input(type='checkbox' checked=true)↓ どちらも以下になります。値には属性名である "checked" が出力されます。 <input type="checkbox" checked="checked" />false の場合、属性自体を出力しません。 input(type='checkbox' checked=false)↓ <input type="checkbox" />文書に doctype html を指定している場合、Pug は HTML の真偽値の表し方で出力します。 つまり true だった場合に属性名を値として出力しません。 input(type='checkbox' checked=true)↓ checked という属性に値は設定されません。 <input type="checkbox" checked />なおこのルールはあくまで指定したのが真偽値だった場合のみ適用されます。 例えば以下の場合、値が true または false の真偽値ではなく、"true" という文字列なので、上記のルールは適用されずにそのまま文字列として出力されます。 input(type='checkbox' checked=true.toString())input(type='checkbox' checked='true')input(type='checkbox' checked=true && 'true')↓ 上記3つはどれも以下のように値に文字列がそのまま出力されます。 <input type="checkbox" checked="true" />&attributes 実はこれは既に「Mixin に属性を渡す」で紹介しています。 JavaScript のオブジェクトをそのまま属性として展開します。 div#foo(data-bar="foo")&attributes({'data-foo': 'bar'})↓ <div id="foo" data-bar="foo" data-foo="bar"></div>もちろん変数に入れたオブジェクトも展開されます。 - const attributes = { class: 'baz' } div#foo(data-bar="foo")&attributes(attributes)↓ <div class="baz" id="foo" data-bar="foo"></div>extends 利用のよくある間違い 公式がわざわざ注意書きを書いているので、説明しておきます。 Pug のテンプレート継承機能は複雑なページ構造を小さくシンプルなファイルに分解する強力な機能だけれども、あまり多くのテンプレートを繋げてしまうと余計複雑になってしまいます^4。 extends を使う子供テンプレートのトップレベル(つまりインデントされていない部分)には block と Mixin 定義だけが現れるようにすべきです。 これは重要です!親のテンプレートはページ全体の構造を定義し、子供のテンプレートはただ特定のブロックのロジックやマークアップを append / pretend したり置き換えるだけにすべきです。 このことはマークアップを含むことのできる unbuffered code(頭が -(ハイフン)で始まる JavaScript コード)にも当てはまります。 同じ理由で // 始まりの HTML に出力されるコメントも子供テンプレートのトップレベルに現れてはいけません。 (//- 始まりの HTML に出力しない Pug コメントはどこに書いても構いません。)
ローカルDocker環境でSQSのモックElasticMQを利用する
AWS 環境で動作させるシステムの開発をしていて、ローカルの Docker 環境に SQS と同じ API で動作するモックを用意したくて ElasticMQ を利用してみました。[!CAUTION] 追記: 結局最終的に LocalStack を使いました。Docker イメージは幾つかありますが、roribio16/alpine-sqsを利用しました。 docker-compose.yml の用意 docker-compose の設定は以下のようにしました。 Node.js 環境も dokcer-compose で用意しておきます。 version: '3.7'services: # Node.js環境。開発時のnpmコマンド実行はここで行う。 node: image: node:12.18.3-alpine3.12 volumes: - ./:/project:delegated tty: true working_dir: /project environment: # AWS-SDKを動作させるのにダミーでいいので設定しておく必要がある AWS_REGION: ap-northeast-1 AWS_ACCESS_KEY_ID: local AWS_SECRET_ACCESS_KEY: dummy# 入力キュー # SQS互換のElasticMQを利用 input_queue: image: roribio16/alpine-sqs ports: # 管理画面へアクセスするポート - 9325:9325この alpine-sqs というイメージは default という名前のキューが最初から作成された状態で起動してくれます。 Node.js 環境の用意 コンテナを起動しておきましょう。 $ docker-compose up -dnode コンテナに入って作業します。alpine 版を使ってるので bash が入っていなくて sh を指定します[^1]。 [^1]: alpine を使っているのは、本番環境の Fargate 用コンテナをサイズの小さい alpine で作るつもりでいて、開発環境も同じに合わせたかったからです。debian を使った方が色々とカスタマイズが楽だと思います。 $ docker-compose exec node shここからは node コンテナ内でのコマンド実行の場合は、先頭を $ ではなく # で表します。 package.json を作成します。中身は適当でいいです。 # npm initAWS SDK をインストールします。 $ npm install aws-sdkAWS SDK でのアクセス まず送信用のスクリプトを作成します。 endpoint に用意した ElasticMQ のコンテナを指すように設定するのがポイントです。 const AWS = require('aws-sdk');const endpointUrl = 'http://input_queue:9324'; const queueUrl = endpointUrl + '/queue/default';const sqs = new AWS.SQS({ apiVersion: '2012-11-05', endpoint: endpointUrl, });const params = { // Remove DelaySeconds parameter and value for FIFO queues DelaySeconds: 10, MessageAttributes: { Title: { DataType: 'String', StringValue: 'The Whistler', }, Author: { DataType: 'String', StringValue: 'John Grisham', }, WeeksOn: { DataType: 'Number', StringValue: '6', }, }, MessageBody: 'Information about current NY Times fiction bestseller for week of 12/11/2016.', QueueUrl: queueUrl, };sqs.sendMessage(params, function (err, data) { if (err) { console.log('Error', err); } else { console.log('Success', data.MessageId); } });node コンテナ上で実行してみます。 # node send.jsホスト側のブラウザで http://localhost:9395/ にアクセスすると、管理画面でキューにメッセージが来ていることが確認できます。 (ただこの管理画面、キューからメッセージを削除しても削除したものも表示されます。それはそれで便利なんですが。) 次はポーリングを仕掛けて受信してみましょう。 const AWS = require('aws-sdk');const endpointUrl = 'http://input_queue:9324'; const queueUrl = endpointUrl + '/queue/default';const sqs = new AWS.SQS({ apiVersion: '2012-11-05', endpoint: endpointUrl, });async function poll() { const params = { QueueUrl: queueUrl, MaxNumberOfMessages: 10, WaitTimeSeconds: 20, MessageAttributeNames: ['Title', 'Author', 'WeeksOn'], }; return new Promise((resolve, reject) => { sqs.receiveMessage(params, (err, data) => { if (err) { console.error(err); reject(err); } else { console.log(data); resolve(data); } }); }); }async function main() { const data = await poll(); }main();node コンテナ上で実行します。 # node receive.js受信できればオッケーです。
ローカルDocker環境でS3Mockを利用する
AWS 環境で動作させているシステムの開発をしていて、ローカルの Docker 環境に S3 と同じ API で動作するモックがあったらいいなと思って調べたら、当然のようにありました。 S3 のモックサーバー候補 とりあえず見つかったのは以下です。abobe/S3Mock findify/S3Mock Fake S3 Mock S3 S3 Proxy上2つが有力候補。 3つ目の Fake S3 は古いバージョンはオープンソースだけど新しいバージョンは有料っぽい? 後ろ2つは adobe/S3Mock が似たようなプロジェクトとして紹介してました。 今回は findify/S3Mock を試した後に、結局 adobe/S3Mock を利用しました。[!CAUTION] 追記: 結局最終的に LocalStack を使いました。実際のプロジェクトでは TypeScript を使っているのですが、ここでは Javascript で解説します。 なぜ adobe/S3Mock にしたか? findify/S3Mock でデフォルトのバケットをコンテナ作成時に最初から作成しておく方法がすぐに分からなかったからです。 もちろん API を呼んで CreateBucket することはできます。 dokcer-compose.yml の用意 docker-compose の設定は以下のようにしました。 Node.js 環境も dokcer-compose で用意しておきます。 version: '3.7'services: # Node.js環境。開発時のnpmコマンド実行はここで行う。 node: image: node:12.18.3-alpine3.12 volumes: - ./:/project:delegated tty: true working_dir: /project environment: # AWS-SDKを動作させるのにダミーでいいので設定しておく必要がある AWS_REGION: ap-northeast-1 AWS_ACCESS_KEY_ID: local AWS_SECRET_ACCESS_KEY: dummy s3: image: adobe/s3mock environment: - initialBuckets=my-bucketinitialBuckets を指定することでコンテナ作成時にバケットも作成することができます。inisitalBuckets と複数形になっていますが、リストで複数作成させることができるようです。 - initialBuckets=my-bucket1,my-bucket2,my-bucket3Node.js 環境の用意 コンテナを起動しておきましょう。 $ docker-compose up -dnode コンテナに入って作業します。alpine 版を使ってるので bash が入っていなくて sh を指定します[^1]。 [^1]: alpine を使っているのは、本番環境の Fargate 用コンテナをサイズの小さい alpine で作るつもりでいて、開発環境も同じに合わせたかったからです。debian を使った方が色々とカスタマイズが楽だと思います。 $ docker-compose exec node shここからは node コンテナ内でのコマンド実行の場合は、先頭を $ ではなく # で表します。 package.json を作成します。中身は適当でいいです。 # npm initAWS SDK をインストールします。 $ npm install aws-sdkAWS SDK でのアクセス 適当に index.js とかの名前でファイルを作成しましょう。 まずは s3 クライアントオブジェクトを作成します。 const AWS = require('aws-sdk');const s3 = new AWS.S3({ apiVersion: '2006-03-01', // docker-compose環境の中ではサービス名がそのままホスト名として使える。s3コンテナの9090番ポートを指定。 endpoint: 'http://s3:9090', // S3Mock使う時は <バゲット名>.hostname:9090 でなく hostname:9090/<バゲット名> になってもらわないと困る s3ForcePathStyle: true, });endpoint と s3ForcePathStyle を指定する必要があります。 S3 の URL は昔は https://s3-<リージョン >.amazonaws.com/<バケット名> というパス形式と呼ばれるものが使われていました。 今は https://<バケット名>.s3-<リージョン>.amazonaws.com/ という仮想ホスト形式が使われています。 しかし S3Mock を使うときに仮想ホスト形式になってもらってはアクセスできないので、パス形式を利用するように指定しています。 9090 番ポートは HTTP 用で、9191 番ポートが HTTPS 用だそうです。HTTP しか試してませんが。 あとは普通に使ってもらえば大丈夫です。
"parserOptions.project" has been set for @typescript-eslint/parser をなんとかする
TypeScript のプロジェクトでプログラムのソースコードを src ディレクトリ下に、テストを tests ディレクトリ下に分けて格納するようにしました。 が、VSCode でテストファイルを開くとファイル先頭で必ずエラーが表示されるように・・・。 エラーの内容は Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. The file does not match your project config: tests/sample.test.ts. The file must be included in at least one of the projects provided.というもので、以下のように必ずファイルの先頭に表示されます。考えられる原因 どうやら tsconfig.json に tests 以下を含めないようにしているのが原因のようです。 { "compilerOptions": { // 省略 }, "include": ["src/**/*.ts"] }これはビルド時にテストファイルを含めないようにするためにそうしています。 しかし eslint の @typescript-eslint/parser もこのファイルを使っているために、このエラーが出てしまっているのでしょう。 { "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", "project": "./tsconfig.json" } // 省略 }@typescript-eslint/parser にとっては、tests 以下は project に指定した設定に含まれていないぞ!と。 考えられる解決方法 解決方法は2通り考えられそうです。eslint 用には別の tsconfig.eslint.json を用意して、そちらには tests 以下を含めるようにする。 tsconfig.json には tests 以下を含めるようにし、ビルド用に tsconfig.build.json を用意してそちらには tests 以下を含めないようにする。[!WARNING] 注: 設定を変更した後に VSCode を再起動しないと反映されませんでした。tsconfig.eslint.json を用意する方法 tsconfig.json を extends に指定して、変更点(include)だけを上書きしたものを用意します。 { "extends": "./tsconfig.json", "include": ["src/**/*.ts", "tests/**/*.ts"] }このファイルを @typescript-eslint/parser が利用するように指定します。 { "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", "project": "./tsconfig.eslint.json" } // 省略 }tsconfig.build.json を用意する方法 tsconfig.json には tests 以下も含めるように指定しておきます。 { "compilerOptions": { // 省略 }, "include": ["src/**/*.ts", "tests/**/*.ts"] }ビルド用の tsconfig.build.json を用意し、tsconfig.json を extends に指定して、変更点だけを上書きします。 { "extends": "./tsconfig.json", "compilerOptions": { // ビルド時にのみ指定したいものがあれば設定 }, "include": ["src/**/*.ts"], "exclude": ["tests/**/*.ts"] }ビルド時にこの設定ファイルを利用するように指定します。 { // 省略 "scripts": { "build": "tsc tsconfig.build.json" // 省略 } }どっちの解決方法がいいの? ビルド時にのみ指定したいコンパイラオプションがあるかどうかで変わってきそうです。 単に問題を解決したいだけなら tsconfig.eslint.json を用意するのがちょっとだけ簡単です。 ビルド時にのみ指定したいものがあるなら、共通設定を tsconfig.json に記載して、ビルド用に tsconfig.build.json を用意した方がいいでしょう。 いっそのこと tsconfig.eslint.json と tsconfig.build.json の両方とも用意して、共通設定を tsconfig.json に記載するというのも考えられます。これが一番柔軟です。 { "compilerOptions": { // 共通に設定するコンパイラオプション }, "exclude": ["node_modules"] }{ "extends": "./tsconfig.json", "compilerOptions": { // eslint でのみ適用したいものを指定 }, "include": ["src/**/*.ts", "tests/**/*.ts"] }{ "extends": "./tsconfig.json", "compilerOptions": { // ビルド時にのみ適用したいものを指定 }, "include": ["src/**/*.ts"] }