Next.jsプロジェクトでTypeScriptのimportの並び順を指定する

JavaやKotlinを長く書いてきて、importの並び順、ソートはフォーマッターにお任せだった。

Next.jsをTypeScriptで書いているプロジェクトでも、Javaと同様、importの順序を統一したいと思い、設定したのでメモ。

環境

Next.js v12.1.5。パッケージマネージャーには Yarn v1.22.15を使用。

ESLintプラグインの導入

importの順序やソート順を変更するプラグインは複数あるが、よく使われている eslint-plugin-import は、Next.jsの公式ESLint設定である eslint-config-next含まれている

そのため、 create-next-app でプロジェクトを作成し、 .eslintrcnext/core-web-vitalsnextextends していれば、プラグインを明示的にインストールしなくても、importの順序の設定が可能。

ただ、ソート順の設定だけでなく、不要なimportの削除もしたいので、eslint-plugin-unused-imports を追加した。また、Prettierとの共存のため、 eslint-config-prettier も追加している。

yarn add -D eslint-config-prettier eslint-plugin-unused-imports

ちなみに、 eslint-plugin-import は、 eslint-config-react-appeslint-config-airbnb などにも含まれている模様。

.eslintrc の記述

以下、JSONCで例を記述。

import順の設定

import/order の記述方法は以下。

github.com

こちらの記事も参考になった。

qiita.com

基本的な並び順は groups で、パッケージ名などによるカスタマイズは pathGroups で行う。

react, nextが上に、CSSファイルは最下段に来るようにしている。

また、Amplifyを使用しているため、APIやgraphqlのimportは上に、Figma連携で作成されたUIコンポーネント(ui-components 配下)は下に来るようにし、Figma連携で作られたファイルはESLintの対象外とした。

{
  "extends": [
    "next/core-web-vitals",
    // eslint-config-prettier を有効化
    "prettier"
  ],
  "ignorePatterns": ["src/ui-components/**"],
  "rules": {
    // importの並び順設定
    "import/order": [
      "error",
      {
        "groups": [
          "builtin",
          "external",
          "internal",
          ["parent", "sibling"],
          "object",
          "type",
          "index"
        ],
        "pathGroups": [
          {
            "pattern": "react",
            "group": "external",
            "position": "before"
          },
          {
            "pattern": "next/**",
            "group": "external",
            "position": "before"
          },
          {
            "pattern": "API",
            "group": "internal",
            "position": "before"
          },
          {
            "pattern": "graphql/**",
            "group": "internal",
            "position": "before"
          },
          {
            "pattern": "ui-components/**",
            "group": "index",
            "position": "after"
          },
          {
            "pattern": "**\\.css",
            "group": "index",
            "position": "after"
          }
        ],
        "pathGroupsExcludedImportTypes": ["react", "next/**"],
        "newlines-between": "always",
        "alphabetize": { "order": "asc", "caseInsensitive": false }
      }
    ],
    // importをファイル先頭に記述
    "import/first": "error",
    // 最後のimportの後に空行を追加
    "import/newline-after-import": "error"
  }
}

ちなみに、importだけでなくrequireを用いてもこの設定は有効になる。Amplifyで追加したLambda関数内ではrequireを使っているが、そちらにも効いてくれた。

不要なimportの削除

前述の記事の通り、 pluginsrules の追加だけで有効となる。

{
  "plugins": ["unused-imports"],
  "rules": {
    "unused-imports/no-unused-imports": "error"
  }
}

波かっこ内のソート

これでimportの順序についてはソートできるようになったが、 import { b, a } from ...import { a, b } from ... にするような設定は、 eslint-plugin-import には存在しない。

調べると、ESLintの標準ルールセットである sort-imports に存在した。

eslint.org

ignoreMemberSort が対象だが、オプションの1つであり、これだけを有効化することはできない模様。

もう少し調べてみると、 eslint-plugin-import に同様の機能追加希望のissueが上がっており、そのコメントに共存させる設定方法が記載されていた。

