Open23

CDK プロジェクトに Biome を導入する

ピン留めされたアイテム
hirenhiren

最終形

biome.jsonc
/*
AWS CDKの命名規則&コードスタイルに可能な限り準拠する
https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md#naming--style

その他はBiomeの推奨設定に従うが、必要に応じて変更する
https://biomejs.dev/analyzer/import-sorting/
https://biomejs.dev/formatter/#options
https://biomejs.dev/linter/rules/#recommended-rules
https://biomejs.dev/linter/rules/use-naming-convention/
*/
{
  "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
  "files": {
    "ignore": ["node_modules/*", "cdk.out/*"]
  },
  "formatter": {
    "ignore": ["*.js", "tsconfig.json", "cdk.json", "*.snap"],
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 150
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "always"
    }
  },
  "linter": {
    // "ignore": [],
    "rules": {
      "correctness": {
        "noUndeclaredVariables": "error",
        "noUnusedVariables": "error"
      },
      "style": {
        "noNamespace": "error",
        "useImportType": "off", // Biome推奨だが、あまり見かけない形のため無効化
        "useNamingConvention": {
          "level": "error",
          "options": {
            "strictCase": false,
            "conventions": [
              { "selector": { "kind": "class" }, "formats": ["PascalCase"] },
              { "selector": { "kind": "classMethod" }, "formats": ["camelCase"] },
              { "selector": { "kind": "typeMethod" }, "formats": ["camelCase"] },
              { "selector": { "kind": "objectLiteralMethod" }, "formats": ["camelCase"] },
              { "selector": { "kind": "function" }, "formats": ["camelCase"] },
              { "selector": { "kind": "interface" }, "formats": ["PascalCase"] },
              { "selector": { "kind": "enum" }, "formats": ["PascalCase"] },
              { "selector": { "kind": "enumMember" }, "formats": ["CONSTANT_CASE"] },
              { "selector": { "kind": "objectLiteralProperty" }, "formats": ["camelCase", "PascalCase", "CONSTANT_CASE"] },
              { "selector": { "kind": "importAlias" }, "formats": ["camelCase", "PascalCase", "snake_case", "CONSTANT_CASE"] }
            ]
          }
        }
      },
      "suspicious": {
        "noEmptyBlockStatements": "error",
        "noSkippedTests": "warn"
      }
    }
  },
  // テストコードに含まれるJest関数が no undeclared variables と判定されるため、テストファイルのみ設定を上書き
  "overrides": [
    {
      "include": ["*.test.ts", "*.test.tsx", "*.spec.ts", "*.spec.tsx", "**/__tests__/**"],
      "javascript": {
        "globals": ["afterAll", "afterEach", "beforeAll", "beforeEach", "describe", "expect", "it", "jest", "test"]
      }
    }
  ]
}
hirenhiren

環境

$ cat /etc/system-release
Amazon Linux release 2023.5.20240708 (Amazon Linux)
$ uname -a
Linux example.ap-northeast-1.compute.internal 6.1.79-99.164.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Feb 27 18:02:23 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

$ node -v
v18.17.0
$ npm --version
9.6.7
$ cdk --version
2.155.0 (build 34dcc5a)

cdk init app --language typescript実行済みディレクトリ
https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html

hirenhiren

https://biomejs.dev/guides/getting-started/

# Installation
$ npm install --save-dev --save-exact @biomejs/biome

# Configuration
# jsonではなくコメントや末尾カンマが付けられるjsoncを扱いたいのでjsoncオプションを追加
$ npx @biomejs/biome init --jsonc

Welcome to Biome! Let's get you started...

Files created 

  - biome.jsonc
    Your project configuration. See https://biomejs.dev/reference/configuration

Next Steps 

  1. Setup an editor extension
     Get live errors as you type and format when you save.
     Learn more at https://biomejs.dev/guides/integrate-in-editor/

  2. Try a command
     biome check  checks formatting, import sorting, and lint rules.
     biome --help displays the available commands.

  3. Migrate from ESLint and Prettier
     biome migrate eslint   migrates your ESLint configuration to Biome.
     biome migrate prettier migrates your Prettier configuration to Biome.

  4. Read the documentation
     Find guides and documentation at https://biomejs.dev/guides/getting-started/

  5. Get involved with the community
     Ask questions and contribute on GitHub: https://github.com/biomejs/biome
     Seek for help on Discord: https://discord.gg/BypW39g6Yc
hirenhiren

実行後

