Skip to content

feat: プロジェクト全体をnative ESMに移行 (#846) - #1236

Draft
hakatashi wants to merge 4 commits into
masterfrom
esm-migration-846
Draft

feat: プロジェクト全体をnative ESMに移行 (#846)#1236
hakatashi wants to merge 4 commits into
masterfrom
esm-migration-846

Conversation

@hakatashi

Copy link
Copy Markdown
Member

Summary

Issue #846 で提起されていたプロジェクト全体のESM化に対応します。ソースコード、および tsgo でコンパイルした .build ディレクトリ内のプロダクションビルドの両方が、Node.jsのnative ESMとして動作することを目標としています。

  • package.json"type": "module" を追加し、tsconfig.jsonmodule/moduleResolutionNodeNext に変更
  • 全CJS形式の .js ファイル(hangman, mahjong, prime, anime, dajare, summary 等)を import/export 構文に変換
  • ESM化に伴って不要になったCJS互換スタブファイル(deploy/index.js, atequiz/index.js, lib/logger.js, lib/slackUtils.js, prime/primes.js — いずれも「〜のテスト環境でのrequire用スタブ」とコメントされていたもの)を削除
  • lodashlodash-es に置き換え
  • 相対importに拡張子(.js)を付与し、JSON importに with {type: 'json'} を付与
  • __dirname/__filenameimport.meta.url ベースの実装に置き換え(約40ファイル)
  • 実際にNode.jsのnative ESMで .build を動かして初めて発覚した、実行時のみに顕在化する問題を多数修正
    • shogi9.js/p-queue/mailgun.js 等、tscのCJS出力に由来するdouble-wrapされたdefault exportの取り扱い(esbuild/vitestとNode.jsのCJS-ESM interopの挙動差にも対応するため ?? によるフォールバックを付与)
    • japanese/pino-std-serializers/jaconv/bcdice/fs-extra/@hakatashi/riichi-core/pluscodes/prism-media/@google-cloud/text-to-speech の深いsubpath importなど、named importが cjs-module-lexer で静的に解決できないケースをdefault importからの分割代入に変更
    • yargs のESMエントリポイントでAPIの形状が変わる問題(yargs.array(...)yargs(hideBin(process.argv)).array(...))
    • require.main === module の残存(sunrise/aiGeneration.ts)
    • discord.js(デフォルトエクスポートなし)、sqlite(名前付きexportのみ)など、パッケージ側の実際のESM export形状に合わせたimportの修正
  • lib/startup.test.ts で「CJSのため拡張子解決ができない」という理由で除外されていた anime/anime/anison/hangman/qrcode-quiz を除外リストから外し、再度テスト対象に含めた

検証

  • npx tsgo --noEmit: エラーなし
  • npx vitest run: 398/398件成功
  • npm run build で生成した .build を実際にplain Nodeで動かす包括的スモークテスト(全ボットをdynamic importして起動を確認)を実施し、73/77ボットが問題なくimportできることを確認
    • 残り4件(topic, api, wordhero/crossword, twenty-questions)はmasterブランチに既存の、NODE_ENV=production 以外では db.collection() をモジュールトップレベルで無条件に呼び出してしまうバグによるもので、本PRの変更とは無関係(実運用ではNODE_ENV=productionのため問題なし)
  • CLAUDE.local.md の手順に従い、ngrok + npm run dev -- --only helloworld でdevサーバーを起動し、実際にSlackイベント(ngrok経由)を受信・処理できることを確認(Slack上でのメッセージ送信による応答確認は、dev workspace向けMCPツールが利用できなかったため未実施)

Test plan

  • npx tsgo --noEmit が通ること
  • npx vitest run が全て通ること
  • npm run build が成功し、.build/index.js がplain Nodeで起動できること
  • 実際のSlackワークスペースでの動作確認(レビュー時に実施をお願いします)

🤖 Generated with Claude Code

https://claude.ai/code/session_01VRu2pE3HCZkvpXgP7YGt9x

package.jsonに"type": "module"を追加し、tsconfigのmodule/moduleResolutionを
NodeNextに変更。ソースコードおよびtsgoでコンパイルした.buildディレクトリの
いずれもNode.jsのnative ESMとして動作するようにした。

- 全CJS形式の.jsファイル(hangman, mahjong, prime, anime, dajare, summary等)を
  import/export構文に変換し、ESM化に伴って不要になったCJS互換スタブファイル
  (deploy/index.js, atequiz/index.js, lib/logger.js, lib/slackUtils.js,
  prime/primes.js)を削除
- lodashをlodash-esに置き換え
- 相対importに拡張子(.js)を付与し、JSON importにimport attributesを付与
- __dirname/__filenameをimport.meta.urlベースの実装に置き換え
- 実際にNode.jsのnative ESMで動かして発覚した実行時のみの問題を修正
  (shogi9.js/p-queue/mailgun.js等のCJSパッケージのdouble-wrapped default export、
  japanese/pino-std-serializers/jaconv/bcdice/fs-extra等の名前付きimportが
  cjs-module-lexerで解決できない問題、yargsのESMエントリでのAPI形状の違い、
  require.main === moduleの残存など)
- lib/startup.test.tsで拡張子解決の制約により除外されていたanime/hangman/
  qrcode-quizを再度有効化

Closes #846

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRu2pE3HCZkvpXgP7YGt9x
@tsg-ut-gemini-assistant

Copy link
Copy Markdown
Contributor

えへ~

ありがとう!

レビューさせてもらうにゃ!

🔍️🔍️🔍️️

@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

hakatashi and others added 3 commits August 12, 2026 22:12
masterで追加された変更(atcoderの問題セット実績、sushi-botの週間早起き
ランキング実績、better-custom-responseのIt Works!実績)をESM移行ブランチに
取り込み、以下のコンフリクト・不整合を解決した。

- atcoder/index.ts: masterで `fetchUserACsInContest` が
  `fetchUserACsInContests` にリネームされたのを取り込みつつ、import指定子に
  ESM移行で必要な `.js` 拡張子を付与
- better-custom-response/custom-response.test.ts: masterで追加された
  It Works! 実績のテストを取り込み、相対importに `.js` / `/index.js` を付与
- better-custom-response/custom-responses.ts, sushi-bot/index.test.ts:
  自動マージで混入した拡張子なしの相対importを修正
- achievements/__mocks__/index.ts: 各exportを `vi.fn()` でラップ。
  ESM移行でimport指定子が `../achievements/index.js` になった結果、
  `vi.mock('../achievements')` がvitestの自動モック(spy)ではなく本ファイルの
  手動モックを解決するようになり、masterから来たsushi-botのテストが
  `expect(increment).toHaveBeenCalledWith(...)` で失敗するため

検証:
- npx tsgo --noEmit: エラーなし
- npx vitest run: 406/406件成功
- npm run build 後、atcoder / sushi-bot / better-custom-response を
  plain NodeのネイティブESMでdynamic importできることを確認

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RCznY9JkQh5fEdyMWvFAAD
- lib/dotenv.ts を追加し、process.env をトップレベルで参照する lib/slack.ts と
  lib/logger.ts の先頭で import するようにした。index.ts の動的 import は
  slackUtils 経由の static import チェーンで先に評価されてしまい機能しておらず、
  本番 (pm2 → 素の node) で Slack のトークン等が undefined になっていた
- lib/utils.ts に unwrapDefault を追加し、6箇所に重複していた CJS interop の
  `X.default ?? X` を集約。any キャストと eslint-disable が不要になった
- achievements/index.ts の NODE_ENV=test 用スタブを削除し index_development に
  フォールバックするようにした (シグネチャ不一致の重複実装を解消)
- bin の remove-achievement / remove-category / state-file-to-firebase を
  TypeScript 化し、import 先を lib/firestore.js に修正
- hitandblow/index.test.ts と kirafan/quiz.ts の `.//index.js` を修正

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RCznY9JkQh5fEdyMWvFAAD
…ting

JSON import assertions make TypeScript resolve ./state.json statically,
which fails in CI where the gitignored file does not exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant