From 3246dad53ebd5037b3000d82f6c46ed68c06d2ba Mon Sep 17 00:00:00 2001 From: "SASAPIYO (SASAGAWA Kiyoshi)" Date: Thu, 4 Jun 2026 10:28:26 +0900 Subject: [PATCH 01/13] =?UTF-8?q?fix(chart):=20PerUserDriveChart.update=20?= =?UTF-8?q?=E3=81=A7=20userId=20=E3=81=8C=20null=20=E3=81=AE=E3=82=B7?= =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=A0=E6=89=80=E6=9C=89=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=82=92=E3=82=B9=E3=82=AD=E3=83=83=E3=83=97?= =?UTF-8?q?=20(#17499)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(chart): PerUserDriveChart.update で userId が null のシステム所有ファイルをスキップ (#17498) Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/backend/src/core/chart/charts/per-user-drive.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d084006562..41657fd987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### Server - Enhance: リモートノートクリーニングジョブのスキップ処理のパフォーマンス改善 +- Fix: PerUserDriveChart がシステム所有ファイル (userId が null) の更新で `"group"` の非NULL制約違反によりクラッシュする問題を修正 (#17498) - Enhance: リモートノートクリーニングジョブの削除対象検索処理のパフォーマンス改善 - Fix: センシティブメディア自動検出周りの依存関係・ファイルの解決に失敗する問題を修正 - Fix: フォロワー限定投稿を指名投稿で引用した際に、引用した投稿の公開範囲が意図せず変更される問題を修正 diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts index f7e92aecea..7075d2134c 100644 --- a/packages/backend/src/core/chart/charts/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -56,6 +56,10 @@ export default class PerUserDriveChart extends Chart { // eslint- @bindThis public async update(file: MiDriveFile, isAdditional: boolean): Promise { + // MiDriveFile.userId is nullable (system-owned files such as the instance + // icon/banner, fetched system avatars, custom emoji image uploads, etc.). + // Per-user drive accounting is not defined for ownerless files, so skip. + if (file.userId == null) return; const fileSizeKb = file.size / 1000; await this.commit({ 'totalCount': isAdditional ? 1 : -1, From 4ae53440b29da0fe64e1eedcefd87520fe972673 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:40:20 +0900 Subject: [PATCH 02/13] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7839e4de66..ed1daaf489 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ vite.config.local-dev.ts.timestamp-* # VSCode addon .favorites.json + +# Affinity +*.af~lock~ From e2bcd9c2b438b5090a556b3feb0ae24b8ea8c54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:50:33 +0900 Subject: [PATCH 03/13] =?UTF-8?q?enhance(frontend):=20=E7=B5=B5=E6=96=87?= =?UTF-8?q?=E5=AD=97=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=81=8B=E3=82=89?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=91=E3=83=AC?= =?UTF-8?q?=E3=83=83=E3=83=88=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#17420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): 絵文字メニューから直接絵文字パレットに追加できるように * Update Changelog * fix lint * Update Changelog * enhance: 追加し直す挙動に変更 * :v: * fix --- CHANGELOG.md | 1 + locales/ja-JP.yml | 5 + .../components/MkReactionsViewer.reaction.vue | 11 +++ packages/frontend/src/components/MkSelect.vue | 3 + .../src/components/global/MkCustomEmoji.vue | 13 +++ .../src/components/global/MkEmoji.vue | 35 +++++-- .../frontend/src/utility/emoji-palette.ts | 94 +++++++++++++++++++ packages/frontend/src/utility/emoji-picker.ts | 4 +- .../frontend/src/utility/reaction-picker.ts | 4 +- packages/i18n/src/autogen/locale.ts | 20 ++++ 10 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 packages/frontend/src/utility/emoji-palette.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 41657fd987..0b4cd6fac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Client - Enhance: ユーザーページのファイルタブでスクロール位置が保持されるように - Enhance: ドライブページでスクロール位置が保持されるように +- Enhance: 絵文字のメニューから直接絵文字パレットに絵文字を追加できるように - Fix: URLプレビューのプレイヤーをウィンドウで開いたとき、プレイヤーが読み込まれるまでの間 `Invalid URL` と表示される問題を修正 - Fix: 一部の実績が正しく表示されない問題を修正 - Fix: アクセストークン発行時のダイアログのタイトルが「確認コード」となっているのを修正 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 532ef9290b..aa2ac97d8a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1415,6 +1415,11 @@ viewRenotedChannel: "リノート先のチャンネルを見る" previewingTheme: "テーマのプレビュー中" previewingThemeRestore: "元に戻す" accessToken: "アクセストークン" +chooseEmojiPalette: "絵文字パレットを選択" +addToEmojiPalette: "絵文字パレットに追加" +emojiPaletteAlreadyAddedConfirm: "この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか?" +append: "末尾に追加" +prepend: "先頭に追加" _imageEditing: _vars: diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index e9ed1cf4ac..db99080de5 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -38,6 +38,7 @@ import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; import { noteEvents } from '@/composables/use-note-capture.js'; import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as isEmojiMuted } from '@/utility/emoji-mute.js'; +import { addToEmojiPalette } from '@/utility/emoji-palette.js'; import { haptic } from '@/utility/haptic.js'; const props = defineProps<{ @@ -206,6 +207,16 @@ async function menu(ev: PointerEvent) { }); } + if (canToggle.value) { + menuItems.push({ + text: i18n.ts.addToEmojiPalette, + icon: 'ti ti-palette', + action: () => { + addToEmojiPalette(isLocalCustomEmoji ? `:${emojiName.value}:` : props.reaction); + }, + }); + } + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 6f6957d504..f1ad91c381 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -46,6 +46,7 @@ export type ItemOption = { type?: 'option'; value: T; label: string; + caption?: string; }; export type ItemGroup = { @@ -177,6 +178,7 @@ function show() { for (const option of item.items) { menu.push({ text: option.label, + caption: option.caption, active: computed(() => model.value === option.value), action: () => { model.value = option.value as ModelTChecked; @@ -186,6 +188,7 @@ function show() { } else { menu.push({ text: item.label, + caption: item.caption, active: computed(() => model.value === item.value), action: () => { model.value = item.value as ModelTChecked; diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 9a171876a0..39662fb7d4 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -50,6 +50,7 @@ import { $i } from '@/i.js'; import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; import { makeEmojiMuteKey, mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkEmojiMuted } from '@/utility/emoji-mute'; +import { addToEmojiPalette } from '@/utility/emoji-palette.js'; const props = defineProps<{ name: string; @@ -167,8 +168,20 @@ function onClick(ev: PointerEvent) { }); } + if (isLocal.value) { + menuItems.push({ + text: i18n.ts.addToEmojiPalette, + icon: 'ti ti-palette', + action: () => { + addToEmojiPalette(`:${props.name}:`); + }, + }); + } + if (($i?.isModerator ?? $i?.isAdmin) && isLocal.value) { menuItems.push({ + type: 'divider', + }, { text: i18n.ts.edit, icon: 'ti ti-pencil', action: async () => { diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index 686720cec2..5321883064 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -20,6 +20,7 @@ import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkMutedEmoji } from '@/utility/emoji-mute.js'; +import { addToEmojiPalette } from '@/utility/emoji-palette.js'; const props = defineProps<{ emoji: string; @@ -94,17 +95,31 @@ function onClick(ev: PointerEvent) { menuItems.push({ type: 'divider', - }, isMuted.value ? { - text: i18n.ts.emojiUnmute, - icon: 'ti ti-mood-smile', + }); + + if (isMuted.value) { + menuItems.push({ + text: i18n.ts.emojiUnmute, + icon: 'ti ti-mood-smile', + action: () => { + unmute(); + }, + }); + } else { + menuItems.push({ + text: i18n.ts.emojiMute, + icon: 'ti ti-mood-off', + action: () => { + mute(); + }, + }); + } + + menuItems.push({ + text: i18n.ts.addToEmojiPalette, + icon: 'ti ti-palette', action: () => { - unmute(); - }, - } : { - text: i18n.ts.emojiMute, - icon: 'ti ti-mood-off', - action: () => { - mute(); + addToEmojiPalette(props.emoji); }, }); diff --git a/packages/frontend/src/utility/emoji-palette.ts b/packages/frontend/src/utility/emoji-palette.ts new file mode 100644 index 0000000000..b3e4b6e0c5 --- /dev/null +++ b/packages/frontend/src/utility/emoji-palette.ts @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { prefer } from '@/preferences.js'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import type { MkSelectItem } from '@/components/MkSelect.vue'; + +export function chooseEmojiPalette() { + return os.select({ + title: i18n.ts.chooseEmojiPalette, + default: prefer.s.emojiPaletteForMain ?? prefer.s.emojiPaletteForReaction ?? prefer.s.emojiPalettes[0]?.id, + items: prefer.s.emojiPalettes.map>((palette) => { + let caption: string | undefined = undefined; + + if (prefer.s.emojiPaletteForMain === palette.id) { + caption = i18n.ts._emojiPalette.paletteForMain; + } else if (prefer.s.emojiPaletteForReaction === palette.id) { + caption = i18n.ts._emojiPalette.paletteForReaction; + } + + return { + label: palette.name || `(${i18n.ts.noName})`, + caption, + value: palette.id, + }; + }), + }); +} + +export async function addToEmojiPalette(emoji: string) { + const res = await chooseEmojiPalette(); + + if (res.canceled || res.result == null) return; + + const palette = prefer.s.emojiPalettes.find((p) => p.id === res.result); + if (!palette) return; + let emojis = [...palette.emojis]; + + if (!emojis.includes(emoji)) { + emojis.push(emoji); + prefer.commit('emojiPalettes', prefer.s.emojiPalettes.map((p) => { + if (p.id === palette.id) { + return { + ...p, + emojis, + }; + } else { + return p; + } + })); + os.success(); + } else { + const res = await os.actions({ + type: 'warning', + text: i18n.ts.emojiPaletteAlreadyAddedConfirm, + actions: [{ + value: 'prepend', + text: i18n.ts.prepend, + }, { + value: 'append', + text: i18n.ts.append, + }, { + value: 'doNothing', + text: i18n.ts.doNothing, + }], + }); + + if (res.canceled || res.result === 'doNothing') return; + + emojis = emojis.filter((e) => e !== emoji); + + if (res.result === 'append') { + emojis.push(emoji); + } else if (res.result === 'prepend') { + emojis.unshift(emoji); + } + + prefer.commit('emojiPalettes', prefer.s.emojiPalettes.map((p) => { + if (p.id === palette.id) { + return { + ...p, + emojis, + }; + } else { + return p; + } + })); + + os.success(); + } +} diff --git a/packages/frontend/src/utility/emoji-picker.ts b/packages/frontend/src/utility/emoji-picker.ts index c3d5ca9b35..e74fb62be1 100644 --- a/packages/frontend/src/utility/emoji-picker.ts +++ b/packages/frontend/src/utility/emoji-picker.ts @@ -22,8 +22,8 @@ class EmojiPicker { } public init() { - watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => { - this.emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? []; + watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], ([newId, newPalettes]) => { + this.emojisRef.value = newId == null ? newPalettes[0].emojis : newPalettes.find(palette => palette.id === newId)?.emojis ?? []; }, { immediate: true, }); diff --git a/packages/frontend/src/utility/reaction-picker.ts b/packages/frontend/src/utility/reaction-picker.ts index c1bdf19758..b0b97f4bb5 100644 --- a/packages/frontend/src/utility/reaction-picker.ts +++ b/packages/frontend/src/utility/reaction-picker.ts @@ -17,8 +17,8 @@ class ReactionPicker { } public init() { - watch([prefer.r.emojiPaletteForReaction, prefer.r.emojiPalettes], () => { - this.reactionsRef.value = prefer.s.emojiPaletteForReaction == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForReaction)?.emojis ?? []; + watch([prefer.r.emojiPaletteForReaction, prefer.r.emojiPalettes], ([newId, newPalettes]) => { + this.reactionsRef.value = newId == null ? newPalettes[0].emojis : newPalettes.find(palette => palette.id === newId)?.emojis ?? []; }, { immediate: true, }); diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts index 92544f3606..b191f5fb1a 100644 --- a/packages/i18n/src/autogen/locale.ts +++ b/packages/i18n/src/autogen/locale.ts @@ -5675,6 +5675,26 @@ export interface Locale extends ILocale { * アクセストークン */ "accessToken": string; + /** + * 絵文字パレットを選択 + */ + "chooseEmojiPalette": string; + /** + * 絵文字パレットに追加 + */ + "addToEmojiPalette": string; + /** + * この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか? + */ + "emojiPaletteAlreadyAddedConfirm": string; + /** + * 末尾に追加 + */ + "append": string; + /** + * 先頭に追加 + */ + "prepend": string; "_imageEditing": { "_vars": { /** From e215ab1091806691e1707a1cb1f71e27459c4bc6 Mon Sep 17 00:00:00 2001 From: Tatsuya_yd Date: Thu, 4 Jun 2026 22:19:15 +0900 Subject: [PATCH 04/13] =?UTF-8?q?fix(frontend):=20=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E3=82=B5=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=B9=E3=83=88=E6=99=82=E3=81=AB=E8=A1=A8=E7=A4=BA=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=82=8B=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=8C=E7=94=BB=E5=83=8F=E3=82=B5=E3=82=A4=E3=82=BA?= =?UTF-8?q?=E6=AC=A1=E7=AC=AC=E3=81=A7=E5=B4=A9=E3=82=8C=E3=82=8B=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#17542)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): メンションのサジェスト時に表示されるアイコン表示が画像サイズ次第で崩れる (#17504) * fix(frontend): メンションのサジェスト時に表示されるアイコン表示が画像サイズ次第で崩れる (#17504) --- CHANGELOG.md | 1 + packages/frontend/src/components/MkAutocomplete.vue | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b4cd6fac4..88a4b832cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fix: 一部のUI要素の色が正しく表示されない問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1243) - Fix: 「D」キーでダークモードを切り替える際にsyncDeviceDarkModeのチェックがバイパスされる問題を修正 +- Fix: メンションのサジェスト時に表示されるアイコン表示が画像サイズ次第で崩れる問題を修正 ### Server - Enhance: リモートノートクリーニングジョブのスキップ処理のパフォーマンス改善 diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index bfe66cdf8f..c5dd48f04a 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -483,6 +483,7 @@ onBeforeUnmount(() => { max-height: 28px; margin: 0 8px 0 0; border-radius: 100%; + object-fit: cover; } .userName { From 312d7c186675f41a503cd95622d211d019326ccb Mon Sep 17 00:00:00 2001 From: Kissa Ruokanen Date: Thu, 4 Jun 2026 23:42:02 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix(frontend):=20=E3=83=91=E3=82=B9?= =?UTF-8?q?=E3=82=AD=E3=83=BC=E7=99=BB=E9=8C=B2=E5=AE=8C=E4=BA=86=E6=99=82?= =?UTF-8?q?=E3=81=AE=E8=AA=8D=E8=A8=BC=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=81=AE=E5=85=A5=E5=8A=9B=E5=80=A4=E3=81=8C=E4=BD=BF?= =?UTF-8?q?=E3=82=8F=E3=82=8C=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#17539)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/frontend/src/pages/settings/2fa.vue | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a4b832cf..b3c2b03fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fix: 一部のUI要素の色が正しく表示されない問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1243) - Fix: 「D」キーでダークモードを切り替える際にsyncDeviceDarkModeのチェックがバイパスされる問題を修正 +- Fix: パスキー登録完了時の認証ダイアログの入力値が使われていない問題を修正 - Fix: メンションのサジェスト時に表示されるアイコン表示が画像サイズ次第で崩れる問題を修正 ### Server diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index a3b4413436..937e2cef8c 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -222,8 +222,8 @@ async function addSecurityKey() { if (auth2.canceled) return; await os.apiWithDialog('i/2fa/key-done', { - password: auth.result.password, - token: auth.result.token, + password: auth2.result.password, + token: auth2.result.token, name: name.result, credential: credential, }); From 67a0ae460d38235a0a2b7d17f7d9976a7e87e6b1 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 5 Jun 2026 12:36:44 +0900 Subject: [PATCH 06/13] fix(frontend): locale inliner is not working (#17543) * feat: support facade module * refactor: migrate typings to ESTree from rolldown/utils * fix: name conflict from function parameter are not detected correctly * refactor: migrate typings to ESTree from rolldown/utils * fix: name conflict from function parameter are not detected correctly * fix: template literal in member expression not supported * fix: improve identifier conflict * feat: add error when no localization are applied by locale inliner * lint: fix lints * fix: let rolldown to not hoist i18n modules with other modules * chore: make error if there is unexpected specifiers * fix license header --- packages/frontend-builder/locale-inliner.ts | 33 +++ .../locale-inliner/collect-modifications.ts | 229 +++++++++++------- .../locale-inliner/facade-chunk-detection.ts | 73 ++++++ packages/frontend-builder/utils.ts | 2 +- packages/frontend-embed/vite.config.ts | 8 +- packages/frontend/vite.config.ts | 8 +- 6 files changed, 253 insertions(+), 100 deletions(-) create mode 100644 packages/frontend-builder/locale-inliner/facade-chunk-detection.ts diff --git a/packages/frontend-builder/locale-inliner.ts b/packages/frontend-builder/locale-inliner.ts index 8d0d6e3d20..5345af688b 100644 --- a/packages/frontend-builder/locale-inliner.ts +++ b/packages/frontend-builder/locale-inliner.ts @@ -9,6 +9,7 @@ import MagicString from 'magic-string'; import { collectModifications } from './locale-inliner/collect-modifications.js'; import { applyWithLocale } from './locale-inliner/apply-with-locale.js'; import { blankLogger } from './logger.js'; +import { detectI18nFacadeChunk } from './locale-inliner/facade-chunk-detection.js'; import type { Logger } from './logger.js'; import type { Locale } from 'i18n'; import type { Manifest as ViteManifest } from 'vite'; @@ -18,6 +19,7 @@ export class LocaleInliner { scriptsDir: string; i18nFile: string; i18nFileName: string; + i18nSymbol: string; logger: Logger; chunks: ScriptChunk[]; @@ -43,8 +45,10 @@ export class LocaleInliner { this.i18nFile = options.i18nFile; this.i18nFileName = this.stripScriptDir(options.manifest[this.i18nFile].file); this.logger = options.logger; + this.i18nSymbol = 'i18n'; this.chunks = Object.values(options.manifest).filter(chunk => this.isScriptFile(chunk.file)).map(chunk => ({ fileName: this.stripScriptDir(chunk.file), + src: chunk.src, chunkName: chunk.name, })); } @@ -57,13 +61,41 @@ export class LocaleInliner { } collectsModifications() { + this.#detectI18nFacadeChunk(); + for (const chunk of this.chunks) { if (chunk.sourceCode == null) { throw new Error(`Source code for ${chunk.fileName} is not loaded.`); } + if (chunk.isFacadeOfI18n) { + chunk.modifications = []; + continue; + } const fileLogger = this.logger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `); chunk.modifications = collectModifications(chunk.sourceCode, chunk.fileName, fileLogger, this); } + + if (!this.chunks.flatMap(x => x.modifications ?? []).some(x => x.type === 'localized')) { + throw new Error('No localizations are inlined! this should mean locale inliner is not working well!'); + } + } + + #detectI18nFacadeChunk() { + // For some reason, even with `preserveEntrySignatures: 'allow-extension'`, rolldown may generate facade chunk + // This method detects facade chunk and replace i18nFile / i18nFileName with correct file name + const chunk = this.chunks.find(x => x.fileName === this.i18nFileName); + if (chunk == null) throw new Error(`i18n script file '${this.i18nFile}' not found`); + if (chunk.sourceCode == null) throw new Error(`Source code for '${this.i18nFile}' not loaded`); + const fileLogger = this.logger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `); + const facadeInfo = detectI18nFacadeChunk(chunk.sourceCode, chunk.fileName, fileLogger); + if (facadeInfo != null) { + const i18nSymbol = facadeInfo.nameMap[this.i18nSymbol]; + if (i18nSymbol == null) throw new Error(`Facade module for i18n file does not map ${this.i18nSymbol}. mapping: ${JSON.stringify(facadeInfo.nameMap)}`); + this.logger.info(`We detected ${this.i18nFileName} is facade chunk maps ${facadeInfo.fileName} with ${i18nSymbol} as ${this.i18nSymbol}`); + chunk.isFacadeOfI18n = true; + this.i18nFileName = facadeInfo.fileName; + this.i18nSymbol = i18nSymbol; + } } async saveAllLocales(locales: Record) { @@ -107,6 +139,7 @@ interface ScriptChunk { fileName: string; chunkName?: string; sourceCode?: string; + isFacadeOfI18n?: true; modifications?: TextModification[]; } diff --git a/packages/frontend-builder/locale-inliner/collect-modifications.ts b/packages/frontend-builder/locale-inliner/collect-modifications.ts index 80ce268f95..2bf53b090f 100644 --- a/packages/frontend-builder/locale-inliner/collect-modifications.ts +++ b/packages/frontend-builder/locale-inliner/collect-modifications.ts @@ -5,10 +5,8 @@ import { parseAst } from 'rolldown/parseAst'; import * as estreeWalker from 'estree-walker'; -import { assertNever, assertType } from '../utils.js'; -import type { ESTree as RolldownESTree } from 'rolldown/utils'; -import type { AstNode } from 'rollup'; -import type * as estree from 'estree'; +import { assertNever } from '../utils.js'; +import type { ESTree } from 'rolldown/utils'; import type { LocaleInliner, TextModification } from '../locale-inliner.js'; import type { Logger } from '../logger.js'; @@ -17,9 +15,15 @@ interface WalkerContext { skip: () => void; } +const walk = estreeWalker.walk as { + (node: ESTree.Node, callback: { + enter?: (this: WalkerContext, node: ESTree.Node, parent: ESTree.Node | null, property: string | number | symbol | null | undefined) => void; + }): void; +}; + export function collectModifications(sourceCode: string, fileName: string, fileLogger: Logger, inliner: LocaleInliner): TextModification[] { if (sourceCode === '') return []; - let programNode: RolldownESTree.Program; + let programNode: ESTree.Program; try { programNode = parseAst(sourceCode); } catch (err) { @@ -37,11 +41,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 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (estreeWalker.walk as any)(programNode, { - enter(this: WalkerContext, node: Node) { - assertType(node); - + walk(programNode, { + enter(this: WalkerContext, node: ESTree.Node) { if (node.type === 'Literal' && typeof node.value === 'string' && node.raw) { if (node.raw.substring(1).startsWith(inliner.scriptsDir)) { // we find `scripts/\w+\.js` literal and replace 'scripts' part with locale code @@ -92,7 +93,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL }, }); - const importSpecifierResult = findImportSpecifier(programNode, inliner.i18nFileName, 'i18n'); + const importSpecifierResult = findImportSpecifier(programNode, inliner.i18nFileName, inliner.i18nSymbol); switch (importSpecifierResult.type) { case 'no-import': @@ -108,7 +109,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL }); return modifications; case 'unexpected-specifiers': - fileLogger.info(`Importing ${inliner.i18nFileName} found but with unexpected specifiers. Skipping inlining.`); + fileLogger.error(`Importing ${inliner.i18nFileName} found but with unexpected specifiers. Skipping inlining.`); return modifications; case 'specifier': fileLogger.debug(`Found import i18n as ${importSpecifierResult.localI18nIdentifier}`); @@ -118,42 +119,18 @@ export function collectModifications(sourceCode: string, fileName: string, fileL const i18nImport = importSpecifierResult.importNode; const localI18nIdentifier = importSpecifierResult.localI18nIdentifier; - // 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; - // 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))) { - if (id === localI18nIdentifier) { - isSupported = false; - } - } - } - }, - }); - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!isSupported) { - fileLogger.error(`Duplicated identifier "${localI18nIdentifier}" in variable declaration. Skipping inlining.`); - return modifications; - } - - fileLogger.debug(`imports i18n as ${localI18nIdentifier}`); + fileLogger.debug(`imports ${inliner.i18nSymbol} /*i18n*/ as ${localI18nIdentifier}`); // In case of substitution failure, we will preserve the import statement // otherwise we will remove it. let preserveI18nImport = false; + const codeModifications: TextModification[] = []; + const toSkip = new Set(); toSkip.add(i18nImport); - // 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); + walk(programNode, { + enter(this: WalkerContext, node, parent, property) { if (toSkip.has(node)) { // This is the import specifier, skip processing it this.skip(); @@ -164,25 +141,24 @@ export function collectModifications(sourceCode: string, fileName: string, fileL if (node.type === 'ImportDeclaration') this.skip(); if (node.type === 'Identifier') { - assertType(node); - assertType(parent); + if (parent == null) throw new Error(); if (parent.type === 'Property' && !parent.computed && property === 'key') return; // we don't care 'id' part of { id: expr } if (parent.type === 'MemberExpression' && !parent.computed && property === 'property') return; // we don't care 'id' part of { id: expr } if (parent.type === 'ExportSpecifier' && property === 'exported') return; // we don't care 'id' part of { id: expr } if (node.name === localI18nIdentifier) { + // the use of identifier is either direct reference to i18n, or unsupported conflict of the identifier, which should report error. fileLogger.error(`${lineCol(sourceCode, node)}: Using i18n identifier "${localI18nIdentifier}" directly. Skipping inlining.`); preserveI18nImport = true; } } else if (node.type === 'MemberExpression') { - assertType(node); const i18nPath = parseI18nPropertyAccess(node); if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] === 'ts') { - if (parent.type === 'CallExpression' && property === 'callee') return; // we don't want to process `i18n.ts.property.stringBuiltinMethod()` + if (parent != null && parent.type === 'CallExpression' && property === 'callee') return; // we don't want to process `i18n.ts.property.stringBuiltinMethod()` if (i18nPath.at(-1)?.startsWith('_')) fileLogger.debug(`found i18n grouped property access ${i18nPath.join('.')}`); else fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n property access ${i18nPath.join('.')}`); // it's i18n.ts.propertyAccess // i18n.ts.* will always be resolved to string or object containing strings - modifications.push({ + codeModifications.push({ type: 'localized', begin: node.start, end: node.end, @@ -194,7 +170,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL // it's parameterized locale substitution (`i18n.tsx.property(parameters)`) // we expect the parameter to be an object literal fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n function access (object) ${i18nPath.join('.')}`); - modifications.push({ + codeModifications.push({ type: 'parameterized-function', begin: node.start, end: node.end, @@ -203,10 +179,41 @@ export function collectModifications(sourceCode: string, fileName: string, fileL }); this.skip(); } - } else if (node.type === 'ArrowFunctionExpression') { - assertType(node); + } + + // Scope check + if (node.type === 'FunctionDeclaration' + || node.type === 'FunctionExpression' + || node.type === 'ArrowFunctionExpression') { + // if i18n is introduced as the Named Function Expression, interior of the function does not matter + if (node.id?.name === localI18nIdentifier) this.skip(); // If there is 'i18n' in the parameters, we care interior of the function if (node.params.flatMap(param => declsOfPattern(param)).includes(localI18nIdentifier)) this.skip(); + + // We find var declation inside the function and if there are + if (findFunctionScopeDecls(node).includes(localI18nIdentifier)) this.skip(); + } + + if (node.type === 'BlockStatement') { + // We find block-scope declaration inside the block, or from parent node if the block is part of for statement or catch clause + if (findBlockScopeDecls(node).includes(localI18nIdentifier)) this.skip(); + } + + // statements and clauses introduces new variables in variable scope + if (node.type === 'CatchClause') { + if (node.param != null) { + if (declsOfPattern(node.param).includes(localI18nIdentifier)) this.skip(); + } + } else if (node.type === 'ForStatement') { + if (node.init?.type === 'VariableDeclaration') { + if (node.init.declarations.flatMap(x => declsOfPattern(x.id)).includes(localI18nIdentifier)) this.skip(); + } + } else if (node.type === 'ForInStatement' || node.type === 'ForOfStatement') { + if (node.left.type === 'VariableDeclaration') { + if (node.left.declarations.flatMap(x => declsOfPattern(x.id)).includes(localI18nIdentifier)) this.skip(); + } else { + if (declsOfPattern(node.left).includes(localI18nIdentifier)) this.skip(); + } } }, }); @@ -214,7 +221,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!preserveI18nImport) { fileLogger.debug('removing i18n import statement'); - modifications.push({ + codeModifications.push({ type: 'delete', begin: i18nImport.start, end: i18nImport.end, @@ -222,7 +229,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL }); } - function parseI18nPropertyAccess(node: estree.Expression | estree.Super): string[] | null { + function parseI18nPropertyAccess(node: ESTree.Expression | ESTree.Super): string[] | null { if (node.type === 'Identifier' && node.name === localI18nIdentifier) return []; // i18n itself if (node.type !== 'MemberExpression') return null; // super.* @@ -236,6 +243,9 @@ export function collectModifications(sourceCode: string, fileName: string, fileL if (node.property.type === 'Literal' && typeof node.property.value === 'string') { id = node.property.value; } + if (node.property.type === 'TemplateLiteral' && node.property.quasis.length === 1) { + id = node.property.quasis[0].value.cooked; + } } else { if (node.property.type === 'Identifier') { id = node.property.name; @@ -249,10 +259,10 @@ export function collectModifications(sourceCode: string, fileName: string, fileL return [...parentAccess, id]; } - return modifications; + return [...modifications, ...codeModifications]; } -function declsOfPattern(pattern: estree.Pattern | null): string[] { +function declsOfPattern(pattern: ESTree.BindingPattern | ESTree.ParamPattern | ESTree.ArrayAssignmentTarget | ESTree.ObjectAssignmentTarget | ESTree.AssignmentTargetMaybeDefault | ESTree.AssignmentTargetRest | null): string[] { if (pattern == null) return []; switch (pattern.type) { case 'Identifier': @@ -275,15 +285,19 @@ function declsOfPattern(pattern: estree.Pattern | null): string[] { case 'AssignmentPattern': return declsOfPattern(pattern.left); case 'MemberExpression': - // assignment pattern so no new variable is declared - return []; + case 'TSAsExpression': + case 'TSSatisfiesExpression': + case 'TSTypeAssertion': + case 'TSNonNullExpression': + return []; // not introducing new symbol + case 'TSParameterProperty': + throw new Error(); default: assertNever(pattern); } } -function lineCol(sourceCode: string, node: estree.Node): string { - assertType(node); +function lineCol(sourceCode: string, node: ESTree.Node): string { const leading = sourceCode.slice(0, node.start); const lines = leading.split('\n'); const line = lines.length; @@ -291,35 +305,65 @@ function lineCol(sourceCode: string, node: estree.Node): string { return `(${line}:${col})`; } +function findFunctionScopeDecls(fn: ESTree.Function | ESTree.ArrowFunctionExpression): string[] { + if (fn.body == null) return []; + const decls: string[] = []; + walk(fn.body, { + enter(node) { + // The only function-scoped symbol declaration in strict mode is 'var' + // If it's non-strict mode, function declaration will also in function scope. + if (node.type === 'VariableDeclaration' && node.kind === 'var') { + decls.push(...node.declarations.flatMap(x => declsOfPattern(x.id))); + } + + if (node.type === 'FunctionDeclaration' + || node.type === 'FunctionExpression' + || node.type === 'ArrowFunctionExpression') { + // The function makes new inner scope + this.skip(); + } + }, + }); + return decls; +} + +function findBlockScopeDecls(block: ESTree.BlockStatement): string[] { + const decls: string[] = []; + + for (const body of block.body) { + walk(body, { + enter(node) { + if (node.type === 'VariableDeclaration' && node.kind !== 'var') { + decls.push(...node.declarations.flatMap(x => declsOfPattern(x.id))); + } else if (node.type === 'FunctionDeclaration') { + if (node.id != null) decls.push(node.id.name); + } else if (node.type === 'ClassDeclaration') { + if (node.id != null) decls.push(node.id.name); + } + + if ( + node.type === 'FunctionDeclaration' + || node.type === 'FunctionExpression' + || node.type === 'ArrowFunctionExpression' + || node.type === 'BlockStatement' + || node.type === 'CatchClause' + || node.type === 'ForStatement' + || node.type === 'ForInStatement' + || node.type === 'ForOfStatement' + ) { + // The function makes new inner scope + this.skip(); + } + }, + }); + } + return decls; +} + //region checker functions -type Node = - | estree.AssignmentProperty - | estree.CatchClause - | estree.Class - | estree.ClassBody - | estree.Expression - | estree.Function - | estree.Identifier - | estree.Literal - | estree.MethodDefinition - | estree.ModuleDeclaration - | estree.ModuleSpecifier - | estree.Pattern - | estree.PrivateIdentifier - | estree.Program - | estree.Property - | estree.PropertyDefinition - | estree.SpreadElement - | estree.Statement - | estree.Super - | estree.SwitchCase - | estree.TemplateElement - | estree.VariableDeclarator - ; - // localStorage.getItem("lang") -function isLocalStorageGetItemLang(getItemCall: Node): boolean { +function isLocalStorageGetItemLang(getItemCall: ESTree.Node): boolean { if (getItemCall.type !== 'CallExpression') return false; if (getItemCall.arguments.length !== 1) return false; @@ -336,7 +380,7 @@ function isLocalStorageGetItemLang(getItemCall: Node): boolean { } // await window.fetch(`/assets/locales/${d}.${x}.json`).then(u => u.json(), ....) -function isAwaitFetchLocaleThenJson(awaitNode: Node): boolean { +function isAwaitFetchLocaleThenJson(awaitNode: ESTree.Node): boolean { if (awaitNode.type !== 'AwaitExpression') return false; const thenCall = awaitNode.argument; @@ -379,16 +423,15 @@ function isAwaitFetchLocaleThenJson(awaitNode: Node): boolean { type SpecifierResult = | { type: 'no-import' } - | { type: 'no-specifiers', importNode: estree.ImportDeclaration & AstNode } - | { type: 'unexpected-specifiers', importNode: estree.ImportDeclaration & AstNode } - | { type: 'specifier', localI18nIdentifier: string, importNode: estree.ImportDeclaration & AstNode } + | { type: 'no-specifiers', importNode: ESTree.ImportDeclaration } + | { type: 'unexpected-specifiers', importNode: ESTree.ImportDeclaration } + | { type: 'specifier', localI18nIdentifier: string, importNode: ESTree.ImportDeclaration } ; -function findImportSpecifier(programNode: RolldownESTree.Program, i18nFileName: string, i18nSymbol: string): SpecifierResult { +function findImportSpecifier(programNode: ESTree.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; + const importNode = imports.find(x => x.source.value === `./${i18nFileName}`); if (!importNode) return { type: 'no-import' }; - assertType(importNode); if (importNode.specifiers.length === 0) { return { type: 'no-specifiers', importNode }; @@ -415,15 +458,15 @@ function findImportSpecifier(programNode: RolldownESTree.Program, i18nFileName: } // checker helpers -function isMemberExpression(node: Node, property: string): node is estree.MemberExpression { +function isMemberExpression(node: ESTree.Node, property: string): node is ESTree.MemberExpression { return node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier' && node.property.name === property; } -function isStringLiteral(node: Node, value: string): node is estree.Literal { +function isStringLiteral(node: ESTree.Node, value: string): node is ESTree.StringLiteral { return node.type === 'Literal' && typeof node.value === 'string' && node.value === value; } -function isIdentifier(node: Node, name: string): node is estree.Identifier { +function isIdentifier(node: ESTree.Node, name: string): node is ESTree.IdentifierReference { return node.type === 'Identifier' && node.name === name; } diff --git a/packages/frontend-builder/locale-inliner/facade-chunk-detection.ts b/packages/frontend-builder/locale-inliner/facade-chunk-detection.ts new file mode 100644 index 0000000000..0a76e8cbcd --- /dev/null +++ b/packages/frontend-builder/locale-inliner/facade-chunk-detection.ts @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import path from 'node:path'; +import { parseAst } from 'rolldown/parseAst'; +import type { Logger } from '../logger.js'; +import type { ESTree as RolldownESTree } from 'rolldown/utils'; + +interface FacadeInfo { + fileName: string, + // facade export name => internal name + nameMap: Partial>, +} + +export function detectI18nFacadeChunk( + sourceCode: string, + fileName: string, + fileLogger: Logger, +): FacadeInfo | null { + let programNode: RolldownESTree.Program; + try { + programNode = parseAst(sourceCode); + } catch (err) { + fileLogger.error(`Failed to parse source code: ${err}`); + return null; + } + if (programNode.sourceType !== 'module') { + fileLogger.error('Source code is not a module.'); + return null; + } + + // check if the file is like facade. + // if file is like following we treat them as facade. + // ``` + // import { something } from "file"; + // export { something }; + // ``` + if (programNode.body.length !== 2) return null; // not a facade + if (programNode.body[0].type !== 'ImportDeclaration') return null; // not a facade + if (programNode.body[1].type !== 'ExportNamedDeclaration') return null; // not a facade + const importDecl = programNode.body[0]; + const exportDecl = programNode.body[1]; + + // the file is a facade file. + + const sourcePath = importDecl.source.value; + const sourceName = path.posix.basename(sourcePath); + + const importNameMap = Object.fromEntries(importDecl.specifiers + .map(specifier => { + if (specifier.type !== 'ImportSpecifier') throw new Error(`${fileName}: Unexpected import specifier in facade module: ${specifier.type}`); + const exportName = getExportName(specifier.imported); + const localName = specifier.local.name; + return [localName, exportName]; + })); + const nameMap = Object.fromEntries(exportDecl.specifiers.map(spec => { + const localName = getExportName(spec.local); + const facadeExportName = getExportName(spec.exported); + const moduleExportName = importNameMap[localName]; + return [facadeExportName, moduleExportName]; + })); + + return { + fileName: sourceName, + nameMap, + }; +} + +function getExportName(node: RolldownESTree.ModuleExportName): string { + return node.type === 'Literal' ? node.value : node.name; +} diff --git a/packages/frontend-builder/utils.ts b/packages/frontend-builder/utils.ts index 71ffebe03e..f85ae7ea0c 100644 --- a/packages/frontend-builder/utils.ts +++ b/packages/frontend-builder/utils.ts @@ -8,5 +8,5 @@ export function assertNever(x: never): never { throw new Error(`Unexpected type: ${(x as any)?.type ?? x}`); } -export function assertType(node: unknown): asserts node is T { +export function assertType(_node: unknown): asserts node is T { } diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index e12e042082..1b10ac5344 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -153,9 +153,11 @@ export function getConfig(): UserConfig { name: 'vue', test: /node_modules[\\/]vue/, }, { - // dependencies of i18n.ts - name: 'config', - test: /@@[\\/]js[\\/]config\.js/, + // split each i18n related module to each distinct module, deny hoisting + name: 'i18n', + test: /i18n\.ts/, + minSize: 0, + maxSize: 1, }], }, entryFileNames: `scripts/${localesHash}-[hash:8].js`, diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index b5aed9484f..88669f2fe9 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -194,9 +194,11 @@ export function getConfig(): UserConfig { name: 'photoswipe', test: /node_modules[\\/]photoswipe/, }, { - // dependencies of i18n.ts - name: 'config', - test: /@@[\\/]js[\\/]config\.js/, + // split each i18n related module to each distinct module, deny hoisting + name: 'i18n', + test: /i18n\.ts/, + minSize: 0, + maxSize: 1, }], }, entryFileNames: `scripts/${localesHash}-[hash:8].js`, From a75f3adc362f5ae27698c6f9d07f06c28f6c4d2a Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 5 Jun 2026 13:51:38 +0900 Subject: [PATCH 07/13] fix: we cannot look up user profile url with self hostname (#16488) --- packages/backend/src/server/ActivityPubServerService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 5d9ce78793..cbbeb78f68 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -777,6 +777,8 @@ export class ActivityPubServerService { } const acct = Acct.parse(request.params.acct); + // normalize acct host + if (this.utilityService.isSelfHost(acct.host)) acct.host = null; const user = await this.usersRepository.findOneBy({ usernameLower: acct.username.toLowerCase(), From a0889acb2a47ee1f8ccacf192b72cafbac9e1ac0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Jun 2026 04:55:33 +0000 Subject: [PATCH 08/13] Bump version to 2026.6.0-alpha.0 --- CHANGELOG.md | 2 +- package.json | 2 +- packages/misskey-js/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c2b03fb1..1b0577fe32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 2026.6.0 ### General - Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように diff --git a/package.json b/package.json index 27e3f87fb9..d22f0344cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2026.5.4", + "version": "2026.6.0-alpha.0", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index f38de81637..4fba054254 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2026.5.4", + "version": "2026.6.0-alpha.0", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 2aa6d4fc7ff9b893836aa1659fdcfa709e7d0f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:07:51 +0900 Subject: [PATCH 09/13] fix(frontend): add antenna handling in antenna-column component (#17553) fix: add antenna handling in antenna-column component --- .../frontend/src/ui/deck/antenna-column.vue | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index c81688131b..76ea4ac738 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -14,8 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only