biome.jsonc
{
	"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
	"organizeImports": {
		"enabled": true
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	}
}
package.json
{
  "name": "example",
  "version": "0.1.0",
  "bin": {
    "example": "bin/example.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
  "devDependencies": {
+    "@biomejs/biome": "1.8.3",
    "@types/jest": "^29.5.12",
    "@types/node": "20.14.9",
    "aws-cdk": "2.155.0",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.5",
    "ts-node": "^10.9.2",
    "typescript": "~5.5.3"
  },
  "dependencies": {
    "aws-cdk-lib": "2.155.0",
    "constructs": "^10.0.0",
    "source-map-support": "^0.5.21"
  }
}
hirenhiren

https://biomejs.dev/reference/cli/

biome check
要求されたファイルに対してフォーマッタ、リンター、インポートのソートを実行します。

--write — 安全な修正、フォーマット、インポートのソートを書き込む。
--apply — --writeのエイリアスで、安全な修正、書式設定、インポートの並べ替えを行う(非推奨です。--writeを使用してください)

biome ci
CI 環境で使用するコマンド。要求されたファイルに対してフォーマッタ、リンター、インポートのソートを実行します。
ファイルは変更されません。コマンドは読み取り専用操作です。

hirenhiren

Biomeコマンドをnpm scriptsに追加

package.json
{
  "name": "example",
  "version": "0.1.0",
  "bin": {
    "example": "bin/example.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk",
+    "format": "npx @biomejs/biome format --write .",
+    "lint": "npx @biomejs/biome lint --write .",
+    "check": "npx @biomejs/biome check --write .",
+    "check:ci": "npx @biomejs/biome ci ."
  },
  "devDependencies": {
    "@biomejs/biome": "1.8.3",
    "@types/jest": "^29.5.12",
    "@types/node": "20.14.9",
    "aws-cdk": "2.155.0",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.5",
    "ts-node": "^10.9.2",
    "typescript": "~5.5.3"
  },
  "dependencies": {
    "aws-cdk-lib": "2.155.0",
    "constructs": "^10.0.0",
    "source-map-support": "^0.5.21"
  }
}
hirenhiren

linterやformatterはCDK公式のコーディングスタイルに合わせたい
https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md#naming--style

Naming & Style

Naming Conventions

  • Class names: PascalCase
  • Properties: camelCase
  • Methods (static and non-static): camelCase
  • Interfaces (“behavioral interface”): IMyInterface
  • Structs (“data interfaces”): MyDataStruct
  • Enums: PascalCase, Members: SNAKE_UPPER

Coding Style

  • Indentation: 2 spaces
  • Line length: 150
  • String literals: use single-quotes (') or backticks (```)
  • Semicolons: Semicolons: at the end of each code statement and declaration (incl. properties and imports).
  • Comments: start with lower-case, end with a period.
  • Properties: camelCase
    CDKの命名規則から外れるが、スナップショットテストやるときにCfnのプロパティ名(VisibilityTimeout)とかがPascalCaseだったりするので例外にしたい

  • Comments: start with lower-case, end with a period.
    コメントは日本語なので、これは無くても良さそう

hirenhiren

https://biomejs.dev/formatter/#options

Biome は言語に依存しないオプションと言語固有のオプションを分離します。

Formatterデフォルト設定

biome.jsonc
{
  "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "ignore": [],
    "attributePosition": "auto",
    "indentStyle": "tab",
    "indentWidth": 2,
    "lineWidth": 80,
    "lineEnding": "lf"
  },
  "javascript": {
    "formatter": {
      "arrowParentheses":"always",
      "bracketSameLine": false,
      "bracketSpacing": true,
      "jsxQuoteStyle": "double",
      "quoteProperties": "asNeeded",
      "semicolons": "always",
      "trailingCommas": "all"
    }
  },
  "json": {
    "formatter": {
      "trailingCommas": "none"
    }
  }
}
hirenhiren

https://biomejs.dev/linter/

デフォルトでは、Biome リンターは推奨ルール↓のみを実行します。

https://biomejs.dev/linter/rules/#recommended-rules

行単位の除外設定

開発者がコードの特定の行の lint ルールを無視したい場合があります。これは、lint 診断を発行する行の上に抑制コメントを追加することで実現できます。

// biome-ignore lint: <explanation>
// biome-ignore lint/suspicious/noDebugger: <explanation>
  • biome-ignore抑制コメントの開始です。
  • lintリンターを抑制します。
  • /suspicious/noDebugger:オプション、抑制するルールのグループと名前。
  • <explanation>ルールが無効になっている理由の説明

https://biomejs.dev/linter/#ignore-code

hirenhiren

良さげなテンプレートを参考に、設定

biome.jsonc
/*
AWS CDKの命名規則&コードスタイルに可能な限り準拠する
https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md#naming--style

その他はBiomeの推奨設定に従うが、必要に応じて変更する
https://biomejs.dev/analyzer/import-sorting/
https://biomejs.dev/formatter/#options
https://biomejs.dev/linter/rules/#recommended-rules
https://biomejs.dev/linter/rules/use-naming-convention/
*/
{
  "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
  "files": {
    "ignore": ["node_modules/*", "cdk.out/*"]
  },
  "formatter": {
    "ignore": ["*.js", "cdk.json", "package.json", "package-lock.json", "tsconfig.json", "*.snap"],
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 150
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "always"
    }
  },
  "linter": {
    // "ignore": [],
    "rules": {
      "correctness": {
        "noUndeclaredVariables": "error",
        "noUnusedVariables": "error"
      },
      "style": {
        "noNamespace": "error",
        "useImportType": "off", // Biome推奨だが、あまり見かけない形のため無効化
        "useNamingConvention": {
          "level": "error",
          "options": {
            "strictCase": false,
            "conventions": [
              { "selector": { "kind": "class" }, "formats": ["PascalCase"] },
              { "selector": { "kind": "classMethod" }, "formats": ["camelCase"] },
              { "selector": { "kind": "typeMethod" }, "formats": ["camelCase"] },
              { "selector": { "kind": "objectLiteralMethod" }, "formats": ["camelCase"] },
              { "selector": { "kind": "function" }, "formats": ["camelCase"] },
              { "selector": { "kind": "interface" }, "formats": ["PascalCase"] },
              { "selector": { "kind": "enum" }, "formats": ["PascalCase"] },
              { "selector": { "kind": "enumMember" }, "formats": ["CONSTANT_CASE"] },
              { "selector": { "kind": "objectLiteralProperty" }, "formats": ["camelCase", "PascalCase", "CONSTANT_CASE"] },
              { "selector": { "kind": "importAlias" }, "formats": ["camelCase", "PascalCase", "snake_case", "CONSTANT_CASE"] }
            ]
          }
        }
      },
      "suspicious": {
        "noEmptyBlockStatements": "error",
        "noSkippedTests": "warn"
      }
    }
  }
}
hirenhiren