github.com

シンプルに、 ignoreMemberSort 以外はすべてoffにすればいい。

{
  "rules": {
    "sort-imports": [
      "error",
      {
        "allowSeparatedGroups": true,
        "ignoreCase": true,
        "ignoreDeclarationSort": true,
        "ignoreMemberSort": false,
        "memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
      }
    ],
    "import/order": [...]
  }
}

VSCodeの設定

エディタとしてVSCodeを使用しているので、ファイル保存時に自動保存をかけたい。

また、これまたJavaのように、不足しているimportがあれば、自動で追加させたい。

以下のように .vscode/settings.json を記述することで対応できた。

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  // 保存時のアクション
  "editor.codeActionsOnSave": {
    // 不足しているimportの追加
    "source.addMissingImports": true,
    // ファイルのfix
    "source.fixAll": true
  }
}

どんな形式のファイルでもフォーマットしてほしいので、デフォルトのフォーマッターはPrettierにして、保存時にフォーマットするよう設定している。

ESLintの対象となるTypeScriptやJavaScriptファイルは、 editor.formatOnSave をoffにしておいたほうがいいかとも思ったが、 eslint-config-prettier を使っているおかげか、今のところ特に問題はなく使えている。

振り返り

importの順序はけっこう癖が出ると思う。

ファイルの修正のついでに人力ソートしてくれる人もいたりするが、それはそれで差分が増えてコードレビューの時に気になったりするので、機械的に統一できるようになってよかった。

eslint-config-next でルールを決めてくれれば、何もしなくていいので最高なんだけど、そこまではやらないか。本質的な部分ではないし。

Amplifyで作成したLambdaにロールを付与してSESでメール送信する

前回の続き。

LambdaからSESによるメール送信を行う方法として、前回はIAMを作成し、アクセスキーを使ったが、今回はLambdaにロールを付与してみる。

環境

前回と同様。

実装

以下のissueに記載の通り、Lambda関数を作成し、生成されたCloudFormation定義を編集して、ロールを付与できる。

github.com

SESのドメインの検証は省略。

Lambdaの作成

amplify add function で、メール送信用のLambdaを作成する。関数名や言語は前回と同じものを指定。

記事作成時点で、コマンド実行時のオプション等で任意のロール付与は行えないため、advanced settingsでは何も設定しない。

また、前回は Select the categories you want this function to have access toAPIを選択したが、不要だったため除外している。

amplify add function
? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: sendMail
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? No
Successfully added resource sendMail locally.

Lambdaへのロール付与

生成された amplify/backend/function/<関数名>/<関数名>-cloudformation-template.json がCloudFormation定義ファイル。

Resources > lambdaexecutionpolicy > Properties > PolicyDocument > Statement に追加すればいい。

今回の作成方法の場合、 amplify/backend/function/sendMail/sendMail-cloudformation-template.json が対象。

生成段階で、ログ用にリソース指定された定義が存在するため、それとは別に "Resource": "*" で追加する。

// 前略
"Statement": [
// ここから追加
  {
    "Effect": "Allow",
    "Action": [
      "ses:SendEmail",
      "ses:SendRawEmail"
    ],
    "Resource": "*"
  },
// ここまで追加
// 以下、もともと存在していたログ用設定
  {
    "Effect": "Allow",
    "Action": [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ],
    "Resource": {
      "Fn::Sub": [
        "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*",
        {
          "region": {
            "Ref": "AWS::Region"
          },
          "account": {
            "Ref": "AWS::AccountId"
          },
          "lambda": {
            "Ref": "LambdaFunction"
          }
        }
      ]
    }
  }
]
// 後略

Lambdaの実装

今回はSystems Managerが不要なので、SESのパッケージだけインストールする。

cd amplify/backend/function/sendMail/src
yarn add @aws-sdk/client-ses

Lambdaの内容も前回と変わらず、 amplify/backend/function/<関数名>/src/index.js だが、実装はシークレットが不要になったのでシンプルになった。

