diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 514abdfb201..3f888504b7c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -19,7 +19,6 @@
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"Vue.volar",
- "Orta.vscode-jest",
"dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"
]
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b93080278dd..61e222b6f8a 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -34,9 +34,6 @@ updates:
patterns:
- "storybook*"
- "@storybook/*"
- swc-core:
- patterns:
- - "@swc/core*"
typescript-eslint:
patterns:
- "@typescript-eslint/*"
diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml
index 7d196785745..4e7ff2c3c40 100644
--- a/.github/workflows/on-release-created.yml
+++ b/.github/workflows/on-release-created.yml
@@ -28,6 +28,9 @@ jobs:
cache: 'pnpm'
# see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry
registry-url: 'https://registry.npmjs.org'
+ # Ensure npm 11.5.1 or later is installed
+ - name: Update npm
+ run: npm install -g npm@latest
- name: Publish package
run: |
pnpm i --frozen-lockfile
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ab691e32d0..3404010b897 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,39 @@
+## 2026.5.0
+
+### General
+- Enhance: アバターデコレーションにカテゴリを設定できるように
+
+### Client
+- Enhance: チャンネル指定リノートでリノート先のチャンネルに移動できるように
+- Enhance: ベータ版でのアップデート時のダイアログの更新情報リンクをGitHubのReleasesページに遷移するようにし、正しく閲覧できるように
+- Fix: 一部のページ内リンクが正しく動作しない問題を修正
+- Fix: ドライブへの画像アップロード時にファイル名の変更が無視される不具合を修正
+- Fix: 連合が無効化されたサーバーで一部の設定項目が空欄で表示される問題を修正
+- Fix: オーディオ、動画の再生速度メニューが開けない問題を修正
+
+### Server
+- Enhance: メモリ使用量を削減
+- Enhance: 起動の高速化
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1410)
+- Enhance: バックエンドの開発モード時の安定性向上
+- Enhance: バックエンドビルド・テスト時に使用する依存関係の整理(swc/esbuild→Rolldown, Jest→Vitest)
+- Fix: ファイルシステムを用いる処理におけるパスの取り扱いを改善
+- Fix: `/api-doc` にアクセスできない問題を修正
+- Fix: support `alsoKnownAs` from remote actors as either array or unwrapped singleton
+- Fix: ローカルに存在しないリモートアカウントに対するアカウント削除リクエストを受信した際に、そのユーザーを新規作成して削除する挙動を修正
+- Fix: Inboxでの特定のエラーによる失敗はDelayedにしない
+- Fix: ID生成アルゴリズムにULIDを使用している場合にMisskeyが正しく動作しない問題を修正
+- Fix: リレー経由で届いたノートがリノートとして表示される問題を修正
+- Fix: robots.txtの内容を調整
+- Fix: 特定のユーザーに管理者権限を持つロールが複数ついている際に、取得できるユーザーIDが重複する問題を修正
+ (Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/17ed4108cec4b6bd2fd989db5a9091db91fa37a7)
+- Fix: ブロックしたサーバーからのInboxジョブが蓄積し続ける問題を修正
+ (Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/3f0f4bfe923f2b3a7837017b54841598f421c6ef)
+- Fix: support activity with `actor` as an id string or embedded object in inbox processor and ActivityPub inbox service
+- Fix: コンフィグファイルに `meilisearch` の設定がある状態でほかの検索プロバイダを利用すると、UI上からリモートのノートの検索ができない問題を修正
+- Fix: ノートに関する通知で公開範囲が考慮されていない問題を修正
+ (Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/cbce96c520a138b8bcd16890ff6f2952830fa166 originally presented in https://github.com/yojo-art/cherrypick/pull/743)
+
## 2026.3.2
### General
@@ -31,7 +67,7 @@
- `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-users-by-birthday` をご利用ください。
### General
-- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
+- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey)
- 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました
- Fix: ユーザーハッシュタグページでユーザーの読み込みが重複する問題を修正
@@ -87,9 +123,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Client
- Enhance: デッキのUI説明を追加
- Enhance: 設定がブラウザによって消去されないようにするオプションを追加
-- Fix: バージョン表記のないPlayが正しく動作しない問題を修正
+- Fix: バージョン表記のないPlayが正しく動作しない問題を修正
バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。
-- Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正
+- Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正
- Fix: 一部のUnicode絵文字のリアクションがボタンにならない問題を修正
### Server
@@ -134,11 +170,11 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: ページの内容がはみ出ることがある問題を修正
- Fix: ナビゲーションバーを下に表示しているときに、項目数が多いと表示が崩れる問題を修正
- Fix: ヘッダーメニューのチャンネルの新規作成の項目でチャンネル作成ページに飛べない問題を修正 #16816
-- Fix: ラジオボタンに空白の選択肢が表示される問題を修正
+- Fix: ラジオボタンに空白の選択肢が表示される問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1105)
- Fix: 一部のシチュエーションで投稿フォームのツアーが正しく表示されない問題を修正
- Fix: 投稿フォームのリセットボタンで注釈がリセットされない問題を修正
-- Fix: PlayのAiScriptバージョン判定(v0.x系・v1.x系の判定)が正しく動作しない問題を修正
+- Fix: PlayのAiScriptバージョン判定(v0.x系・v1.x系の判定)が正しく動作しない問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1129)
- Fix: フォロー申請をキャンセルする際の確認ダイアログの文言が不正確な問題を修正
- Fix: 初回読み込み時にエラーになることがある問題を修正
@@ -148,12 +184,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Server
- Enhance: メモリ使用量を削減しました
- Enhance: 依存関係の更新
-- Fix: ワードミュートの文字数計算を修正
+- Fix: ワードミュートの文字数計算を修正
- Fix: チャンネルのリアルタイム更新時に、ロックダウン設定にて非ログイン時にノートを表示しない設定にしている場合でもノートが表示されてしまう問題を修正
-- Fix: DeepL APIのAPIキー指定方式変更に対応
+- Fix: DeepL APIのAPIキー指定方式変更に対応
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1096)
- 内部実装の変更にて対応可能な更新です。Misskey側の設定方法に変更はありません。
-- Fix: DBレプリケーションを利用する環境でクエリーが失敗する問題を修正
+- Fix: DBレプリケーションを利用する環境でクエリーが失敗する問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1123)
## 2025.11.0
@@ -196,7 +232,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
## 2025.10.1
### General
-- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン)
+- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン)
パフォーマンス上の問題からデフォルトで無効化されています。「コントロールパネル > パフォーマンス」から有効化できます。
- 依存関係の更新
@@ -323,7 +359,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: レンダリングパフォーマンスの向上
- Enhance: 依存ソフトウェアの更新
- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正
-- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
+- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
- Fix: テーマエディタが動作しない問題を修正
- Fix: チャンネルのハイライトページにノートが表示されない問題を修正
@@ -483,7 +519,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加
- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように
- Enhance: リプライ元にアンケートがあることが表示されるように
-- Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上
+- Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上
(Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283)
- Enhance: ユーザー設定でURLプレビューを無効化できるように
- Enhance: ヒントとコツを追加
@@ -572,7 +608,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Server
- Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に
-- Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように
+- Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように
(Cherry-picked from https://github.com/yojo-art/cherrypick/pull/568 and https://github.com/team-shahu/misskey/pull/38)
- Enhance: ユーザーごとにノートの表示が高速化するように
- Fix: システムアカウントの名前がサーバー名と同期されない問題を修正
@@ -678,7 +714,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### General
- Enhance: プロキシアカウントをシステムアカウントとして作成するように
-- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように
+- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように
書式は https://indieauth.spec.indieweb.org/20220212/#example-2 に準じます。
- Fix: システムアカウントが削除できる問題を修正
@@ -692,7 +728,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Server
- Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正
-- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正
+- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/895)
@@ -713,7 +749,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: リアクションする際に確認ダイアログを表示できるように
- Enhance: コントロールパネルのユーザ検索で入力された情報をページ遷移で損なわないように `#15437`
- Enhance: CWの注釈で入力済みの文字数を表示
-- Enhance: ノート検索ページのデザイン調整
+- Enhance: ノート検索ページのデザイン調整
(Cherry-picked from https://github.com/taiyme/misskey/pull/273)
- Fix: ノートページで、クリップ一覧が表示されないことがある問題を修正
- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529`
@@ -730,7 +766,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正
- Fix: HTTPプロキシとその除外設定を行った状態でカスタム絵文字の一括インポートをしたとき、除外設定が効かないのを修正( #8766 )
-- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正
+- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886)
- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように
- Fix: `update-meta`でobjectStoragePrefixにS3_SAFEかつURL-safeでない文字列を使えないように
@@ -740,12 +776,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
## 2025.2.0
### General
-- Fix: Docker のビルドに失敗する問題を修正
+- Fix: Docker のビルドに失敗する問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/883)
### Client
- Fix: パスキーでパスワードレスログインが出来ない問題を修正
-- Fix: 一部環境でセンシティブなファイルを含むノートの非表示が効かない問題
+- Fix: 一部環境でセンシティブなファイルを含むノートの非表示が効かない問題
- Fix: データセーバー有効時にもユーザーページの「ファイル」タブで画像が読み込まれてしまう問題を修正
- Fix: MFMの `sparkle` エフェクトが正しく表示されない問題を修正
- Fix: ページのURLにスラッシュが含まれている場合にページが正しく表示されない問題を修正
@@ -772,14 +808,14 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
* β版として公開のため、旧画面も引き続き利用可能です
### Client
-- Enhance: PC画面でチャンネルが複数列で表示されるように
+- Enhance: PC画面でチャンネルが複数列で表示されるように
(Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
- Enhance: 照会に失敗した場合、その理由を表示するように
- Enhance: ワードミュートで検知されたワードを表示できるように
- Enhance: リモートのノートのリンクをコピーできるように
- Enhance: 連合がホワイトリスト化・無効化されているサーバー向けのデザイン修正
- Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加
-- Enhance: ノートの添付ファイルを一覧で遡れる「ファイル」タブを追加
+- Enhance: ノートの添付ファイルを一覧で遡れる「ファイル」タブを追加
(Based on https://github.com/Otaku-Social/maniakey/pull/14)
- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
- Enhance: クエリパラメータでuiを一時的に変更できるように #15240
@@ -787,26 +823,26 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
-- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正
+- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803)
- Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正
- Fix: プラグイン `register_note_view_interruptor` でノートのサーバー情報の書き換えができない問題を修正
- Fix: Botプロテクションの設定変更時は実際に検証を通過しないと保存できないように( #15137 )
- Fix: ノート検索が使用できない場合でもチャンネルのノート検索欄がでていた問題を修正
- Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正
-- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正
+- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正
(Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4)
- Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正
- Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正
- Fix: MacOSでChrome系ブラウザを使用している場合に、Misskeyを閉じた際に他のタブのオーディオ機能と干渉する問題を修正
- Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正
- Fix: 「削除して編集」でノートの引用を解除出来なかった問題を修正( #14476 )
-- Fix: RSSウィジェットが正しく表示されない問題を修正
+- Fix: RSSウィジェットが正しく表示されない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/857)
- Fix: ワードミュートの保存失敗時にAPIエラーが握りつぶされる事があるのを修正
- Fix: アンケートでリモートの絵文字が正しく描画できない問題の修正
(Cherry-picked from https://github.com/yojo-art/cherrypick/pull/153)
-- Fix: 非ログイン時のサーバー概要画面のメニューボタンが押せないことがあるのを修正
+- Fix: 非ログイン時のサーバー概要画面のメニューボタンが押せないことがあるのを修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/656)
- Fix: URLにはじめから`#pswp`が含まれている場合に画像ビューワーがブラウザの戻るボタンで閉じられない問題を修正
- Fix: ロール作成画面で設定できるアイコンデコレーションの最大取付個数を16に制限
@@ -815,18 +851,18 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Server
- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように
- Enhance: ノート検索の選択肢としてpgroongaに対応 ( #14730 )
-- Enhance: チャート更新時にDBに同時接続しないように
+- Enhance: チャート更新時にDBに同時接続しないように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830)
- Enhance: config(default.yml)からSQLログ全文を出力するか否かを設定可能に ( #15266 )
- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
-- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
+- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
- Fix: ノートの閲覧にログイン必須にしてもFeedでノートが表示されてしまう問題を修正
- Fix: 絵文字の連合でライセンス欄を相互にやり取りするように ( #10859, #14109 )
- Fix: ロックダウンされた期間指定のノートがStreaming経由でLTLに出現するのを修正 ( #15200 )
- Fix: disableClustering設定時の初期化ロジックを調整( #15223 )
- Fix: URLとURIが異なるエンティティの照会に失敗する問題を修正( #15039 )
-- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正
+- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869)
- Fix: `/api/pages/update`にて`name`を指定せずにリクエストするとエラーが発生する問題を修正
- Fix: AIセンシティブ判定が arm64 環境で動作しない問題を修正
@@ -852,12 +888,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 )
### Client
-- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
+- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
- Enhance: ドライブでソートができるように
- Enhance: アイコンデコレーション管理画面の改善
- Enhance: 「単なるラッキー」の取得条件を変更
-- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 )
+- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 )
- Enhance: MiAuth, OAuthの認可画面の改善
- どのアカウントで認証しようとしているのかがわかるように
- 認証するアカウントを切り替えられるように
@@ -865,29 +901,29 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: カタルーニャ語 (ca-ES) に対応
- Enhance: 個別お知らせページではMetaタグを出力するように
- Enhance: ノート詳細画面にロールのバッジを表示
-- Enhance: 過去に送信したフォローリクエストを確認できるように
+- Enhance: 過去に送信したフォローリクエストを確認できるように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 )
- Enhance: リノートメニューに「リノートの詳細」を追加
- Enhance: 非ログイン状態でMisskeyを開いた際のパフォーマンスを向上
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
-- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
+- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
- Fix: リンク切れを修正
-- Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
+- Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
-- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正
+- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/815)
-- Fix: TypeScriptの型チェック対象ファイルを限定してビルドを高速化するように
+- Fix: TypeScriptの型チェック対象ファイルを限定してビルドを高速化するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/725)
### Server
- Enhance: DockerのNode.jsを22.11.0に更新
-- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
- (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
+- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
+ (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
- Fix: sharedInboxが無いActorに紐づくリモートユーザーを照会できない
@@ -895,18 +931,18 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: フォロワーへのメッセージの絵文字をemojisに含めるように
- Fix: Nested proxy requestsを検出した際にブロックするように
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
-- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
+- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
-- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
+- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
-- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正
+- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
-- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
+- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
-- Fix: User Webhookテスト機能のMock Payloadを修正
-- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996)
+- Fix: User Webhookテスト機能のMock Payloadを修正
+- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996)
- Fix: リノートミュートが新規投稿通知に対して作用していなかった問題を修正
-- Fix: Inboxの処理で生じるエラーを誤ってActivityとして処理することがある問題を修正
+- Fix: Inboxの処理で生じるエラーを誤ってActivityとして処理することがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/730)
- Fix: セキュリティに関する修正
@@ -933,13 +969,13 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように
- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
- Fix: RBT有効時、リノートのリアクションが反映されない問題を修正
-- Fix: キューのエラーログを簡略化するように
+- Fix: キューのエラーログを簡略化するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
## 2024.10.0
### Note
-- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
+- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
- ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
- なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。
- ユーザーデータを読み込む際の型が一部変更されました。
@@ -959,7 +995,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Client
- Enhance: デザインの調整
- Enhance: ログイン画面の認証フローを改善
-- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正
+- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/657)
### Server
@@ -977,7 +1013,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Feat: フォローされた際のメッセージを設定できるように
- Feat: 連合をホワイトリスト制にできるように
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
-- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
+- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
- Feat: データエクスポートが完了した際に通知を発行するように
- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
@@ -996,12 +1032,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
-- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正
+- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/265)
-- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
+- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
-- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正
+- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110)
- Fix: 一部画面のページネーションが動作しにくくなっていたのを修正 ( #12766 , #11449 )
@@ -1010,14 +1046,14 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
- この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
-- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正
+- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
- Fix: Continue importing from file if single emoji import fails
-- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正
+- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624)
-- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように
+- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634)
-- Fix: ``を追って照会するのはOKレスポンスが返却された場合のみに
+- Fix: ``を追って照会するのはOKレスポンスが返却された場合のみに
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633)
- Fix: メールにスタイルが適用されていなかった問題を修正
@@ -1046,15 +1082,15 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
- これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。
- Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正
-- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正
+- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582)
-- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように
+- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679)
- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように
- キュー処理のつまりが改善される可能性があります
- Fix: リバーシの対局設定の変更が反映されないのを修正
- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
-- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正
+- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
- Fix: Prevent memory leak from memory caches (#14310)
- Fix: More reliable memory cache eviction (#14311)
@@ -1086,9 +1122,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
- Enhance: 非ログイン時のハイライトTLのデザインを改善
-- Enhance: フロントエンドのアクセシビリティ改善
+- Enhance: フロントエンドのアクセシビリティ改善
(Based on https://github.com/taiyme/misskey/pull/226)
-- Enhance: サーバー情報ページ・お問い合わせページを改善
+- Enhance: サーバー情報ページ・お問い合わせページを改善
(Cherry-picked from https://github.com/taiyme/misskey/pull/238)
- Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
@@ -1097,7 +1133,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように
- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように
- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように
-- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように
+- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように
(Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99)
- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
- Enhance: ブラウザのコンテキストメニューを使用できるように
@@ -1105,19 +1141,19 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
-- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
+- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
- Fix: アンテナの編集画面のボタンに隙間を追加
- Fix: テーマプレビューが見れない問題を修正
-- Fix: ショートカットキーが連打できる問題を修正
+- Fix: ショートカットキーが連打できる問題を修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/234)
- Fix: MkSignin.vueのcredentialRequestからReactivityを削除(ProxyがPasskey認証処理に渡ることを避けるため)
-- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正
+- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574)
- Fix: Twitchの埋め込みが開けない問題を修正
- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正
-- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正
+- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672)
- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正
- Fix: deck uiの通知音が重なる問題 (#14029)
@@ -1160,14 +1196,14 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
4. フォローしていない非アクティブなユーザ
また、自分自身のアカウントもサジェストされるようになりました。
-- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
+- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
- Fix: FTT有効時にリモートユーザーのノートがHTLにキャッシュされる問題を修正
- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正
- Fix: エラーメッセージの誤字を修正 (#14213)
- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
-- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
+- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
@@ -1194,7 +1230,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### General
- Feat: エラートラッキングにSentryを使用できるようになりました
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
-- Enhance: アンテナでBotによるノートを除外できるように
+- Enhance: アンテナでBotによるノートを除外できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
- Enhance: クリップのノート数を表示するように
- Enhance: コンディショナルロールの条件として以下を新たに追加 (#13667)
@@ -1213,7 +1249,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Client
- Feat: アップロードするファイルの名前をランダム文字列にできるように
-- Feat: 個別のお知らせにリンクで飛べるように
+- Feat: 個別のお知らせにリンクで飛べるように
(Based on https://github.com/MisskeyIO/misskey/pull/639)
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
@@ -1243,9 +1279,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
- Fix: ローカルURLのプレビューポップアップが左上に表示される
-- Fix: WebGL2をサポートしないブラウザで「季節に応じた画面の演出」が有効になっているとき、Misskeyが起動できなくなる問題を修正
+- Fix: WebGL2をサポートしないブラウザで「季節に応じた画面の演出」が有効になっているとき、Misskeyが起動できなくなる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/459)
-- Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正
+- Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528)
- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177
- CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。
@@ -1268,13 +1304,13 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: ドライブのファイルがNSFWかどうか個別に連合されるように (#13756)
- 可能な場合、ノートの添付ファイルのセンシティブ判定がファイル単位になります
- Fix: リモートから配送されたアクティビティにJSON-LD compactionをかける
-- Fix: フォローリクエストを作成する際に既存のものは削除するように
+- Fix: フォローリクエストを作成する際に既存のものは削除するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440)
- Fix: エンドポイント`notes/translate`のエラーを改善
- Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632)
- Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正
- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正
-- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように
+- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606)
- Fix: Add Cache-Control to Bull Board
- Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正
@@ -1467,10 +1503,10 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Note
- 依存関係の更新に伴い、Node.js 20.10.0が最小要件になりました
- 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします
-- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。
+- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。
- **影響:**
- それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。
+ **影響:**
+ それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。
投稿用のピン留め絵文字をアップデート前の状態にするには、以下の手順で操作します。
1. 「設定」メニューに移動し、「絵文字ピッカー」タブを選択します。
@@ -1517,7 +1553,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: Unicode 15.0のサポート
- Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように
- MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました
- - 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります
+ - 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります
(例: ` ```js ` → Javascript, ` ```ais ` → AiScript)
- Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように
- Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる
@@ -1924,9 +1960,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### General
- 招待機能を改善しました
- * 過去に発行した招待コードを確認できるようになりました
- * ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
- * 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
+ * 過去に発行した招待コードを確認できるようになりました
+ * ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
+ * 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
@@ -2089,9 +2125,9 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
* 「フォロワーのみ」の投稿は検索結果に表示されません。
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
- ユーザーへの自分用メモ機能
- * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
+ * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
(自分自身に対してもメモを追加できます。)
- * ユーザーメニューから追加できます。
+ * ユーザーメニューから追加できます。
(デスクトップ表示ではusernameの右側のボタンからも追加可能)
- チャンネルに色を設定できるようになりました。各ノートに設定した色のインジケーターが表示されます。
- チャンネルをアーカイブできるようになりました。
diff --git a/Dockerfile b/Dockerfile
index 19f9e8c9dca..d6c8d7e4150 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -102,7 +102,6 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
-COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/src-js ./packages/backend/src-js
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
COPY --chown=misskey:misskey . ./
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
index 6fe7f32cc44..b91e34dc124 100644
--- a/cypress/tsconfig.json
+++ b/cypress/tsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
- "lib": ["dom", "es5"],
- "target": "es5",
+ "lib": ["dom"],
+ "target": "esnext",
"types": ["cypress", "node"]
},
"include": ["./**/*.ts"]
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index f2867585c21..ae7f4a03f1a 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1073,8 +1073,8 @@ thisPostMayBeAnnoying: "Aquesta nota pot ser molesta per algú."
thisPostMayBeAnnoyingHome: "Publicar a la línia de temps d'Inici"
thisPostMayBeAnnoyingCancel: "Cancel·lar "
thisPostMayBeAnnoyingIgnore: "Publicar de totes maneres"
-collapseRenotes: "Col·lapsar les renotes que ja has vist"
-collapseRenotesDescription: "Col·lapse les notes a les quals ja has reaccionat o que ja has renotat"
+collapseRenotes: "Col·lapsar els impulsos que ja has vist"
+collapseRenotesDescription: "Col·lapse les notes a les quals ja has reaccionat o que ja has impulsat."
internalServerError: "Error intern del servidor"
internalServerErrorDescription: "El servidor ha fallat de manera inexplicable."
copyErrorInfo: "Copiar la informació de l'error "
@@ -1408,6 +1408,7 @@ frame: "Marc"
presets: "Predefinit"
zeroPadding: "Sense omplir"
nothingToConfigure: "No hi ha res a configurar"
+viewRenotedChannel: "Mirar el canal d'impulsos "
_imageEditing:
_vars:
caption: "Títol de l'arxiu"
@@ -1687,7 +1688,7 @@ _initialTutorial:
description: "Pots limitar qui pot veure les teves notes."
public: "La teva nota serà visible per a tots els usuaris."
home: "Publicar només a línia de temps d'Inici. La gent que visiti el teu perfil o mitjançant les remotes també la podran veure."
- followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer renotes."
+ followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer impulsos."
direct: "Només visible per a alguns seguidors, el destinatari rebre una notificació. Es pot fer servir com una alternativa als missatges directes."
doNotSendConfidencialOnDirect1: "Tingues cura quan enviïs informació sensible."
doNotSendConfidencialOnDirect2: "Els administradors del servidor poden veure tot el que escrius. Ves compte quan enviïs informació sensible en enviar notes directes a altres usuaris en servidors de poca confiança."
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 72b78921284..3c7852cd055 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -3312,7 +3312,7 @@ _clientPerformanceIssueTip:
_clip:
tip: "Clip es una función que permite organizar varias notas."
_userLists:
- tip: "Las listas pueden contener cualquier usuario que especifiques al crearlas, la lista creada puede mostrarse entonces como una línea de tiempo mostrando solo los usuarios especificados."
+ tip: "Puedes crear listas que incluyan a cualquier usuario. Las listas creadas se pueden visualizar en forma de cronología."
watermark: "Marca de Agua"
defaultPreset: "Por defecto"
_watermarkEditor:
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 2401bd84aa0..08c91972512 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -1,7 +1,7 @@
---
_lang_: "Italiano"
headlineMisskey: "Rete collegata tramite Note"
-introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n\n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n\n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n\n🚀 Esplora un nuovo mondo insieme a noi!"
+introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n🚀 Esplora un nuovo mondo insieme a noi!"
poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source Misskey."
monthAndDay: "{day}/{month}"
search: "Cerca"
@@ -1408,6 +1408,7 @@ frame: "Cornice"
presets: "Preimpostato"
zeroPadding: "Al vivo"
nothingToConfigure: "Niente da configurare"
+viewRenotedChannel: "Visualizza il canale del Rinota"
_imageEditing:
_vars:
caption: "Didascalia dell'immagine"
@@ -3338,7 +3339,7 @@ _watermarkEditor:
stripeWidth: "Larghezza della linea"
stripeFrequency: "Il numero di linee"
polkadot: "A pallini"
- checker: "revisore"
+ checker: "Scacchiera"
polkadotMainDotOpacity: "Opacità del punto principale"
polkadotMainDotRadius: "Dimensione del punto principale"
polkadotSubDotOpacity: "Opacità del punto secondario"
@@ -3367,7 +3368,7 @@ _imageEffector:
zoomLines: "Linea di saturazione"
stripe: "Strisce"
polkadot: "A pallini"
- checker: "revisore"
+ checker: "Scacchiera"
blockNoise: "Attenua rumore"
tearing: "Strappa immagine"
fill: "Riempimento"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 4af17dd39e7..93679aa24b6 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1408,6 +1408,7 @@ frame: "フレーム"
presets: "プリセット"
zeroPadding: "ゼロ埋め"
nothingToConfigure: "設定項目はありません"
+viewRenotedChannel: "リノート先のチャンネルを見る"
_imageEditing:
_vars:
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 52da6d071a0..294791cce31 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -1408,6 +1408,7 @@ frame: "프레임"
presets: "프리셋"
zeroPadding: "0으로 채우기"
nothingToConfigure: "설정 항목이 없습니다."
+viewRenotedChannel: "리노트된 채널 보기"
_imageEditing:
_vars:
caption: "파일 설명"
diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml
index 7017d817332..3779155ae16 100644
--- a/locales/lo-LA.yml
+++ b/locales/lo-LA.yml
@@ -5,6 +5,7 @@ introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນຊອ
poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. Misskey (ເອີ້ນວ່າ \"Misskey instance\")"
monthAndDay: "ເດືອນ{month} / ວັນ{day}"
search: "ຄົ້ນຫາ"
+reset: "ຣີເຊັດ"
notifications: "ການແຈ້ງເຕືອນ"
username: "ຊື່ຜູ້ໃຊ້"
password: "ລະຫັດຜ່ານ"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index 6bcff59979a..bd53d283006 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -3401,6 +3401,8 @@ _imageEffector:
threshold: "เทรชโฮลด์"
centerX: "กลาง X"
centerY: "กลาง Y"
+ density: "ความหนาทึบ"
+ zoomLinesOutlineThickness: "ความหนาของเงาเส้น"
zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง"
circle: "ทรงกลม"
drafts: "ร่าง"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 5cfa90e9100..cda2fc65317 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1408,6 +1408,7 @@ frame: "边框"
presets: "预设值"
zeroPadding: "填充 0"
nothingToConfigure: "没有项目"
+viewRenotedChannel: "查看转帖所属频道"
_imageEditing:
_vars:
caption: "文件标题"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index fa8a3eead85..c1347f54c0b 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -10,7 +10,7 @@ notifications: "通知"
username: "使用者名稱"
password: "密碼"
initialPasswordForSetup: "啟動初始設定的密碼"
-initialPasswordIsIncorrect: "啟動初始設定的密碼錯誤。"
+initialPasswordIsIncorrect: "啟動初始設定密碼錯誤。"
initialPasswordForSetupDescription: "如果您自己安裝了 Misskey,請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼,請將其留空並繼續。"
forgotPassword: "忘記密碼"
fetchingAsApObject: "從聯邦宇宙取得中..."
@@ -1408,6 +1408,7 @@ frame: "邊框"
presets: "預設值"
zeroPadding: "補零"
nothingToConfigure: "無可設定的項目"
+viewRenotedChannel: "顯示轉發貼文者的頻道"
_imageEditing:
_vars:
caption: "檔案標題"
diff --git a/package.json b/package.json
index aeff8522c27..9f5972a0b58 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"name": "misskey",
- "version": "2026.3.2",
+ "version": "2026.5.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
- "packageManager": "pnpm@10.32.1",
+ "packageManager": "pnpm@10.33.0",
"workspaces": [
"packages/misskey-js",
"packages/i18n",
@@ -28,9 +28,9 @@
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook",
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
- "start": "cd packages/backend && pnpm compile-config && node ./built/boot/entry.js",
- "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js",
- "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
+ "start": "cd packages/backend && pnpm compile-config && node ./built/entry.js",
+ "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/entry.js",
+ "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/entry.js",
"cli": "cd packages/backend && pnpm cli",
"init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm migrate",
@@ -44,8 +44,8 @@
"cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
- "jest": "cd packages/backend && pnpm jest",
- "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
+ "backend-unit-test": "cd packages/backend && pnpm test",
+ "backend-unit-test-and-coverage": "cd packages/backend && pnpm test-and-coverage",
"test": "pnpm -r test",
"test-and-coverage": "pnpm -r test-and-coverage",
"clean": "node scripts/clean.mjs",
@@ -53,30 +53,30 @@
"cleanall": "pnpm clean-all"
},
"dependencies": {
- "cssnano": "7.1.3",
- "esbuild": "0.27.4",
+ "cssnano": "7.1.5",
+ "esbuild": "0.28.0",
"execa": "9.6.1",
"ignore-walk": "8.0.0",
"js-yaml": "4.1.1",
- "postcss": "8.5.8",
- "tar": "7.5.11",
- "terser": "5.46.0"
+ "postcss": "8.5.9",
+ "tar": "7.5.13",
+ "terser": "5.46.1"
},
"devDependencies": {
"@eslint/js": "9.39.4",
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9",
- "@types/node": "24.12.0",
- "@typescript-eslint/eslint-plugin": "8.57.0",
- "@typescript-eslint/parser": "8.57.0",
- "@typescript/native-preview": "7.0.0-dev.20260116.1",
+ "@types/node": "24.12.2",
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@typescript/native-preview": "7.0.0-dev.20260421.2",
"cross-env": "10.1.0",
- "cypress": "15.11.0",
+ "cypress": "15.13.1",
"eslint": "9.39.4",
- "globals": "17.4.0",
+ "globals": "17.5.0",
"ncp": "2.0.0",
- "pnpm": "10.32.1",
- "start-server-and-test": "2.1.5",
+ "pnpm": "10.33.0",
+ "start-server-and-test": "3.0.2",
"typescript": "5.9.3"
},
"optionalDependencies": {
@@ -86,7 +86,7 @@
"overrides": {
"@aiscript-dev/aiscript-languageserver": "-",
"chokidar": "5.0.0",
- "lodash": "4.17.23"
+ "lodash": "4.18.1"
},
"ignoredBuiltDependencies": [
"@sentry-internal/node-cpu-profiler",
diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc
deleted file mode 100644
index 7e1767a67ab..00000000000
--- a/packages/backend/.swcrc
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "$schema": "https://swc.rs/schema.json",
- "jsc": {
- "parser": {
- "syntax": "typescript",
- "jsx": true,
- "dynamicImport": true,
- "decorators": true
- },
- "transform": {
- "legacyDecorator": true,
- "decoratorMetadata": true,
- "react": {
- "runtime": "automatic",
- "importSource": "@kitajs/html"
- }
- },
- "experimental": {
- "keepImportAssertions": true
- },
- "baseUrl": "src",
- "paths": {
- "@/*": ["*"]
- },
- "target": "es2022"
- },
- "minify": false,
- "sourceMaps": "inline"
-}
diff --git a/packages/backend/assets/api-doc.html b/packages/backend/assets/api-doc.html
deleted file mode 100644
index 19e0349d47d..00000000000
--- a/packages/backend/assets/api-doc.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
- Misskey API
-
-
-
-
-
-
-
-
-
diff --git a/packages/backend/assets/api-doc.png b/packages/backend/assets/api-doc.png
deleted file mode 100644
index 9b07f1f3980..00000000000
Binary files a/packages/backend/assets/api-doc.png and /dev/null differ
diff --git a/packages/backend/assets/robots.txt b/packages/backend/assets/robots.txt
deleted file mode 100644
index dc17e04e3f3..00000000000
--- a/packages/backend/assets/robots.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-user-agent: *
-allow: /
-
-# todo: sitemap
diff --git a/packages/backend/build.js b/packages/backend/build.js
deleted file mode 100644
index 52ca09b7a84..00000000000
--- a/packages/backend/build.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import fs from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname, join } from 'node:path';
-import { build } from 'esbuild';
-import { swcPlugin } from 'esbuild-plugin-swc';
-
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
-
-const resolveTsPathsPlugin = {
- name: 'resolve-ts-paths',
- setup(build) {
- build.onResolve({ filter: /^\.{1,2}\/.*\.js$/ }, (args) => {
- if (args.importer) {
- const absPath = join(args.resolveDir, args.path);
- const tsPath = absPath.slice(0, -3) + '.ts';
- if (fs.existsSync(tsPath)) return { path: tsPath };
- const tsxPath = absPath.slice(0, -3) + '.tsx';
- if (fs.existsSync(tsxPath)) return { path: tsxPath };
- }
- });
- },
-};
-
-const externalIpaddrPlugin = {
- name: 'external-ipaddr',
- setup(build) {
- build.onResolve({ filter: /^ipaddr\.js$/ }, (args) => {
- return { path: args.path, external: true };
- });
- },
-};
-
-/** @type {import('esbuild').BuildOptions} */
-const options = {
- entryPoints: ['./src/boot/entry.ts'],
- minify: true,
- keepNames: true,
- bundle: true,
- outdir: './built/boot',
- target: 'node22',
- platform: 'node',
- format: 'esm',
- sourcemap: 'linked',
- packages: 'external',
- banner: {
- js: 'import { createRequire as topLevelCreateRequire } from "module";' +
- 'import ___url___ from "url";' +
- 'const require = topLevelCreateRequire(import.meta.url);' +
- 'const __filename = ___url___.fileURLToPath(import.meta.url);' +
- 'const __dirname = ___url___.fileURLToPath(new URL(".", import.meta.url));',
- },
- plugins: [
- externalIpaddrPlugin,
- resolveTsPathsPlugin,
- swcPlugin({
- jsc: {
- parser: {
- syntax: 'typescript',
- decorators: true,
- dynamicImport: true,
- },
- transform: {
- legacyDecorator: true,
- decoratorMetadata: true,
- },
- experimental: {
- keepImportAssertions: true,
- },
- baseUrl: join(_dirname, 'src'),
- paths: {
- '@/*': ['*'],
- },
- target: 'esnext',
- keepClassNames: true,
- },
- }),
- externalIpaddrPlugin,
- ],
- // external: [
- // 'slacc-*',
- // 'class-transformer',
- // 'class-validator',
- // '@sentry/*',
- // '@nestjs/websockets/socket-module',
- // '@nestjs/microservices/microservices-module',
- // '@nestjs/microservices',
- // '@napi-rs/canvas-win32-x64-msvc',
- // 'mock-aws-s3',
- // 'aws-sdk',
- // 'nock',
- // 'sharp',
- // 'jsdom',
- // 're2',
- // '@napi-rs/canvas',
- // ],
-};
-
-const args = process.argv.slice(2).map(arg => arg.toLowerCase());
-
-if (!args.includes('--no-clean')) {
- fs.rmSync('./built', { recursive: true, force: true });
-}
-
-await buildSrc();
-
-async function buildSrc() {
- console.log(`[${_package.name}] start building...`);
-
- await build(options)
- .then(() => {
- console.log(`[${_package.name}] build succeeded.`);
- })
- .catch((err) => {
- process.stderr.write(err.stderr || err.message || err);
- process.exit(1);
- });
-
- console.log(`[${_package.name}] finish building.`);
-}
diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs
deleted file mode 100644
index 22ffbbee5cf..00000000000
--- a/packages/backend/jest.config.cjs
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
-* For a detailed explanation regarding each configuration property and type check, visit:
-* https://jestjs.io/docs/en/configuration.html
-*/
-
-module.exports = {
- // All imported modules in your tests should be mocked automatically
- // automock: false,
-
- // Stop running tests after `n` failures
- // bail: 0,
-
- // The directory where Jest should store its cached dependency information
- // cacheDirectory: "C:\\Users\\ai\\AppData\\Local\\Temp\\jest",
-
- // Automatically clear mock calls and instances between every test
- // clearMocks: false,
-
- // Indicates whether the coverage information should be collected while executing the test
- // collectCoverage: false,
-
- // An array of glob patterns indicating a set of files for which coverage information should be collected
- collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'],
-
- // The directory where Jest should output its coverage files
- coverageDirectory: "coverage",
-
- // An array of regexp pattern strings used to skip coverage collection
- // coveragePathIgnorePatterns: [
- // "\\\\node_modules\\\\"
- // ],
-
- // Indicates which provider should be used to instrument code for coverage
- coverageProvider: "v8",
-
- // A list of reporter names that Jest uses when writing coverage reports
- // coverageReporters: [
- // "json",
- // "text",
- // "lcov",
- // "clover"
- // ],
-
- // An object that configures minimum threshold enforcement for coverage results
- // coverageThreshold: undefined,
-
- // A path to a custom dependency extractor
- // dependencyExtractor: undefined,
-
- // Make calling deprecated APIs throw helpful error messages
- // errorOnDeprecated: false,
-
- // Force coverage collection from ignored files using an array of glob patterns
- // forceCoverageMatch: [],
-
- // A path to a module which exports an async function that is triggered once before all test suites
- // globalSetup: undefined,
-
- // A path to a module which exports an async function that is triggered once after all test suites
- // globalTeardown: undefined,
-
- // A set of global variables that need to be available in all test environments
- globals: {
- },
-
- // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
- // maxWorkers: "50%",
-
- // An array of directory names to be searched recursively up from the requiring module's location
- // moduleDirectories: [
- // "node_modules"
- // ],
-
- // An array of file extensions your modules use
- // moduleFileExtensions: [
- // "js",
- // "json",
- // "jsx",
- // "ts",
- // "tsx",
- // "node"
- // ],
-
- // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
- moduleNameMapper: {
- // Do not resolve .wasm.js to .wasm by the rule below
- '^(.+)\\.wasm\\.js$': '$1.wasm.js',
- // SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule
- // converts it again to `../../src/foo/bar` which then can be resolved to
- // `.ts` files.
- // See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
- // TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
- // directly import `.ts` files without this hack.
- '^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
- },
-
- // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
- // modulePathIgnorePatterns: [],
-
- // Activates notifications for test results
- // notify: false,
-
- // An enum that specifies notification mode. Requires { notify: true }
- // notifyMode: "failure-change",
-
- // A preset that is used as a base for Jest's configuration
- //preset: "ts-jest/presets/js-with-ts-esm",
-
- // Run tests from one or more projects
- // projects: undefined,
-
- // Use this configuration option to add custom reporters to Jest
- // reporters: undefined,
-
- // Automatically reset mock state between every test
- // resetMocks: false,
-
- // Reset the module registry before running each individual test
- // resetModules: false,
-
- // A path to a custom resolver
- // resolver: './jest-resolver.cjs',
-
- // Automatically restore mock state between every test
- restoreMocks: true,
-
- // The root directory that Jest should scan for tests and modules within
- // rootDir: undefined,
-
- // A list of paths to directories that Jest should use to search for files in
- roots: [
- ""
- ],
-
- // Allows you to use a custom runner instead of Jest's default test runner
- // runner: "jest-runner",
-
- // The paths to modules that run some code to configure or set up the testing environment before each test
- // setupFiles: [],
-
- // A list of paths to modules that run some code to configure or set up the testing framework before each test
- // setupFilesAfterEnv: [],
-
- // The number of seconds after which a test is considered as slow and reported as such in the results.
- // slowTestThreshold: 5,
-
- // A list of paths to snapshot serializer modules Jest should use for snapshot testing
- // snapshotSerializers: [],
-
- // The test environment that will be used for testing
- testEnvironment: "node",
-
- // Options that will be passed to the testEnvironment
- // testEnvironmentOptions: {},
-
- // Adds a location field to test results
- // testLocationInResults: false,
-
- // The glob patterns Jest uses to detect test files
- testMatch: [
- "/test/unit/**/*.ts",
- "/src/**/*.test.ts",
- ],
-
- // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
- // testPathIgnorePatterns: [
- // "\\\\node_modules\\\\"
- // ],
-
- // The regexp pattern or array of patterns that Jest uses to detect test files
- // testRegex: [],
-
- // This option allows the use of a custom results processor
- // testResultsProcessor: undefined,
-
- // This option allows use of a custom test runner
- // testRunner: "jasmine2",
-
- // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
- // testURL: "http://localhost",
-
- // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
- // timers: "real",
-
- // A map from regular expressions to paths to transformers
- transform: {
- "^.+\\.(t|j)sx?$": ["@swc/jest"],
- },
-
- // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
- // transformIgnorePatterns: [
- // "\\\\node_modules\\\\",
- // "\\.pnp\\.[^\\\\]+$"
- // ],
-
- // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
- // unmockedModulePathPatterns: undefined,
-
- // Indicates whether each individual test should be reported during the run
- // verbose: undefined,
-
- // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
- // watchPathIgnorePatterns: [],
-
- // Whether to use watchman for file crawling
- // watchman: true,
-
- extensionsToTreatAsEsm: ['.ts', '.tsx'],
-
- testTimeout: 60000,
-
- // Let Jest kill the test worker whenever it grows too much
- // (It seems there's a known memory leak issue in Node.js' vm.Script used by Jest)
- // https://github.com/facebook/jest/issues/11956
- maxWorkers: 1, // Make it use worker (that can be killed and restarted)
- logHeapUsage: true, // To debug when out-of-memory happens on CI
- workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB)
-
- maxConcurrency: 32,
-};
diff --git a/packages/backend/jest.config.e2e.cjs b/packages/backend/jest.config.e2e.cjs
deleted file mode 100644
index 4502da47df6..00000000000
--- a/packages/backend/jest.config.e2e.cjs
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-* For a detailed explanation regarding each configuration property and type check, visit:
-* https://jestjs.io/docs/en/configuration.html
-*/
-
-const base = require('./jest.config.cjs')
-
-module.exports = {
- ...base,
- globalSetup: "/built-test/entry.js",
- setupFilesAfterEnv: ["/test/jest.setup.ts"],
- testMatch: [
- "/test/e2e/**/*.ts",
- ],
-};
diff --git a/packages/backend/jest.config.fed.cjs b/packages/backend/jest.config.fed.cjs
deleted file mode 100644
index fae187bc23f..00000000000
--- a/packages/backend/jest.config.fed.cjs
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * For a detailed explanation regarding each configuration property and type check, visit:
- * https://jestjs.io/docs/en/configuration.html
- */
-
-const base = require('./jest.config.cjs');
-
-module.exports = {
- ...base,
- testMatch: [
- '/test-federation/test/**/*.test.ts',
- ],
-};
diff --git a/packages/backend/jest.config.unit.cjs b/packages/backend/jest.config.unit.cjs
deleted file mode 100644
index 957d0635c10..00000000000
--- a/packages/backend/jest.config.unit.cjs
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-* For a detailed explanation regarding each configuration property and type check, visit:
-* https://jestjs.io/docs/en/configuration.html
-*/
-
-const base = require('./jest.config.cjs')
-
-module.exports = {
- ...base,
- globalSetup: "/test/jest.setup.unit.cjs",
- testMatch: [
- "/test/unit/**/*.ts",
- "/src/**/*.test.ts",
- ],
-};
diff --git a/packages/backend/jest.js b/packages/backend/jest.js
deleted file mode 100644
index 61f6b00e854..00000000000
--- a/packages/backend/jest.js
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env node
-import child_process from 'node:child_process';
-import path from 'node:path';
-import url from 'node:url';
-
-import semver from 'semver';
-
-const __filename = url.fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-
-const args = [];
-args.push(...[
- ...semver.satisfies(process.version, '^20.17.0 || ^22.0.0 || ^24.10.0') ? ['--no-experimental-require-module'] : [],
- '--experimental-vm-modules',
- '--experimental-import-meta-resolve',
- path.join(__dirname, 'node_modules/jest/bin/jest.js'),
- ...process.argv.slice(2),
-]);
-
-const child = child_process.spawn(process.execPath, args, { stdio: 'inherit' });
-child.on('error', (err) => {
- console.error('Failed to start Jest:', err);
- process.exit(1);
-});
-child.on('exit', (code, signal) => {
- if (code === null) {
- process.exit(128 + signal);
- } else {
- process.exit(code);
- }
-});
diff --git a/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js
new file mode 100644
index 00000000000..a3410aa88e6
--- /dev/null
+++ b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AddCategoryToAvatarDecorations1766652173085 {
+ name = 'AddCategoryToAvatarDecorations1766652173085';
+
+ /**
+ * @param {QueryRunner} queryRunner
+ */
+ async up(queryRunner) {
+ await queryRunner.query('ALTER TABLE "avatar_decoration" ADD "category" character varying(128)');
+ }
+
+ /**
+ * @param {QueryRunner} queryRunner
+ */
+ async down(queryRunner) {
+ await queryRunner.query('ALTER TABLE "avatar_decoration" DROP COLUMN "category"');
+ }
+};
diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js
index 1a8c1464514..dabc0893f40 100644
--- a/packages/backend/ormconfig.js
+++ b/packages/backend/ormconfig.js
@@ -1,6 +1,6 @@
import { DataSource } from 'typeorm';
-import { loadConfig } from './src-js/config.js';
-import { entities } from './src-js/postgres.js';
+import { loadConfig } from './built/config.js';
+import { entities } from './built/postgres.js';
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 921e89eff97..3908243ba93 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -7,51 +7,33 @@
"node": "^22.15.0 || ^24.10.0"
},
"scripts": {
- "start": "pnpm compile-config && node ./built/boot/entry.js",
- "start:inspect": "pnpm compile-config && node --inspect ./built/boot/entry.js",
- "start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
+ "start": "pnpm compile-config && node ./built/entry.js",
+ "start:inspect": "pnpm compile-config && node --inspect ./built/entry.js",
+ "start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/entry.js",
"migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js",
- "cli": "pnpm compile-config && node ./src-js/boot/cli.js",
+ "cli": "pnpm compile-config && node ./built/cli.js",
"check:connect": "pnpm compile-config && node ./scripts/check_connect.js",
"compile-config": "node ./scripts/compile_config.js",
- "build": "swc src -d src-js -D --strip-leading-paths && node ./build.js",
- "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
- "watch:swc": "swc src -d built -D -w --strip-leading-paths",
+ "build": "rolldown -c",
+ "build:unit": "rolldown -c --sourcemap",
+ "build:e2e": "rolldown -c --e2e",
"build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "pnpm compile-config && node ./scripts/watch.mjs",
"restart": "pnpm build && pnpm start",
- "dev": "pnpm compile-config && node ./scripts/dev.mjs",
+ "dev": "pnpm compile-config && rolldown -c --watch",
"typecheck": "tsgo --noEmit && tsgo -p test --noEmit && tsgo -p test-federation --noEmit",
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
- "jest": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
- "jest:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
- "jest:fed": "pnpm compile-config && node ./jest.js --forceExit --config jest.config.fed.cjs",
- "jest-and-coverage": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
- "jest-and-coverage:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
- "jest-clear": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --clearCache",
- "test": "pnpm jest",
- "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
- "test:fed": "pnpm jest:fed",
- "test-and-coverage": "pnpm jest-and-coverage",
- "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
+ "test": "pnpm build:unit && cross-env NODE_ENV=test pnpm compile-config && vitest --config vitest.config.unit.ts",
+ "test:e2e": "pnpm build:e2e && cross-env NODE_ENV=test pnpm compile-config && vitest --config vitest.config.e2e.ts",
+ "test:fed": "cross-env NODE_ENV=test pnpm compile-config && vitest --config vitest.config.fed.ts",
+ "test-and-coverage": "pnpm build:unit && cross-env NODE_ENV=test pnpm compile-config && vitest --coverage --config vitest.config.unit.ts",
+ "test-and-coverage:e2e": "pnpm build:e2e && cross-env NODE_ENV=test pnpm compile-config && vitest --coverage --config vitest.config.e2e.ts",
"check-migrations": "node scripts/check_migrations_clean.js",
"generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js"
},
"optionalDependencies": {
- "@swc/core-android-arm64": "1.3.11",
- "@swc/core-darwin-arm64": "1.15.18",
- "@swc/core-darwin-x64": "1.15.18",
- "@swc/core-freebsd-x64": "1.3.11",
- "@swc/core-linux-arm-gnueabihf": "1.15.18",
- "@swc/core-linux-arm64-gnu": "1.15.18",
- "@swc/core-linux-arm64-musl": "1.15.18",
- "@swc/core-linux-x64-gnu": "1.15.18",
- "@swc/core-linux-x64-musl": "1.15.18",
- "@swc/core-win32-arm64-msvc": "1.15.18",
- "@swc/core-win32-ia32-msvc": "1.15.18",
- "@swc/core-win32-x64-msvc": "1.15.18",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.1.0",
@@ -71,30 +53,29 @@
"utf-8-validate": "6.0.6"
},
"dependencies": {
- "@aws-sdk/client-s3": "3.1008.0",
- "@aws-sdk/lib-storage": "3.1008.0",
+ "@aws-sdk/client-s3": "3.1030.0",
+ "@aws-sdk/lib-storage": "3.1030.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0",
- "@fastify/express": "4.0.4",
- "@fastify/http-proxy": "11.4.1",
- "@fastify/multipart": "9.4.0",
- "@fastify/static": "9.0.0",
+ "@fastify/express": "4.0.5",
+ "@fastify/http-proxy": "11.4.4",
+ "@fastify/multipart": "10.0.0",
+ "@fastify/static": "9.1.3",
"@kitajs/html": "4.2.13",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.5",
- "@napi-rs/canvas": "0.1.96",
- "@nestjs/common": "11.1.16",
- "@nestjs/core": "11.1.16",
- "@nestjs/testing": "11.1.16",
+ "@napi-rs/canvas": "0.1.97",
+ "@nestjs/common": "11.1.19",
+ "@nestjs/core": "11.1.19",
+ "@nestjs/testing": "11.1.19",
+ "@oxc-project/runtime": "0.125.0",
"@peertube/http-signature": "1.7.0",
- "@sentry/node": "10.43.0",
- "@sentry/profiling-node": "10.43.0",
+ "@sentry/node": "10.48.0",
+ "@sentry/profiling-node": "10.48.0",
"@simplewebauthn/server": "13.3.0",
- "@sinonjs/fake-timers": "15.1.1",
- "@smithy/node-http-handler": "4.4.16",
- "@swc/cli": "0.8.0",
- "@swc/core": "1.15.18",
+ "@sinonjs/fake-timers": "15.3.2",
+ "@smithy/node-http-handler": "4.5.2",
"@twemoji/parser": "16.0.0",
"accepts": "1.3.8",
"ajv": "8.18.0",
@@ -103,44 +84,44 @@
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"body-parser": "2.2.2",
- "bullmq": "5.71.0",
+ "bullmq": "5.73.5",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
"chokidar": "5.0.0",
"color-convert": "3.1.3",
- "content-disposition": "1.0.1",
+ "content-disposition": "1.1.0",
"date-fns": "4.1.0",
"deep-email-validator": "0.1.21",
- "fastify": "5.8.2",
+ "fastify": "5.8.5",
"fastify-raw-body": "5.0.0",
"feed": "5.2.0",
- "file-type": "21.3.2",
+ "file-type": "22.0.1",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.5",
"got": "14.6.6",
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
- "ioredis": "5.10.0",
+ "ioredis": "5.10.1",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.3.0",
"is-svg": "6.1.0",
"json5": "2.2.3",
"jsonld": "9.0.0",
"juice": "11.1.1",
- "meilisearch": "0.55.0",
+ "meilisearch": "0.57.0",
"mfm-js": "0.25.0",
"mime-types": "3.0.2",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828",
- "nanoid": "5.1.6",
+ "nanoid": "5.1.7",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-html-parser": "7.1.0",
- "nodemailer": "8.0.2",
- "nsfwjs": "4.2.0",
+ "nodemailer": "8.0.5",
+ "nsfwjs": "4.3.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
@@ -152,19 +133,19 @@
"qrcode": "1.5.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
- "re2": "1.23.3",
+ "re2": "1.24.0",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.2",
- "sanitize-html": "2.17.1",
+ "sanitize-html": "2.17.3",
"secure-json-parse": "4.1.0",
"semver": "7.7.4",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
- "systeminformation": "5.31.4",
+ "systeminformation": "5.31.5",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
@@ -172,32 +153,31 @@
"ulid": "3.0.2",
"vary": "1.1.2",
"web-push": "3.6.7",
- "ws": "8.19.0",
+ "ws": "8.20.0",
"xev": "3.0.2"
},
"devDependencies": {
- "@jest/globals": "29.7.0",
"@kitajs/ts-html-plugin": "4.1.4",
- "@nestjs/platform-express": "11.1.16",
- "@sentry/vue": "10.43.0",
+ "@nestjs/platform-express": "11.1.19",
+ "@rollup/plugin-esm-shim": "0.1.8",
+ "@sentry/vue": "10.48.0",
"@simplewebauthn/types": "12.0.0",
- "@swc/jest": "0.2.39",
"@types/accepts": "1.3.7",
"@types/archiver": "7.0.0",
"@types/body-parser": "1.19.6",
- "@types/color-convert": "2.0.4",
+ "@types/color-convert": "3.0.1",
"@types/content-disposition": "0.5.9",
"@types/fluent-ffmpeg": "2.1.28",
"@types/http-link-header": "1.0.7",
- "@types/jest": "29.5.14",
+ "@types/js-yaml": "4.0.9",
"@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
- "@types/node": "24.12.0",
- "@types/nodemailer": "7.0.11",
+ "@types/node": "24.12.2",
+ "@types/nodemailer": "8.0.0",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
- "@types/pg": "8.18.0",
+ "@types/pg": "8.20.0",
"@types/qrcode": "1.5.6",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
@@ -206,28 +186,28 @@
"@types/semver": "7.7.1",
"@types/simple-oauth2": "5.0.8",
"@types/sinonjs__fake-timers": "15.0.1",
- "@types/supertest": "6.0.3",
+ "@types/supertest": "7.2.0",
"@types/tinycolor2": "1.4.6",
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
- "@typescript-eslint/eslint-plugin": "8.57.0",
- "@typescript-eslint/parser": "8.57.0",
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@vitest/coverage-v8": "4.1.4",
"aws-sdk-client-mock": "4.1.0",
"cbor": "10.0.12",
"cross-env": "10.1.0",
- "esbuild-plugin-swc": "1.0.1",
"eslint-plugin-import": "2.32.0",
"execa": "9.6.1",
"fkill": "10.0.3",
- "jest": "29.7.0",
- "jest-mock": "29.7.0",
"js-yaml": "4.1.1",
- "nodemon": "3.1.14",
- "pid-port": "2.0.1",
+ "pid-port": "2.1.1",
+ "rolldown": "1.0.0-rc.15",
"simple-oauth2": "5.1.0",
"supertest": "7.2.2",
- "vite": "7.3.1"
+ "vite": "8.0.8",
+ "vitest": "4.1.4",
+ "vitest-mock-extended": "4.0.0"
}
}
diff --git a/packages/backend/rolldown.config.ts b/packages/backend/rolldown.config.ts
new file mode 100644
index 00000000000..950bc635607
--- /dev/null
+++ b/packages/backend/rolldown.config.ts
@@ -0,0 +1,128 @@
+import { defineConfig } from 'rolldown';
+import type { Plugin, ExternalOption } from 'rolldown';
+import { execa, execaNode } from 'execa';
+import type { ResultPromise } from 'execa';
+import esmShim from '@rollup/plugin-esm-shim';
+
+/**
+ * Watchモード時にバックエンドの起動・停止制御を行うプラグイン
+ */
+function backendDevServerPlugin(): Plugin {
+ let backendProcess: ResultPromise | null = null;
+
+ async function runBuildAssets() {
+ await execa('pnpm', ['run', 'build-assets'], {
+ cwd: '../../',
+ stdout: process.stdout,
+ stderr: process.stderr,
+ });
+ }
+
+ async function killBackendProcess() {
+ if (backendProcess) {
+ backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
+ backendProcess.kill();
+ await new Promise(resolve => backendProcess!.on('exit', resolve));
+ backendProcess = null;
+ }
+ }
+
+ return {
+ name: 'backend-dev-server',
+ async closeBundle() {
+ await runBuildAssets();
+ if (backendProcess) {
+ await killBackendProcess();
+ }
+ backendProcess = execaNode('./built/entry.js', [], {
+ stdout: process.stdout,
+ stderr: process.stderr,
+ env: {
+ NODE_ENV: 'development',
+ },
+ });
+ },
+ async watchChange() {
+ if (backendProcess) {
+ await killBackendProcess();
+ await runBuildAssets();
+ }
+ },
+ };
+}
+
+export default defineConfig((args) => {
+ const isWatchMode = args.watch != null && args.watch !== 'false';
+ const isE2E = args.e2e != null && args.e2e !== 'false';
+
+ // 通常のビルド時にexternalとするモジュール
+ const externalModules: ExternalOption = [
+ /^slacc-.*/,
+ 'class-transformer',
+ 'class-validator',
+ /^@sentry\/.*/,
+ /^@sentry-internal\/.*/,
+ '@nestjs/websockets/socket-module',
+ '@nestjs/microservices/microservices-module',
+ '@nestjs/microservices',
+ /^@napi-rs\/.*/,
+ 'mock-aws-s3',
+ 'aws-sdk',
+ 'nock',
+ 'sharp',
+ 'jsdom',
+ 're2',
+ 'ipaddr.js',
+ 'oauth2orize',
+ 'file-type',
+ ];
+
+ if (isE2E) {
+ return {
+ input: './test-server/entry.ts',
+ platform: 'node',
+ tsconfig: './test-server/tsconfig.json',
+ plugins: [
+ esmShim(),
+ ],
+ output: {
+ keepNames: true,
+ sourcemap: true,
+ dir: './built-test',
+ cleanDir: true,
+ format: 'esm',
+ },
+ external: externalModules,
+ };
+ } else {
+ return {
+ input: [
+ './src/boot/entry.ts',
+ './src/boot/cli.ts',
+ './src/config.ts',
+ './src/postgres.ts',
+ './src/server/api/openapi/gen-spec.ts',
+ ],
+ platform: 'node',
+ tsconfig: true,
+ plugins: [
+ esmShim(),
+ (isWatchMode ? backendDevServerPlugin() : undefined),
+ ],
+ output: {
+ keepNames: true,
+ minify: !isWatchMode,
+ sourcemap: isWatchMode,
+ dir: './built',
+ cleanDir: !isWatchMode,
+ format: 'esm',
+ },
+ watch: {
+ include: ['src/**/*.{ts,js,mjs,cjs,tsx,json}'],
+ clearScreen: false,
+ },
+ // ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする
+ external: isWatchMode ? /^(?!@\/)[^.\/](?!:[\/\\])/ : externalModules,
+ };
+ }
+});
diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js
index a1cb839303f..9e2f214e93a 100644
--- a/packages/backend/scripts/check_connect.js
+++ b/packages/backend/scripts/check_connect.js
@@ -4,8 +4,8 @@
*/
import Redis from 'ioredis';
-import { loadConfig } from '../src-js/config.js';
-import { createPostgresDataSource } from '../src-js/postgres.js';
+import { loadConfig } from '../built/config.js';
+import { createPostgresDataSource } from '../built/postgres.js';
const config = loadConfig();
diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs
deleted file mode 100644
index db96eaf9765..00000000000
--- a/packages/backend/scripts/dev.mjs
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { execa, execaNode } from 'execa';
-
-/** @type {import('execa').ExecaChildProcess | undefined} */
-let backendProcess;
-
-async function execBuildAssets() {
- await execa('pnpm', ['run', 'build-assets'], {
- cwd: '../../',
- stdout: process.stdout,
- stderr: process.stderr,
- })
-}
-
-function execStart() {
- // pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので
- // 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい
- backendProcess = execaNode('./built/boot/entry.js', [], {
- stdout: process.stdout,
- stderr: process.stderr,
- env: {
- 'NODE_ENV': 'development',
- },
- });
-}
-
-async function killProc() {
- if (backendProcess) {
- backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
- backendProcess.kill();
- await new Promise(resolve => backendProcess.on('exit', resolve));
- backendProcess = undefined;
- }
-}
-
-(async () => {
- execaNode(
- './node_modules/nodemon/bin/nodemon.js',
- [
- '-w', 'src',
- '-e', 'ts,js,mjs,cjs,tsx,json,pug',
- '--exec', 'pnpm', 'run', 'build',
- ],
- {
- stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
- serialization: "json",
- })
- .on('message', async (message) => {
- if (message.type === 'exit') {
- // かならずbuild->build-assetsの順番で呼び出したいので、
- // 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。
- // pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある
-
- await killProc();
- await execBuildAssets();
- execStart();
- }
- })
-})();
diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js
index 237f63a4d30..8a7e0b062de 100644
--- a/packages/backend/scripts/generate_api_json.js
+++ b/packages/backend/scripts/generate_api_json.js
@@ -19,10 +19,10 @@ async function main() {
}
/** @type {import('../src/config.js')} */
- const { loadConfig } = await import('../src-js/config.js');
+ const { loadConfig } = await import('../built/config.js');
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
- const { genOpenapiSpec } = await import('../src-js/server/api/openapi/gen-spec.js');
+ const { genOpenapiSpec } = await import('../built/gen-spec.js');
const config = loadConfig();
const spec = genOpenapiSpec(config, true);
diff --git a/packages/backend/scripts/measure-memory.mjs b/packages/backend/scripts/measure-memory.mjs
index 3f30e24fb46..7c058a131d5 100644
--- a/packages/backend/scripts/measure-memory.mjs
+++ b/packages/backend/scripts/measure-memory.mjs
@@ -55,7 +55,7 @@ async function getMemoryUsage(pid) {
async function measureMemory() {
// Start the Misskey backend server using fork to enable IPC
- const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], {
+ const serverProcess = fork(join(__dirname, '../built/entry.js'), ['expose-gc'], {
cwd: join(__dirname, '..'),
env: {
...process.env,
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 435bd8dd45e..adccb4dc3ee 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -6,7 +6,7 @@
import { Global, Inject, Module } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
-import { MeiliSearch } from 'meilisearch';
+import { Meilisearch } from 'meilisearch';
import { MiMeta } from '@/models/Meta.js';
import { DI } from './di-symbols.js';
import { Config, loadConfig } from './config.js';
@@ -40,10 +40,10 @@ const $meilisearch: Provider = {
useFactory: (config: Config) => {
if (config.fulltextSearch?.provider === 'meilisearch') {
if (!config.meilisearch) {
- throw new Error('MeiliSearch is enabled but no configuration is provided');
+ throw new Error('Meilisearch is enabled but no configuration is provided');
}
- return new MeiliSearch({
+ return new Meilisearch({
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
apiKey: config.meilisearch.apiKey,
});
diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts
index 268c07582d9..25cc7c6797e 100644
--- a/packages/backend/src/boot/common.ts
+++ b/packages/backend/src/boot/common.ts
@@ -4,16 +4,12 @@
*/
import { NestFactory } from '@nestjs/core';
-import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
-import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
import { NestLogger } from '@/NestLogger.js';
-import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
-import { QueueStatsService } from '@/daemons/QueueStatsService.js';
-import { ServerStatsService } from '@/daemons/ServerStatsService.js';
-import { ServerService } from '@/server/ServerService.js';
-import { MainModule } from '@/MainModule.js';
export async function server() {
+ const { MainModule } = await import('../MainModule.js');
+ const { ServerService } = await import('../server/ServerService.js');
+
const app = await NestFactory.createApplicationContext(MainModule, {
logger: new NestLogger(),
});
@@ -22,6 +18,10 @@ export async function server() {
await serverService.launch();
if (process.env.NODE_ENV !== 'test') {
+ const { ChartManagementService } = await import('../core/chart/ChartManagementService.js');
+ const { QueueStatsService } = await import('../daemons/QueueStatsService.js');
+ const { ServerStatsService } = await import('../daemons/ServerStatsService.js');
+
app.get(ChartManagementService).start();
app.get(QueueStatsService).start();
app.get(ServerStatsService).start();
@@ -31,6 +31,10 @@ export async function server() {
}
export async function jobQueue() {
+ const { QueueProcessorModule } = await import('../queue/QueueProcessorModule.js');
+ const { QueueProcessorService } = await import('../queue/QueueProcessorService.js');
+ const { ChartManagementService } = await import('../core/chart/ChartManagementService.js');
+
const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
logger: new NestLogger(),
});
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index 3a33d198a54..6e37bf9e1cf 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -13,8 +13,6 @@ import chalk from 'chalk';
import Xev from 'xev';
import Logger from '@/logger.js';
import { envOption } from '../env.js';
-import { masterMain } from './master.js';
-import { workerMain } from './worker.js';
import { readyRef } from './ready.js';
import 'reflect-metadata';
@@ -71,10 +69,12 @@ process.on('exit', code => {
if (!envOption.disableClustering) {
if (cluster.isPrimary) {
logger.info(`Start main process... pid: ${process.pid}`);
+ const { masterMain } = await import('./master.js');
await masterMain();
ev.mount();
} else if (cluster.isWorker) {
logger.info(`Start worker process... pid: ${process.pid}`);
+ const { workerMain } = await import('./worker.js');
await workerMain();
} else {
throw new Error('Unknown process type');
@@ -82,6 +82,7 @@ if (!envOption.disableClustering) {
} else {
// 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない)
logger.info(`Start main process... pid: ${process.pid}`);
+ const { masterMain } = await import('./master.js');
await masterMain();
ev.mount();
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 6a83359d383..d2b11ef9f43 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -190,6 +190,7 @@ export type Config = {
userAgent: string;
frontendManifestExists: boolean;
frontendEmbedManifestExists: boolean;
+ rootDir: string;
mediaProxy: string;
externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null;
@@ -330,6 +331,7 @@ export function loadConfig(): Config {
userAgent: `Misskey/${version} (${config.url})`,
frontendManifestExists: frontendManifestExists,
frontendEmbedManifestExists: frontendEmbedManifestExists,
+ rootDir,
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts
index cbae280030f..855ccc8b987 100644
--- a/packages/backend/src/core/AiService.ts
+++ b/packages/backend/src/core/AiService.ts
@@ -4,27 +4,31 @@
*/
import * as fs from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
-import { Injectable } from '@nestjs/common';
+import { pathToFileURL } from 'node:url';
+import { resolve } from 'node:path';
+import { Injectable, Inject } from '@nestjs/common';
import { Mutex } from 'async-mutex';
import fetch from 'node-fetch';
+import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
-import type { NSFWJS, PredictionType } from 'nsfwjs';
-
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
+import type { Config } from '@/config.js';
+import type { NSFWJS, PredictionType } from 'nsfwjs/core';
const REQUIRED_CPU_FLAGS_X64 = ['avx2', 'fma'];
let isSupportedCpu: undefined | boolean = undefined;
@Injectable()
export class AiService {
+ private readonly modelDir: string;
private model: NSFWJS;
private modelLoadMutex: Mutex = new Mutex();
constructor(
+ @Inject(DI.config)
+ private config: Config,
) {
+ const md = resolve(this.config.rootDir, 'packages/backend/nsfw-model');
+ this.modelDir = md.endsWith('/') ? md : md + '/';
}
@bindThis
@@ -43,10 +47,10 @@ export class AiService {
tf.env().global.fetch = fetch;
if (this.model == null) {
- const nsfw = await import('nsfwjs');
+ const nsfw = await import('nsfwjs/core');
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
- this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
+ this.model = await nsfw.load(pathToFileURL(this.modelDir).toString(), { size: 299 });
}
});
}
diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts
index 4fb8a93e491..1f2f543962e 100644
--- a/packages/backend/src/core/InternalStorageService.ts
+++ b/packages/backend/src/core/InternalStorageService.ts
@@ -5,29 +5,25 @@
import * as fs from 'node:fs';
import * as Path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-const path = Path.resolve(_dirname, '../../../../files');
-
@Injectable()
export class InternalStorageService {
+ private readonly path: string;
+
constructor(
@Inject(DI.config)
private config: Config,
) {
+ this.path = Path.resolve(this.config.rootDir, 'files');
}
@bindThis
public resolvePath(key: string) {
- return Path.resolve(path, key);
+ return Path.resolve(this.path, key);
}
@bindThis
@@ -37,14 +33,14 @@ export class InternalStorageService {
@bindThis
public saveFromPath(key: string, srcPath: string) {
- fs.mkdirSync(path, { recursive: true });
+ fs.mkdirSync(this.path, { recursive: true });
fs.copyFileSync(srcPath, this.resolvePath(key));
return `${this.config.url}/files/${key}`;
}
@bindThis
public saveFromBuffer(key: string, data: Buffer) {
- fs.mkdirSync(path, { recursive: true });
+ fs.mkdirSync(this.path, { recursive: true });
fs.writeFileSync(this.resolvePath(key), data);
return `${this.config.url}/files/${key}`;
}
diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts
index f102461a506..db58d11e644 100644
--- a/packages/backend/src/core/LoggerService.ts
+++ b/packages/backend/src/core/LoggerService.ts
@@ -6,7 +6,7 @@
import { Injectable } from '@nestjs/common';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import type { KEYWORD } from 'color-convert/conversions.js';
+import type { Keyword } from 'color-convert';
@Injectable()
export class LoggerService {
@@ -15,7 +15,7 @@ export class LoggerService {
}
@bindThis
- public getLogger(domain: string, color?: KEYWORD | undefined) {
+ public getLogger(domain: string, color?: Keyword | undefined) {
return new Logger(domain, color);
}
}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 748f2cbad9b..c364c029e86 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -63,20 +63,21 @@ type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
class NotificationManager {
private notifier: { id: MiUser['id']; };
private note: MiNote;
- private queue: {
+ private queue: Map;
constructor(
private mutingsRepository: MutingsRepository,
private notificationService: NotificationService,
+ private followingsRepository: FollowingsRepository,
notifier: { id: MiUser['id']; },
note: MiNote,
) {
this.notifier = notifier;
this.note = note;
- this.queue = [];
+ this.queue = new Map();
}
@bindThis
@@ -84,7 +85,7 @@ class NotificationManager {
// 自分自身へは通知しない
if (this.notifier.id === notifiee) return;
- const exist = this.queue.find(x => x.target === notifiee);
+ const exist = this.queue.get(notifiee);
if (exist) {
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
@@ -92,7 +93,7 @@ class NotificationManager {
exist.reason = reason;
}
} else {
- this.queue.push({
+ this.queue.set(notifiee, {
reason: reason,
target: notifiee,
});
@@ -101,7 +102,49 @@ class NotificationManager {
@bindThis
public async notify() {
- for (const x of this.queue) {
+ if (this.queue.size === 0) {
+ return;
+ }
+
+ let visibleUserIds: Set | null;
+
+ switch (this.note.visibility) {
+ case 'public':
+ case 'home':
+ visibleUserIds = null;
+ break;
+
+ case 'specified':
+ visibleUserIds = new Set(this.note.visibleUserIds);
+ break;
+
+ // TODO: フォロワー限定ノートにフォロワーではない人がメンションされた場合通知されるのが正しい挙動なのか確認(一部に挙動の不一致がありそう)。現状は通知されるためフィルタしない
+ // case 'followers': {
+ // const targetUserIds = this.queue.map(x => x.target);
+ // const followers = await this.followingsRepository.find({
+ // where: {
+ // followeeId: this.note.userId,
+ // followerId: In(targetUserIds),
+ // isFollowerHibernated: false,
+ // },
+ // select: ['followerId'],
+ // });
+ // visibleUserIds = new Set(followers.map(f => f.followerId));
+ // break;
+ // }
+
+ default:
+ visibleUserIds = new Set();
+ break;
+ }
+
+ for (const x of this.queue.values()) {
+ const isVisibleToTarget = visibleUserIds === null || visibleUserIds.has(x.target);
+
+ if (!isVisibleToTarget) {
+ continue;
+ }
+
if (x.reason === 'renote') {
this.notificationService.createNotification(x.target, 'renote', {
noteId: this.note.id,
@@ -772,7 +815,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.webhookService.enqueueUserWebhook(user.id, 'note', { note: noteObj });
- const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
+ const nm = new NotificationManager(this.mutingsRepository, this.notificationService, this.followingsRepository, user, note);
await this.createMentionedEvents(mentionedUsers, note, nm);
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index 9120de1f9f3..d96d6c70d08 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -91,13 +91,27 @@ export class RelayService {
return JSON.stringify(result);
}
+ @bindThis
+ private getAcceptedRelays(): Promise {
+ return this.relaysCache.fetch(() => this.relaysRepository.findBy({
+ status: 'accepted',
+ }));
+ }
+
+ @bindThis
+ public async isRelayActor(actor: { inbox: string | null; sharedInbox: string | null }): Promise {
+ const relays = await this.getAcceptedRelays();
+ return relays.some(relay =>
+ (actor.inbox != null && relay.inbox === actor.inbox)
+ || (actor.sharedInbox != null && relay.inbox === actor.sharedInbox),
+ );
+ }
+
@bindThis
public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise {
if (activity == null) return;
- const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
- status: 'accepted',
- }));
+ const relays = await this.getAcceptedRelays();
if (relays.length === 0) return;
const copy = deepClone(activity);
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 2ffee69c21c..4515cfd29cd 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -533,7 +533,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
roleId: In(administratorRoles.map(r => r.id)),
}) : [];
// TODO: isRootなアカウントも含める
- return assigns.map(a => a.userId);
+ // Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
+ return [...new Set(assigns.map(a => a.userId))].sort((x, y) => x.localeCompare(y));
}
@bindThis
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
index 87097ada93a..91cc90db342 100644
--- a/packages/backend/src/core/SearchService.ts
+++ b/packages/backend/src/core/SearchService.ts
@@ -17,7 +17,7 @@ import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js';
import { IdService } from '@/core/IdService.js';
import { LoggerService } from '@/core/LoggerService.js';
-import type { Index, MeiliSearch } from 'meilisearch';
+import type { Index, Meilisearch } from 'meilisearch';
type K = string;
type V = string | number | boolean;
@@ -85,7 +85,7 @@ export class SearchService {
private config: Config,
@Inject(DI.meilisearch)
- private meilisearch: MeiliSearch | null,
+ private meilisearch: Meilisearch | null,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -187,7 +187,7 @@ export class SearchService {
return this.searchNoteByLike(q, me, opts, pagination);
}
case 'meilisearch': {
- return this.searchNoteByMeiliSearch(q, me, opts, pagination);
+ return this.searchNoteByMeilisearch(q, me, opts, pagination);
}
default: {
const _: never = this.provider;
@@ -239,14 +239,14 @@ export class SearchService {
}
@bindThis
- private async searchNoteByMeiliSearch(
+ private async searchNoteByMeilisearch(
q: string,
me: MiUser | null,
opts: SearchOpts,
pagination: SearchPagination,
): Promise {
if (!this.meilisearch || !this.meilisearchNoteIndex) {
- throw new Error('MeiliSearch is not available');
+ throw new Error('Meilisearch is not available');
}
const filter: Q = {
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index a85da62b86f..bb1b8f9f3ac 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -164,4 +164,3 @@ export class SignupService {
return { account, secret };
}
}
-
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index ff47ca930d7..27ab0e34479 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -259,7 +259,7 @@ export class ApInboxService {
@bindThis
private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise {
- if (actor.uri !== activity.actor) {
+ if (actor.uri !== getApId(activity.actor)) {
return 'invalid actor';
}
@@ -302,12 +302,14 @@ export class ApInboxService {
@bindThis
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise {
- const uri = getApId(activity);
-
if (actor.isSuspended) {
return;
}
+ // リレーからのAnnounceかチェック
+ const fromRelay = await this.relayService.isRelayActor(actor);
+ const uri = getApId(fromRelay ? target : activity);
+
// アナウンス先が許可されているかチェック
if (!this.utilityService.isFederationAllowedUri(uri)) return;
@@ -336,6 +338,14 @@ export class ApInboxService {
throw err;
}
+ // リレーからのAnnounceはリノートを作成せず、ノートを直接公開する
+ if (fromRelay) {
+ this.logger.info(`Publishing relay-delivered note: ${uri}`);
+ const noteObj = await this.noteEntityService.pack(renote, null, { skipHide: true, withReactionAndUserPairCache: true });
+ this.globalEventService.publishNotesStream(noteObj);
+ return;
+ }
+
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
return 'skip: invalid actor for this activity';
}
@@ -459,7 +469,7 @@ export class ApInboxService {
@bindThis
private async delete(actor: MiRemoteUser, activity: IDelete): Promise {
- if (actor.uri !== activity.actor) {
+ if (actor.uri !== getApId(activity.actor)) {
return 'invalid actor';
}
@@ -613,7 +623,7 @@ export class ApInboxService {
@bindThis
private async remove(actor: MiRemoteUser, activity: IRemove, resolver?: Resolver): Promise {
- if (actor.uri !== activity.actor) {
+ if (actor.uri !== getApId(activity.actor)) {
return 'invalid actor';
}
@@ -633,7 +643,7 @@ export class ApInboxService {
@bindThis
private async undo(actor: MiRemoteUser, activity: IUndo, resolver?: Resolver): Promise {
- if (actor.uri !== activity.actor) {
+ if (actor.uri !== getApId(activity.actor)) {
return 'invalid actor';
}
@@ -767,7 +777,7 @@ export class ApInboxService {
@bindThis
private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise {
- if (actor.uri !== activity.actor) {
+ if (actor.uri !== getApId(activity.actor)) {
return 'skip: invalid actor';
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index ebe8e9c9649..39396cb7411 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -376,7 +376,7 @@ export class ApPersonService implements OnModuleInit {
isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo,
movedAt: person.movedTo ? new Date() : null,
- alsoKnownAs: person.alsoKnownAs,
+ alsoKnownAs: toArray(person.alsoKnownAs),
isExplorable: person.discoverable,
username: person.preferredUsername,
usernameLower: person.preferredUsername?.toLowerCase(),
@@ -568,7 +568,7 @@ export class ApPersonService implements OnModuleInit {
isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,
- alsoKnownAs: person.alsoKnownAs ?? null,
+ alsoKnownAs: person.alsoKnownAs ? toArray(person.alsoKnownAs) : null,
isExplorable: person.discoverable,
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
} as Partial & Pick;
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 8e56ddbc022..b9b656bd104 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -132,7 +132,7 @@ export class MetaEntityService {
sentryForFrontend: this.config.sentryForFrontend ?? null,
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
- noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
+ noteSearchableScope: (this.config.fulltextSearch?.provider === 'meilisearch' && this.config.meilisearch?.scope === 'local') ? 'local' : 'global',
maxFileSize: this.config.maxFileSize,
federation: this.meta.federation,
};
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 0f4051e7b89..996f0bad2e8 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -51,6 +51,7 @@ import { ChatService } from '@/core/ChatService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
+import { toArray } from '@/misc/prelude/array.js';
const Ajv = _Ajv.default;
const ajv = new Ajv();
@@ -527,10 +528,10 @@ export class UserEntityService implements OnModuleInit {
url: profile!.url,
uri: user.uri,
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
- alsoKnownAs: user.alsoKnownAs
- ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
- .then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
- : null,
+ alsoKnownAs: user.alsoKnownAs ?
+ Promise.all(toArray(user.alsoKnownAs).map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
+ .then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
+ : null,
createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts
index ff5363a425d..ce76f8d05e5 100644
--- a/packages/backend/src/logger.ts
+++ b/packages/backend/src/logger.ts
@@ -9,11 +9,11 @@ import { default as convertColor } from 'color-convert';
import { format as dateFormat } from 'date-fns';
import { bindThis } from '@/decorators.js';
import { envOption } from './env.js';
-import type { KEYWORD } from 'color-convert/conversions.js';
+import type { Keyword } from 'color-convert';
type Context = {
name: string;
- color?: KEYWORD;
+ color?: Keyword;
};
type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
@@ -23,7 +23,7 @@ export default class Logger {
private context: Context;
private parentLogger: Logger | null = null;
- constructor(context: string, color?: KEYWORD) {
+ constructor(context: string, color?: Keyword) {
this.context = {
name: context,
color: color,
@@ -31,7 +31,7 @@ export default class Logger {
}
@bindThis
- public createSubLogger(context: string, color?: KEYWORD): Logger {
+ public createSubLogger(context: string, color?: Keyword): Logger {
const logger = new Logger(context, color);
logger.parentLogger = this;
return logger;
diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts
index 8b81702d19b..291a33385f0 100644
--- a/packages/backend/src/misc/id/ulid.ts
+++ b/packages/backend/src/misc/id/ulid.ts
@@ -5,12 +5,19 @@
// Crockford's Base32
// https://github.com/ulid/spec#encoding
-import { parseBigInt32 } from '@/misc/bigint.js';
const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
+function parseBigIntCrockford(str: string): bigint {
+ let result = 0n;
+ for (let i = 0; i < str.length; i++) {
+ result = result * 32n + BigInt(CHARS.indexOf(str[i]));
+ }
+ return result;
+}
+
function parseBase32(timestamp: string) {
let time = 0;
for (let i = 0; i < timestamp.length; i++) {
@@ -26,6 +33,6 @@ export function parseUlid(id: string): { date: Date; } {
export function parseUlidFull(id: string): { date: number; additional: bigint; } {
return {
date: parseBase32(id.slice(0, 10)),
- additional: parseBigInt32(id.slice(10, 26)),
+ additional: parseBigIntCrockford(id.slice(10, 26)),
};
}
diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts
index 13f0b056674..d14a210392c 100644
--- a/packages/backend/src/models/AvatarDecoration.ts
+++ b/packages/backend/src/models/AvatarDecoration.ts
@@ -36,4 +36,9 @@ export class MiAvatarDecoration {
array: true, length: 128, default: '{}',
})
public roleIdsThatCanBeUsedThisDecoration: string[];
+
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public category: string | null;
}
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 079e014da83..0e4cd598888 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -13,7 +13,7 @@ import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataServic
import InstanceChart from '@/core/chart/charts/instance.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
import FederationChart from '@/core/chart/charts/federation.js';
-import { getApId } from '@/core/activitypub/type.js';
+import { getApId, isActor, isDelete } from '@/core/activitypub/type.js';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiUserPublickey } from '@/models/UserPublickey.js';
@@ -84,6 +84,23 @@ export class InboxProcessorService implements OnApplicationShutdown {
return `Old keyId is no longer supported. ${keyIdLower}`;
}
+ {
+ let userExistenceCheckApId: string | null = null;
+
+ // 存在しないActorに対するActorのDeleteアクティビティは無視する。
+ // actorとobjectが同じならばそれはActorに違いない
+ if (isDelete(activity) && typeof activity.object === 'object' && (isActor(activity.object) || getApId(activity.actor) === getApId(activity.object))) {
+ userExistenceCheckApId = getApId(activity.object);
+ }
+
+ if (userExistenceCheckApId != null) {
+ const user = await this.apDbResolverService.getUserFromApId(userExistenceCheckApId);
+ if (user == null) {
+ return `skip: user not found for delete activity. ${getApId(userExistenceCheckApId)}`;
+ }
+ }
+ }
+
// HTTP-Signature keyIdを元にDBから取得
let authUser: {
user: MiRemoteUser;
@@ -98,9 +115,9 @@ export class InboxProcessorService implements OnApplicationShutdown {
// 対象が4xxならスキップ
if (err instanceof StatusError) {
if (!err.isRetryable) {
- throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
+ throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${getApId(activity.actor)} - ${err.statusCode}`);
}
- throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
+ throw new Error(`Error in actor ${getApId(activity.actor)} - ${err.statusCode}`);
}
}
}
@@ -119,7 +136,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
// また、signatureのsignerは、activity.actorと一致する必要がある
- if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
+ if (!httpSignatureValidated || authUser.user.uri !== getApId(activity.actor)) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
const ldSignature = activity.signature;
if (ldSignature) {
@@ -170,8 +187,8 @@ export class InboxProcessorService implements OnApplicationShutdown {
//#endregion
// もう一度actorチェック
- if (authUser.user.uri !== activity.actor) {
- throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
+ if (authUser.user.uri !== getApId(activity.actor)) {
+ throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${getApId(activity.actor)})`);
}
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
@@ -226,14 +243,17 @@ export class InboxProcessorService implements OnApplicationShutdown {
}
} catch (e) {
if (e instanceof IdentifiableError) {
- if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
- return 'blocked notes with prohibited words';
- }
- if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') {
- return 'actor has been suspended';
- }
- if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note
- return e.message;
+ switch (e.id) {
+ case '689ee33f-f97c-479a-ac49-1b9f8140af99':
+ return 'blocked notes with prohibited words';
+ case '85ab9bd7-3a41-4530-959d-f07073900109':
+ return 'actor has been suspended';
+ case 'd450b8a9-48e4-4dab-ae36-f4db763fda7c': // invalid Note
+ return e.message;
+ case '9f466dab-c856-48cd-9e65-ff90ff750580':
+ return 'note contains too many mentions';
+ case '09d79f9e-64f1-4316-9cfa-e75c4d091574': // Instance is blocked
+ return 'skip: blocked instance';
}
}
throw e;
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index f5034d07330..4a5ac799ad9 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -4,8 +4,7 @@
*/
import * as fs from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
+import { resolve } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import type { Config } from '@/config.js';
import type { DriveFilesRepository } from '@/models/_.js';
@@ -25,11 +24,6 @@ import { FileServerFileResolver } from './file/FileServerFileResolver.js';
import { FileServerProxyHandler } from './file/FileServerProxyHandler.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-const assets = `${_dirname}/../../server/file/assets/`;
-
@Injectable()
export class FileServerService {
private logger: Logger;
@@ -37,6 +31,8 @@ export class FileServerService {
private proxyHandler: FileServerProxyHandler;
private fileResolver: FileServerFileResolver;
+ private readonly assets: string;
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -52,6 +48,7 @@ export class FileServerService {
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
+ this.assets = resolve(this.config.rootDir, 'packages/backend/src/server/file/assets');
this.fileResolver = new FileServerFileResolver(
this.driveFilesRepository,
this.fileInfoService,
@@ -61,13 +58,13 @@ export class FileServerService {
this.driveHandler = new FileServerDriveHandler(
this.config,
this.fileResolver,
- assets,
+ this.assets,
this.videoProcessingService,
);
this.proxyHandler = new FileServerProxyHandler(
this.config,
this.fileResolver,
- assets,
+ this.assets,
this.imageProcessingService,
);
@@ -87,7 +84,7 @@ export class FileServerService {
fastify.register((fastify, options, done) => {
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
fastify.get('/files/app-default.jpg', (request, reply) => {
- const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
+ const file = fs.createReadStream(`${this.assets}/dummy.png`);
reply.header('Content-Type', 'image/jpeg');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return reply.send(file);
@@ -121,7 +118,7 @@ export class FileServerService {
reply.header('Cache-Control', 'max-age=300');
if (request.query && 'fallback' in request.query) {
- return reply.sendFile('/dummy.png', assets);
+ return reply.sendFile('/dummy.png', this.assets);
}
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts
index 5980609f02b..7c9710c6939 100644
--- a/packages/backend/src/server/HealthServerService.ts
+++ b/packages/backend/src/server/HealthServerService.ts
@@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import { readyRef } from '@/boot/ready.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
-import type { MeiliSearch } from 'meilisearch';
+import type { Meilisearch } from 'meilisearch';
@Injectable()
export class HealthServerService {
@@ -34,7 +34,7 @@ export class HealthServerService {
private db: DataSource,
@Inject(DI.meilisearch)
- private meilisearch: MeiliSearch | null,
+ private meilisearch: Meilisearch | null,
) {}
@bindThis
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
index 0121c302ace..baa87dbbbe2 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
@@ -55,6 +55,10 @@ export const meta = {
format: 'id',
},
},
+ category: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
},
},
} as const;
@@ -68,6 +72,7 @@ export const paramDef = {
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
+ category: { type: 'string', nullable: true },
},
required: ['name', 'description', 'url'],
} as const;
@@ -84,6 +89,7 @@ export default class extends Endpoint { // eslint-
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
+ category: ps.category,
}, me);
return {
@@ -94,6 +100,7 @@ export default class extends Endpoint { // eslint-
description: created.description,
url: created.url,
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
+ category: created.category,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
index 765bfd67666..7be3d79fee4 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
@@ -60,6 +60,10 @@ export const meta = {
format: 'id',
},
},
+ category: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
},
@@ -95,6 +99,7 @@ export default class extends Endpoint { // eslint-
description: avatarDecoration.description,
url: avatarDecoration.url,
roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
+ category: avatarDecoration.category,
}));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
index 22476a68886..b84b4c50859 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
@@ -30,6 +30,7 @@ export const paramDef = {
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
+ category: { type: 'string', nullable: true },
},
required: ['id'],
} as const;
@@ -45,6 +46,7 @@ export default class extends Endpoint { // eslint-
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
+ category: ps.category,
}, me);
});
}
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index 62b04e1df36..58610c76b4c 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -119,7 +119,7 @@ export default class extends Endpoint { // eslint-
}
// Update
- this.driveFoldersRepository.update(folder.id, {
+ await this.driveFoldersRepository.update(folder.id, {
name: folder.name,
parentId: folder.parentId,
});
diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts
index fe7e9c36f3a..872346e560a 100644
--- a/packages/backend/src/server/api/endpoints/endpoint.ts
+++ b/packages/backend/src/server/api/endpoints/endpoint.ts
@@ -5,7 +5,13 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import endpoints from '../endpoints.js';
+
+// 循環参照を回避
+let endpointsPromise: Promise | undefined;
+
+function getEndpoints() {
+ return endpointsPromise ??= import('../endpoints.js').then(module => module.default);
+}
export const meta = {
requireCredential: false,
@@ -43,6 +49,7 @@ export default class extends Endpoint { // eslint-
constructor(
) {
super(meta, paramDef, async (ps) => {
+ const endpoints = await getEndpoints();
const ep = endpoints.find(x => x.name === ps.endpoint);
if (ep == null) return null;
return {
diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts
index 4aedf62a849..0837b2d4e96 100644
--- a/packages/backend/src/server/api/endpoints/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints/endpoints.ts
@@ -5,7 +5,13 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import endpoints from '../endpoints.js';
+
+// 循環参照を回避
+let endpointsPromise: Promise | undefined;
+
+function getEndpoints() {
+ return endpointsPromise ??= import('../endpoints.js').then(module => module.default);
+}
export const meta = {
requireCredential: false,
@@ -39,6 +45,7 @@ export default class extends Endpoint { // eslint-
constructor(
) {
super(meta, paramDef, async () => {
+ const endpoints = await getEndpoints();
return endpoints.map(x => x.name);
});
}
diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts
index 52acee1cfbc..ca0a5e2e257 100644
--- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts
+++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts
@@ -49,6 +49,10 @@ export const meta = {
format: 'id',
},
},
+ category: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
},
@@ -76,6 +80,7 @@ export default class extends Endpoint { // eslint-
description: decoration.description,
url: decoration.url,
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
+ category: decoration.category,
}));
});
}
diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts
index 6097f9c562e..629214382ba 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.test.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts
@@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
-import { describe, test, expect } from '@jest/globals';
+import { describe, test, expect } from 'vitest';
import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
import { paramDef } from './create.js';
diff --git a/packages/backend/src/server/api/endpoints/users/show.test.ts b/packages/backend/src/server/api/endpoints/users/show.test.ts
index 068ffd8bc93..7d8f44592d0 100644
--- a/packages/backend/src/server/api/endpoints/users/show.test.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.test.ts
@@ -5,6 +5,7 @@
process.env.NODE_ENV = 'test';
+import { describe, test, expect } from 'vitest';
import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
import { paramDef } from './show.js';
diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
index f124aa9f39e..24fc46e4ba5 100644
--- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts
+++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
@@ -3,16 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { genOpenapiSpec } from './gen-spec.js';
+import { ApiDocPage } from './api-doc.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
-const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url));
-
@Injectable()
export class OpenApiServerService {
constructor(
@@ -25,7 +23,8 @@ export class OpenApiServerService {
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/api-doc', async (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=86400');
- return await reply.sendFile('/api-doc.html', staticAssets);
+ reply.type('text/html; charset=utf-8');
+ reply.send(await ApiDocPage());
});
fastify.get('/api.json', (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=600');
diff --git a/packages/backend/src/server/api/openapi/api-doc.tsx b/packages/backend/src/server/api/openapi/api-doc.tsx
new file mode 100644
index 00000000000..663d9f5be3f
--- /dev/null
+++ b/packages/backend/src/server/api/openapi/api-doc.tsx
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function ApiDocPage() {
+ return (
+ <>
+ {''}
+
+
+
+ Misskey API
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 24bc619e792..9990d57f2ba 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -4,9 +4,7 @@
*/
import { randomUUID } from 'node:crypto';
-import { dirname, resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import * as fs from 'node:fs';
+import { resolve } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import sharp from 'sharp';
@@ -67,35 +65,17 @@ import { ErrorPage } from './views/error.js';
import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-let rootDir = _dirname;
-// 見つかるまで上に遡る
-while (!fs.existsSync(resolve(rootDir, 'packages'))) {
- const parentDir = dirname(rootDir);
- if (parentDir === rootDir) {
- throw new Error('Cannot find root directory');
- }
- rootDir = parentDir;
-}
-
-const backendRootDir = resolve(rootDir, 'packages/backend');
-const frontendRootDir = resolve(rootDir, 'packages/frontend');
-
-const staticAssets = resolve(backendRootDir, 'assets');
-const clientAssets = resolve(frontendRootDir, 'assets');
-const assets = resolve(rootDir, 'built/_frontend_dist_');
-const swAssets = resolve(rootDir, 'built/_sw_dist_');
-const fluentEmojisDir = resolve(rootDir, 'fluent-emojis/dist');
-const twemojiDir = resolve(backendRootDir, 'node_modules/@discordapp/twemoji/dist/svg');
-const frontendViteOut = resolve(rootDir, 'built/_frontend_vite_');
-const frontendEmbedViteOut = resolve(rootDir, 'built/_frontend_embed_vite_');
-const tarball = resolve(rootDir, 'built/tarball');
-
@Injectable()
export class ClientServerService {
- private logger: Logger;
+ private readonly staticAssets: string;
+ private readonly clientAssets: string;
+ private readonly assets: string;
+ private readonly swAssets: string;
+ private readonly fluentEmojisDir: string;
+ private readonly twemojiDir: string;
+ private readonly frontendViteOut: string;
+ private readonly frontendEmbedViteOut: string;
+ private readonly tarball: string;
constructor(
@Inject(DI.config)
@@ -149,6 +129,17 @@ export class ClientServerService {
private clientLoggerService: ClientLoggerService,
) {
//this.createServer = this.createServer.bind(this);
+ const backendRootdir = resolve(this.config.rootDir, 'packages/backend');
+ const frontendRootdir = resolve(this.config.rootDir, 'packages/frontend');
+ this.staticAssets = resolve(backendRootdir, 'assets');
+ this.clientAssets = resolve(frontendRootdir, 'assets');
+ this.assets = resolve(this.config.rootDir, 'built/_frontend_dist_');
+ this.swAssets = resolve(this.config.rootDir, 'built/_sw_dist_');
+ this.fluentEmojisDir = resolve(this.config.rootDir, 'fluent-emojis/dist');
+ this.twemojiDir = resolve(backendRootdir, 'node_modules/@discordapp/twemoji/dist/svg');
+ this.frontendViteOut = resolve(this.config.rootDir, 'built/_frontend_vite_');
+ this.frontendEmbedViteOut = resolve(this.config.rootDir, 'built/_frontend_embed_vite_');
+ this.tarball = resolve(this.config.rootDir, 'built/tarball');
}
@bindThis
@@ -223,17 +214,17 @@ export class ClientServerService {
//#region vite assets
if (this.config.frontendEmbedManifestExists) {
- console.log(`[ClientServerService] Using built frontend vite assets. ${frontendViteOut}`);
+ this.clientLoggerService.logger.info(`[ClientServerService] Using built frontend vite assets. ${this.frontendViteOut}`);
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
- root: frontendViteOut,
+ root: this.frontendViteOut,
prefix: '/vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.register(fastifyStatic, {
- root: frontendEmbedViteOut,
+ root: this.frontendEmbedViteOut,
prefix: '/embed_vite/',
maxAge: ms('30 days'),
immutable: true,
@@ -265,21 +256,21 @@ export class ClientServerService {
//#region static assets
fastify.register(fastifyStatic, {
- root: staticAssets,
+ root: this.staticAssets,
prefix: '/static-assets/',
maxAge: ms('7 days'),
decorateReply: false,
});
fastify.register(fastifyStatic, {
- root: clientAssets,
+ root: this.clientAssets,
prefix: '/client-assets/',
maxAge: ms('7 days'),
decorateReply: false,
});
fastify.register(fastifyStatic, {
- root: assets,
+ root: this.assets,
prefix: '/assets/',
maxAge: ms('7 days'),
decorateReply: false,
@@ -287,7 +278,7 @@ export class ClientServerService {
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
- root: tarball,
+ root: this.tarball,
prefix: '/tarball/',
maxAge: ms('30 days'),
immutable: true,
@@ -298,11 +289,11 @@ export class ClientServerService {
});
fastify.get('/favicon.ico', async (request, reply) => {
- return reply.sendFile('/favicon.ico', staticAssets);
+ return reply.sendFile('/favicon.ico', this.staticAssets);
});
fastify.get('/apple-touch-icon.png', async (request, reply) => {
- return reply.sendFile('/apple-touch-icon.png', staticAssets);
+ return reply.sendFile('/apple-touch-icon.png', this.staticAssets);
});
fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
@@ -315,7 +306,7 @@ export class ClientServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
- return reply.sendFile(path, fluentEmojisDir, {
+ return reply.sendFile(path, this.fluentEmojisDir, {
maxAge: ms('30 days'),
});
});
@@ -330,7 +321,7 @@ export class ClientServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
- return reply.sendFile(path, twemojiDir, {
+ return reply.sendFile(path, this.twemojiDir, {
maxAge: ms('30 days'),
});
});
@@ -344,7 +335,7 @@ export class ClientServerService {
}
const mask = await sharp(
- `${twemojiDir}/${path.replace('.png', '')}.svg`,
+ `${this.twemojiDir}/${path.replace('.png', '')}.svg`,
{ density: 1000 },
)
.resize(488, 488)
@@ -380,7 +371,7 @@ export class ClientServerService {
// ServiceWorker
fastify.get('/sw.js', async (request, reply) => {
- return await reply.sendFile('/sw.js', swAssets, {
+ return await reply.sendFile('/sw.js', this.swAssets, {
maxAge: ms('10 minutes'),
});
});
@@ -390,13 +381,40 @@ export class ClientServerService {
// Embed Javascript
fastify.get('/embed.js', async (request, reply) => {
- return await reply.sendFile('/embed.js', staticAssets, {
+ return await reply.sendFile('/embed.js', this.staticAssets, {
maxAge: ms('1 day'),
});
});
fastify.get('/robots.txt', async (request, reply) => {
- return await reply.sendFile('/robots.txt', staticAssets);
+ const disallowedPaths = [
+ '/settings',
+ '/admin',
+ '/custom-emojis-manager',
+ '/avatar-decorations',
+ '/share',
+ '/my',
+ '/api',
+ '/inbox',
+ '/oauth',
+ '/proxy',
+ '/url',
+ ];
+
+ if (this.meta.ugcVisibilityForVisitor === 'none') {
+ disallowedPaths.push(
+ '/@',
+ '/notes',
+ );
+ }
+
+ let content = `User-agent: *\n`;
+ content += disallowedPaths.map((path) => `Disallow: ${path}`).join('\n') + '\n';
+ content += 'Allow: /\n';
+ content += '\n# todo: sitemap\n';
+
+ reply.header('Content-Type', 'text/plain; charset=utf-8');
+ return await reply.send(content);
});
// OpenSearch XML
diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts
index 36272c81d52..2859b2b9852 100644
--- a/packages/backend/src/server/web/HtmlTemplateService.ts
+++ b/packages/backend/src/server/web/HtmlTemplateService.ts
@@ -3,9 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { dirname, resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { promises as fsp, existsSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { promises as fsp } from 'node:fs';
import { languages } from 'i18n/const';
import { Injectable, Inject } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
@@ -18,25 +17,11 @@ import type { Config } from '@/config.js';
import type { MiMeta } from '@/models/Meta.js';
import type { CommonData, ViteFiles } from './views/_.js';
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-let rootDir = _dirname;
-// 見つかるまで上に遡る
-while (!existsSync(resolve(rootDir, 'packages'))) {
- const parentDir = dirname(rootDir);
- if (parentDir === rootDir) {
- throw new Error('Cannot find root directory');
- }
- rootDir = parentDir;
-}
-
-const frontendViteBuilt = resolve(rootDir, 'built/_frontend_vite_');
-const frontendEmbedViteBuilt = resolve(rootDir, 'built/_frontend_embed_vite_');
-
@Injectable()
export class HtmlTemplateService {
private frontendAssetsFetched = false;
+ private readonly frontendViteBuilt: string;
+ private readonly frontendEmbedViteBuilt: string;
public frontendViteFiles: ViteFiles | null = null;
public frontendBootloaderJs: string | null = null;
public frontendBootloaderCss: string | null = null;
@@ -53,6 +38,8 @@ export class HtmlTemplateService {
private metaEntityService: MetaEntityService,
) {
+ this.frontendViteBuilt = resolve(this.config.rootDir, 'built/_frontend_vite_');
+ this.frontendEmbedViteBuilt = resolve(this.config.rootDir, 'built/_frontend_embed_vite_');
}
// 初期ロードで読み込むべきファイルのパスを収集する。
@@ -118,22 +105,22 @@ export class HtmlTemplateService {
embedBootJs,
embedBootCss,
] = await Promise.all([
- fsp.readFile(resolve(frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
- fsp.readFile(resolve(frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null),
- fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
- fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null),
+ fsp.readFile(resolve(this.frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
+ fsp.readFile(resolve(this.frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null),
+ fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
+ fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null),
]);
let feViteManifest: Manifest | null = null;
let embedFeViteManifest: Manifest | null = null;
if (this.config.frontendManifestExists) {
- const manifestContent = await fsp.readFile(resolve(frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null);
+ const manifestContent = await fsp.readFile(resolve(this.frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null);
feViteManifest = manifestContent ? JSON.parse(manifestContent) : null;
}
if (this.config.frontendEmbedManifestExists) {
- const manifestContent = await fsp.readFile(resolve(frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null);
+ const manifestContent = await fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null);
embedFeViteManifest = manifestContent ? JSON.parse(manifestContent) : null;
}
diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml
index 4d1b4b0d60f..53e297f8678 100644
--- a/packages/backend/test-federation/compose.yml
+++ b/packages/backend/test-federation/compose.yml
@@ -43,16 +43,12 @@ services:
target: /misskey/packages/backend/test-federation/test
read_only: true
- type: bind
- source: ../jest.config.cjs
- target: /misskey/packages/backend/jest.config.cjs
+ source: ../vitest.config.ts
+ target: /misskey/packages/backend/vitest.config.ts
read_only: true
- type: bind
- source: ../jest.config.fed.cjs
- target: /misskey/packages/backend/jest.config.fed.cjs
- read_only: true
- - type: bind
- source: ../jest.js
- target: /misskey/packages/backend/jest.js
+ source: ../vitest.config.fed.ts
+ target: /misskey/packages/backend/vitest.config.fed.ts
read_only: true
- type: bind
source: ../scripts/compile_config.js
diff --git a/packages/backend/test-federation/test/abuse-report.test.ts b/packages/backend/test-federation/test/abuse-report.test.ts
index ddc8e4f9d03..44342e77431 100644
--- a/packages/backend/test-federation/test/abuse-report.test.ts
+++ b/packages/backend/test-federation/test/abuse-report.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll } from 'vitest';
import { rejects, strictEqual } from 'node:assert';
import * as Misskey from 'misskey-js';
import { createAccount, createModerator, resolveRemoteUser, sleep, type LoginUser } from './utils.js';
diff --git a/packages/backend/test-federation/test/block.test.ts b/packages/backend/test-federation/test/block.test.ts
index ef910eeaead..0cc5ca046be 100644
--- a/packages/backend/test-federation/test/block.test.ts
+++ b/packages/backend/test-federation/test/block.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll } from 'vitest';
import { deepStrictEqual, rejects, strictEqual } from 'node:assert';
import * as Misskey from 'misskey-js';
import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
diff --git a/packages/backend/test-federation/test/drive.test.ts b/packages/backend/test-federation/test/drive.test.ts
index f755183b4db..8c392148fc6 100644
--- a/packages/backend/test-federation/test/drive.test.ts
+++ b/packages/backend/test-federation/test/drive.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll } from 'vitest';
import assert, { strictEqual } from 'node:assert';
import * as Misskey from 'misskey-js';
import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
diff --git a/packages/backend/test-federation/test/emoji.test.ts b/packages/backend/test-federation/test/emoji.test.ts
index 3119ca6e4de..308323a7cec 100644
--- a/packages/backend/test-federation/test/emoji.test.ts
+++ b/packages/backend/test-federation/test/emoji.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll } from 'vitest';
import assert, { deepStrictEqual, strictEqual } from 'assert';
import * as Misskey from 'misskey-js';
import { addCustomEmoji, createAccount, type LoginUser, resolveRemoteUser, sleep } from './utils.js';
diff --git a/packages/backend/test-federation/test/move.test.ts b/packages/backend/test-federation/test/move.test.ts
index 56a57de8a4e..0801ce84ee2 100644
--- a/packages/backend/test-federation/test/move.test.ts
+++ b/packages/backend/test-federation/test/move.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll } from 'vitest';
import assert, { strictEqual } from 'node:assert';
import { createAccount, type LoginUser, sleep } from './utils.js';
diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts
index a339cd86d2e..d9556b0e932 100644
--- a/packages/backend/test-federation/test/note.test.ts
+++ b/packages/backend/test-federation/test/note.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll, afterAll } from 'vitest';
import assert, { rejects, strictEqual } from 'node:assert';
import * as Misskey from 'misskey-js';
import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
@@ -214,7 +215,7 @@ describe('Note', () => {
* @see https://github.com/misskey-dev/misskey/issues/15548
*/
describe('To only resolved and not followed user', () => {
- test.failing('Check', async () => {
+ test.skip('Check', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, alice);
await sleep();
@@ -254,7 +255,7 @@ describe('Note', () => {
* FIXME: implement soft deletion as well as user?
* @see https://github.com/misskey-dev/misskey/issues/11437
*/
- test.failing('Not found even if resolve again', async () => {
+ test.skip('Not found even if resolve again', async () => {
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
await rejects(
async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
diff --git a/packages/backend/test-federation/test/notification.test.ts b/packages/backend/test-federation/test/notification.test.ts
index 6d55353653e..7058da8a42b 100644
--- a/packages/backend/test-federation/test/notification.test.ts
+++ b/packages/backend/test-federation/test/notification.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll, afterAll } from 'vitest';
import * as Misskey from 'misskey-js';
import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts
index 00635e654be..191bd416b9b 100644
--- a/packages/backend/test-federation/test/timeline.test.ts
+++ b/packages/backend/test-federation/test/timeline.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll, afterAll } from 'vitest';
import { strictEqual } from 'assert';
import * as Misskey from 'misskey-js';
import { createAccount, fetchAdmin, isNoteUpdatedEventFired, isFired, type LoginUser, type Request, resolveRemoteUser, sleep, createRole } from './utils.js';
@@ -117,7 +118,7 @@ describe('Timeline', () => {
* FIXME: can receive this
* @see https://github.com/misskey-dev/misskey/issues/14083
*/
- test.failing('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => {
+ test.skip('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => {
await postAndCheckReception(homeTimeline, false, { text: `@${bob.username}@b.test Hello`, visibility: 'specified' });
});
@@ -125,7 +126,7 @@ describe('Timeline', () => {
* FIXME: cannot receive this
* @see https://github.com/misskey-dev/misskey/issues/14084
*/
- test.failing('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => {
+ test.skip('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => {
const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'specified' })).createdNote;
await postAndCheckReception(homeTimeline, true, { replyId: note.id, visibility: 'specified', visibleUserIds: [bobInA.id] });
});
diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts
index ebbe9ff5ba3..c6a93c2dd5f 100644
--- a/packages/backend/test-federation/test/user.test.ts
+++ b/packages/backend/test-federation/test/user.test.ts
@@ -1,3 +1,4 @@
+import { describe, test, beforeAll } from 'vitest';
import assert, { rejects, strictEqual } from 'node:assert';
import * as Misskey from 'misskey-js';
import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
diff --git a/packages/backend/test-server/.swcrc b/packages/backend/test-server/.swcrc
deleted file mode 100644
index 3859603da39..00000000000
--- a/packages/backend/test-server/.swcrc
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "$schema": "https://swc.rs/schema.json",
- "jsc": {
- "parser": {
- "syntax": "typescript",
- "dynamicImport": true,
- "decorators": true
- },
- "transform": {
- "legacyDecorator": true,
- "decoratorMetadata": true
- },
- "experimental": {
- "keepImportAssertions": true
- },
- "baseUrl": "../src-js",
- "paths": {
- "@/*": ["*"]
- },
- "target": "es2022"
- },
- "minify": false
-}
diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts
index 04bf62d2096..5abe8dd2967 100644
--- a/packages/backend/test-server/entry.ts
+++ b/packages/backend/test-server/entry.ts
@@ -19,7 +19,7 @@ let serverService: ServerService;
/**
* テスト用のサーバインスタンスを起動する
*/
-async function launch() {
+export async function setup() {
await killTestServer();
console.log('starting application...');
@@ -38,6 +38,15 @@ async function launch() {
console.log('application initialized.');
}
+/**
+ * テスト用のサーバインスタンスを停止する
+ */
+export async function teardown() {
+ await serverService.dispose();
+ await app.close();
+ await killTestServer();
+}
+
/**
* 既に重複したポートで待ち受けしているサーバがある場合はkillする
*/
@@ -94,5 +103,3 @@ async function startControllerEndpoints(port = config.port + 1000) {
await fastify.listen({ port: port, host: 'localhost' });
}
-
-export default launch;
diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json
index 7ed7c10ed74..33978ec95f9 100644
--- a/packages/backend/test-server/tsconfig.json
+++ b/packages/backend/test-server/tsconfig.json
@@ -25,8 +25,6 @@
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "@kitajs/html",
- "rootDir": "../src",
- "baseUrl": "./",
"paths": {
"@/*": ["../src/*"]
},
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 48e1bababb7..bd529067cc2 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -20,6 +20,7 @@ import type {
RegistrationResponseJSON,
} from '@simplewebauthn/types';
import type * as misskey from 'misskey-js';
+import { describe, beforeAll, test } from 'vitest';
describe('2要素認証', () => {
let alice: misskey.entities.SignupResponse;
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index 70a5c9579ee..ea7cd77d665 100644
--- a/packages/backend/test/e2e/antennas.ts
+++ b/packages/backend/test/e2e/antennas.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, beforeEach, test } from 'vitest';
import {
api,
failedApiCall,
diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts
index 2dd645d97a9..4f244c0cce6 100644
--- a/packages/backend/test/e2e/api-visibility.ts
+++ b/packages/backend/test/e2e/api-visibility.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, beforeEach, test } from 'vitest';
import { UserToken, api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts
index 49c6a0636be..5eb7934c1ae 100644
--- a/packages/backend/test/e2e/api.ts
+++ b/packages/backend/test/e2e/api.ts
@@ -6,7 +6,8 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
-import { IncomingMessage } from 'http';
+import { describe, beforeAll, test } from 'vitest';
+import { IncomingMessage } from 'node:http';
import {
api,
connectStream,
diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts
index 35b0e59383f..9ef4dd8be94 100644
--- a/packages/backend/test/e2e/block.ts
+++ b/packages/backend/test/e2e/block.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, test } from 'vitest';
import { api, castAsError, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts
index fe9a217ee88..465089db2e9 100644
--- a/packages/backend/test/e2e/clips.ts
+++ b/packages/backend/test/e2e/clips.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, beforeEach, afterEach, test } from 'vitest';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
import type * as Misskey from 'misskey-js';
@@ -176,7 +177,9 @@ describe('クリップ', () => {
{ label: 'descriptionがnull', parameters: { description: null } },
{ label: 'descriptionが最大長', parameters: { description: 'a'.repeat(2048) } },
];
- test.each(createClipAllowedPattern)('の作成は$labelでもできる', async ({ parameters }) => await create(parameters));
+ test.each(createClipAllowedPattern)('の作成は$labelでもできる', async ({ parameters }) => {
+ await create(parameters);
+ });
const createClipDenyPattern = [
{ label: 'nameがnull', parameters: { name: null } },
@@ -233,11 +236,13 @@ describe('クリップ', () => {
assert.strictEqual(res.isFavorited, false);
});
- test.each(createClipAllowedPattern)('の更新は$labelでもできる', async ({ parameters }) => await update({
- clipId: (await create()).id,
- name: 'updated',
- ...parameters,
- }));
+ test.each(createClipAllowedPattern)('の更新は$labelでもできる', async ({ parameters }) => {
+ await update({
+ clipId: (await create()).id,
+ name: 'updated',
+ ...parameters,
+ });
+ });
test.each([
{ label: 'clipIdがnull', parameters: { clipId: null } },
diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts
index 43a73163eba..5b92abba16b 100644
--- a/packages/backend/test/e2e/drive.ts
+++ b/packages/backend/test/e2e/drive.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, test } from 'vitest';
import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index 469f19e2b92..09198384c4d 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -6,10 +6,11 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, test, expect } from 'vitest';
// node-fetch only supports it's own Blob yet
// https://github.com/node-fetch/node-fetch/pull/1664
import { Blob } from 'node-fetch';
-import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
+import { api, castAsError, initTestDb, post, role, signup, simpleGet, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
import { MiUser } from '@/models/_.js';
@@ -581,6 +582,30 @@ describe('Endpoints', () => {
});
describe('drive/files/create', () => {
+ const assignRole = async (userId: string, policies: Record) => {
+ const createdRole = await role(alice, {}, policies);
+
+ const assign = await api('admin/roles/assign', {
+ userId,
+ roleId: createdRole.id,
+ }, alice);
+
+ assert.strictEqual(assign.status, 204);
+
+ return createdRole;
+ };
+
+ const cleanupRole = async (userId: string, roleId: string) => {
+ await api('admin/roles/unassign', {
+ userId,
+ roleId,
+ }, alice);
+
+ await api('admin/roles/delete', {
+ roleId,
+ }, alice);
+ };
+
test('ファイルを作成できる', async () => {
const res = await uploadFile(alice);
@@ -659,6 +684,104 @@ describe('Endpoints', () => {
assert.strictEqual(webpublicType, 'image/webp');
});
}
+
+ test('uploadableFileTypes が */* なら任意のファイルをアップロードできる', async () => {
+ const createdRole = await assignRole(bob.id, {
+ uploadableFileTypes: {
+ useDefault: false,
+ priority: 1,
+ value: ['*/*'],
+ },
+ });
+
+ try {
+ const res = await uploadFile(bob, {
+ blob: new Blob([new Uint8Array(10)]),
+ });
+
+ assert.strictEqual(res.status, 200);
+ } finally {
+ await cleanupRole(bob.id, createdRole.id);
+ }
+ });
+
+ test('uploadableFileTypes に含まれない MIME type は拒否される', async () => {
+ const createdRole = await assignRole(bob.id, {
+ uploadableFileTypes: {
+ useDefault: false,
+ priority: 1,
+ value: ['image/png'],
+ },
+ });
+
+ try {
+ const res = await uploadFile(bob, { path: '192.jpg' });
+
+ assert.strictEqual(res.status, 400);
+ assert.ok(res.body);
+ assert.strictEqual(castAsError(res.body).error.code, 'UNALLOWED_FILE_TYPE');
+ } finally {
+ await cleanupRole(bob.id, createdRole.id);
+ }
+ });
+
+ test('maxFileSizeMb 制限付きロールでも制限内ならアップロードできる', async () => {
+ const allowAllTypesRole = await assignRole(bob.id, {
+ uploadableFileTypes: {
+ useDefault: false,
+ priority: 1,
+ value: ['*/*'],
+ },
+ });
+ const tinyAttachmentRole = await assignRole(bob.id, {
+ maxFileSizeMb: {
+ useDefault: false,
+ priority: 1,
+ value: 10 / 1024 / 1024, // 10バイト
+ },
+ });
+
+ try {
+ const res = await uploadFile(bob, {
+ blob: new Blob([new Uint8Array(10)]),
+ });
+
+ assert.strictEqual(res.status, 200);
+ } finally {
+ await cleanupRole(bob.id, tinyAttachmentRole.id);
+ await cleanupRole(bob.id, allowAllTypesRole.id);
+ }
+ });
+
+ test('maxFileSizeMb 制限を超えると 413 になる', async () => {
+ const allowAllTypesRole = await assignRole(bob.id, {
+ uploadableFileTypes: {
+ useDefault: false,
+ priority: 1,
+ value: ['*/*'],
+ },
+ });
+ const tinyAttachmentRole = await assignRole(bob.id, {
+ maxFileSizeMb: {
+ useDefault: false,
+ priority: 1,
+ value: 10 / 1024 / 1024, // 10バイト
+ },
+ });
+
+ try {
+ const res = await uploadFile(bob, {
+ blob: new Blob([new Uint8Array(11)]),
+ });
+
+ assert.strictEqual(res.status, 413);
+ assert.ok(res.body);
+ assert.strictEqual(castAsError(res.body).error.code, 'MAX_FILE_SIZE_EXCEEDED');
+ } finally {
+ await cleanupRole(bob.id, tinyAttachmentRole.id);
+ await cleanupRole(bob.id, allowAllTypesRole.id);
+ }
+ });
});
describe('drive/files/update', () => {
diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts
index 19433f3c882..1742506306c 100644
--- a/packages/backend/test/e2e/exports.ts
+++ b/packages/backend/test/e2e/exports.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest';
import { api, port, post, signup, startJobQueue } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index f00843de106..f2d1c8e2e26 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { beforeAll, beforeEach, describe, test } from 'vitest';
import { api, channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
import type { SimpleGetResponse } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/fetch-validate-ap-deny.ts b/packages/backend/test/e2e/fetch-validate-ap-deny.ts
index 434a9fe209b..44e29258850 100644
--- a/packages/backend/test/e2e/fetch-validate-ap-deny.ts
+++ b/packages/backend/test/e2e/fetch-validate-ap-deny.ts
@@ -5,6 +5,7 @@
process.env.NODE_ENV = 'test';
+import { beforeAll, describe, test, expect } from 'vitest';
import { validateContentTypeSetAsActivityPub, validateContentTypeSetAsJsonLD } from '@/core/activitypub/misc/validator.js';
import { signup, uploadFile, relativeFetch } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts
index 02582ae815c..e0891d424d5 100644
--- a/packages/backend/test/e2e/ff-visibility.ts
+++ b/packages/backend/test/e2e/ff-visibility.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, test } from 'vitest';
import { api, signup, simpleGet } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts
index fd798bdb25b..791b4f1d9b4 100644
--- a/packages/backend/test/e2e/move.ts
+++ b/packages/backend/test/e2e/move.ts
@@ -9,6 +9,7 @@ process.env.NODE_ENV = 'test';
import { setTimeout } from 'node:timers/promises';
import * as assert from 'assert';
+import { afterAll, beforeAll, afterEach, describe, test } from 'vitest';
import { loadConfig } from '@/config.js';
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts
index b464c242870..f5cc875e7cb 100644
--- a/packages/backend/test/e2e/mute.ts
+++ b/packages/backend/test/e2e/mute.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { beforeAll, describe, test } from 'vitest';
import { api, post, react, signup, waitFire } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts
index 28b96fe8c89..8fe910ed780 100644
--- a/packages/backend/test/e2e/nodeinfo.ts
+++ b/packages/backend/test/e2e/nodeinfo.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, test } from 'vitest';
import { relativeFetch } from '../utils.js';
describe('nodeinfo', () => {
diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts
index 5937eb9b492..4e506a62027 100644
--- a/packages/backend/test/e2e/note.ts
+++ b/packages/backend/test/e2e/note.ts
@@ -8,6 +8,7 @@ import type { Repository } from "typeorm";
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, afterAll, test } from 'vitest';
import { MiNote } from '@/models/Note.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts
index 67a9026eb5f..82816f705ee 100644
--- a/packages/backend/test/e2e/oauth.ts
+++ b/packages/backend/test/e2e/oauth.ts
@@ -11,6 +11,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest';
import {
AuthorizationCode,
type AuthorizationTokenConfig,
diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts
index 0f636b9ae20..785c9dff8bb 100644
--- a/packages/backend/test/e2e/renote-mute.ts
+++ b/packages/backend/test/e2e/renote-mute.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { beforeAll, describe, test } from 'vitest';
import { setTimeout } from 'node:timers/promises';
import { api, post, signup, waitFire } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/reversi-game.ts b/packages/backend/test/e2e/reversi-game.ts
index 788255beac1..7ff8d09cf33 100644
--- a/packages/backend/test/e2e/reversi-game.ts
+++ b/packages/backend/test/e2e/reversi-game.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, test } from 'vitest';
import { ReversiMatchResponse } from 'misskey-js/entities.js';
import { api, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts
index 72f26a38e0f..18762ac3832 100644
--- a/packages/backend/test/e2e/streaming.ts
+++ b/packages/backend/test/e2e/streaming.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { describe, beforeAll, test } from 'vitest';
import { WebSocket } from 'ws';
import { MiFollowing } from '@/models/Following.js';
import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js';
@@ -172,7 +173,7 @@ describe('Streaming', () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts
- msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo',
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.replyId === note.id,
);
assert.strictEqual(fired, true);
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index c98d199f35d..a6ff6f1f5eb 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -4,7 +4,14 @@
*/
import { entities } from 'misskey-js';
-import { beforeEach, describe, test } from '@jest/globals';
+import {
+ beforeEach,
+ beforeAll,
+ afterAll,
+ describe,
+ expect,
+ test,
+} from 'vitest';
import {
api,
captureWebhook,
diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts
index cb0f68dfeab..8910954cd69 100644
--- a/packages/backend/test/e2e/synalio/user-create.ts
+++ b/packages/backend/test/e2e/synalio/user-create.ts
@@ -5,7 +5,7 @@
import { setTimeout } from 'node:timers/promises';
import { entities } from 'misskey-js';
-import { beforeEach, describe, test } from '@jest/globals';
+import { beforeEach, describe, test, beforeAll, afterAll, expect } from 'vitest';
import {
api,
captureWebhook,
diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts
index 1edc178fc20..e7723791251 100644
--- a/packages/backend/test/e2e/thread-mute.ts
+++ b/packages/backend/test/e2e/thread-mute.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { beforeAll, describe, test } from 'vitest';
import { api, connectStream, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index 4fd826100d0..8a23657772a 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -9,6 +9,7 @@
// pnpm jest -- e2e/timelines.ts
import * as assert from 'assert';
+import { describe, beforeAll, test } from 'vitest';
import { setTimeout } from 'node:timers/promises';
import { entities } from 'misskey-js';
import { Redis } from 'ioredis';
diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts
index cc07c5ae71a..2f89ac54ceb 100644
--- a/packages/backend/test/e2e/user-notes.ts
+++ b/packages/backend/test/e2e/user-notes.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { beforeAll, describe, test } from 'vitest';
import { api, post, signup, uploadUrl } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index a342bba64cf..be5fb3b0a70 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { beforeAll, beforeEach, describe, test } from 'vitest';
import { inspect } from 'node:util';
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts
index 538a990a4eb..51774c68a66 100644
--- a/packages/backend/test/e2e/well-known.ts
+++ b/packages/backend/test/e2e/well-known.ts
@@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
+import { beforeAll, describe, test } from 'vitest';
import { api, host, origin, relativeFetch, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
diff --git a/packages/backend/test/eslint.config.js b/packages/backend/test/eslint.config.js
index a0f43babadf..e3bcf4c0fe2 100644
--- a/packages/backend/test/eslint.config.js
+++ b/packages/backend/test/eslint.config.js
@@ -9,7 +9,6 @@ export default [
languageOptions: {
globals: {
...globals.node,
- ...globals.jest,
},
parserOptions: {
parser: tsParser,
diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts
index b26ae094443..a2412288b49 100644
--- a/packages/backend/test/prelude/url.ts
+++ b/packages/backend/test/prelude/url.ts
@@ -4,6 +4,7 @@
*/
import * as assert from 'assert';
+import { describe, test } from 'vitest';
import { query } from '../../src/misc/prelude/url.js';
describe('url', () => {
diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/setup.e2e.ts
similarity index 67%
rename from packages/backend/test/jest.setup.ts
rename to packages/backend/test/setup.e2e.ts
index 9185f58acb8..3141dc15ad3 100644
--- a/packages/backend/test/jest.setup.ts
+++ b/packages/backend/test/setup.e2e.ts
@@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { beforeAll } from 'vitest';
import { initTestDb, sendEnvResetRequest } from './utils.js';
beforeAll(async () => {
- await initTestDb(false);
- await sendEnvResetRequest();
+ await initTestDb(false);
+ await sendEnvResetRequest();
});
-
diff --git a/packages/backend/test/jest.setup.unit.cjs b/packages/backend/test/setup.unit.ts
similarity index 86%
rename from packages/backend/test/jest.setup.unit.cjs
rename to packages/backend/test/setup.unit.ts
index dd879c81c88..e9adab9d64d 100644
--- a/packages/backend/test/jest.setup.unit.cjs
+++ b/packages/backend/test/setup.unit.ts
@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-module.exports = async () => {
+export default function setup() {
// DBはUTC(っぽい)ので、テスト側も合わせておく
process.env.TZ = 'UTC';
process.env.NODE_ENV = 'test';
-};
+}
diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json
index a2a86c696ea..b857cf927a0 100644
--- a/packages/backend/test/tsconfig.json
+++ b/packages/backend/test/tsconfig.json
@@ -3,7 +3,7 @@
"allowJs": true,
"noEmitOnError": false,
"noImplicitAny": true,
- "noImplicitReturns": true,
+ "noImplicitReturns": false,
"noUnusedParameters": false,
"noUnusedLocals": false,
"noFallthroughCasesInSwitch": true,
@@ -35,7 +35,7 @@
"lib": [
"esnext"
],
- "types": ["jest", "node"]
+ "types": ["node"]
},
"compileOnSave": false,
"include": [
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
index 9dad8e229df..f4d8e9098ec 100644
--- a/packages/backend/test/unit/AbuseReportNotificationService.ts
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { describe, jest } from '@jest/globals';
+import { describe, expect, beforeAll, afterAll, beforeEach, afterEach, test, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
@@ -42,9 +43,9 @@ describe('AbuseReportNotificationService', () => {
let systemWebhooksRepository: SystemWebhooksRepository;
let abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository;
let idService: IdService;
- let roleService: jest.Mocked;
- let emailService: jest.Mocked;
- let webhookService: jest.Mocked;
+ let roleService: Mocked;
+ let emailService: Mocked;
+ let webhookService: Mocked;
// --------------------------------------------------------------------------------------
@@ -107,10 +108,10 @@ describe('AbuseReportNotificationService', () => {
AbuseReportNotificationService,
IdService,
{
- provide: RoleService, useFactory: () => ({ getModeratorIds: jest.fn() }),
+ provide: RoleService, useFactory: () => ({ getModeratorIds: vi.fn() }),
},
{
- provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
+ provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: vi.fn() }),
},
{
provide: UserEntityService, useFactory: () => ({
@@ -119,16 +120,16 @@ describe('AbuseReportNotificationService', () => {
}),
},
{
- provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
+ provide: EmailService, useFactory: () => ({ sendEmail: vi.fn() }),
},
{
- provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
+ provide: MetaService, useFactory: () => ({ fetch: vi.fn() }),
},
{
provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }),
},
{
- provide: GlobalEventService, useFactory: () => ({ publishAdminStream: jest.fn() }),
+ provide: GlobalEventService, useFactory: () => ({ publishAdminStream: vi.fn() }),
},
],
})
@@ -141,9 +142,9 @@ describe('AbuseReportNotificationService', () => {
service = app.get(AbuseReportNotificationService);
idService = app.get(IdService);
- roleService = app.get(RoleService) as jest.Mocked;
- emailService = app.get(EmailService) as jest.Mocked;
- webhookService = app.get(SystemWebhookService) as jest.Mocked;
+ roleService = app.get(RoleService) as Mocked;
+ emailService = app.get(EmailService) as Mocked;
+ webhookService = app.get(SystemWebhookService) as Mocked;
app.enableShutdownHooks();
});
diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts
index b3f7f426fea..ea0a1abc175 100644
--- a/packages/backend/test/unit/AnnouncementService.ts
+++ b/packages/backend/test/unit/AnnouncementService.ts
@@ -5,8 +5,9 @@
process.env.NODE_ENV = 'test';
-import { jest } from '@jest/globals';
-import { ModuleMocker } from 'jest-mock';
+import { describe, expect, beforeEach, afterEach, test, vi } from 'vitest';
+import type { Mocked } from 'vitest';
+import { mockDeep } from 'vitest-mock-extended';
import { Test } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
@@ -26,9 +27,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
-
-const moduleMocker = new ModuleMocker(global);
describe('AnnouncementService', () => {
let app: TestingModule;
@@ -36,8 +34,8 @@ describe('AnnouncementService', () => {
let usersRepository: UsersRepository;
let announcementsRepository: AnnouncementsRepository;
let announcementReadsRepository: AnnouncementReadsRepository;
- let globalEventService: jest.Mocked;
- let moderationLogService: jest.Mocked;
+ let globalEventService: Mocked;
+ let moderationLogService: Mocked;
function createUser(data: Partial = {}) {
const un = secureRndstr(16);
@@ -76,17 +74,15 @@ describe('AnnouncementService', () => {
.useMocker((token) => {
if (token === GlobalEventService) {
return {
- publishMainStream: jest.fn(),
- publishBroadcastStream: jest.fn(),
+ publishMainStream: vi.fn(),
+ publishBroadcastStream: vi.fn(),
};
} else if (token === ModerationLogService) {
return {
- log: jest.fn(),
+ log: vi.fn(),
};
} else if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
- const Mock = moduleMocker.generateFromMetadata(mockMetadata);
- return new Mock();
+ return mockDeep();
}
})
.compile();
@@ -97,8 +93,8 @@ describe('AnnouncementService', () => {
usersRepository = app.get(DI.usersRepository);
announcementsRepository = app.get(DI.announcementsRepository);
announcementReadsRepository = app.get(DI.announcementReadsRepository);
- globalEventService = app.get(GlobalEventService) as jest.Mocked;
- moderationLogService = app.get(ModerationLogService) as jest.Mocked;
+ globalEventService = app.get(GlobalEventService) as Mocked;
+ moderationLogService = app.get(ModerationLogService) as Mocked;
});
afterEach(async () => {
@@ -203,7 +199,7 @@ describe('AnnouncementService', () => {
});
});
- describe('read', () => {
+ describe.todo('read', () => {
// TODO
});
});
diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts
index 93efa5d7d35..c020ca98c09 100644
--- a/packages/backend/test/unit/ApMfmService.ts
+++ b/packages/backend/test/unit/ApMfmService.ts
@@ -4,6 +4,7 @@
*/
import * as assert from 'assert';
+import { describe, test, beforeAll } from 'vitest';
import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts
index 24bb81118ed..5a013d5cd2b 100644
--- a/packages/backend/test/unit/CaptchaService.ts
+++ b/packages/backend/test/unit/CaptchaService.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { afterAll, beforeAll, beforeEach, describe, expect, jest } from '@jest/globals';
+import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { Response } from 'node-fetch';
import {
@@ -22,8 +23,8 @@ import { LoggerService } from '@/core/LoggerService.js';
describe('CaptchaService', () => {
let app: TestingModule;
let service: CaptchaService;
- let httpRequestService: jest.Mocked;
- let metaService: jest.Mocked;
+ let httpRequestService: Mocked;
+ let metaService: Mocked;
beforeAll(async () => {
app = await Test.createTestingModule({
@@ -34,12 +35,12 @@ describe('CaptchaService', () => {
CaptchaService,
LoggerService,
{
- provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }),
+ provide: HttpRequestService, useFactory: () => ({ send: vi.fn() }),
},
{
provide: MetaService, useFactory: () => ({
- fetch: jest.fn(),
- update: jest.fn(),
+ fetch: vi.fn(),
+ update: vi.fn(),
}),
},
],
@@ -48,8 +49,8 @@ describe('CaptchaService', () => {
app.enableShutdownHooks();
service = app.get(CaptchaService);
- httpRequestService = app.get(HttpRequestService) as jest.Mocked;
- metaService = app.get(MetaService) as jest.Mocked;
+ httpRequestService = app.get(HttpRequestService) as Mocked;
+ metaService = app.get(MetaService) as Mocked;
});
beforeEach(() => {
diff --git a/packages/backend/test/unit/ChannelFollowingService.ts b/packages/backend/test/unit/ChannelFollowingService.ts
index 2d3196f2f45..3b1ad72287f 100644
--- a/packages/backend/test/unit/ChannelFollowingService.ts
+++ b/packages/backend/test/unit/ChannelFollowingService.ts
@@ -5,7 +5,7 @@
/* eslint-disable */
-import { afterEach, beforeEach, describe, expect } from '@jest/globals';
+import { afterEach, beforeEach, describe, expect, beforeAll, afterAll, test } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
diff --git a/packages/backend/test/unit/ChannelMutingService.ts b/packages/backend/test/unit/ChannelMutingService.ts
index 6916701d1f4..0c59f141a46 100644
--- a/packages/backend/test/unit/ChannelMutingService.ts
+++ b/packages/backend/test/unit/ChannelMutingService.ts
@@ -5,7 +5,7 @@
/* eslint-disable */
-import { afterEach, beforeEach, describe, expect } from '@jest/globals';
+import { afterEach, beforeEach, describe, expect, beforeAll, afterAll, test } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
diff --git a/packages/backend/test/unit/CustomEmojiService.ts b/packages/backend/test/unit/CustomEmojiService.ts
index d6c73a20912..e8cad09e9b6 100644
--- a/packages/backend/test/unit/CustomEmojiService.ts
+++ b/packages/backend/test/unit/CustomEmojiService.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { afterEach, beforeAll, describe, test } from '@jest/globals';
+import { afterEach, beforeAll, describe, test, expect } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts
index 48b108fbba0..eafc49973bb 100644
--- a/packages/backend/test/unit/DriveService.ts
+++ b/packages/backend/test/unit/DriveService.ts
@@ -5,6 +5,7 @@
process.env.NODE_ENV = 'test';
+import { afterAll, beforeAll, beforeEach, describe, test, expect } from 'vitest';
import { Test } from '@nestjs/testing';
import {
DeleteObjectCommand,
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index 1e3605aafc9..d2c2dcf283e 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -5,7 +5,8 @@
process.env.NODE_ENV = 'test';
-import { jest } from '@jest/globals';
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { Test } from '@nestjs/testing';
import { Redis } from 'ioredis';
import type { TestingModule } from '@nestjs/testing';
@@ -18,22 +19,32 @@ import { UtilityService } from '@/core/UtilityService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
-function mockRedis() {
- const hash = {} as any;
- const set = jest.fn((key: string, value) => {
- const ret = hash[key];
- hash[key] = value;
- return ret;
+function createMockRedis() {
+ const store = new Map();
+
+ const del = vi.fn((key: string) => {
+ const existed = store.delete(key);
+ return Promise.resolve(existed ? 1 : 0);
});
- return set;
+
+ const set = vi.fn((key: string, value: string, ...args: any[]) => {
+ const prev = store.get(key) ?? null;
+ store.set(key, value);
+
+ // ioredis: SET key value ... GET => returns old value or null
+ const hasGet = args.some(a => typeof a === 'string' && a.toUpperCase() === 'GET');
+ return Promise.resolve(hasGet ? prev : 'OK');
+ });
+
+ return { set, del };
}
describe('FetchInstanceMetadataService', () => {
let app: TestingModule;
- let fetchInstanceMetadataService: jest.Mocked;
- let federatedInstanceService: jest.Mocked;
- let httpRequestService: jest.Mocked;
- let redisClient: jest.Mocked;
+ let fetchInstanceMetadataService: Mocked;
+ let federatedInstanceService: Mocked;
+ let httpRequestService: Mocked;
+ let redisClient: Mocked;
beforeEach(async () => {
app = await Test
@@ -50,11 +61,11 @@ describe('FetchInstanceMetadataService', () => {
})
.useMocker((token) => {
if (token === HttpRequestService) {
- return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() };
+ return { getJson: vi.fn(), getHtml: vi.fn(), send: vi.fn() };
} else if (token === FederatedInstanceService) {
- return { fetchOrRegister: jest.fn() };
+ return { fetchOrRegister: vi.fn() };
} else if (token === DI.redis) {
- return mockRedis;
+ return createMockRedis();
}
return null;
})
@@ -62,23 +73,24 @@ describe('FetchInstanceMetadataService', () => {
app.enableShutdownHooks();
- fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as jest.Mocked;
- federatedInstanceService = app.get(FederatedInstanceService) as jest.Mocked;
- redisClient = app.get(DI.redis) as jest.Mocked;
- httpRequestService = app.get(HttpRequestService) as jest.Mocked;
+ fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as Mocked;
+ federatedInstanceService = app.get(FederatedInstanceService) as Mocked;
+ redisClient = app.get(DI.redis) as Mocked;
+ httpRequestService = app.get(HttpRequestService) as Mocked;
});
afterEach(async () => {
await app.close();
+ vi.resetAllMocks();
+ vi.clearAllMocks();
});
test('Lock and update', async () => {
- redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
- const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
- const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+ const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock');
+ const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
@@ -88,12 +100,11 @@ describe('FetchInstanceMetadataService', () => {
});
test('Lock and don\'t update', async () => {
- redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
- const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
- const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+ const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock');
+ const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
@@ -103,13 +114,12 @@ describe('FetchInstanceMetadataService', () => {
});
test('Do nothing when lock not acquired', async () => {
- redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
- const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
- const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+ const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock');
+ const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
@@ -119,13 +129,12 @@ describe('FetchInstanceMetadataService', () => {
});
test('Do when lock not acquired but forced', async () => {
- redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
- const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
- const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
+ const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock');
+ const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
expect(tryLockSpy).toHaveBeenCalledTimes(0);
diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts
index 28a2a971f47..fa4a13b1719 100644
--- a/packages/backend/test/unit/FileInfoService.ts
+++ b/packages/backend/test/unit/FileInfoService.ts
@@ -8,23 +8,20 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
-import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
-import { afterAll, beforeAll, describe, test } from '@jest/globals';
+import { afterAll, beforeAll, describe, test } from 'vitest';
+import { mockDeep } from 'vitest-mock-extended';
import { GlobalModule } from '@/GlobalModule.js';
import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
//import { DI } from '@/di-symbols.js';
import { AiService } from '@/core/AiService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const resources = `${_dirname}/../resources`;
-const moduleMocker = new ModuleMocker(global);
-
describe('FileInfoService', () => {
let app: TestingModule;
let fileInfoService: FileInfoService;
@@ -54,9 +51,7 @@ describe('FileInfoService', () => {
// return { };
//}
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
- const Mock = moduleMocker.generateFromMetadata(mockMetadata);
- return new Mock();
+ return mockDeep();
}
})
.compile();
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
index 91c2286ff60..a5f7a78195b 100644
--- a/packages/backend/test/unit/FlashService.ts
+++ b/packages/backend/test/unit/FlashService.ts
@@ -5,6 +5,7 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
+import { afterAll, afterEach, beforeEach, describe, expect, test } from 'vitest';
import { FlashService } from '@/core/FlashService.js';
import { IdService } from '@/core/IdService.js';
import { FlashLikesRepository, FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
diff --git a/packages/backend/test/unit/MetaService.ts b/packages/backend/test/unit/MetaService.ts
index 19c98eab3dc..4396e26f795 100644
--- a/packages/backend/test/unit/MetaService.ts
+++ b/packages/backend/test/unit/MetaService.ts
@@ -5,7 +5,7 @@
process.env.NODE_ENV = 'test';
-import { jest } from '@jest/globals';
+import { afterAll, beforeAll, describe, test, expect, vi } from 'vitest';
import { Test } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { DI } from '@/di-symbols.js';
@@ -40,7 +40,7 @@ describe('MetaService', () => {
test('fetch (cache)', async () => {
const db = app.get(DI.db);
- const spy = jest.spyOn(db, 'transaction');
+ const spy = vi.spyOn(db, 'transaction');
const result = await metaService.fetch();
@@ -50,7 +50,7 @@ describe('MetaService', () => {
test('fetch (force)', async () => {
const db = app.get(DI.db);
- const spy = jest.spyOn(db, 'transaction');
+ const spy = vi.spyOn(db, 'transaction');
const result = await metaService.fetch(true);
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index 2f5f3745dea..358209b4d78 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -5,6 +5,7 @@
import * as assert from 'assert';
import * as mfm from 'mfm-js';
+import { beforeAll, describe, test } from 'vitest';
import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts
index f3d3d1da99f..3e493fd3d9a 100644
--- a/packages/backend/test/unit/NoteCreateService.ts
+++ b/packages/backend/test/unit/NoteCreateService.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { beforeAll, describe, test, expect } from 'vitest';
import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts
index 3cfb4ff3f82..8093e3a3ec1 100644
--- a/packages/backend/test/unit/ReactionService.ts
+++ b/packages/backend/test/unit/ReactionService.ts
@@ -4,6 +4,7 @@
*/
import * as assert from 'assert';
+import { beforeAll, describe, test } from 'vitest';
import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts
index bee580d0c75..0d3ce83b116 100644
--- a/packages/backend/test/unit/RelayService.ts
+++ b/packages/backend/test/unit/RelayService.ts
@@ -5,11 +5,11 @@
process.env.NODE_ENV = 'test';
-import { jest } from '@jest/globals';
+import { afterAll, beforeAll, describe, test, expect, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { Test } from '@nestjs/testing';
-import { ModuleMocker } from 'jest-mock';
+import { mockDeep } from 'vitest-mock-extended';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdService } from '@/core/IdService.js';
@@ -19,12 +19,10 @@ import { SystemAccountService } from '@/core/SystemAccountService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { UtilityService } from '@/core/UtilityService.js';
-const moduleMocker = new ModuleMocker(global);
-
describe('RelayService', () => {
let app: TestingModule;
let relayService: RelayService;
- let queueService: jest.Mocked;
+ let queueService: Mocked;
beforeAll(async () => {
app = await Test.createTestingModule({
@@ -42,12 +40,10 @@ describe('RelayService', () => {
})
.useMocker((token) => {
if (token === QueueService) {
- return { deliver: jest.fn() };
+ return { deliver: vi.fn() };
}
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
- const Mock = moduleMocker.generateFromMetadata(mockMetadata);
- return new Mock();
+ return mockDeep();
}
})
.compile();
@@ -55,7 +51,7 @@ describe('RelayService', () => {
app.enableShutdownHooks();
relayService = app.get(RelayService);
- queueService = app.get(QueueService) as jest.Mocked;
+ queueService = app.get(QueueService) as Mocked;
});
afterAll(async () => {
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index 9b17b1fbb9e..ec1e7ca1348 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -6,12 +6,12 @@
process.env.NODE_ENV = 'test';
import { setTimeout } from 'node:timers/promises';
-import { describe, jest } from '@jest/globals';
-import { ModuleMocker } from 'jest-mock';
+import { describe, beforeEach, afterEach, test, expect, vi } from 'vitest';
+import type { Mocked } from 'vitest';
+import { mockDeep } from 'vitest-mock-extended';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import {
@@ -34,17 +34,15 @@ import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-const moduleMocker = new ModuleMocker(global);
-
describe('RoleService', () => {
let app: TestingModule;
let roleService: RoleService;
let usersRepository: UsersRepository;
let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository;
- let meta: jest.Mocked;
- let notificationService: jest.Mocked;
- let clock: lolex.InstalledClock;
+ let meta: Mocked;
+ let notificationService: Mocked;
+ let clock: lolex.Clock;
async function createUser(data: Partial = {}) {
const un = secureRndstr(16);
@@ -123,7 +121,7 @@ describe('RoleService', () => {
{
provide: NotificationService,
useFactory: () => ({
- createNotification: jest.fn(),
+ createNotification: vi.fn(),
}),
},
{
@@ -134,12 +132,10 @@ describe('RoleService', () => {
})
.useMocker((token) => {
if (token === MetaService) {
- return { fetch: jest.fn() };
+ return { fetch: vi.fn() };
}
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
- const Mock = moduleMocker.generateFromMetadata(mockMetadata);
- return new Mock();
+ return mockDeep();
}
})
.compile();
@@ -151,8 +147,8 @@ describe('RoleService', () => {
rolesRepository = app.get(DI.rolesRepository);
roleAssignmentsRepository = app.get(DI.roleAssignmentsRepository);
- meta = app.get(DI.meta) as jest.Mocked;
- notificationService = app.get(NotificationService) as jest.Mocked;
+ meta = app.get(DI.meta) as Mocked;
+ notificationService = app.get(NotificationService) as Mocked;
await roleService.onModuleInit();
});
@@ -163,7 +159,7 @@ describe('RoleService', () => {
/**
* Delete meta and roleAssignment first to avoid deadlock due to schema dependencies
* https://github.com/misskey-dev/misskey/issues/16783
- */
+ */
await app.get(DI.metasRepository).createQueryBuilder().delete().execute();
await roleAssignmentsRepository.createQueryBuilder().delete().execute();
await Promise.all([
@@ -700,6 +696,19 @@ describe('RoleService', () => {
expect(adminIds).toHaveLength(0);
});
+ test('should not include duplicate user IDs if a user has multiple administrator roles', async () => {
+ const adminUser = await createUser();
+ const adminRole1 = await createRole({ name: 'admin1', isAdministrator: true });
+ const adminRole2 = await createRole({ name: 'admin2', isAdministrator: true });
+
+ await roleService.assign(adminUser.id, adminRole1.id);
+ await roleService.assign(adminUser.id, adminRole2.id);
+
+ const adminIds = await roleService.getAdministratorIds();
+
+ expect(adminIds).toEqual([adminUser.id]);
+ });
+
// TODO: rootユーザーは現在実装に含まれていないため、テストもそれに倣う
test('should not include the root user', async () => {
const rootUser = await createUser();
diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts
index 6e7e5a8b598..dce1e2065d3 100644
--- a/packages/backend/test/unit/S3Service.ts
+++ b/packages/backend/test/unit/S3Service.ts
@@ -5,6 +5,7 @@
process.env.NODE_ENV = 'test';
+import { afterAll, beforeAll, beforeEach, describe, test, expect } from 'vitest';
import { Test } from '@nestjs/testing';
import {
CompleteMultipartUploadCommand,
diff --git a/packages/backend/test/unit/SearchService.ts b/packages/backend/test/unit/SearchService.ts
index 6e17bef1c32..867d1a424f2 100644
--- a/packages/backend/test/unit/SearchService.ts
+++ b/packages/backend/test/unit/SearchService.ts
@@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { afterAll, afterEach, beforeAll, describe, expect, test } from '@jest/globals';
+import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
-import type { Index, MeiliSearch } from 'meilisearch';
+import type { Index, Meilisearch } from 'meilisearch';
import { type Config, loadConfig } from '@/config.js';
import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
@@ -416,7 +416,7 @@ describe('SearchService', () => {
describe('meilisearch', () => {
let ctx: TestContext;
- let meilisearch: MeiliSearch;
+ let meilisearch: Meilisearch;
let meilisearchIndex: Index;
let meiliConfig: Config;
@@ -438,7 +438,7 @@ describe('SearchService', () => {
};
ctx = await buildContext(meiliConfig);
- meilisearch = ctx.app.get(DI.meilisearch) as MeiliSearch;
+ meilisearch = ctx.app.get(DI.meilisearch) as Meilisearch;
meilisearchIndex = meilisearch.index(`${meiliConfig.meilisearch!.index}---notes`);
const settingsTask = await meilisearchIndex.updateSettings(meilisearchSettings);
diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
index 8ef46024ac4..f989d2d29a6 100644
--- a/packages/backend/test/unit/SigninWithPasskeyApiService.ts
+++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
@@ -4,12 +4,12 @@
*/
import { IncomingHttpHeaders } from 'node:http';
-import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals';
+import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { mockDeep } from 'vitest-mock-extended';
import { Test, TestingModule } from '@nestjs/testing';
import { FastifyReply, FastifyRequest } from 'fastify';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { HttpHeader } from 'fastify/types/utils.js';
-import { MockMetadata, ModuleMocker } from 'jest-mock';
import { MiUser } from '@/models/User.js';
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
@@ -22,8 +22,6 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
import { SigninService } from '@/server/api/SigninService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
-const moduleMocker = new ModuleMocker(global);
-
class FakeLimiter {
public async limit() {
return;
@@ -95,9 +93,7 @@ describe('SigninWithPasskeyApiService', () => {
],
}).useMocker((token) => {
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
- const Mock = moduleMocker.generateFromMetadata(mockMetadata);
- return new Mock();
+ return mockDeep();
}
}).compile();
passkeyApiService = app.get(SigninWithPasskeyApiService);
@@ -112,7 +108,7 @@ describe('SigninWithPasskeyApiService', () => {
FakeWebauthnVerify = async () => {
return uid;
};
- jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify);
+ vi.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify);
const dummyUser = {
id: uid, username: uid, usernameLower: uid.toLowerCase(), uri: null, host: null,
@@ -159,7 +155,7 @@ describe('SigninWithPasskeyApiService', () => {
it('Should return 403 When Challenge Verify fail', async () => {
const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
const res = new DummyFastifyReply() as FastifyReply;
- jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication')
+ vi.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication')
.mockImplementation(async () => {
throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
});
diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
index 1128d83be1b..a6534a5b1be 100644
--- a/packages/backend/test/unit/SystemWebhookService.ts
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -5,7 +5,8 @@
*/
import { setTimeout } from 'node:timers/promises';
-import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
+import { afterEach, beforeEach, afterAll, beforeAll, describe, test, expect, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { MiUser } from '@/models/User.js';
@@ -29,7 +30,7 @@ describe('SystemWebhookService', () => {
let usersRepository: UsersRepository;
let systemWebhooksRepository: SystemWebhooksRepository;
let idService: IdService;
- let queueService: jest.Mocked;
+ let queueService: Mocked;
// --------------------------------------------------------------------------------------
@@ -73,7 +74,7 @@ describe('SystemWebhookService', () => {
LoggerService,
GlobalEventService,
{
- provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
+ provide: QueueService, useFactory: () => ({ systemWebhookDeliver: vi.fn() }),
},
{
provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }),
@@ -87,7 +88,7 @@ describe('SystemWebhookService', () => {
service = app.get(SystemWebhookService);
idService = app.get(IdService);
- queueService = app.get(QueueService) as jest.Mocked;
+ queueService = app.get(QueueService) as Mocked;
app.enableShutdownHooks();
}
diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts
index 75d3e58adc7..28fcb0cb5a2 100644
--- a/packages/backend/test/unit/UserSearchService.ts
+++ b/packages/backend/test/unit/UserSearchService.ts
@@ -4,7 +4,7 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
-import { describe, jest, test } from '@jest/globals';
+import { describe, beforeEach, beforeAll, afterEach, afterAll, vi, test, expect } from 'vitest';
import { In } from 'typeorm';
import { UserSearchService } from '@/core/UserSearchService.js';
import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@@ -92,7 +92,7 @@ describe('UserSearchService', () => {
providers: [
UserSearchService,
{
- provide: UserEntityService, useFactory: jest.fn(() => ({
+ provide: UserEntityService, useFactory: vi.fn(() => ({
// とりあえずIDが返れば確認が出来るので
packMany: (value: any) => value,
})),
diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts
index 928b9d3c2b0..5250b66cc65 100644
--- a/packages/backend/test/unit/UserWebhookService.ts
+++ b/packages/backend/test/unit/UserWebhookService.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
+import { afterEach, beforeEach, describe, expect, test, beforeAll, afterAll, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { MiUser } from '@/models/User.js';
@@ -25,7 +26,7 @@ describe('UserWebhookService', () => {
let usersRepository: UsersRepository;
let userWebhooksRepository: WebhooksRepository;
let idService: IdService;
- let queueService: jest.Mocked;
+ let queueService: Mocked;
// --------------------------------------------------------------------------------------
@@ -70,7 +71,7 @@ describe('UserWebhookService', () => {
LoggerService,
GlobalEventService,
{
- provide: QueueService, useFactory: () => ({ userWebhookDeliver: jest.fn() }),
+ provide: QueueService, useFactory: () => ({ userWebhookDeliver: vi.fn() }),
},
],
})
@@ -81,7 +82,7 @@ describe('UserWebhookService', () => {
service = app.get(UserWebhookService);
idService = app.get(IdService);
- queueService = app.get(QueueService) as jest.Mocked;
+ queueService = app.get(QueueService) as Mocked;
app.enableShutdownHooks();
}
diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts
index 0e965021c2a..91a17719d2c 100644
--- a/packages/backend/test/unit/WebhookTestService.ts
+++ b/packages/backend/test/unit/WebhookTestService.ts
@@ -5,7 +5,8 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
-import { beforeAll, describe, jest } from '@jest/globals';
+import { beforeAll, afterAll, beforeEach, afterEach, test, expect, describe, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { WebhookTestService } from '@/core/WebhookTestService.js';
import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
@@ -24,9 +25,9 @@ describe('WebhookTestService', () => {
let usersRepository: UsersRepository;
let userProfilesRepository: UserProfilesRepository;
- let queueService: jest.Mocked;
- let userWebhookService: jest.Mocked;
- let systemWebhookService: jest.Mocked;
+ let queueService: Mocked;
+ let userWebhookService: Mocked;
+ let systemWebhookService: Mocked;
let idService: IdService;
let root: MiUser;
@@ -59,23 +60,23 @@ describe('WebhookTestService', () => {
IdService,
{
provide: CustomEmojiService, useFactory: () => ({
- populateEmojis: jest.fn(),
+ populateEmojis: vi.fn(),
}),
},
{
provide: QueueService, useFactory: () => ({
- systemWebhookDeliver: jest.fn(),
- userWebhookDeliver: jest.fn(),
+ systemWebhookDeliver: vi.fn(),
+ userWebhookDeliver: vi.fn(),
}),
},
{
provide: UserWebhookService, useFactory: () => ({
- fetchWebhooks: jest.fn(),
+ fetchWebhooks: vi.fn(),
}),
},
{
provide: SystemWebhookService, useFactory: () => ({
- fetchSystemWebhooks: jest.fn(),
+ fetchSystemWebhooks: vi.fn(),
}),
},
],
@@ -86,9 +87,9 @@ describe('WebhookTestService', () => {
service = app.get(WebhookTestService);
idService = app.get(IdService);
- queueService = app.get(QueueService) as jest.Mocked;
- userWebhookService = app.get(UserWebhookService) as jest.Mocked;
- systemWebhookService = app.get(SystemWebhookService) as jest.Mocked;
+ queueService = app.get(QueueService) as Mocked;
+ userWebhookService = app.get(UserWebhookService) as Mocked;
+ systemWebhookService = app.get(SystemWebhookService) as Mocked;
app.enableShutdownHooks();
});
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index c6e09bdda2d..1ad61001f05 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -9,8 +9,8 @@ import * as assert from 'assert';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
+import { describe, beforeAll, beforeEach, test, vi } from 'vitest';
import { Test } from '@nestjs/testing';
-import { jest } from '@jest/globals';
import { MockResolver } from '../misc/mock-resolver.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
@@ -155,7 +155,7 @@ describe('ActivityPub', () => {
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
const federatedInstanceService = app.get(FederatedInstanceService);
- jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { }));
+ vi.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { }));
});
beforeEach(() => {
@@ -224,6 +224,51 @@ describe('ActivityPub', () => {
});
});
+ describe('alsoKnownAs field', () => {
+ test('Handle alsoKnownAs as an array', async () => {
+ const actor = {
+ ...createRandomActor(),
+ alsoKnownAs: ['https://example.com/users/alice', 'https://example.com/users/alice2'],
+ };
+
+ resolver.register(actor.id, actor);
+
+ const user = await personService.createPerson(actor.id, resolver);
+
+ assert.deepStrictEqual(user.alsoKnownAs, actor.alsoKnownAs);
+ });
+
+ test('Handle alsoKnownAs as a string', async () => {
+ const actor = {
+ ...createRandomActor(),
+ alsoKnownAs: 'https://example.com/users/alice',
+ };
+
+ resolver.register(actor.id, actor);
+
+ const user = await personService.createPerson(actor.id, resolver);
+
+ assert.deepStrictEqual(user.alsoKnownAs, [actor.alsoKnownAs]);
+ });
+
+ test('Update person with alsoKnownAs as a string', async () => {
+ const actor = createRandomActor();
+ resolver.register(actor.id, actor);
+ const user = await personService.createPerson(actor.id, resolver);
+
+ const updatedActor = {
+ ...actor,
+ alsoKnownAs: 'https://example.com/users/alice',
+ };
+ resolver.register(actor.id, updatedActor);
+
+ await personService.updatePerson(actor.id, resolver, updatedActor);
+
+ const updatedUser = await personService.fetchPerson(actor.id);
+ assert.deepStrictEqual(updatedUser?.alsoKnownAs, [updatedActor.alsoKnownAs]);
+ });
+ });
+
describe('Collection visibility', () => {
test('Public following/followers', async () => {
const actor = createRandomActor();
diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts
index f8b2a697f2b..83bcdb717b3 100644
--- a/packages/backend/test/unit/ap-request.ts
+++ b/packages/backend/test/unit/ap-request.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { describe, test } from 'vitest';
import * as assert from 'assert';
import httpSignature from '@peertube/http-signature';
@@ -78,7 +79,7 @@ describe('ap-request', () => {
'https://alice.example.com/abc',
FetchAllowSoftFailMask.Any,
), 'validation should fail no matter what if the response URL is inconsistent with the object ID');
-
+
assert.doesNotThrow(() => assertActivityMatchesUrl(
'https://alice.example.com/abc#test',
{ id: 'https://alice.example.com/abc' } as IObject,
diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts
index 364a2c2fbd5..7c10e4de721 100644
--- a/packages/backend/test/unit/chart.ts
+++ b/packages/backend/test/unit/chart.ts
@@ -6,7 +6,8 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
-import { jest } from '@jest/globals';
+import { describe, beforeEach, afterEach, afterAll, test } from 'vitest';
+import type { Mocked } from 'vitest';
import * as lolex from '@sinonjs/fake-timers';
import { DataSource } from 'typeorm';
import * as Redis from 'ioredis';
@@ -28,13 +29,13 @@ describe('Chart', () => {
let redisClient = {
set: () => Promise.resolve('OK'),
get: () => Promise.resolve(null),
- } as unknown as jest.Mocked;
+ } as unknown as Mocked;
let testChart: TestChart;
let testGroupedChart: TestGroupedChart;
let testUniqueChart: TestUniqueChart;
let testIntersectionChart: TestIntersectionChart;
- let clock: lolex.InstalledClock;
+ let clock: lolex.Clock;
beforeEach(async () => {
if (db) db.destroy();
diff --git a/packages/backend/test/unit/entities/DriveFileEntityService.ts b/packages/backend/test/unit/entities/DriveFileEntityService.ts
index 2e416326ee3..8fca5ffe12b 100644
--- a/packages/backend/test/unit/entities/DriveFileEntityService.ts
+++ b/packages/backend/test/unit/entities/DriveFileEntityService.ts
@@ -5,7 +5,7 @@
process.env.NODE_ENV = 'test';
-import { afterAll, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals';
+import { afterAll, beforeAll, beforeEach, describe, expect, vi, test } from 'vitest';
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import type { DriveFilesRepository, DriveFoldersRepository, UsersRepository } from '@/models/_.js';
@@ -30,13 +30,13 @@ describe('DriveFileEntityService', () => {
let idCounter = 0;
const userEntityServiceMock = {
- packMany: jest.fn(async (users: Array) => {
+ packMany: vi.fn(async (users: Array) => {
return users.map(u => ({
id: typeof u === 'string' ? u : u.id,
username: 'user',
}));
}),
- pack: jest.fn(async (user: string | { id: string }) => {
+ pack: vi.fn(async (user: string | { id: string }) => {
return {
id: typeof user === 'string' ? user : user.id,
username: 'user',
@@ -195,7 +195,7 @@ describe('DriveFileEntityService', () => {
test('detail: true uses DriveFolderEntityService pack', async () => {
const folder = await createFolder('packmany-folder', null);
const file = await createFile(folder.id, null);
- const packSpy = jest.spyOn(driveFolderEntityService, 'pack');
+ const packSpy = vi.spyOn(driveFolderEntityService, 'pack');
await service.packMany([file], { detail: true, self: true });
expect(packSpy).toHaveBeenCalled();
diff --git a/packages/backend/test/unit/entities/DriveFolderEntityService.ts b/packages/backend/test/unit/entities/DriveFolderEntityService.ts
index 299ee5f42bc..9030cea3fe2 100644
--- a/packages/backend/test/unit/entities/DriveFolderEntityService.ts
+++ b/packages/backend/test/unit/entities/DriveFolderEntityService.ts
@@ -5,7 +5,7 @@
process.env.NODE_ENV = 'test';
-import { afterAll, beforeAll, describe, expect, test } from '@jest/globals';
+import { afterAll, beforeAll, describe, expect, test } from 'vitest';
import { Test } from '@nestjs/testing';
import type { TestingModule } from '@nestjs/testing';
import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js';
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
index ca6a639be8d..1b5f9ed874c 100644
--- a/packages/backend/test/unit/entities/UserEntityService.ts
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -4,6 +4,7 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
+import { describe, expect, beforeAll, afterAll, test } from 'vitest';
import type { MiUser } from '@/models/User.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalModule } from '@/GlobalModule.js';
@@ -248,6 +249,16 @@ describe('UserEntityService', () => {
expect(actual.achievements).toEqual(achievements);
});
+ test('alsoKnownAs as string does not throw', async () => {
+ const me = await createUser();
+ const who = await createUser();
+
+ const whoWithStringAlsoKnownAs: MiUser = { ...who, alsoKnownAs: 'https://remote.example.com/users/alice' as any };
+
+ const actual = await service.pack(whoWithStringAlsoKnownAs, me, { schema: 'UserDetailedNotMe' }) as any;
+ expect(Array.isArray(actual.alsoKnownAs)).toBe(true);
+ });
+
describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => {
test('no-preload', async() => {
const me = await createUser();
diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts
index 3403387e30e..0d02b27111b 100644
--- a/packages/backend/test/unit/extract-mentions.ts
+++ b/packages/backend/test/unit/extract-mentions.ts
@@ -4,6 +4,7 @@
*/
import * as assert from 'assert';
+import { describe, test } from 'vitest';
import { parse } from 'mfm-js';
import { extractMentions } from '@/misc/extract-mentions.js';
diff --git a/packages/backend/test/unit/misc/check-word-mute.ts b/packages/backend/test/unit/misc/check-word-mute.ts
index eb0ca0f6cfe..756f503aef3 100644
--- a/packages/backend/test/unit/misc/check-word-mute.ts
+++ b/packages/backend/test/unit/misc/check-word-mute.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { describe, expect, it } from 'vitest';
import { checkWordMute } from '@/misc/check-word-mute.js';
describe(checkWordMute, () => {
diff --git a/packages/backend/test/unit/misc/correct-filename.ts b/packages/backend/test/unit/misc/correct-filename.ts
index c76fb4c494b..fd792b83ca8 100644
--- a/packages/backend/test/unit/misc/correct-filename.ts
+++ b/packages/backend/test/unit/misc/correct-filename.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { describe, expect, it, test } from 'vitest';
import { correctFilename } from '@/misc/correct-filename.js';
describe(correctFilename, () => {
diff --git a/packages/backend/test/unit/misc/id.ts b/packages/backend/test/unit/misc/id.ts
index d14efb10a6f..ccb0fff4bf3 100644
--- a/packages/backend/test/unit/misc/id.ts
+++ b/packages/backend/test/unit/misc/id.ts
@@ -4,7 +4,7 @@
*/
import { ulid } from 'ulid';
-import { describe, expect, test } from '@jest/globals';
+import { describe, expect, test } from 'vitest';
import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js';
import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js';
import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js';
diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts
index 3c628d82983..4be3b4992b1 100644
--- a/packages/backend/test/unit/misc/is-renote.ts
+++ b/packages/backend/test/unit/misc/is-renote.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { describe, expect, test } from 'vitest';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { MiNote } from '@/models/Note.js';
diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts
index 2cf54e15559..cfeb9b5b2e0 100644
--- a/packages/backend/test/unit/misc/loader.ts
+++ b/packages/backend/test/unit/misc/loader.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { expect, describe, it } from 'vitest';
import { DebounceLoader } from '@/misc/loader.js';
class Mock {
diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts
index 3bc134a2b8e..92f26ff41ae 100644
--- a/packages/backend/test/unit/misc/others.ts
+++ b/packages/backend/test/unit/misc/others.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { describe, expect, test } from '@jest/globals';
+import { describe, expect, test } from 'vitest';
import { contentDisposition } from '@/misc/content-disposition.js';
describe('misc:content-disposition', () => {
diff --git a/packages/backend/test/unit/misc/should-hide-note-by-time.ts b/packages/backend/test/unit/misc/should-hide-note-by-time.ts
index 1c463c82c6f..f15750027e6 100644
--- a/packages/backend/test/unit/misc/should-hide-note-by-time.ts
+++ b/packages/backend/test/unit/misc/should-hide-note-by-time.ts
@@ -3,12 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
+import { describe, expect, test, beforeEach, afterEach } from 'vitest';
import * as lolex from '@sinonjs/fake-timers';
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
describe('misc:should-hide-note-by-time', () => {
- let clock: lolex.InstalledClock;
+ let clock: lolex.Clock;
const epoch = Date.UTC(2000, 0, 1, 0, 0, 0);
beforeEach(() => {
diff --git a/packages/backend/test/unit/misc/ulid.ts b/packages/backend/test/unit/misc/ulid.ts
new file mode 100644
index 00000000000..4dbcc2b7e86
--- /dev/null
+++ b/packages/backend/test/unit/misc/ulid.ts
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { describe, expect, test } from 'vitest';
+import { parseUlidFull } from '@/misc/id/ulid.js';
+
+// Timestamp part "01KPS7S300" encodes 1776816000000ms (2026-04-22T00:00:00.000Z)
+// Verified: 1*32^8 + 19*32^7 + 22*32^6 + 25*32^5 + 7*32^4 + 25*32^3 + 3*32^2 = 1776816000000
+
+describe('misc:ulid', () => {
+ test('parseUlidFull - timestamp is parsed correctly', () => {
+ // id[10..25] = all zeros (valid Crockford Base32)
+ // 2026-04-22T00:00:00.000Z
+ const { date } = parseUlidFull('01KPS7S3000000000000000000');
+ expect(date).toBe(1776816000000);
+ });
+
+ test('parseUlidFull - W/X/Y/Z at id[10] (chunk 1 head) do not throw', () => {
+ // id[10] = W
+ expect(() => parseUlidFull('01KPS7S300W000000000000000')).not.toThrow();
+ // id[10] = X
+ expect(() => parseUlidFull('01KPS7S300X000000000000000')).not.toThrow();
+ // id[10] = Y
+ expect(() => parseUlidFull('01KPS7S300Y000000000000000')).not.toThrow();
+ // id[10] = Z
+ expect(() => parseUlidFull('01KPS7S300Z000000000000000')).not.toThrow();
+ });
+
+ test('parseUlidFull - W/X/Y/Z at id[16] (chunk 2 head) do not throw', () => {
+ // id[16] = W
+ expect(() => parseUlidFull('01KPS7S300ABCDEFW000000000')).not.toThrow();
+ // id[16] = X
+ expect(() => parseUlidFull('01KPS7S300ABCDEFX000000000')).not.toThrow();
+ // id[16] = Y
+ expect(() => parseUlidFull('01KPS7S300ABCDEFY000000000')).not.toThrow();
+ // id[16] = Z
+ expect(() => parseUlidFull('01KPS7S300ABCDEFZ000000000')).not.toThrow();
+ });
+});
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
index 01a36c9feff..7295ab75115 100644
--- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { jest } from '@jest/globals';
+import { describe, expect, test, beforeAll, beforeEach, afterEach, afterAll, vi } from 'vitest';
+import type { Mocked } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
@@ -24,7 +25,7 @@ const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
describe('CheckModeratorsActivityProcessorService', () => {
let app: TestingModule;
- let clock: lolex.InstalledClock;
+ let clock: lolex.Clock;
let service: CheckModeratorsActivityProcessorService;
// --------------------------------------------------------------------------------------
@@ -32,10 +33,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
let usersRepository: UsersRepository;
let userProfilesRepository: UserProfilesRepository;
let idService: IdService;
- let roleService: jest.Mocked;
- let announcementService: jest.Mocked;
- let emailService: jest.Mocked;
- let systemWebhookService: jest.Mocked;
+ let roleService: Mocked;
+ let announcementService: Mocked;
+ let emailService: Mocked;
+ let systemWebhookService: Mocked;
let systemWebhook1: MiSystemWebhook;
let systemWebhook2: MiSystemWebhook;
@@ -94,30 +95,30 @@ describe('CheckModeratorsActivityProcessorService', () => {
CheckModeratorsActivityProcessorService,
IdService,
{
- provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
+ provide: RoleService, useFactory: () => ({ getModerators: vi.fn() }),
},
{
- provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
+ provide: MetaService, useFactory: () => ({ fetch: vi.fn() }),
},
{
- provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }),
+ provide: AnnouncementService, useFactory: () => ({ create: vi.fn() }),
},
{
- provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
+ provide: EmailService, useFactory: () => ({ sendEmail: vi.fn() }),
},
{
provide: SystemWebhookService, useFactory: () => ({
- fetchActiveSystemWebhooks: jest.fn(),
- enqueueSystemWebhook: jest.fn(),
+ fetchActiveSystemWebhooks: vi.fn(),
+ enqueueSystemWebhook: vi.fn(),
}),
},
{
provide: QueueLoggerService, useFactory: () => ({
logger: ({
createSubLogger: () => ({
- info: jest.fn(),
- warn: jest.fn(),
- succ: jest.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ succ: vi.fn(),
}),
}),
}),
@@ -131,10 +132,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
service = app.get(CheckModeratorsActivityProcessorService);
idService = app.get(IdService);
- roleService = app.get(RoleService) as jest.Mocked;
- announcementService = app.get(AnnouncementService) as jest.Mocked;
- emailService = app.get(EmailService) as jest.Mocked;
- systemWebhookService = app.get(SystemWebhookService) as jest.Mocked;
+ roleService = app.get(RoleService) as Mocked;
+ announcementService = app.get(AnnouncementService) as Mocked;
+ emailService = app.get(EmailService) as Mocked;
+ systemWebhookService = app.get(SystemWebhookService) as Mocked;
app.enableShutdownHooks();
});
diff --git a/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts
index 631e160afc5..b773857bea1 100644
--- a/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts
+++ b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { jest } from '@jest/globals';
+import { describe, expect, test, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import ms from 'ms';
import {
@@ -44,8 +44,8 @@ describe('CleanRemoteNotesProcessorService', () => {
// Mock job object
const createMockJob = () => ({
- log: jest.fn(),
- updateProgress: jest.fn(),
+ log: vi.fn(),
+ updateProgress: vi.fn(),
});
async function createUser(data: Partial = {}) {
@@ -96,9 +96,9 @@ describe('CleanRemoteNotesProcessorService', () => {
useFactory: () => ({
logger: {
createSubLogger: () => ({
- info: jest.fn(),
- warn: jest.fn(),
- succ: jest.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ succ: vi.fn(),
}),
},
}),
@@ -125,7 +125,7 @@ describe('CleanRemoteNotesProcessorService', () => {
beforeEach(() => {
// Reset mocks
- jest.clearAllMocks();
+ vi.clearAllMocks();
// Set default meta values
meta.enableRemoteNotesCleaning = true;
diff --git a/packages/backend/test/unit/server/FileServerService.ts b/packages/backend/test/unit/server/FileServerService.ts
index c88175c5c7b..4c3335b9020 100644
--- a/packages/backend/test/unit/server/FileServerService.ts
+++ b/packages/backend/test/unit/server/FileServerService.ts
@@ -7,7 +7,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import fastifyStatic from '@fastify/static';
import Fastify, { type FastifyInstance } from 'fastify';
-import { describe, expect, test } from '@jest/globals';
+import { describe, expect, test, beforeAll, afterAll, afterEach } from 'vitest';
import sharp from 'sharp';
import { DataSource, type Repository } from 'typeorm';
import { initTestDb, randomString } from '../../utils.js';
@@ -296,7 +296,7 @@ describe('FileServerService', () => {
});
expect(res.statusCode).toBe(404);
- expect(res.headers['cache-control']).toBe('max-age=86400');
+ expect(res.headers['cache-control']).toBe('public, max-age=0');
});
test('GET /files/:key 画像配信ヘッダを検証する', async () => {
diff --git a/packages/backend/test/unit/server/api/drive/files/create.ts b/packages/backend/test/unit/server/api/drive/files/create.ts
deleted file mode 100644
index e86b818ca51..00000000000
--- a/packages/backend/test/unit/server/api/drive/files/create.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Test, TestingModule } from '@nestjs/testing';
-import { FastifyInstance } from 'fastify';
-import request from 'supertest';
-import { randomString } from '../../../../../utils.js';
-import { CoreModule } from '@/core/CoreModule.js';
-import { RoleService } from '@/core/RoleService.js';
-import { DI } from '@/di-symbols.js';
-import { GlobalModule } from '@/GlobalModule.js';
-import { DriveFoldersRepository, MiDriveFolder, MiRole, UserProfilesRepository, UsersRepository } from '@/models/_.js';
-import { MiUser } from '@/models/User.js';
-import { ServerModule } from '@/server/ServerModule.js';
-import { ServerService } from '@/server/ServerService.js';
-import { IdService } from '@/core/IdService.js';
-
-// TODO: uploadableFileTypes で許可されていないファイルが弾かれるかのテスト
-
-describe('/drive/files/create', () => {
- let module: TestingModule;
- let server: FastifyInstance;
- let roleService: RoleService;
- let idService: IdService;
-
- let root: MiUser;
- let role_tinyAttachment: MiRole;
- let role_imageOnly: MiRole;
- let role_allowAllTypes: MiRole;
-
- let folder: MiDriveFolder;
-
- beforeAll(async () => {
- module = await Test.createTestingModule({
- imports: [GlobalModule, CoreModule, ServerModule],
- }).compile();
- module.enableShutdownHooks();
-
- const serverService = module.get(ServerService);
- await serverService.launch();
- server = serverService.fastify;
-
- idService = module.get(IdService);
-
- const usersRepository = module.get(DI.usersRepository);
- await usersRepository.createQueryBuilder().delete().execute();
- root = await usersRepository.insert({
- id: idService.gen(),
- username: 'root',
- usernameLower: 'root',
- token: '1234567890123456',
- }).then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
-
- const userProfilesRepository = module.get(DI.userProfilesRepository);
- await userProfilesRepository.createQueryBuilder().delete().execute();
- await userProfilesRepository.insert({
- userId: root.id,
- });
-
- const driveFoldersRepository = module.get(DI.driveFoldersRepository);
- folder = await driveFoldersRepository.insertOne({
- id: idService.gen(),
- name: 'root-folder',
- parentId: null,
- userId: root.id,
- });
-
- roleService = module.get(RoleService);
- role_imageOnly = await roleService.create({
- name: 'test-role001',
- description: 'Test role001 description',
- target: 'manual',
- policies: {
- uploadableFileTypes: {
- useDefault: false,
- priority: 1,
- value: ['image/png'],
- },
- },
- });
- role_allowAllTypes = await roleService.create({
- name: 'test-role002',
- description: 'Test role002 description',
- target: 'manual',
- policies: {
- uploadableFileTypes: {
- useDefault: false,
- priority: 1,
- value: ['*/*'],
- },
- },
- });
- role_tinyAttachment = await roleService.create({
- name: 'test-role003',
- description: 'Test role003 description',
- target: 'manual',
- policies: {
- maxFileSizeMb: {
- useDefault: false,
- priority: 1,
- // 10byte
- value: 10 / 1024 / 1024,
- },
- },
- });
- });
-
- beforeEach(async () => {
- await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {
- });
- await roleService.unassign(root.id, role_imageOnly.id).catch(() => {
- });
- await roleService.unassign(root.id, role_allowAllTypes.id).catch(() => {
- });
- });
-
- afterAll(async () => {
- await server.close();
- await module.close();
- });
-
- async function postFile(props: {
- name: string,
- comment: string,
- isSensitive: boolean,
- force: boolean,
- fileContent: Buffer | string,
- }) {
- const { name, comment, isSensitive, force, fileContent } = props;
-
- return await request(server.server)
- .post('/api/drive/files/create')
- .set('Content-Type', 'multipart/form-data')
- .attach('file', fileContent)
- .field('name', name)
- .field('comment', comment)
- .field('isSensitive', isSensitive)
- .field('force', force)
- .field('folderId', folder.id)
- .field('i', root.token ?? '');
- }
-
- test('200 ok (all types allowed)', async () => {
- await roleService.assign(root.id, role_allowAllTypes.id);
-
- const name = randomString();
- const comment = randomString();
- const result = await postFile({
- name: name,
- comment: comment,
- isSensitive: true,
- force: true,
- fileContent: Buffer.from('a'.repeat(1000 * 1000)),
- });
- expect(result.statusCode).toBe(200);
- expect(result.body.name).toBe(name + '.unknown');
- expect(result.body.comment).toBe(comment);
- expect(result.body.isSensitive).toBe(true);
- expect(result.body.folderId).toBe(folder.id);
- });
-
- test('400 when not allowed type', async () => {
- await roleService.assign(root.id, role_imageOnly.id);
-
- const name = randomString();
- const comment = randomString();
- const result = await postFile({
- name: name,
- comment: comment,
- isSensitive: true,
- force: true,
- fileContent: Buffer.from('a'.repeat(10)),
- });
- expect(result.statusCode).toBe(400);
- expect(result.body.error.code).toBe('UNALLOWED_FILE_TYPE');
- });
-
- test('200 ok (with size limited role)', async () => {
- await roleService.assign(root.id, role_allowAllTypes.id);
- await roleService.assign(root.id, role_tinyAttachment.id);
-
- const name = randomString();
- const comment = randomString();
- const result = await postFile({
- name: name,
- comment: comment,
- isSensitive: true,
- force: true,
- fileContent: Buffer.from('a'.repeat(10)),
- });
- expect(result.statusCode).toBe(200);
- expect(result.body.name).toBe(name + '.unknown');
- expect(result.body.comment).toBe(comment);
- expect(result.body.isSensitive).toBe(true);
- expect(result.body.folderId).toBe(folder.id);
- });
-
- test('413 too large', async () => {
- await roleService.assign(root.id, role_allowAllTypes.id);
- await roleService.assign(root.id, role_tinyAttachment.id);
-
- const name = randomString();
- const comment = randomString();
- const result = await postFile({
- name: name,
- comment: comment,
- isSensitive: true,
- force: true,
- fileContent: Buffer.from('a'.repeat(11)),
- });
- expect(result.statusCode).toBe(413);
- expect(result.body.error.code).toBe('MAX_FILE_SIZE_EXCEEDED');
- });
-});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index f91fb7f9b1c..e9938c5fa83 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -9,13 +9,13 @@ import { basename, isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
-import fetch, { File, RequestInit, type Headers } from 'node-fetch';
+import fetch, { RequestInit, type Headers } from 'node-fetch';
import * as htmlParser from 'node-html-parser';
import { DataSource } from 'typeorm';
import { type Response } from 'node-fetch';
import Fastify from 'fastify';
-import { entities } from '../src/postgres.js';
-import { loadConfig } from '../src/config.js';
+import { entities } from '@/postgres.js';
+import { loadConfig } from '@/config.js';
import type * as misskey from 'misskey-js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
diff --git a/packages/backend/vitest.config.e2e.ts b/packages/backend/vitest.config.e2e.ts
new file mode 100644
index 00000000000..cbfc0a288c9
--- /dev/null
+++ b/packages/backend/vitest.config.e2e.ts
@@ -0,0 +1,13 @@
+import { defineConfig, mergeConfig } from 'vitest/config';
+import { baseConfig } from './vitest.config.js';
+
+export default mergeConfig(
+ baseConfig,
+ defineConfig({
+ test: {
+ include: ['./test/e2e/**/*.ts'],
+ globalSetup: './built-test/entry.js',
+ setupFiles: ['./test/setup.e2e.ts'],
+ },
+ }),
+);
diff --git a/packages/backend/vitest.config.fed.ts b/packages/backend/vitest.config.fed.ts
new file mode 100644
index 00000000000..fcd58f8abbb
--- /dev/null
+++ b/packages/backend/vitest.config.fed.ts
@@ -0,0 +1,11 @@
+import { defineConfig, mergeConfig } from 'vitest/config';
+import { baseConfig } from './vitest.config.js';
+
+export default mergeConfig(
+ baseConfig,
+ defineConfig({
+ test: {
+ include: ['test-federation/test/**/*.test.ts'],
+ },
+ }),
+);
diff --git a/packages/backend/vitest.config.ts b/packages/backend/vitest.config.ts
new file mode 100644
index 00000000000..cfcf8c776f6
--- /dev/null
+++ b/packages/backend/vitest.config.ts
@@ -0,0 +1,32 @@
+import { EventEmitter } from 'node:events';
+import { resolve } from 'node:path';
+import { defineConfig } from 'vitest/config';
+
+// Raise the global EventEmitter listener limit before Vitest wires CLI listeners.
+EventEmitter.defaultMaxListeners = 20;
+
+export const baseConfig = defineConfig({
+ test: {
+ dir: import.meta.dirname,
+ exclude: ['node_modules', 'dist'],
+ coverage: {
+ provider: 'v8',
+ reportsDirectory: 'coverage',
+ include: ['src/**/*.ts'],
+ exclude: ['src/**/*.test.ts'],
+ },
+ restoreMocks: true,
+ testTimeout: 60000,
+ maxWorkers: 1,
+ logHeapUsage: true,
+ vmMemoryLimit: 1024,
+ maxConcurrency: 32,
+ },
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ },
+ },
+});
+
+export default baseConfig;
diff --git a/packages/backend/vitest.config.unit.ts b/packages/backend/vitest.config.unit.ts
new file mode 100644
index 00000000000..8f341fdc09b
--- /dev/null
+++ b/packages/backend/vitest.config.unit.ts
@@ -0,0 +1,12 @@
+import { defineConfig, mergeConfig } from 'vitest/config';
+import { baseConfig } from './vitest.config.js';
+
+export default mergeConfig(
+ baseConfig,
+ defineConfig({
+ test: {
+ globalSetup: './test/setup.unit.ts',
+ include: ['test/unit/**/*.ts', 'src/**/*.test.ts'],
+ },
+ }),
+);
diff --git a/packages/frontend-builder/locale-inliner/collect-modifications.ts b/packages/frontend-builder/locale-inliner/collect-modifications.ts
index 59e5d965172..2e92a407c90 100644
--- a/packages/frontend-builder/locale-inliner/collect-modifications.ts
+++ b/packages/frontend-builder/locale-inliner/collect-modifications.ts
@@ -3,10 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { parseAst } from 'vite';
+import { parseAst } from 'rolldown/parseAst';
import * as estreeWalker from 'estree-walker';
import { assertNever, assertType } from '../utils.js';
-import type { AstNode, ProgramNode } from 'rollup';
+import type { ESTree as RolldownESTree } from 'rolldown/utils';
+import type { AstNode } from 'rollup';
import type * as estree from 'estree';
import type { LocaleInliner, TextModification } from '../locale-inliner.js';
import type { Logger } from '../logger.js';
@@ -17,7 +18,7 @@ interface WalkerContext {
}
export function collectModifications(sourceCode: string, fileName: string, fileLogger: Logger, inliner: LocaleInliner): TextModification[] {
- let programNode: ProgramNode;
+ let programNode: RolldownESTree.Program;
try {
programNode = parseAst(sourceCode);
} catch (err) {
@@ -35,7 +36,8 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
// 1) replace all `scripts/` path literals with locale code
// 2) replace all `localStorage.getItem("lang")` with `localeName` variable
// 3) replace all `await window.fetch(`/assets/locales/${d}.${x}.json`).then(u=>u.json())` with `localeJson` variable
- estreeWalker.walk(programNode, {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (estreeWalker.walk as any)(programNode, {
enter(this: WalkerContext, node: Node) {
assertType(node);
@@ -118,8 +120,9 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
// Check if the identifier is already declared in the file.
// If it is, we may overwrite it and cause issues so we skip inlining
let isSupported = true;
- estreeWalker.walk(programNode, {
- enter(node) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (estreeWalker.walk as any)(programNode, {
+ enter(node: Node) {
if (node.type === 'VariableDeclaration') {
assertType(node);
for (const id of node.declarations.flatMap(x => declsOfPattern(x.id))) {
@@ -145,8 +148,9 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
const toSkip = new Set();
toSkip.add(i18nImport);
- estreeWalker.walk(programNode, {
- enter(this: WalkerContext, node, parent, property) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (estreeWalker.walk as any)(programNode, {
+ enter(this: WalkerContext, node: Node, parent: Node | null, property: string | number | symbol | null | undefined) {
assertType(node);
assertType(parent);
if (toSkip.has(node)) {
@@ -379,7 +383,7 @@ type SpecifierResult =
| { type: 'specifier', localI18nIdentifier: string, importNode: estree.ImportDeclaration & AstNode }
;
-function findImportSpecifier(programNode: ProgramNode, i18nFileName: string, i18nSymbol: string): SpecifierResult {
+function findImportSpecifier(programNode: RolldownESTree.Program, i18nFileName: string, i18nSymbol: string): SpecifierResult {
const imports = programNode.body.filter(x => x.type === 'ImportDeclaration');
const importNode = imports.find(x => x.source.value === `./${i18nFileName}`) as estree.ImportDeclaration | undefined;
if (!importNode) return { type: 'no-import' };
diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json
index 874a56e02a3..f19e901b340 100644
--- a/packages/frontend-builder/package.json
+++ b/packages/frontend-builder/package.json
@@ -11,15 +11,16 @@
},
"devDependencies": {
"@types/estree": "1.0.8",
- "@types/node": "24.12.0",
- "@typescript-eslint/eslint-plugin": "8.57.0",
- "@typescript-eslint/parser": "8.57.0",
- "rollup": "4.59.0"
+ "@types/node": "24.12.2",
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "rollup": "4.60.1"
},
"dependencies": {
- "i18n": "workspace:*",
"estree-walker": "3.0.3",
+ "i18n": "workspace:*",
"magic-string": "0.30.21",
- "vite": "7.3.1"
+ "rolldown": "1.0.0-rc.15",
+ "vite": "8.0.8"
}
}
diff --git a/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts b/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts
index 4a2bfa67d97..6ff62b8f773 100644
--- a/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts
+++ b/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts
@@ -4,11 +4,11 @@
*/
import * as estreeWalker from 'estree-walker';
-import MagicString from 'magic-string';
+import { RolldownMagicString } from 'rolldown';
import { assertType } from './utils.js';
+import type { ESTree } from 'rolldown/utils';
import type { Plugin } from 'vite';
-import type { CallExpression, Expression, Program } from 'estree';
-import type { AstNode } from 'rollup';
+import type { CallExpression, Expression } from 'estree';
// This plugin transforms `unref(i18n)` to `i18n` in the code, which is useful for removing unnecessary unref calls
// and helps locale inliner runs after vite build to inline the locale data into the final build.
@@ -23,12 +23,13 @@ export function pluginRemoveUnrefI18n(
} = {}): Plugin {
return {
name: 'UnwindCssModuleClassName',
- renderChunk(code) {
+ renderChunk(code, _chunk, _options, meta) {
if (!code.includes('unref(i18n)')) return null;
- const ast = this.parse(code) as Program;
- const magicString = new MagicString(code);
- estreeWalker.walk(ast, {
- enter(node) {
+ const ast = this.parse(code);
+ const magicString = meta.magicString ?? new RolldownMagicString(code);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (estreeWalker.walk as any)(ast, {
+ enter(node: ESTree.Node) {
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'unref'
&& node.arguments.length === 1) {
// calls to unref with single argument
@@ -36,18 +37,16 @@ export function pluginRemoveUnrefI18n(
if (arg.type === 'Identifier' && arg.name === i18nSymbolName) {
// this is unref(i18n) so replace it with i18n
// to replace, remove the 'unref(' and the trailing ')'
- assertType(node);
- assertType(arg);
+ assertType(node);
+ assertType(arg);
magicString.remove(node.start, arg.start);
magicString.remove(arg.end, node.end);
}
}
},
});
- return {
- code: magicString.toString(),
- map: magicString.generateMap({ hires: true }),
- };
+
+ return magicString;
},
};
}
diff --git a/packages/frontend-embed/build.ts b/packages/frontend-embed/build.ts
index 4e1f5888028..0b4058f33a7 100644
--- a/packages/frontend-embed/build.ts
+++ b/packages/frontend-embed/build.ts
@@ -3,7 +3,7 @@ import url from 'node:url';
import path from 'node:path';
import { execa } from 'execa';
import locales from 'i18n';
-import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
+import { LocaleInliner } from '../frontend-builder/locale-inliner.js';
import { createLogger } from '../frontend-builder/logger';
// requires node 21 or later
diff --git a/packages/frontend-embed/vite.json5.ts b/packages/frontend-embed/lib/vite-plugin-json5.ts
similarity index 90%
rename from packages/frontend-embed/vite.json5.ts
rename to packages/frontend-embed/lib/vite-plugin-json5.ts
index 87b67c21424..921324b67a4 100644
--- a/packages/frontend-embed/vite.json5.ts
+++ b/packages/frontend-embed/lib/vite-plugin-json5.ts
@@ -1,7 +1,10 @@
-// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
import JSON5 from 'json5';
-import { Plugin } from 'rollup';
+import { Plugin } from 'vite';
import { createFilter, dataToEsm } from '@rollup/pluginutils';
import { RollupJsonOptions } from '@rollup/plugin-json';
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 941651f3eaf..154a71b92da 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -12,10 +12,9 @@
"dependencies": {
"@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0",
- "@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
- "@vitejs/plugin-vue": "6.0.5",
+ "@vitejs/plugin-vue": "6.0.6",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
"frontend-shared": "workspace:*",
@@ -25,13 +24,11 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
- "rollup": "4.59.0",
- "sass": "1.98.0",
- "shiki": "3.23.0",
+ "rollup": "4.60.1",
+ "shiki": "4.0.2",
"tinycolor2": "1.6.0",
"uuid": "13.0.0",
- "vite": "7.3.1",
- "vue": "3.5.30"
+ "vue": "3.5.32"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
@@ -39,29 +36,30 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.10",
- "@types/node": "24.12.0",
+ "@types/node": "24.12.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
- "@typescript-eslint/eslint-plugin": "8.57.0",
- "@typescript-eslint/parser": "8.57.0",
- "@vitest/coverage-v8": "4.1.0",
- "@vue/runtime-core": "3.5.30",
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@vitest/coverage-v8": "4.1.4",
+ "@vue/runtime-core": "3.5.32",
"acorn": "8.16.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.8.0",
- "happy-dom": "20.8.4",
+ "happy-dom": "20.9.0",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
- "msw": "2.12.10",
- "nodemon": "3.1.14",
- "prettier": "3.8.1",
- "start-server-and-test": "2.1.5",
+ "msw": "2.13.3",
+ "prettier": "3.8.3",
+ "sass-embedded": "1.99.0",
+ "start-server-and-test": "3.0.2",
"tsx": "4.21.0",
+ "vite": "8.0.8",
"vite-plugin-turbosnap": "1.0.3",
- "vue-component-type-helpers": "3.2.5",
+ "vue-component-type-helpers": "3.2.6",
"vue-eslint-parser": "10.4.0",
- "vue-tsc": "3.2.5"
+ "vue-tsc": "3.2.6"
}
}
diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
index 9e5c24f9d4d..7f5be591e88 100644
--- a/packages/frontend-embed/vite.config.ts
+++ b/packages/frontend-embed/vite.config.ts
@@ -7,10 +7,10 @@ import { promises as fsp } from 'fs';
import locales from 'i18n';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
-import pluginJson5 from './vite.json5.js';
+import pluginJson5 from './lib/vite-plugin-json5.js';
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n';
-const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
+const url = process.env.NODE_ENV === 'development' ? (yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')) as any).url : null;
const host = url ? (new URL(url)).hostname : undefined;
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
@@ -113,11 +113,6 @@ export function getConfig(): UserConfig {
}
},
},
- preprocessorOptions: {
- scss: {
- api: 'modern-compiler',
- },
- },
},
define: {
@@ -137,7 +132,10 @@ export function getConfig(): UserConfig {
'safari16',
],
manifest: 'manifest.json',
- rollupOptions: {
+ rolldownOptions: {
+ experimental: {
+ nativeMagicString: true,
+ },
input: {
i18n: './src/i18n.ts',
entry: './src/boot.ts',
@@ -145,10 +143,15 @@ export function getConfig(): UserConfig {
external: externalPackages.map(p => p.match),
preserveEntrySignatures: 'allow-extension',
output: {
- manualChunks: {
- vue: ['vue'],
- // dependencies of i18n.ts
- 'config': ['@@/js/config.js'],
+ codeSplitting: {
+ groups: [{
+ name: 'vue',
+ test: /node_modules[\\/]vue/,
+ }, {
+ // dependencies of i18n.ts
+ name: 'config',
+ test: /@@[\\/]js[\\/]config\.js/,
+ }],
},
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json
index 817129dabaa..60c630af14f 100644
--- a/packages/frontend-shared/package.json
+++ b/packages/frontend-shared/package.json
@@ -21,10 +21,10 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
- "@types/node": "24.12.0",
- "@typescript-eslint/eslint-plugin": "8.57.0",
- "@typescript-eslint/parser": "8.57.0",
- "esbuild": "0.27.4",
+ "@types/node": "24.12.2",
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "esbuild": "0.28.0",
"eslint-plugin-vue": "10.8.0",
"nodemon": "3.1.14",
"vue-eslint-parser": "10.4.0"
@@ -35,6 +35,6 @@
"dependencies": {
"i18n": "workspace:*",
"misskey-js": "workspace:*",
- "vue": "3.5.30"
+ "vue": "3.5.32"
}
}
diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json
index 6b1804a0fcf..c2600dc45e5 100644
--- a/packages/frontend-shared/tsconfig.json
+++ b/packages/frontend-shared/tsconfig.json
@@ -7,7 +7,7 @@
"declaration": true,
"declarationMap": true,
"sourceMap": false,
- "outDir": "./js-built/",
+ "outDir": "./js-built",
"removeComments": true,
"resolveJsonModule": true,
"strict": true,
@@ -17,6 +17,7 @@
"noImplicitReturns": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true,
+ "rootDir": "./js",
"paths": {
"@/*": ["./*"],
"@@/*": ["./*"]
diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
index ccfa08575bd..0a5000f46d5 100644
--- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
+++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
@@ -3,15 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { parse } from 'acorn';
-import { generate } from 'astring';
import { describe, expect, it } from 'vitest';
import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name.js';
-import type * as estree from 'estree';
+import { parseAst } from 'rolldown/parseAst';
+import type { ESTree } from 'rolldown/utils';
+import { RolldownMagicString } from 'rolldown';
-function parseExpression(code: string): estree.Expression {
- const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program;
- const statement = program.body[0] as estree.ExpressionStatement;
+function parseExpression(code: string): ESTree.Expression {
+ const program = parseAst(code, { sourceType: 'module' });
+ const statement = program.body[0] as ESTree.ExpressionStatement;
return statement.expression;
}
@@ -57,7 +57,7 @@ describe(normalizeClass.name, () => {
});
it('Composition API (standard)', () => {
- const ast = parse(`
+ const code = `
import { c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
import { M as MkContainer } from './MkContainer-!~{03M}~.js';
import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
@@ -170,17 +170,19 @@ const cssModules = {
const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { index_photos as default };
-`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
- unwindCssModuleClassName(ast);
- expect(generate(ast)).toBe(`
-import {c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
-import {M as MkContainer} from './MkContainer-!~{03M}~.js';
-import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
+`.slice(1);
+ const ast = parseAst(code, { sourceType: 'module' });
+ const magicString = new RolldownMagicString(code);
+ unwindCssModuleClassName(ast, magicString);
+ expect(magicString.toString()).toBe(
+ `
+import { c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
+import { M as MkContainer } from './MkContainer-!~{03M}~.js';
+import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
import './photoswipe-!~{003}~.js';
-const _hoisted_1 = createBaseVNode("i", {
- class: "ti ti-photo"
-}, null, -1);
-const index_photos = defineComponent({
+
+const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
+const index_photos = /* @__PURE__ */ defineComponent({
__name: "index.photos",
props: {
user: {}
@@ -193,12 +195,20 @@ const index_photos = defineComponent({
return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
- const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
+ const image = [
+ "image/jpeg",
+ "image/webp",
+ "image/avif",
+ "image/png",
+ "image/gif",
+ "image/apng",
+ "image/vnd.mozilla.apng"
+ ];
api("users/notes", {
userId: props.user.id,
fileType: image,
limit: 10
- }).then(notes => {
+ }).then((notes) => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
@@ -213,60 +223,77 @@ const index_photos = defineComponent({
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
- return (openBlock(), createBlock(MkContainer, {
+ return openBlock(), createBlock(MkContainer, {
"max-height": 300,
foldable: true
}, {
- icon: withCtx(() => [_hoisted_1]),
- header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]),
- default: withCtx(() => [createBaseVNode("div", {
- class: "xenMW"
- }, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, {
- key: 0
- })) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
- key: 1,
- class: "xaZzf"
- }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => {
- return (openBlock(), createBlock(_component_MkA, {
- key: image.note.id + image.file.id,
- class: "xtA8t",
- to: unref(notePage)(image.note)
- }, {
- default: withCtx(() => [createVNode(ImgWithBlurhash, {
- hash: image.file.blurhash,
- src: thumbnail(image.file),
- title: image.file.name
- }, null, 8, ["hash", "src", "title"])]),
- _: 2
- }, 1032, ["class", "to"]));
- }), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
- key: 2,
- class: "xhYKj"
- }, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]),
+ icon: withCtx(() => [
+ _hoisted_1
+ ]),
+ header: withCtx(() => [
+ createTextVNode(toDisplayString(unref(i18n).ts.images), 1)
+ ]),
+ default: withCtx(() => [
+ createBaseVNode("div", {
+ class: "xenMW"
+ }, [
+ unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true),
+ !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
+ key: 1,
+ class: "xaZzf"
+ }, [
+ (openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => {
+ return openBlock(), createBlock(_component_MkA, {
+ key: image.note.id + image.file.id,
+ class: "xtA8t",
+ to: unref(notePage)(image.note)
+ }, {
+ default: withCtx(() => [
+ createVNode(ImgWithBlurhash, {
+ hash: image.file.blurhash,
+ src: thumbnail(image.file),
+ title: image.file.name
+ }, null, 8, ["hash", "src", "title"])
+ ]),
+ _: 2
+ }, 1032, ["class", "to"]);
+ }), 128))
+ ], 2)) : createCommentVNode("", true),
+ !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
+ key: 2,
+ class: "xhYKj"
+ }, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)
+ ], 2)
+ ]),
_: 1
- }));
+ });
};
}
});
+
const root = "xenMW";
const stream = "xaZzf";
const img = "xtA8t";
const empty = "xhYKj";
const style0 = {
- root: root,
- stream: stream,
- img: img,
- empty: empty
+ root: root,
+ stream: stream,
+ img: img,
+ empty: empty
};
+
const cssModules = {
"$style": style0
};
-export {index_photos as default};
-`.slice(1));
+
+
+export { index_photos as default };
+`.slice(1),
+ );
});
it('Composition API (with `useCssModule()`)', () => {
- const ast = parse(`
+ const code = `
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
@@ -437,11 +464,15 @@ const cssModules = {
const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { MkDateSeparatedList as M };
-`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
- unwindCssModuleClassName(ast);
- expect(generate(ast)).toBe(`
-import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
-import {d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
+`.slice(1);
+ const ast = parseAst(code, { sourceType: 'module' });
+ const magicString = new RolldownMagicString(code);
+ unwindCssModuleClassName(ast, magicString);
+ expect(magicString.toString()).toBe(
+ `
+import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
+import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
+
function isDebuggerEnabled(id) {
try {
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
@@ -458,6 +489,7 @@ function stackTraceInstances() {
}
return stack;
}
+
const _sfc_main = defineComponent({
props: {
items: {
@@ -485,7 +517,7 @@ const _sfc_main = defineComponent({
default: false
}
},
- setup(props, {slots, expose}) {
+ setup(props, { slots, expose }) {
const $style = useCssModule();
function getDateText(time) {
const date = new Date(time).getDate();
@@ -495,28 +527,40 @@ const _sfc_main = defineComponent({
day: date.toString()
});
}
- if (props.items.length === 0) return;
+ if (props.items.length === 0)
+ return;
const renderChildrenImpl = () => props.items.map((item, i) => {
- if (!slots || !slots.default) return;
+ if (!slots || !slots.default)
+ return;
const el = slots.default({
item
})[0];
- if (el.key == null && item.id) el.key = item.id;
+ if (el.key == null && item.id)
+ el.key = item.id;
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
const separator = h("div", {
class: $style["separator"],
key: item.id + ":separator"
}, h("p", {
class: $style["date"]
- }, [h("span", {
- class: $style["date-1"]
- }, [h("i", {
- class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
- }), getDateText(item.createdAt)]), h("span", {
- class: $style["date-2"]
- }, [getDateText(props.items[i + 1].createdAt), h("i", {
- class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
- })])]));
+ }, [
+ h("span", {
+ class: $style["date-1"]
+ }, [
+ h("i", {
+ class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
+ }),
+ getDateText(item.createdAt)
+ ]),
+ h("span", {
+ class: $style["date-2"]
+ }, [
+ getDateText(props.items[i + 1].createdAt),
+ h("i", {
+ class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
+ })
+ ])
+ ]));
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
@@ -532,17 +576,13 @@ const _sfc_main = defineComponent({
const renderChildren = () => {
const children = renderChildrenImpl();
if (isDebuggerEnabled(6864)) {
- const nodes = children.flatMap(node => node ?? []);
- const keys = new Set(nodes.map(node => node.key));
+ const nodes = children.flatMap((node) => node ?? []);
+ const keys = new Set(nodes.map((node) => node.key));
if (keys.size !== nodes.length) {
const id = crypto.randomUUID();
const instances = stackTraceInstances();
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
- console.warn({
- id,
- debugId: 6864,
- stack: instances
- });
+ console.warn({ id, debugId: 6864, stack: instances });
}
}
return children;
@@ -555,45 +595,136 @@ const _sfc_main = defineComponent({
el.style.top = "";
el.style.left = "";
}
- return () => h(prefer.s.animation ? TransitionGroup : "div", {
- class: {
- [$style["date-separated-list"]]: true,
- [$style["date-separated-list-nogap"]]: props.noGap,
- [$style["reversed"]]: props.reversed,
- [$style["direction-down"]]: props.direction === "down",
- [$style["direction-up"]]: props.direction === "up"
+ return () => h(
+ prefer.s.animation ? TransitionGroup : "div",
+ {
+ class: {
+ [$style["date-separated-list"]]: true,
+ [$style["date-separated-list-nogap"]]: props.noGap,
+ [$style["reversed"]]: props.reversed,
+ [$style["direction-down"]]: props.direction === "down",
+ [$style["direction-up"]]: props.direction === "up"
+ },
+ ...prefer.s.animation ? {
+ name: "list",
+ tag: "div",
+ onBeforeLeave,
+ onLeaveCanceled
+ } : {}
},
- ...prefer.s.animation ? {
- name: "list",
- tag: "div",
- onBeforeLeave,
- onLeaveCanceled
- } : {}
- }, {
- default: renderChildren
- });
+ { default: renderChildren }
+ );
}
});
+
const reversed = "xxiZh";
const separator = "xxeDx";
const date = "xxawD";
const style0 = {
- "date-separated-list": "xfKPa",
- "date-separated-list-nogap": "xf9zr",
- "direction-up": "x7AeO",
- "direction-down": "xBIqc",
- reversed: reversed,
- separator: separator,
- date: date,
- "date-1": "xwtmh",
- "date-1-icon": "xsNPa",
- "date-2": "x1xvw",
- "date-2-icon": "x9ZiG"
+ "date-separated-list": "xfKPa",
+ "date-separated-list-nogap": "xf9zr",
+ "direction-up": "x7AeO",
+ "direction-down": "xBIqc",
+ reversed: reversed,
+ separator: separator,
+ date: date,
+ "date-1": "xwtmh",
+ "date-1-icon": "xsNPa",
+ "date-2": "x1xvw",
+ "date-2-icon": "x9ZiG"
};
+
const cssModules = {
"$style": style0
};
-const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
-export {MkDateSeparatedList as M};
+const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
+
+export { MkDateSeparatedList as M };
`.slice(1));
});
+
+it('Composition API (inlined output)', () => {
+ const code = `
+import { a as normalizeClass, b as defineComponent, c as _export_sfc } from './runtime.js';
+
+const CurrentComponent = /* @__PURE__ */ _export_sfc(defineComponent({
+ __name: "CurrentComponent",
+ setup() {
+ return (e, n) => h("div", {
+ class: normalizeClass([e.$style.root, "extra"])
+ }, null, 2);
+ }
+}), [["__cssModules", {
+ "$style": {
+ root: "x1234"
+ }
+}]]);
+
+export { CurrentComponent as default };
+`.slice(1);
+ const ast = parseAst(code, { sourceType: 'module' });
+ const magicString = new RolldownMagicString(code);
+ unwindCssModuleClassName(ast, magicString);
+ const output = magicString.toString();
+ expect(output).toContain('class: "x1234 extra"');
+ expect(output).toContain('defineComponent({');
+ expect(output).toContain('}), []);');
+ expect(output).not.toContain('$style');
+});
+
+it('should keep cssModules when unresolved references remain', () => {
+ const code = `
+import { a as normalizeClass, b as defineComponent, c as _export_sfc } from './runtime.js';
+
+const CurrentComponent = /* @__PURE__ */ _export_sfc(defineComponent({
+ __name: "CurrentComponent",
+ setup() {
+ return (e, n) => h("div", {
+ class: normalizeClass([e.$style.root, e.$style[side]])
+ }, null, 2);
+ }
+}), [["__cssModules", {
+ "$style": {
+ root: "x1234"
+ }
+}]]);
+
+export { CurrentComponent as default };
+`.slice(1);
+ const ast = parseAst(code, { sourceType: 'module' });
+ const magicString = new RolldownMagicString(code);
+ unwindCssModuleClassName(ast, magicString);
+ const output = magicString.toString();
+ expect(output).toContain('e.$style[side]');
+ expect(output).toContain('__cssModules');
+ expect(output).not.toContain('}), []);');
+});
+
+it('should inline cssModules references used inside class expressions', () => {
+ const code = `
+import { a as classHelper, b as defineComponent, c as _export_sfc } from './runtime.js';
+
+const CurrentComponent = /* @__PURE__ */ _export_sfc(defineComponent({
+ __name: "CurrentComponent",
+ setup() {
+ return (e, n) => h("div", {
+ class: classHelper([e.$style.root, { [e.$style.main]: isActive }])
+ }, null, 2);
+ }
+}), [["__cssModules", {
+ "$style": {
+ root: "x1234",
+ main: "x5678"
+ }
+}]]);
+
+export { CurrentComponent as default };
+`.slice(1);
+ const ast = parseAst(code, { sourceType: 'module' });
+ const magicString = new RolldownMagicString(code);
+ unwindCssModuleClassName(ast, magicString);
+ const output = magicString.toString();
+ expect(output).toContain('class: classHelper(["x1234", { ["x5678"]: isActive }])');
+ expect(output).toContain('}), []);');
+ expect(output).not.toContain('$style');
+});
diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts
index 7ecb1e9179e..d82f1512fca 100644
--- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts
+++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts
@@ -3,17 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { generate } from 'astring';
-import { walk } from '../node_modules/estree-walker/src/index.js';
-import type * as estree from 'estree';
-import type * as estreeWalker from 'estree-walker';
+import * as estreeWalker from 'estree-walker';
import type { Plugin } from 'vite';
+import type { ESTree } from 'rolldown/utils';
+import { RolldownMagicString } from 'rolldown';
-function isFalsyIdentifier(identifier: estree.Identifier): boolean {
+function isFalsyIdentifier(identifier: Extract): boolean {
return identifier.name === 'undefined' || identifier.name === 'NaN';
}
-function normalizeClassWalker(tree: estree.Node, stack: string | undefined): string | null {
+function normalizeClassWalker(tree: ESTree.Node, stack: string | undefined): string | null {
if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null;
if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : '';
if (tree.type === 'BinaryExpression') {
@@ -26,7 +25,7 @@ function normalizeClassWalker(tree: estree.Node, stack: string | undefined): str
if (tree.type === 'TemplateLiteral') {
if (tree.expressions.some((x) => x.type !== 'Literal' && (x.type !== 'Identifier' || !isFalsyIdentifier(x)))) return null;
return tree.quasis.reduce((a, c, i) => {
- const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial).value;
+ const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial>).value;
return a + c.value.raw + (typeof v === 'string' ? v : '');
}, '');
}
@@ -72,44 +71,144 @@ function normalizeClassWalker(tree: estree.Node, stack: string | undefined): str
tree.type !== 'ChainExpression' &&
tree.type !== 'ConditionalExpression' &&
tree.type !== 'LogicalExpression' &&
- tree.type !== 'MemberExpression') {
+ tree.type !== 'MemberExpression'
+ ) {
console.error(stack ? `Unexpected node type: ${tree.type} (in ${stack})` : `Unexpected node type: ${tree.type}`);
}
return null;
}
-export function normalizeClass(tree: estree.Node, stack?: string): string | null {
+export function normalizeClass(tree: ESTree.Node, stack?: string): string | null {
const walked = normalizeClassWalker(tree, stack);
return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, '');
}
-export function unwindCssModuleClassName(ast: estree.Node): void {
- (walk as typeof estreeWalker.walk)(ast, {
- enter(node, parent): void {
+function getPropertyName(node: ESTree.Node, computed: boolean): string | null {
+ if (node.type === 'Identifier') return computed ? null : node.name;
+ if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
+ return null;
+}
+
+function getMemberPropertyName(node: ESTree.MemberExpression['property'], computed: boolean): string | null {
+ if (node.type === 'Identifier') return computed ? null : node.name;
+ if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
+ return null;
+}
+
+function findVariableDeclaration(program: ESTree.Program, name: string): ESTree.VariableDeclaration | null {
+ return program.body.find((x) => {
+ if (x.type !== 'VariableDeclaration') return false;
+ if (x.declarations.length !== 1) return false;
+ if (x.declarations[0].id.type !== 'Identifier') return false;
+ return x.declarations[0].id.name === name;
+ }) as ESTree.VariableDeclaration | null;
+}
+
+function resolveObjectExpression(program: ESTree.Program, tree: ESTree.Expression): ESTree.ObjectExpression | null {
+ if (tree.type === 'ObjectExpression') return tree;
+ if (tree.type !== 'Identifier') return null;
+ const declaration = findVariableDeclaration(program, tree.name);
+ if (declaration?.declarations[0].init?.type !== 'ObjectExpression') return null;
+ return declaration.declarations[0].init;
+}
+
+function resolveComponentOptions(program: ESTree.Program, tree: ESTree.Expression): ESTree.ObjectExpression | null {
+ const target = tree.type === 'Identifier'
+ ? findVariableDeclaration(program, tree.name)?.declarations[0].init ?? null
+ : tree;
+ if (target?.type === 'ObjectExpression') return target;
+ if (target?.type !== 'CallExpression') return null;
+ if (target.arguments.length !== 1) return null;
+ if (target.arguments[0].type !== 'ObjectExpression') return null;
+ return target.arguments[0];
+}
+
+function resolveModuleTree(program: ESTree.Program, tree: ESTree.Expression): Map | null {
+ const objectExpression = resolveObjectExpression(program, tree);
+ if (objectExpression === null) return null;
+ return new Map(objectExpression.properties.flatMap((property) => {
+ if (property.type !== 'Property') return [];
+ const actualKey = getPropertyName(property.key, property.computed);
+ if (actualKey === null) return [];
+ if (property.value.type === 'Literal') {
+ return typeof property.value.value === 'string' ? [[actualKey, property.value.value]] : [];
+ }
+ if (property.value.type === 'Identifier') {
+ const actualValue = findVariableDeclaration(program, property.value.name);
+ if (actualValue?.declarations[0].init?.type !== 'Literal') return [];
+ return typeof actualValue.declarations[0].init.value === 'string' ? [[actualKey, actualValue.declarations[0].init.value]] : [];
+ }
+ return [];
+ }));
+}
+
+function resolveModuleForest(program: ESTree.Program, tree: ESTree.Expression): Map> | null {
+ const objectExpression = resolveObjectExpression(program, tree);
+ if (objectExpression === null) return null;
+ return new Map(objectExpression.properties.flatMap((property) => {
+ if (property.type !== 'Property') return [];
+ const actualKey = getPropertyName(property.key, property.computed);
+ if (actualKey === null) return [];
+ const moduleTree = resolveModuleTree(program, property.value);
+ return moduleTree === null ? [] : [[actualKey, moduleTree]];
+ }));
+}
+
+function findRenderArrow(options: ESTree.ObjectExpression): Extract | null {
+ const setup = options.properties.find((x) => {
+ if (x.type !== 'Property') return false;
+ return getPropertyName(x.key, x.computed) === 'setup';
+ }) as Extract | undefined;
+ if (setup?.value.type !== 'FunctionExpression' && setup?.value.type !== 'ArrowFunctionExpression') return null;
+ if (setup.value.body == null) return null;
+ if (setup.value.body.type !== 'BlockStatement') return null;
+ const render = setup.value.body.body.find((x) => x.type === 'ReturnStatement');
+ if (render?.type !== 'ReturnStatement') return null;
+ return render.argument?.type === 'ArrowFunctionExpression' ? render.argument : null;
+}
+
+function isCssModuleAccess(node: ESTree.Node, ctxName: string, key: string): node is Extract {
+ if (node.type !== 'MemberExpression') return false;
+ if (node.object.type !== 'MemberExpression') return false;
+ if (node.object.object.type !== 'Identifier') return false;
+ if (node.object.object.name !== ctxName) return false;
+ return getMemberPropertyName(node.object.property, node.object.computed) === key;
+ }
+
+function isCssModuleReference(node: ESTree.Node, ctxName: string, key: string): node is Extract {
+ if (!isCssModuleAccess(node, ctxName, key)) return false;
+ return getMemberPropertyName(node.property, node.computed) !== null;
+}
+
+function isClassProperty(node: ESTree.Node | null): node is Extract {
+ return node?.type === 'Property' && getPropertyName(node.key, node.computed) === 'class';
+}
+
+export function unwindCssModuleClassName(ast: ESTree.Node, magicString: RolldownMagicString): void {
+ (estreeWalker.walk as any)(ast, {
+ enter(node: ESTree.Node, parent: ESTree.Node | null): void {
//#region
if (parent?.type !== 'Program') return;
+ if (ast.type !== 'Program') return;
if (node.type !== 'VariableDeclaration') return;
if (node.declarations.length !== 1) return;
if (node.declarations[0].id.type !== 'Identifier') return;
const name = node.declarations[0].id.name;
if (node.declarations[0].init?.type !== 'CallExpression') return;
- if (node.declarations[0].init.callee.type !== 'Identifier') return;
- if (node.declarations[0].init.callee.name !== '_export_sfc') return;
if (node.declarations[0].init.arguments.length !== 2) return;
- if (node.declarations[0].init.arguments[0].type !== 'Identifier') return;
- const ident = node.declarations[0].init.arguments[0].name;
- if (!ident.startsWith('_sfc_main')) return;
+ const componentNode = node.declarations[0].init.arguments[0];
+ if (componentNode.type !== 'Identifier' && componentNode.type !== 'CallExpression' && componentNode.type !== 'ObjectExpression') return;
if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return;
if (node.declarations[0].init.arguments[1].elements.length === 0) return;
- const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.findIndex((x) => {
+ const cssModulesEntry = node.declarations[0].init.arguments[1].elements.find((x) => {
if (x?.type !== 'ArrayExpression') return false;
if (x.elements.length !== 2) return false;
if (x.elements[0]?.type !== 'Literal') return false;
if (x.elements[0].value !== '__cssModules') return false;
- if (x.elements[1]?.type !== 'Identifier') return false;
return true;
- });
- if (!~__cssModulesIndex) return;
+ }) as ESTree.ArrayExpression | undefined;
+ const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.indexOf(cssModulesEntry ?? null);
+ if (cssModulesEntry === undefined || __cssModulesIndex < 0) return;
/* This region assumeed that the entered node looks like the following code.
*
* ```ts
@@ -118,21 +217,10 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
- const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name;
- const cssModuleForestNode = parent.body.find((x) => {
- if (x.type !== 'VariableDeclaration') return false;
- if (x.declarations.length !== 1) return false;
- if (x.declarations[0].id.type !== 'Identifier') return false;
- if (x.declarations[0].id.name !== cssModuleForestName) return false;
- if (x.declarations[0].init?.type !== 'ObjectExpression') return false;
- return true;
- }) as unknown as estree.VariableDeclaration;
- const moduleForest = new Map((cssModuleForestNode.declarations[0].init as estree.ObjectExpression).properties.flatMap((property) => {
- if (property.type !== 'Property') return [];
- if (property.key.type !== 'Literal') return [];
- if (property.value.type !== 'Identifier') return [];
- return [[property.key.value as string, property.value.name as string]];
- }));
+ const cssModuleForest = cssModulesEntry.elements[1];
+ if (cssModuleForest?.type !== 'Identifier' && cssModuleForest?.type !== 'ObjectExpression') return;
+ const moduleForest = resolveModuleForest(ast, cssModuleForest);
+ if (moduleForest === null) return;
/* This region collected a VariableDeclaration node in the module that looks like the following code.
*
* ```ts
@@ -143,35 +231,13 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
- const sfcMain = parent.body.find((x) => {
- if (x.type !== 'VariableDeclaration') return false;
- if (x.declarations.length !== 1) return false;
- if (x.declarations[0].id.type !== 'Identifier') return false;
- if (x.declarations[0].id.name !== ident) return false;
- return true;
- }) as unknown as estree.VariableDeclaration;
- if (sfcMain.declarations[0].init?.type !== 'CallExpression') return;
- if (sfcMain.declarations[0].init.callee.type !== 'Identifier') return;
- if (sfcMain.declarations[0].init.callee.name !== 'defineComponent') return;
- if (sfcMain.declarations[0].init.arguments.length !== 1) return;
- if (sfcMain.declarations[0].init.arguments[0].type !== 'ObjectExpression') return;
- const setup = sfcMain.declarations[0].init.arguments[0].properties.find((x) => {
- if (x.type !== 'Property') return false;
- if (x.key.type !== 'Identifier') return false;
- if (x.key.name !== 'setup') return false;
- return true;
- }) as unknown as estree.Property;
- if (setup.value.type !== 'FunctionExpression') return;
- const render = setup.value.body.body.find((x) => {
- if (x.type !== 'ReturnStatement') return false;
- return true;
- }) as unknown as estree.ReturnStatement;
- if (render.argument?.type !== 'ArrowFunctionExpression') return;
- if (render.argument.params.length !== 2) return;
- const ctx = render.argument.params[0];
+ const options = resolveComponentOptions(ast, componentNode);
+ if (options === null) return;
+ const render = findRenderArrow(options);
+ if (render === null) return;
+ if (render.params.length !== 2) return;
+ const ctx = render.params[0];
if (ctx.type !== 'Identifier') return;
- if (ctx.name !== '_ctx') return;
- if (render.argument.body.type !== 'BlockStatement') return;
/* This region assumed that `sfcMain` looks like the following code.
*
* ```ts
@@ -186,33 +252,8 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
* ```
*/
//#endregion
- for (const [key, value] of moduleForest) {
+ for (const [key, moduleTree] of moduleForest) {
//#region
- const cssModuleTreeNode = parent.body.find((x) => {
- if (x.type !== 'VariableDeclaration') return false;
- if (x.declarations.length !== 1) return false;
- if (x.declarations[0].id.type !== 'Identifier') return false;
- if (x.declarations[0].id.name !== value) return false;
- return true;
- }) as unknown as estree.VariableDeclaration;
- if (cssModuleTreeNode.declarations[0].init?.type !== 'ObjectExpression') return;
- const moduleTree = new Map(cssModuleTreeNode.declarations[0].init.properties.flatMap((property) => {
- if (property.type !== 'Property') return [];
- const actualKey = property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? property.key.value : null;
- if (typeof actualKey !== 'string') return [];
- if (property.value.type === 'Literal') return [[actualKey, property.value.value as string]];
- if (property.value.type !== 'Identifier') return [];
- const labelledValue = property.value.name;
- const actualValue = parent.body.find((x) => {
- if (x.type !== 'VariableDeclaration') return false;
- if (x.declarations.length !== 1) return false;
- if (x.declarations[0].id.type !== 'Identifier') return false;
- if (x.declarations[0].id.name !== labelledValue) return false;
- return true;
- }) as unknown as estree.VariableDeclaration;
- if (actualValue.declarations[0].init?.type !== 'Literal') return [];
- return [[actualKey, actualValue.declarations[0].init.value as string]];
- }));
/* This region collected VariableDeclaration nodes in the module that looks like the following code.
*
* ```ts
@@ -226,17 +267,14 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
- (walk as typeof estreeWalker.walk)(render.argument.body, {
- enter(childNode) {
- if (childNode.type !== 'MemberExpression') return;
- if (childNode.object.type !== 'MemberExpression') return;
- if (childNode.object.object.type !== 'Identifier') return;
- if (childNode.object.object.name !== ctx.name) return;
- if (childNode.object.property.type !== 'Identifier') return;
- if (childNode.object.property.name !== key) return;
- if (childNode.property.type !== 'Identifier') return;
- const actualValue = moduleTree.get(childNode.property.name);
+ (estreeWalker.walk as any)(render.body, {
+ enter(childNode: ESTree.Node) {
+ if (!isCssModuleReference(childNode, ctx.name, key)) return;
+ const actualKey = getMemberPropertyName(childNode.property, childNode.computed);
+ if (actualKey === null) return;
+ const actualValue = moduleTree.get(actualKey);
if (actualValue === undefined) return;
+ magicString.overwrite(childNode.start, childNode.end, JSON.stringify(actualValue));
this.replace({
type: 'Literal',
value: actualValue,
@@ -276,20 +314,13 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
- (walk as typeof estreeWalker.walk)(render.argument.body, {
- enter(childNode) {
- if (childNode.type !== 'MemberExpression') return;
- if (childNode.object.type !== 'MemberExpression') return;
- if (childNode.object.object.type !== 'Identifier') return;
- if (childNode.object.object.name !== ctx.name) return;
- if (childNode.object.property.type !== 'Identifier') return;
- if (childNode.object.property.name !== key) return;
- if (childNode.property.type !== 'Identifier') return;
- console.error(`Undefined style detected: ${key}.${childNode.property.name} (in ${name})`);
- this.replace({
- type: 'Identifier',
- name: 'undefined',
- });
+ (estreeWalker.walk as any)(render.body, {
+ enter(childNode: ESTree.Node) {
+ if (!isCssModuleReference(childNode, ctx.name, key)) return;
+ const actualKey = getMemberPropertyName(childNode.property, childNode.computed);
+ if (actualKey === null) return;
+ console.error(`Undefined style detected: ${key}.${actualKey} (in ${name})`);
+ magicString.overwrite(childNode.start, childNode.end, 'undefined');
},
});
/* This region replaced the reference identifier of missing class names in the render function with `undefined`, as in the following code.
@@ -300,7 +331,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
* ...
* return (_ctx, _cache) => {
* ...
- * return openBlock(), createElementBlock("div", {
+ * return openBlock(), createElementBlock('div', {
* class: normalizeClass(_ctx.$style.hoge),
* }, null);
* };
@@ -316,7 +347,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
* ...
* return (_ctx, _cache) => {
* ...
- * return openBlock(), createElementBlock("div", {
+ * return openBlock(), createElementBlock('div', {
* class: normalizeClass(undefined),
* }, null);
* };
@@ -326,18 +357,15 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
- (walk as typeof estreeWalker.walk)(render.argument.body, {
- enter(childNode) {
+ (estreeWalker.walk as any)(render.body, {
+ enter(childNode: ESTree.Node, childParent: ESTree.Node | null) {
if (childNode.type !== 'CallExpression') return;
- if (childNode.callee.type !== 'Identifier') return;
- if (childNode.callee.name !== 'normalizeClass') return;
if (childNode.arguments.length !== 1) return;
+ if (childNode.callee.type === 'Identifier' && childNode.callee.name !== 'normalizeClass' && !isClassProperty(childParent)) return;
+ if (childNode.callee.type !== 'Identifier' && !isClassProperty(childParent)) return;
const normalized = normalizeClass(childNode.arguments[0], name);
if (normalized === null) return;
- this.replace({
- type: 'Literal',
- value: normalized,
- });
+ magicString.overwrite(childNode.start, childNode.end, JSON.stringify(normalized));
},
});
/* This region compiled the `normalizeClass` call into a pseudo-AOT compilation, as in the following code.
@@ -374,19 +402,34 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
}
- //#region
- if (node.declarations[0].init.arguments[1].elements.length === 1) {
- (walk as typeof estreeWalker.walk)(ast, {
- enter(childNode) {
- if (childNode.type !== 'Identifier') return;
- if (childNode.name !== ident) return;
- this.replace({
- type: 'Identifier',
- name: node.declarations[0].id.name,
- });
+ const hasRemainingCssModuleReference = Array.from(moduleForest.keys()).some((key) => {
+ let found = false;
+ (estreeWalker.walk as any)(render.body, {
+ enter(childNode: ESTree.Node) {
+ if (!isCssModuleAccess(childNode, ctx.name, key)) return;
+ found = true;
+ this.skip();
},
});
- this.remove();
+ return found;
+ });
+ if (hasRemainingCssModuleReference) return;
+ //#region
+ if (node.declarations[0].init.arguments[1].elements.length === 1) {
+ if (componentNode.type === 'Identifier') {
+ (estreeWalker.walk as any)(ast, {
+ enter(childNode: ESTree.Node) {
+ if (childNode.type !== 'Identifier') return;
+ if (childNode.name !== componentNode.name) return;
+ magicString.overwrite(childNode.start, childNode.end, name);
+ },
+ });
+ magicString.remove(node.start, node.end);
+ } else {
+ const removeStart = cssModulesEntry.start;
+ const removeEnd = node.declarations[0].init.arguments[1].end - 1;
+ magicString.remove(removeStart, removeEnd);
+ }
/* NOTE: The above logic is valid as long as the following two conditions are met.
*
* - the uniqueness of `ident` is kept throughout the module
@@ -411,31 +454,10 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
});
*/
} else {
- this.replace({
- type: 'VariableDeclaration',
- declarations: [{
- type: 'VariableDeclarator',
- id: {
- type: 'Identifier',
- name: node.declarations[0].id.name,
- },
- init: {
- type: 'CallExpression',
- callee: {
- type: 'Identifier',
- name: '_export_sfc',
- },
- arguments: [{
- type: 'Identifier',
- name: ident,
- }, {
- type: 'ArrayExpression',
- elements: node.declarations[0].init.arguments[1].elements.slice(0, __cssModulesIndex).concat(node.declarations[0].init.arguments[1].elements.slice(__cssModulesIndex + 1)),
- }],
- },
- }],
- kind: 'const',
- });
+ const nextElement = node.declarations[0].init.arguments[1].elements[__cssModulesIndex + 1];
+ const removeStart = node.declarations[0].init.arguments[1].elements[__cssModulesIndex]!.start;
+ const removeEnd = nextElement ? nextElement.start : node.declarations[0].init.arguments[1].end - 1;
+ magicString.remove(removeStart, removeEnd);
}
/* This region removed the `__cssModules` reference from the second argument of `_export_sfc`, as in the following code.
*
@@ -474,10 +496,11 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
export default function pluginUnwindCssModuleClassName(): Plugin {
return {
name: 'UnwindCssModuleClassName',
- renderChunk(code): { code: string } {
- const ast = this.parse(code) as unknown as estree.Node;
- unwindCssModuleClassName(ast);
- return { code: generate(ast) };
+ renderChunk(code, _chunk, _options, meta) {
+ const ast = ('ast' in meta ? meta.ast ?? this.parse(code) : this.parse(code)) as ESTree.Program;
+ const magicString = meta.magicString ?? new RolldownMagicString(code);
+ unwindCssModuleClassName(ast, magicString);
+ return magicString;
},
};
}
diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts
index cfbba0823c3..4126a4f8c0f 100644
--- a/packages/frontend/lib/vite-plugin-create-search-index.ts
+++ b/packages/frontend/lib/vite-plugin-create-search-index.ts
@@ -13,11 +13,12 @@ import {
type LogOptions,
normalizePath,
type Plugin,
- type PluginOption
+ type PluginOption,
} from 'vite';
import fs from 'node:fs';
import JSON5 from 'json5';
-import MagicString, { SourceMap } from 'magic-string';
+import { RolldownMagicString } from 'rolldown';
+import type { TransformResult } from 'rolldown';
import path from 'node:path'
import { hash, toBase62 } from '../vite.config';
import { minimatch } from 'minimatch';
@@ -63,7 +64,7 @@ interface MarkerRelation {
let logger = {
info: (msg: string, options?: LogOptions) => { },
warn: (msg: string, options?: LogOptions) => { },
- error: (msg: string, options?: LogErrorOptions | unknown) => { },
+ error: (msg: string, options?: LogErrorOptions) => { },
};
let loggerInitialized = false;
@@ -460,9 +461,18 @@ function propertyAccessProxy(path: string[]): AccessProxy {
const i18nProxy = propertyAccessProxy(['i18n']);
-export function collectFileMarkers(id: string, code: string): SearchIndexItem[] {
+export function collectFileMarkers(id: string, code: string | RolldownMagicString | undefined): SearchIndexItem[] {
try {
- const { descriptor, errors } = vueSfcParse(code, {
+ let codeStr: string;
+ if (typeof code === 'string') {
+ codeStr = code;
+ } else if (code != null) {
+ codeStr = code.toString();
+ } else {
+ throw new Error(`Code is undefined for file ${id}`);
+ }
+
+ const { descriptor, errors } = vueSfcParse(codeStr, {
filename: id,
});
@@ -473,7 +483,8 @@ export function collectFileMarkers(id: string, code: string): SearchIndexItem[]
return extractUsageInfoFromTemplateAst(descriptor.template?.ast, id);
} catch (error) {
- logger.error(`Error analyzing file ${id}:`, error);
+ let _error = error instanceof Error ? error : new Error(String(error));
+ logger.error(`Error analyzing file ${id}:`, { error: _error });
}
return [];
@@ -481,10 +492,7 @@ export function collectFileMarkers(id: string, code: string): SearchIndexItem[]
// endregion
-type TransformedCode = {
- code: string,
- map: SourceMap,
-};
+type TransformedCode = Exclude;
export class MarkerIdAssigner {
// key: file id
@@ -509,13 +517,12 @@ export class MarkerIdAssigner {
}
#processImpl(id: string, code: string): TransformedCode {
- const s = new MagicString(code); // magic-string のインスタンスを作成
+ const s = new RolldownMagicString(code); // magic-string のインスタンスを作成
const parsed = vueSfcParse(code, { filename: id });
if (!parsed.descriptor.template) {
return {
- code,
- map: s.generateMap({ source: id, includeContent: true }),
+ code, // テンプレートがない場合は元のコードを返す
};
}
const ast = parsed.descriptor.template.ast; // テンプレート AST を取得
@@ -523,8 +530,7 @@ export class MarkerIdAssigner {
if (!ast) {
return {
- code: s.toString(), // 変更後のコードを返す
- map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
+ code,
};
}
@@ -611,7 +617,6 @@ export class MarkerIdAssigner {
return {
code: s.toString(), // 変更後のコードを返す
- map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
};
}
@@ -642,7 +647,7 @@ export class MarkerIdAssigner {
}
}
-// Rollup プラグインとして export
+// Vite プラグインとして export
export default function pluginCreateSearchIndex(options: Options): PluginOption {
const assigner = new MarkerIdAssigner();
return [
diff --git a/packages/frontend/vite.json5.ts b/packages/frontend/lib/vite-plugin-json5.ts
similarity index 90%
rename from packages/frontend/vite.json5.ts
rename to packages/frontend/lib/vite-plugin-json5.ts
index 87b67c21424..921324b67a4 100644
--- a/packages/frontend/vite.json5.ts
+++ b/packages/frontend/lib/vite-plugin-json5.ts
@@ -1,7 +1,10 @@
-// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
import JSON5 from 'json5';
-import { Plugin } from 'rollup';
+import { Plugin } from 'vite';
import { createFilter, dataToEsm } from '@rollup/pluginutils';
import { RollupJsonOptions } from '@rollup/plugin-json';
diff --git a/packages/frontend/lib/vite-plugin-watch-locales.ts b/packages/frontend/lib/vite-plugin-watch-locales.ts
index 372e9039d5d..2fed7f60fa1 100644
--- a/packages/frontend/lib/vite-plugin-watch-locales.ts
+++ b/packages/frontend/lib/vite-plugin-watch-locales.ts
@@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import path from 'node:path'
+import path from 'node:path';
import locales from 'i18n';
+import type { Plugin } from 'vite';
const localesDir = path.resolve(__dirname, '../../../locales')
/**
* 外部ファイルを監視し、必要に応じてwebSocketでメッセージを送るViteプラグイン
- * @returns {import('vite').Plugin}
*/
-export default function pluginWatchLocales() {
+export default function pluginWatchLocales(): Plugin {
return {
name: 'watch-locales',
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 5cdf0cf6626..6404b2c10a7 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -21,14 +21,11 @@
"@github/webauthn-json": "2.1.1",
"@mcaptcha/core-glue": "0.1.0-alpha-5",
"@misskey-dev/browser-image-resizer": "2024.1.0",
- "@rollup/plugin-json": "6.1.0",
- "@rollup/plugin-replace": "6.0.3",
- "@rollup/pluginutils": "5.3.0",
- "@sentry/vue": "10.43.0",
+ "@sentry/vue": "10.48.0",
"@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
- "@vitejs/plugin-vue": "6.0.5",
+ "@vitejs/plugin-vue": "6.0.6",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.16",
"analytics": "0.8.19",
"broadcast-channel": "7.3.0",
@@ -39,13 +36,13 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
- "chromatic": "15.2.0",
+ "chromatic": "15.3.1",
"compare-versions": "6.1.1",
- "cropperjs": "2.1.0",
+ "cropperjs": "2.1.1",
"date-fns": "4.1.0",
"eventemitter3": "5.0.4",
"execa": "9.6.1",
- "exifreader": "4.37.0",
+ "exifreader": "4.38.1",
"frontend-shared": "workspace:*",
"i18n": "workspace:*",
"icons-subsetter": "workspace:*",
@@ -55,7 +52,7 @@
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.20.0",
- "mediabunny": "1.39.2",
+ "mediabunny": "1.40.1",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -64,24 +61,23 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
- "rollup": "4.59.0",
- "sanitize-html": "2.17.1",
- "sass": "1.98.0",
- "shiki": "3.23.0",
+ "sanitize-html": "2.17.3",
+ "shiki": "4.0.2",
"textarea-caret": "3.1.0",
"three": "0.183.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"v-code-diff": "1.13.1",
- "vite": "7.3.1",
- "vue": "3.5.30",
+ "vue": "3.5.32",
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
+ "@rollup/plugin-json": "6.1.0",
+ "@rollup/pluginutils": "5.3.0",
"@storybook/addon-essentials": "8.6.18",
"@storybook/addon-interactions": "8.6.18",
- "@storybook/addon-links": "10.2.17",
+ "@storybook/addon-links": "10.3.5",
"@storybook/addon-mdx-gfm": "8.6.18",
"@storybook/addon-storysource": "8.6.18",
"@storybook/blocks": "8.6.18",
@@ -89,13 +85,13 @@
"@storybook/core-events": "8.6.18",
"@storybook/manager-api": "8.6.18",
"@storybook/preview-api": "8.6.18",
- "@storybook/react": "10.2.17",
- "@storybook/react-vite": "10.2.17",
+ "@storybook/react": "10.3.5",
+ "@storybook/react-vite": "10.3.5",
"@storybook/test": "8.6.18",
"@storybook/theming": "8.6.18",
"@storybook/types": "8.6.18",
- "@storybook/vue3": "10.2.17",
- "@storybook/vue3-vite": "10.2.17",
+ "@storybook/vue3": "10.3.5",
+ "@storybook/vue3-vite": "10.3.5",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
@@ -103,46 +99,48 @@
"@types/insert-text-at-cursor": "0.3.2",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10",
- "@types/node": "24.12.0",
+ "@types/node": "24.12.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.1",
"@types/seedrandom": "3.0.8",
"@types/textarea-caret": "3.0.4",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
- "@typescript-eslint/eslint-plugin": "8.57.0",
- "@typescript-eslint/parser": "8.57.0",
- "@vitest/coverage-v8": "4.1.0",
- "@vue/compiler-core": "3.5.30",
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@vitest/coverage-v8": "4.1.4",
+ "@vue/compiler-core": "3.5.32",
"acorn": "8.16.0",
"astring": "1.9.0",
"cross-env": "10.1.0",
- "cypress": "15.11.0",
+ "cypress": "15.13.1",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.8.0",
"estree-walker": "3.0.3",
- "happy-dom": "20.8.4",
+ "happy-dom": "20.9.0",
"intersection-observer": "0.12.2",
- "magic-string": "0.30.21",
"micromatch": "4.0.8",
- "minimatch": "10.2.4",
- "msw": "2.12.10",
- "msw-storybook-addon": "2.0.6",
+ "minimatch": "10.2.5",
+ "msw": "2.13.3",
+ "msw-storybook-addon": "2.0.7",
"nodemon": "3.1.14",
- "prettier": "3.8.1",
- "react": "19.2.4",
- "react-dom": "19.2.4",
+ "prettier": "3.8.3",
+ "react": "19.2.5",
+ "react-dom": "19.2.5",
+ "rolldown": "1.0.0-rc.15",
+ "sass-embedded": "1.99.0",
"seedrandom": "3.0.5",
- "start-server-and-test": "2.1.5",
- "storybook": "10.2.17",
+ "start-server-and-test": "3.0.2",
+ "storybook": "10.3.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.21.0",
- "vite-plugin-glsl": "1.5.5",
+ "vite": "8.0.8",
+ "vite-plugin-glsl": "1.6.0",
"vite-plugin-turbosnap": "1.0.3",
- "vitest": "4.1.0",
+ "vitest": "4.1.4",
"vitest-fetch-mock": "0.4.5",
- "vue-component-type-helpers": "3.2.5",
+ "vue-component-type-helpers": "3.2.6",
"vue-eslint-parser": "10.4.0",
- "vue-tsc": "3.2.5"
+ "vue-tsc": "3.2.6"
}
}
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 854ed31ed52..d7a622f1d92 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -4,14 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
-
-
-
-
-
-
+
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index c78cc444258..ba68971034d 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -263,7 +263,7 @@ const emit = defineEmits<{
const inTimeline = inject('inTimeline', false);
const tl_withSensitive = inject[>('tl_withSensitive', ref(true));
-const inChannel = inject('inChannel', null);
+const inChannel = inject(DI.inChannel, null);
const currentClip = inject][ | null>('currentClip', null);
let note = deepClone(props.note);
@@ -650,23 +650,35 @@ async function showRenoteMenu() {
};
}
- const renoteDetailsMenu: MenuItem = {
+ const renoteDetailsMenu: MenuItem[] = [{
type: 'link',
text: i18n.ts.renoteDetails,
icon: 'ti ti-info-circle',
to: notePage(note),
- };
+ }];
+
+ if (
+ props.note.channelId != null &&
+ (inChannel == null || props.note.channelId !== inChannel.value)
+ ) {
+ renoteDetailsMenu.push({
+ type: 'link',
+ text: i18n.ts.viewRenotedChannel,
+ icon: 'ti ti-device-tv',
+ to: `/channels/${props.note.channelId}`,
+ });
+ }
if (isMyRenote) {
os.popupMenu([
- renoteDetailsMenu,
+ ...renoteDetailsMenu,
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
{ type: 'divider' },
getUnrenote(),
], renoteTime.value);
} else {
os.popupMenu([
- renoteDetailsMenu,
+ ...renoteDetailsMenu,
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
{ type: 'divider' },
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 083e3e5da06..114edc6204d 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -238,6 +238,7 @@ import { isLink } from '@@/js/is-link.js';
import { host } from '@@/js/config.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import type { Keymap } from '@/utility/hotkey.js';
+import type { MenuItem } from '@/types/menu.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -286,7 +287,7 @@ const props = withDefaults(defineProps<{
initialTab: 'replies',
});
-const inChannel = inject('inChannel', null);
+const inChannel = inject(DI.inChannel, null);
let note = deepClone(props.note);
@@ -581,18 +582,36 @@ async function showRenoteMenu() {
const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
if (!isLoggedIn) return;
- os.popupMenu([{
- text: i18n.ts.unrenote,
- icon: 'ti ti-trash',
- danger: true,
- action: () => {
- misskeyApi('notes/delete', {
- noteId: note.id,
- }).then(() => {
- globalEvents.emit('noteDeleted', note.id);
- });
- },
- }], renoteTime.value);
+ const menu: MenuItem[] = [];
+
+ if (isMyRenote) {
+ menu.push({
+ text: i18n.ts.unrenote,
+ icon: 'ti ti-trash',
+ danger: true,
+ action: () => {
+ misskeyApi('notes/delete', {
+ noteId: note.id,
+ }).then(() => {
+ globalEvents.emit('noteDeleted', note.id);
+ });
+ },
+ });
+ }
+
+ if (
+ props.note.channelId != null &&
+ (inChannel == null || props.note.channelId !== inChannel.value)
+ ) {
+ menu.push({
+ type: 'link',
+ text: i18n.ts.viewRenotedChannel,
+ icon: 'ti ti-device-tv',
+ to: `/channels/${props.note.channelId}`,
+ });
+ }
+
+ os.popupMenu(menu, renoteTime.value);
}
function focus() {
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 4b6467fdda2..f2acec32f0e 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
diff --git a/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue b/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue
new file mode 100644
index 00000000000..ae36f4e2793
--- /dev/null
+++ b/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+
+
+ ]
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/roles.policy-editor.vue b/packages/frontend/src/pages/admin/roles.policy-editor.vue
new file mode 100644
index 00000000000..f93cb703e6c
--- /dev/null
+++ b/packages/frontend/src/pages/admin/roles.policy-editor.vue
@@ -0,0 +1,471 @@
+
+
+
+
+
+ {{ i18n.ts._role._options.rateLimitFactor }}
+ {{ Math.floor(valuesModel.rateLimitFactor * 100) }}%
+
+
+ {{ i18n.ts._role._options.descriptionOfRateLimitFactor }}
+
+
+
+
+
+ {{ i18n.ts._role._options.gtlAvailable }}
+ {{ valuesModel.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.ltlAvailable }}
+ {{ valuesModel.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canPublicNote }}
+ {{ valuesModel.canPublicNote ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.chatAvailability }}
+ {{ valuesModel.chatAvailability === 'available' ? i18n.ts.yes : valuesModel.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.mentionMax }}
+ {{ valuesModel.mentionLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.canInvite }}
+ {{ valuesModel.canInvite ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.inviteLimit }}
+ {{ valuesModel.inviteLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.inviteLimitCycle }}
+ {{ valuesModel.inviteLimitCycle + i18n.ts._time.minute }}
+
+
+ {{ i18n.ts._time.minute }}
+
+
+
+
+
+ {{ i18n.ts._role._options.inviteExpirationTime }}
+ {{ valuesModel.inviteExpirationTime + i18n.ts._time.minute }}
+
+
+ {{ i18n.ts._time.minute }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canManageAvatarDecorations }}
+ {{ valuesModel.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canManageCustomEmojis }}
+ {{ valuesModel.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canSearchNotes }}
+ {{ valuesModel.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canSearchUsers }}
+ {{ valuesModel.canSearchUsers ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canUseTranslator }}
+ {{ valuesModel.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.driveCapacity }}
+ {{ valuesModel.driveCapacityMb }}MB
+
+
+ MB
+
+
+
+
+
+ {{ i18n.ts._role._options.maxFileSize }}
+ {{ valuesModel.maxFileSizeMb }}MB
+
+
+ MB
+
+ {{ i18n.ts._role._options.maxFileSize_caption }}
+
+
+
+
+
+
+ {{ i18n.ts._role._options.uploadableFileTypes }}
+ ...
+
+ valuesModel.uploadableFileTypes = v.split('\n')">
+
+ {{ i18n.ts._role._options.uploadableFileTypes_caption }}
+ {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}
+
+
+
+
+
+
+ {{ i18n.ts._role._options.alwaysMarkNsfw }}
+ {{ valuesModel.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canUpdateBioMedia }}
+ {{ valuesModel.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.pinMax }}
+ {{ valuesModel.pinLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.antennaMax }}
+ {{ valuesModel.antennaLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.wordMuteMax }}
+ {{ valuesModel.wordMuteLimit }}
+
+
+ chars
+
+
+
+
+
+ {{ i18n.ts._role._options.webhookMax }}
+ {{ valuesModel.webhookLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.clipMax }}
+ {{ valuesModel.clipLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.noteEachClipsMax }}
+ {{ valuesModel.noteEachClipsLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.userListMax }}
+ {{ valuesModel.userListLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.userEachUserListsMax }}
+ {{ valuesModel.userEachUserListsLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.canHideAds }}
+ {{ valuesModel.canHideAds ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.avatarDecorationLimit }}
+ {{ valuesModel.avatarDecorationLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportAntennas }}
+ {{ valuesModel.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportBlocking }}
+ {{ valuesModel.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportFollowing }}
+ {{ valuesModel.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportMuting }}
+ {{ valuesModel.canImportMuting ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportUserLists }}
+ {{ valuesModel.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+ {{ i18n.ts._role._options.noteDraftLimit }}
+ {{ valuesModel.noteDraftLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.scheduledNoteLimit }}
+ {{ valuesModel.scheduledNoteLimit }}
+
+
+
+
+
+
+
+ {{ i18n.ts._role._options.watermarkAvailable }}
+ {{ valuesModel.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }}
+
+
+ {{ i18n.ts.enable }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index e65a3c5ba88..94fc75657ad 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -17,310 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
-
- {{ i18n.ts._role._options.rateLimitFactor }}
- {{ Math.floor(policies.rateLimitFactor * 100) }}%
- policies.rateLimitFactor = (v / 100)">
- {{ i18n.ts._role._options.descriptionOfRateLimitFactor }}
-
-
-
-
- {{ i18n.ts._role._options.gtlAvailable }}
- {{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.ltlAvailable }}
- {{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canPublicNote }}
- {{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.chatAvailability }}
- {{ policies.chatAvailability === 'available' ? i18n.ts.yes : policies.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.mentionMax }}
- {{ policies.mentionLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.canInvite }}
- {{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.inviteLimit }}
- {{ policies.inviteLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.inviteLimitCycle }}
- {{ policies.inviteLimitCycle + i18n.ts._time.minute }}
-
- {{ i18n.ts._time.minute }}
-
-
-
-
- {{ i18n.ts._role._options.inviteExpirationTime }}
- {{ policies.inviteExpirationTime + i18n.ts._time.minute }}
-
- {{ i18n.ts._time.minute }}
-
-
-
-
- {{ i18n.ts._role._options.canManageAvatarDecorations }}
- {{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canManageCustomEmojis }}
- {{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canSearchNotes }}
- {{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canSearchUsers }}
- {{ policies.canSearchUsers ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canUseTranslator }}
- {{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.driveCapacity }}
- {{ policies.driveCapacityMb }}MB
-
- MB
-
-
-
-
- {{ i18n.ts._role._options.maxFileSize }}
- {{ policies.maxFileSizeMb }}MB
-
- MB
-
- {{ i18n.ts._role._options.maxFileSize_caption }}
-
-
-
-
-
- {{ i18n.ts._role._options.uploadableFileTypes }}
- ...
- policies.uploadableFileTypes = v.split('\n')">
-
- {{ i18n.ts._role._options.uploadableFileTypes_caption }}
- {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}
-
-
-
-
-
- {{ i18n.ts._role._options.alwaysMarkNsfw }}
- {{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canUpdateBioMedia }}
- {{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.pinMax }}
- {{ policies.pinLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.antennaMax }}
- {{ policies.antennaLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.wordMuteMax }}
- {{ policies.wordMuteLimit }}
-
- chars
-
-
-
-
- {{ i18n.ts._role._options.webhookMax }}
- {{ policies.webhookLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.clipMax }}
- {{ policies.clipLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.noteEachClipsMax }}
- {{ policies.noteEachClipsLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.userListMax }}
- {{ policies.userListLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.userEachUserListsMax }}
- {{ policies.userEachUserListsLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.canHideAds }}
- {{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.avatarDecorationLimit }}
- {{ policies.avatarDecorationLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.canImportAntennas }}
- {{ policies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canImportBlocking }}
- {{ policies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canImportFollowing }}
- {{ policies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canImportMuting }}
- {{ policies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.canImportUserLists }}
- {{ policies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
-
-
- {{ i18n.ts._role._options.noteDraftLimit }}
- {{ policies.noteDraftLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.scheduledNoteLimit }}
- {{ policies.scheduledNoteLimit }}
-
-
-
-
-
- {{ i18n.ts._role._options.watermarkAvailable }}
- {{ policies.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }}
-
- {{ i18n.ts.enable }}
-
-
+
{{ i18n.ts._role.new }}
@@ -345,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only