コメントに挟まれている空行が削除されるのが割と気になるが、設定を変更できるFormatterオプションは無し
Biomeを使うなら割り切るしかなさそう

example.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { ExampleStack } from '../lib/example-stack';

const app = new cdk.App();
new ExampleStack(app, 'ExampleStack', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */
-
  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
-
  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },
-
  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
example.test.ts
// import * as cdk from 'aws-cdk-lib';
// import { Template } from 'aws-cdk-lib/assertions';
// import * as Example from '../lib/example-stack';

// example test. To run these tests, uncomment this file along with the
// example resource in lib/example-stack.ts
test('SQS Queue Created', () => {
//   const app = new cdk.App();
//     // WHEN
//   const stack = new Example.ExampleStack(app, 'MyTestStack');
//     // THEN
//   const template = Template.fromStack(stack);
- 
//   template.hasResourceProperties('AWS::SQS::Queue', {
//     VisibilityTimeout: 300
//   });
});
```
hirenhiren

BLEAで試してみる
安全な修正では、以下3つが修正されていた

  • Max Line150の改行
  • importの並べ替え
  • 同名のas削除

--unsafeを付けると、以下が修正されていた

  • グレイヴ・アクセント(``)をシングルクォートに('')
  • lambdaのexportするメソッドで使ってない引数のeventやcontextを_eventや_contextに、var変数をconstに
  • 変数含む文字列結合を+から`${var}`で展開する形へ
hirenhiren

テストコードに含まれるJestの関数が、軒並み未宣言としてエラーに
https://biomejs.dev/linter/rules/no-undeclared-variables/

./usecases/blea-guest-ecs-app-sample/test/blea-guest-ecs-app-sample-pipeline.test.ts:22:3 lint/correctness/noUndeclaredVariables ━━━━━━━━━━

  ✖ The expect variable is undeclared.
  
    20 │   });
    21 │ 
  > 22 │   expect(Template.fromStack(pipeline)).toMatchSnapshot();
       │   ^^^^^^
    23 │ });
    24 │ 
  
  ℹ By default, Biome recognizes browser and Node.js globals.
    You can ignore more globals using the javascript.globals configuration.

./usecases/blea-guest-ecs-app-sample/test/blea-guest-ecs-app-sample-pipeline.test.ts:7:1 lint/correctness/noUndeclaredVariables ━━━━━━━━━━

  ✖ The test variable is undeclared.
  
    5 │ import { devParameter, devPipelineParameter } from '../parameter';
    6 │ 
  > 7 │ test('Snapshot test for BLEA ECS App Stacks', () => {
      │ ^^^^
    8 │   const app = new App();
    9 │   const pipeline = new BLEAEcsAppPipelineStack(app, 'Dev-BLEAEcsAppPipeline', {
  
  ℹ By default, Biome recognizes browser and Node.js globals.
    You can ignore more globals using the javascript.globals configuration.

overridesでテストに使うファイルでのみ、無視するglobalsとして指定を追加

biome.jsonc
  "overrides": [
    {
      "include": ["*.test.ts", "*.test.tsx", "*.spec.ts", "*.spec.tsx", "**/__tests__/**"],
      "javascript": {
        "globals": ["afterAll", "afterEach", "beforeAll", "beforeEach", "describe", "expect", "it", "jest", "test"]
      }
    }
  ]

https://biomejs.dev/reference/configuration/#overrides
https://biomejs.dev/reference/configuration/#javascriptglobals

hirenhiren

命名規則でもエラーが出ている
https://biomejs.dev/linter/rules/use-naming-convention/

これはBLEAのここだけでのケースなので無視してもよさそう

./usecases/blea-guest-ec2-app-sample/lib/construct/investigation-instance.ts:11:19 lint/style/useNamingConvention ━━━━━━━━━━

  ✖ This class property name should be in camelCase.
  
    10 │ export class InvestigationInstance extends Construct {
  > 11 │   public readonly InvestigationInstanceSecurityGroup: ec2.ISecurityGroup;
       │                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    12 │ 
    13 │   constructor(scope: Construct, id: string, props: InvestigationInstanceProps) {

DynamoDBに入れるアイテムのプロパティ名は問題無いので、objectLiteralPropertysnake_caseを追加しても良いかも

./usecases/blea-guest-serverless-api-sample/lambda/nodejs/putItem.js:15:7 lint/style/useNamingConvention ━━━━━━━━━━

  ✖ This object property name should be in camelCase or PascalCase or CONSTANT_CASE.
  
    13 │       title: { S: request.title },
    14 │       content: { S: request.content },
  > 15 │       created_at: { S: datetime },
       │       ^^^^^^^^^^
    16 │     },
    17 │   };

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/API_PutItem_v20111205.html
https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes

hirenhiren

https://biomejs.dev/linter/rules/no-for-each/

./usecases/blea-gov-base-ct/lib/stack/blea-gov-base-ct-via-cdk-pipelines-stack.ts:33:5 lint/complexity/noForEach ━━━━━━━━━━

  ✖ Prefer for...of instead of forEach.
  
    31 │     });
    32 │ 
  > 33 │     props.targetParameters.forEach((params) => {
       │     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  > 34 │       pipeline.addStage(new BLEAGovBaseCtStage(this, 'Dev', params));
  > 35 │     });
       │     ^^
    36 │   }
    37 │ }

./usecases/blea-guest-ecs-app-sample/lib/stack/blea-guest-ecs-app-sample-via-cdk-pipelines-stack.ts:48:5 lint/complexity/noForEach ━━━━━━━━━━

  ✖ Prefer for...of instead of forEach.
  
    46 │     });
    47 │ 
  > 48 │     props.targetParameters.forEach((params) => {
       │     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  > 49 │       pipeline.addStage(new BLEAEcsAppStage(this, 'Dev', params));
  > 50 │     });
       │     ^^
    51 │   }
    52 │ }

  ℹ forEach may lead to performance issues when working with large arrays. When combined with functions like filter or map, this causes multiple iterations over the same type.

Stack定義周りでステージのAppParameterとかでよく使う印象なので一旦保留

hirenhiren

CDKにも試してみる

相当量のエラーが出ていた

Skipped 239924 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --fix --unsafe

The number of diagnostics exceeds the number allowed by Biome.
Diagnostics not shown: 26969.
Checked 14045 files in 47s. Fixed 12268 files.
Found 14882 errors.
Found 4 warnings.

snapshot含む、テスト関係のファイルも対象に含まれてしまっていたが、cdk公式のようなディレクトリ構成にはしないので無視
その他、150文字以内なのに改行されたり、超えていて改行されているのに1行に戻されたりと前後の構文を解釈した上で差分が出ている箇所も多かった

hirenhiren

VSCode拡張機能を設定
https://biomejs.dev/reference/vscode/

一旦全部設定

.vscode/extensions.json
{
  "recommendations": ["biomejs.biome"]
}
.vscode/settings.json
{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  }
}

VSCodeで開いているウィンドウのホームディレクトリに作成した.vscodeフォルダに入れれば、Formatterとimportのソートがちゃんと保存時に反映された

hirenhiren

BiomeのCI設定例
https://biomejs.dev/recipes/continuous-integration/

pull_request.yml
name: Code quality

on:
  push:
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Biome
        uses: biomejs/setup-biome@v2
        with:
          version: latest
      - name: Run Biome
        run: biome ci .