// ファイル先頭の Amplify Params コメントは省略

const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses')

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
exports.handler = async event => {
  console.log(JSON.stringify(event))

  const sesClient = new SESClient({
    region: 'ap-northeast-1',
  })

  const params = event.arguments
  const sendEmailCommandInput = {
    Source: params.from,
    Destination: {
      ToAddresses: params.to,
      CcAddresses: params.cc,
      BccAddresses: params.bcc,
    },
    Message: {
      Subject: { Data: params.subject },
      Body: {
        Text: {
          Data: params.body,
        },
      },
    },
  }

  const sendEmailCommand = new SendEmailCommand(sendEmailCommandInput)
  const sendEmailCommandOutput = await sesClient.send(sendEmailCommand)

  return {
    statusCode: 200,
    body: sendEmailCommandOutput.MessageId,
  }
}

自分の設定が悪いのか、この方法だと amplify mock function <関数名> でのテストが失敗する。

amplify function pushAWS側に反映し、AWS ConsoleのLambdaのテストから実行できることを確認。

GraphQL定義およびコードからの呼び出し

前回と同様のため省略。

振り返り

Amplify CLIから設定できないAWSのサービスも、今回の方法を応用すれば、Lambda経由でどうとでもなりそう。

一方で、Amplifyの仕組みの外となるので、使いすぎるのは管理対象が増えるなど、デメリットにもなりそう。使いどころは考える必要があると感じた。

Amplifyで作成したLambdaからアクセスキーを用いてSESでメール送信する

Amplifyで作成したアプリケーションでメール送信をしようと思い、Lambda経由でSESを呼んでみた。

メール送信用のIAMを作成してアクセスキーを使うパターンと、LambdaにSESでのメール送信権限を付与するパターン、両方試したが、まずはアクセスキーを用いるパターンをメモ。

環境

Amplify CLI v8.0.2。

パッケージマネージャーにはYarn、AWS SDK for JavaScriptはv3を使用。

フロントエンドの実装にはTypeScriptを使用。

実装

メール送信用のIAMの作成や、SESのドメインの検証は省略。アクセスキーIDとシークレットアクセスキーを確認しておく。

方法はこちらを参照。

Lambdaの作成

amplify add function で、メール送信用のLambdaを作成する。関数名は sendMail 、言語はNode.jsを指定した。

amplify add function

> Lambda function (serverless function)

? Provide an AWS Lambda function name: sendMail

? Choose the runtime that you want to use: (Use arrow keys)
> NodeJS

? Choose the function template that you want to use: (Use arrow keys)
> Hello World

? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes

? Select the categories you want this function to have access to. (Press <space> to select, <a> to toggle all, <i> to in
>(*) api

? Select the operations you want to permit on amplifyexample (Press <space> to select, <a> to toggle all, <i> to invert
selection)
>(*) Mutation

? Do you want to invoke this function on a recurring schedule? No
? Do you want to enable Lambda layers for this function? No
? Do you want to configure environment variables for this function? No
? Do you want to configure secret values this function can access? Yes
? Enter a secret name (this is the key used to look up the secret value): AWS_SES_ACCESS_KEY_ID
? Enter the value for AWS_SES_ACCESS_KEY_ID: [input is hidden]
? What do you want to do? Add a secret
? Enter a secret name (this is the key used to look up the secret value): AWS_SES_SECRET_ACCESS_KEY
? Enter the value for AWS_SES_SECRET_ACCESS_KEY: [input is hidden]
? What do you want to do? I'm done

Select the categories you want this function to have access toAPIを選択しているが、これは不要だったかも。

Do you want to configure secret values this function can access にて、シークレットを指定できるため、そこでアクセスキーIDとシークレットアクセスキーを指定した。

シークレットはAWS Systems Managerのパラメータストアに、 /amplify/<AmplifyアプリID>/<環境名>/AMPLIFY_<関数名>_<シークレット名> という名前の、SecureStringで保存される。

これで amplify/backend/function/<関数名> というディレクトリが生成される。

Lambdaの実装

Systems ManagerおよびSESを使うので、作成されたディレクトリ配下のsrcディレクトリに、AWS SDKをインストールする。

cd amplify/backend/function/sendMail/src
yarn add @aws-sdk/client-ssm @aws-sdk/client-ses

Lambdaの内容は、 amplify/backend/function/<関数名>/src/index.js に記述する。

GraphQL の引数は、 handler関数の引数.arguments に設定されるので、メール送信に必要な情報を取得するようにした。

// ファイル先頭の Amplify Params コメントは省略

const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm')
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses')

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
exports.handler = async event => {
  // シークレットの取得
  const ssmClient = new SSMClient()
  const ssmGetParametersCommand = new GetParametersCommand({
    Names: ['AWS_SES_ACCESS_KEY_ID', 'AWS_SES_SECRET_ACCESS_KEY'].map(
      secretName => process.env[secretName],
    ),
    WithDecryption: true,
  })

  const { Parameters } = await ssmClient.send(ssmGetParametersCommand)

  // SESクライアントの作成
  const sesClient = new SESClient({
    region: 'ap-northeast-1',
    credentials: {
      accessKeyId: Parameters[0].Value,
      secretAccessKey: Parameters[1].Value,
    },
  })

  const params = event.arguments
  const sendEmailCommandInput = {
    Source: params.from,
    Destination: {
      ToAddresses: params.to,
      CcAddresses: params.cc,
      BccAddresses: params.bcc,
    },
    Message: {
      Subject: { Data: params.subject },
      Body: {
        Text: {
          Data: params.body,
        },
      },
    },
  }

  const sendEmailCommand = new SendEmailCommand(sendEmailCommandInput)
  const sendEmailCommandOutput = await sesClient.send(sendEmailCommand)

  // メッセージIDを返す
  return {
    statusCode: 200,
    body: sendEmailCommandOutput.MessageId,
  }
}

実装が終わったら、 amplify mock function <関数名> で動作確認できる。

以下のような event.json でメール送信できることを確認。

{
  "arguments": {
    "subject": "mail subject",
    "body": "mail body",
    "from": "from@example.com",
    "to": ["to@example.com"],
    "cc": [],
    "bcc": []
  }
}

amplify function push で、AWS側に反映。

GraphQL定義

schema.graphqltype Mutation に、Lambdaの呼び出しを記述する。

Lambda関数は、<関数名>-<環境名> のように、環境ごとに作成されるため、 @functionname関数名-${env} のように指定する。

type Mutation {
  sendMail(
    subject: String!
    body: String!
    from: String!
    to: [String!]!
    cc: [String!]
    bcc: [String!]
  ): String
    @function(name: "sendMail-${env}")
    @auth(rules: [{ allow: private }])
}

amplify api push で、AWS側に反映。また、push時にファイル定義の更新を行うと、 graphql/mutations.tssendMailAPI.tsSendMailMutationVariables が追加される

GraphQL定義の注意点

GraphQLではデフォルト値を指定できるため、当初、from: String = "from@example.com" のように記述していた。

だが、これだとLambdaから event.arguments.from のように値を参照した際、 from 未指定だと null になっていたため、デフォルト値は無視される模様。

コードから呼び出し

APIのpushで追加されたインターフェースを使用して、呼び出しを実装する。

Cognitoでログインしているユーザーにメールを送りたい場合、以下のようになる。

import { Amplify, API, Auth, graphqlOperation } from 'aws-amplify'
import { sendMail } from 'graphql/mutations'
import { SendMailMutationVariables } from 'API'

const sendMail = async () => {
  const currentUser = await Auth.currentAuthenticatedUser()
  const sendMailParams: SendMailMutationVariables = {
    subject: 'subject',
    body: 'body',
    to: [currentUser.attributes.email],
  }

  return await API.graphql(
    graphqlOperation(sendMail, sendMailParams),
  )
}

振り返り

ひとまず実装できた。Amplifyに含まれない仕組みは、Lambdaを使って何とかするのが想定されているのだろうか?

シークレットを指定した場合の動きを確認したかったのでアクセスキーを使用したが、実際にはLambdaにSESでのメール送信権限を付与するパターンで実装したので、そちらもまとめる予定。

Google Apps Scriptでプライベート関数を宣言し、「実行する関数を選択」に表示させない

Google Apps Scriptで宣言した関数が、「実行する関数を選択」に一覧表示される。

privateとして扱いたい関数については非表示にしたいと思い、調べたのでメモ。

試行錯誤

何パターンか試してみたが、関数宣言(function 関数名...)、関数式(const 関数名 = function...)、ラムダ式のいずれも、グローバルに記述した場合は選択欄に表示される。

クラスにメソッドとして定義したり、IIFEで関数を返すと、表示されなくなった。

また、関数名の指定の仕方で何とかならないかと思い、関数名に記号をつけたりしてみたところ、関数名の末尾にアンダースコアをつけた場合も表示されなくなった。

非表示になるパターン

関数名の末尾にアンダースコア

これが一番簡単。

最初はPythonのように、関数名の先頭にアンダースコア2つをつけたりしたが効果なし。

そういえば、Google JavaScript Style Guideでは、privateなフィールドメソッドの末尾はアンダースコアにするんだったなと思い、試してみたらうまくいった。

Google Apps Scriptのベストプラクティスにも記載あり

関数宣言、関数式、ラムダ式、いずれの場合も有効だった。

// 関数宣言
function hiddenFunc1_() {}

// 関数式
const hiddenFunc2_ = function() {};

// ラムダ式
const hiddenFunc3_ = () => {};

また、この方法で宣言した関数は、サーバー側の関数呼び出しの対象外ともなる模様。

developers.google.com

クラスにメソッドとして定義

グローバル関数が選択肢として表示されるようなので、メソッドにしてやれば表示されない。

2022/4現在、Google Apps Scriptでもclassキーワードによるクラス宣言が使えるので、メソッドにするのは簡単。

適当なオブジェクトを用意して、そこにメソッドを追加しても非表示になりそうだが、未確認。

IIFEで関数を返す

エンクロージャ的に、関数を返す即実行関数だと、認識されない模様。

const hiddenFunc = (() => {
  return () => {};
})();

一応できることはできるが、末尾アンダースコアがあるので使わないか。

振り返り

何年か前にも試していて、できることは覚えていたが、どうやればいいかすっかり忘れていたのでメモしておく。

claspは使わず、久しぶりにGASを書いたが、クラス宣言が使えて結構感動した。

AWS Amplifyでビルドに失敗する

新規プロジェクトにて、バックエンドにAWS Amplifyを使うことにした。

Gitリポジトリと連携して、開発ブランチに変更があるたびにAWS側でビルドするように設定したが、いくつかの原因でビルドに失敗したのでメモ。

環境

Amplify CLI v8.0.1。インストールは yarn global add @aws-amplify/cli にて実施。

問題1

Next.jsによるフロントエンドアプリケーションのGitリポジトリをAmplifyのフロントエンドとして連携させ、指定したブランチに変更があればビルドおよびデプロイするよう設定した。

対象のブランチに変更があると、Amplifyのビルドは実行されるが、 Cannot find file './aws-exports' in ./src エラーにより、ビルドが失敗する。

問題1の対応

amplify init でAmplifyの初期設定を行ったが、その場合 src/aws-exports.js.gitignore に指定されるため、Gitリポジトリには含まれないのが原因。

対応はこちらに記載あり。

aws.amazon.com

  1. Amplify Consoleを開く
  2. Frontend environments タブにて、「継続的なデプロイの設定 (編集)」から、「フルスタックの継続的デプロイ (CI/CD) を有効化」のチェックを外して保存
  3. アプリの設定 > 全般 > 編集より、サービスロールに AdministratorAccess-Amplify を持ったロールを指定して保存
    • 自分の場合、「amplifyconsole-backend-role」が設定済みだった

問題2

前述の設定で解消するかと思ったが、ビルドエラーが解消せず。

問題2の対応

Amplifyのバージョンが、開発に使用しているものとAWS上のビルドに使用されるものとで差異があったのが原因の模様。

アプリの設定 > ビルドの設定 > Build image settings の Edit にて変更可能。 Amplify CLI のバージョンにはデフォルトで latest が指定されていたが、PCにインストールしたものを指定したところ、ビルドが通るようになった。

その他にも、Node.js や Next.js のバージョンなどを指定可能。2022/4/29 時点で、LTSのv16ではなくv14が指定されているため、指定しておいた。

振り返り

Amplify、生産性の高さがすごい。

ただ、発展途上なためか、ちょこちょこハマる時があり、そうなったときの情報が少なかったり、出てきても古かったりするのが難点。

Amplify CLIGitHubを検索すると対応方法など見つかることも多いので、適宜調査・学習していきたい。

SikuliXの画像のファイル名を指定する

久しぶりにSikuliXを触ったが、IDEで画像をキャプチャしたときに、ファイル名を指定できたのでメモ。

環境

SikuliX IDE Version 2.0.5。

画像ファイル名について

SikuliX IDEで画像キャプチャすると、通常は数値のみ(おそらくエポックミリ秒)をファイル名としたPNG画像が作成される。

IDE上の画像イメージをクリックして、ファイル名を変更することはできるが、画像キャプチャ前に変数への代入式( 変数名 = )を記述してから、右辺にカーソルを置いた状態で画像キャプチャすると、代入先の変数名をファイル名としてPNG画像が作成される。

振り返り

昔SikuliXをよく触っていた時は、バージョンが1.0.2くらいだったと思うが、その当時からこの仕様だったか覚えていない。

毎回画像をキャプチャしていると、プロジェクトのファイルサイズが増えてしまうので、ファイル名を指定して使いまわせると嬉しい。

余談

JythonがいまだにCPython v2.7相当だが、v3対応のプロジェクトも存在する模様。

the-very-slow-jython-project.readthedocs.io

GraalVM Implementation of Pythonはv3.8相当らしいが、実装を切り替えるのも難しそう。

更新頻度の高いJRubyを使うか、SikuliX API+ Groovyあたりで実装するほうがいいかなぁ。

JavaのString#replaceAllによる正規表現置換でも、$1などでグループ化した文字列を後方参照できる

タイトル通りだが、チーム内の誰もできるということを知らなかったのでメモ。

replaceAllでの後方参照

タイトル通り、第1引数の検索文字列に () で囲んだグループを記述し、第2引数の置換文字列に ${n}$n を記述すると、検索文字内のグループ化した文字列を後方参照できる。

また、 String#replaceFirst でも同様に置換可能。

assert "abc".replaceAll("(^.)", "$1$1").equals("aabc");
assert "abc".replaceFirst("(^.)", "$1$1").equals("aabc");

Groovyの場合

Groovyでは、ダブルクォーテーションによる文字列リテラルを使用すると、$ がGStringの変数展開とみなされる。

シングルクォーテーションまたはスラッシュで囲むか、ダブルクォーテーション内では $ をバックスラッシュでエスケープする必要がある。

assert "abc".replaceAll("(^.)", /$1$1/) == "aabc"
assert "abc".replaceFirst("(^.)", "\$1\$1") == "aabc"

Kotlinの場合

KotlinのString#replace()でもGroovyと同様に、第2引数の $エスケープなど必要かと思ったが、不要だった。

変数名として数値が使えないため、そのあたり判定してくれているのかもしれない。

assert("abc".replace("(^.)".toRegex(), "$1$1") == "aabc")

振り返り

なんでできること知らなかったの? と聞くと、

unageanu.hatenablog.com

のように、「Javaでできると思わなかった」という答えが返ってきた。

上記リンクや下記リンクのように、定期的に記事になっている気がする。

kazuhira-r.hatenablog.com