summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
authoranatawa12 <anatawa12@icloud.com>2025-04-05 08:46:17 +0900
committerGitHub <noreply@github.com>2025-04-05 08:46:17 +0900
commit5949bb6c7fec5616e9d3f9ba21eb1ab1b41ed905 (patch)
treea09f7a48c07d0b99fc7c071cc22d88706ef045d7 /packages/frontend
parentNew Crowdin updates (#15740) (diff)
downloadmisskey-5949bb6c7fec5616e9d3f9ba21eb1ab1b41ed905.tar.gz
misskey-5949bb6c7fec5616e9d3f9ba21eb1ab1b41ed905.tar.bz2
misskey-5949bb6c7fec5616e9d3f9ba21eb1ab1b41ed905.zip
fix: unnecesary HMR when we opened setting page (#15756)
* refactor: add MarkerIdAssigner instead of processVueFile and remove transformedCodeCache object * chore: add minimatch, a glob matcher * chore: expose MarkerIdAssigner from plugin * Revert "chore: expose MarkerIdAssigner from plugin" This reverts commit 88c6d820f8635c35f1c15b4aac0987075d7cf8aa. * chore: add plugin to generate virtual module * chore: parse inlining earlier * chore: use virtual module in search * chore: use remove old generation * chore: fix type errors * chore: add patch to workaround vitejs/vite#19792 * chore: improve filtering files to process * chore: rename plugin * docs: add comment for plugin ordering * fix: unnecessary log * fix: spdx license header
Diffstat (limited to 'packages/frontend')
-rw-r--r--packages/frontend/lib/vite-plugin-create-search-index.ts609
-rw-r--r--packages/frontend/package.json3
-rw-r--r--packages/frontend/scripts/generate-search-index.ts15
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue2
-rw-r--r--packages/frontend/src/pages/settings/index.vue2
-rw-r--r--packages/frontend/src/utility/autogen/settings-search-index.ts993
-rw-r--r--packages/frontend/src/utility/settings-search-index.ts43
-rw-r--r--packages/frontend/src/utility/virtual.d.ts18
-rw-r--r--packages/frontend/vite.config.ts3
9 files changed, 369 insertions, 1319 deletions
diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts
index d506e84bb6..99af81fb70 100644
--- a/packages/frontend/lib/vite-plugin-create-search-index.ts
+++ b/packages/frontend/lib/vite-plugin-create-search-index.ts
@@ -4,77 +4,68 @@
*/
import { parse as vueSfcParse } from 'vue/compiler-sfc';
-import type { LogOptions, Plugin } from 'vite';
+import {
+ createLogger,
+ EnvironmentModuleGraph,
+ normalizePath,
+ type LogErrorOptions,
+ type LogOptions,
+ type Plugin,
+ type PluginOption
+} from 'vite';
import fs from 'node:fs';
import { glob } from 'glob';
import JSON5 from 'json5';
-import MagicString from 'magic-string';
+import MagicString, { SourceMap } from 'magic-string';
import path from 'node:path'
import { hash, toBase62 } from '../vite.config';
-import { createLogger } from 'vite';
+import { minimatch } from 'minimatch';
+import type {
+ AttributeNode, CompoundExpressionNode, DirectiveNode,
+ ElementNode,
+ RootNode, SimpleExpressionNode,
+ TemplateChildNode,
+} from '@vue/compiler-core';
+import { NodeTypes } from '@vue/compiler-core';
-interface VueAstNode {
- type: number;
- tag?: string;
- loc?: {
- start: { offset: number, line: number, column: number },
- end: { offset: number, line: number, column: number },
- source?: string
- };
- props?: Array<{
- name: string;
- type: number;
- value?: { content?: string };
- arg?: { content?: string };
- exp?: { content?: string; loc?: any };
- }>;
- children?: VueAstNode[];
- content?: any;
- __markerId?: string;
- __children?: string[];
-}
-
-export type AnalysisResult = {
+export type AnalysisResult<T = SearchIndexItem> = {
filePath: string;
- usage: SearchIndexItem[];
+ usage: T[];
}
-export type SearchIndexItem = {
+export type SearchIndexItem = SearchIndexItemLink<SearchIndexItem>;
+export type SearchIndexStringItem = SearchIndexItemLink<string>;
+export interface SearchIndexItemLink<T> {
id: string;
path?: string;
label: string;
keywords: string | string[];
icon?: string;
inlining?: string[];
- children?: SearchIndexItem[];
-};
+ children?: T[];
+}
export type Options = {
targetFilePaths: string[],
- exportFilePath: string,
+ mainVirtualModule: string,
+ modulesToHmrOnUpdate: string[],
+ fileVirtualModulePrefix?: string,
+ fileVirtualModuleSuffix?: string,
verbose?: boolean,
};
-// 関連するノードタイプの定数化
-const NODE_TYPES = {
- ELEMENT: 1,
- EXPRESSION: 2,
- TEXT: 3,
- INTERPOLATION: 5, // Mustache
-};
-
// マーカー関係を表す型
interface MarkerRelation {
parentId?: string;
markerId: string;
- node: VueAstNode;
+ node: ElementNode;
}
// ロガー
let logger = {
info: (msg: string, options?: LogOptions) => { },
warn: (msg: string, options?: LogOptions) => { },
- error: (msg: string, options?: LogOptions) => { },
+ error: (msg: string, options?: LogErrorOptions) => { },
};
let loggerInitialized = false;
@@ -99,14 +90,11 @@ function initLogger(options: Options) {
}
}
-/**
- * 解析結果をTypeScriptファイルとして出力する
- */
-function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void {
+function collectSearchItemIndexes(analysisResults: AnalysisResult<SearchIndexStringItem>[]): SearchIndexItem[] {
logger.info(`Processing ${analysisResults.length} files for output`);
// 新しいツリー構造を構築
- const allMarkers = new Map<string, SearchIndexItem>();
+ const allMarkers = new Map<string, SearchIndexStringItem>();
// 1. すべてのマーカーを一旦フラットに収集
for (const file of analysisResults) {
@@ -115,10 +103,9 @@ function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisR
for (const marker of file.usage) {
if (marker.id) {
// キーワードとchildren処理を共通化
- const processedMarker = {
+ const processedMarker: SearchIndexStringItem = {
...marker,
keywords: processMarkerProperty(marker.keywords, 'keywords'),
- children: processMarkerProperty(marker.children || [], 'children')
};
allMarkers.set(marker.id, processedMarker);
@@ -143,14 +130,13 @@ function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisR
const { totalMarkers, totalChildren } = countMarkers(resolvedRootMarkers);
logger.info(`Total markers in tree: ${totalMarkers} (${resolvedRootMarkers.length} roots + ${totalChildren} nested children)`);
- // 6. 結果をTS形式で出力
- writeOutputFile(outputPath, resolvedRootMarkers);
+ return resolvedRootMarkers;
}
/**
* マーカーのプロパティ(keywordsやchildren)を処理する
*/
-function processMarkerProperty(propValue: any, propType: 'keywords' | 'children'): any {
+function processMarkerProperty(propValue: string | string[], propType: 'keywords' | 'children'): string | string[] {
// 文字列の配列表現を解析
if (typeof propValue === 'string' && propValue.startsWith('[') && propValue.endsWith(']')) {
try {
@@ -169,7 +155,7 @@ function processMarkerProperty(propValue: any, propType: 'keywords' | 'children'
/**
* 全マーカーから子IDを収集する
*/
-function collectChildIds(allMarkers: Map<string, SearchIndexItem>): Set<string> {
+function collectChildIds(allMarkers: Map<string, SearchIndexStringItem>): Set<string> {
const childIds = new Set<string>();
allMarkers.forEach((marker, id) => {
@@ -232,10 +218,10 @@ function collectChildIds(allMarkers: Map<string, SearchIndexItem>): Set<string>
* ルートマーカー(他の子でないマーカー)を特定する
*/
function identifyRootMarkers(
- allMarkers: Map<string, SearchIndexItem>,
+ allMarkers: Map<string, SearchIndexStringItem>,
childIds: Set<string>
-): SearchIndexItem[] {
- const rootMarkers: SearchIndexItem[] = [];
+): SearchIndexStringItem[] {
+ const rootMarkers: SearchIndexStringItem[] = [];
allMarkers.forEach((marker, id) => {
if (!childIds.has(id)) {
@@ -251,12 +237,12 @@ function identifyRootMarkers(
* 子マーカーの参照をIDから実際のオブジェクトに解決する
*/
function resolveChildReferences(
- rootMarkers: SearchIndexItem[],
- allMarkers: Map<string, SearchIndexItem>
+ rootMarkers: SearchIndexStringItem[],
+ allMarkers: Map<string, SearchIndexStringItem>
): SearchIndexItem[] {
- function resolveChildrenForMarker(marker: SearchIndexItem): SearchIndexItem {
+ function resolveChildrenForMarker(marker: SearchIndexStringItem): SearchIndexItem {
// マーカーのディープコピーを作成
- const resolvedMarker = { ...marker };
+ const resolvedMarker: SearchIndexItem = { ...marker, children: [] };
// 明示的に子マーカー配列を作成
const resolvedChildren: SearchIndexItem[] = [];
@@ -352,54 +338,18 @@ function countMarkers(markers: SearchIndexItem[]): { totalMarkers: number, total
}
/**
- * 最終的なTypeScriptファイルを出力
- */
-function writeOutputFile(outputPath: string, resolvedRootMarkers: SearchIndexItem[]): void {
- try {
- const tsOutput = generateTypeScriptCode(resolvedRootMarkers);
- fs.writeFileSync(outputPath, tsOutput, 'utf-8');
- // 強制的に出力させるためにViteロガーを使わない
- console.log(`Successfully wrote search index to ${outputPath} with ${resolvedRootMarkers.length} root entries`);
- } catch (error) {
- logger.error('[create-search-index]: error writing output: ', error);
- }
-}
-
-/**
* TypeScriptコード生成
*/
-function generateTypeScriptCode(resolvedRootMarkers: SearchIndexItem[]): string {
- return `
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// This file was automatically generated by create-search-index.
-// Do not edit this file.
-
-import { i18n } from '@/i18n.js';
-
-export type SearchIndexItem = {
- id: string;
- path?: string;
- label: string;
- keywords: string[];
- icon?: string;
- children?: SearchIndexItem[];
-};
-
-export const searchIndexes: SearchIndexItem[] = ${customStringify(resolvedRootMarkers)} as const;
-
-export type SearchIndex = typeof searchIndexes;
-`;
+function generateJavaScriptCode(resolvedRootMarkers: SearchIndexItem[]): string {
+ return `import { i18n } from '@/i18n.js';\n`
+ + `export const searchIndexes = ${customStringify(resolvedRootMarkers)};\n`;
}
/**
* オブジェクトを特殊な形式の文字列に変換する
* i18n参照を保持しつつ適切な形式に変換
*/
-function customStringify(obj: any, depth = 0): string {
+function customStringify(obj: unknown, depth = 0): string {
const INDENT_STR = '\t';
// 配列の処理
@@ -441,7 +391,6 @@ function customStringify(obj: any, depth = 0): string {
.filter(([key, value]) => {
if (value === undefined) return false;
if (key === 'children' && Array.isArray(value) && value.length === 0) return false;
- if (key === 'inlining') return false;
return true;
})
// 各プロパティを変換
@@ -462,7 +411,7 @@ function customStringify(obj: any, depth = 0): string {
/**
* 特殊プロパティの書式設定
*/
-function formatSpecialProperty(key: string, value: any): string {
+function formatSpecialProperty(key: string, value: unknown): string {
// 値がundefinedの場合は空文字列を返す
if (value === undefined) {
return '""';
@@ -499,7 +448,7 @@ function formatSpecialProperty(key: string, value: any): string {
/**
* 配列式の文字列表現を生成
*/
-function formatArrayForOutput(items: any[]): string {
+function formatArrayForOutput(items: unknown[]): string {
return items.map(item => {
// i18n.ts. 参照の文字列はそのままJavaScript式として出力
if (typeof item === 'string' && isI18nReference(item)) {
@@ -516,17 +465,18 @@ function formatArrayForOutput(items: any[]): string {
* 要素ノードからテキスト内容を抽出する
* 各抽出方法を分離して可読性を向上
*/
-function extractElementText(node: VueAstNode): string | null {
+function extractElementText(node: TemplateChildNode): string | null {
if (!node) return null;
+ if (node.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION");
- logger.info(`Extracting text from node type=${node.type}, tag=${node.tag || 'unknown'}`);
+ logger.info(`Extracting text from node type=${node.type}, tag=${'tag' in node ? node.tag : 'unknown'}`);
// 1. 直接コンテンツの抽出を試行
const directContent = extractDirectContent(node);
if (directContent) return directContent;
// 子要素がない場合は終了
- if (!node.children || !Array.isArray(node.children)) {
+ if (!('children' in node) || !Array.isArray(node.children)) {
return null;
}
@@ -548,12 +498,13 @@ function extractElementText(node: VueAstNode): string | null {
/**
* ノードから直接コンテンツを抽出
*/
-function extractDirectContent(node: VueAstNode): string | null {
- if (!node.content) return null;
+function extractDirectContent(node: TemplateChildNode): string | null {
+ if (!('content' in node)) return null;
+ if (typeof node.content == 'object' && node.content.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION");
- const content = typeof node.content === 'string'
- ? node.content.trim()
- : (node.content.content ? node.content.content.trim() : null);
+ const content = typeof node.content === 'string' ? node.content.trim()
+ : node.content.type !== NodeTypes.INTERPOLATION ? node.content.content.trim()
+ : null;
if (!content) return null;
@@ -582,9 +533,9 @@ function extractDirectContent(node: VueAstNode): string | null {
/**
* インターポレーションノード(Mustache)からコンテンツを抽出
*/
-function extractInterpolationContent(children: VueAstNode[]): string | null {
+function extractInterpolationContent(children: TemplateChildNode[]): string | null {
for (const child of children) {
- if (child.type === NODE_TYPES.INTERPOLATION) {
+ if (child.type === NodeTypes.INTERPOLATION) {
logger.info(`Found interpolation node (Mustache): ${JSON.stringify(child.content).substring(0, 100)}...`);
if (child.content && child.content.type === 4 && child.content.content) {
@@ -595,6 +546,7 @@ function extractInterpolationContent(children: VueAstNode[]): string | null {
return content;
}
} else if (child.content && typeof child.content === 'object') {
+ if (child.content.type == NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION");
// オブジェクト形式のcontentを探索
logger.info(`Complex interpolation node: ${JSON.stringify(child.content).substring(0, 100)}...`);
@@ -616,10 +568,10 @@ function extractInterpolationContent(children: VueAstNode[]): string | null {
/**
* 式ノードからコンテンツを抽出
*/
-function extractExpressionContent(children: VueAstNode[]): string | null {
+function extractExpressionContent(children: TemplateChildNode[]): string | null {
// i18n.ts. 参照パターンを持つものを優先
for (const child of children) {
- if (child.type === NODE_TYPES.EXPRESSION && child.content) {
+ if (child.type === NodeTypes.TEXT && child.content) {
const expr = child.content.trim();
if (isI18nReference(expr)) {
@@ -631,7 +583,7 @@ function extractExpressionContent(children: VueAstNode[]): string | null {
// その他の式
for (const child of children) {
- if (child.type === NODE_TYPES.EXPRESSION && child.content) {
+ if (child.type === NodeTypes.TEXT && child.content) {
const expr = child.content.trim();
logger.info(`Found expression: ${expr}`);
return expr;
@@ -644,9 +596,9 @@ function extractExpressionContent(children: VueAstNode[]): string | null {
/**
* テキストノードからコンテンツを抽出
*/
-function extractTextContent(children: VueAstNode[]): string | null {
+function extractTextContent(children: TemplateChildNode[]): string | null {
for (const child of children) {
- if (child.type === NODE_TYPES.TEXT && child.content) {
+ if (child.type === NodeTypes.COMMENT && child.content) {
const text = child.content.trim();
if (text) {
@@ -672,16 +624,16 @@ function extractTextContent(children: VueAstNode[]): string | null {
/**
* 子ノードを再帰的に探索してコンテンツを抽出
*/
-function extractNestedContent(children: VueAstNode[]): string | null {
+function extractNestedContent(children: TemplateChildNode[]): string | null {
for (const child of children) {
- if (child.children && Array.isArray(child.children) && child.children.length > 0) {
+ if ('children' in child && Array.isArray(child.children) && child.children.length > 0) {
const nestedContent = extractElementText(child);
if (nestedContent) {
logger.info(`Found nested content: ${nestedContent}`);
return nestedContent;
}
- } else if (child.type === NODE_TYPES.ELEMENT) {
+ } else if (child.type === NodeTypes.ELEMENT) {
// childrenがなくても内部を調査
const nestedContent = extractElementText(child);
@@ -699,16 +651,16 @@ function extractNestedContent(children: VueAstNode[]): string | null {
/**
* SearchLabelとSearchKeywordを探して抽出する関数
*/
-function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null, keywords: any[] } {
+function extractLabelsAndKeywords(nodes: TemplateChildNode[]): { label: string | null, keywords: string[] } {
let label: string | null = null;
- const keywords: any[] = [];
+ const keywords: string[] = [];
logger.info(`Extracting labels and keywords from ${nodes.length} nodes`);
// 再帰的にSearchLabelとSearchKeywordを探索(ネストされたSearchMarkerは処理しない)
- function findComponents(nodes: VueAstNode[]) {
+ function findComponents(nodes: TemplateChildNode[]) {
for (const node of nodes) {
- if (node.type === NODE_TYPES.ELEMENT) {
+ if (node.type === NodeTypes.ELEMENT) {
logger.info(`Checking element: ${node.tag}`);
// SearchMarkerの場合は、その子要素は別スコープなのでスキップ
@@ -730,11 +682,12 @@ function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null,
logger.info(`SearchLabel found but extraction failed, trying direct children inspection`);
// バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認
- if (node.children && Array.isArray(node.children)) {
+ {
for (const child of node.children) {
// Mustacheインターポレーション
- if (child.type === NODE_TYPES.INTERPOLATION && child.content) {
+ if (child.type === NodeTypes.INTERPOLATION && child.content) {
// content内の式を取り出す
+ if (child.content.type == NodeTypes.COMPOUND_EXPRESSION) throw new Error("unexpected COMPOUND_EXPRESSION");
const expression = child.content.content ||
(child.content.type === 4 ? child.content.content : null) ||
JSON.stringify(child.content);
@@ -747,13 +700,13 @@ function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null,
}
}
// 式ノード
- else if (child.type === NODE_TYPES.EXPRESSION && child.content && isI18nReference(child.content)) {
+ else if (child.type === NodeTypes.TEXT && child.content && isI18nReference(child.content)) {
label = child.content.trim();
logger.info(`Found i18n in expression: ${label}`);
break;
}
// テキストノードでもMustache構文を探す
- else if (child.type === NODE_TYPES.TEXT && child.content) {
+ else if (child.type === NodeTypes.COMMENT && child.content) {
const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/);
if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) {
label = mustacheMatch[1].trim();
@@ -778,11 +731,12 @@ function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null,
logger.info(`SearchKeyword found but extraction failed, trying direct children inspection`);
// バックアップ: 子直接確認 - type=5のMustacheインターポレーションを重点的に確認
- if (node.children && Array.isArray(node.children)) {
+ {
for (const child of node.children) {
// Mustacheインターポレーション
- if (child.type === NODE_TYPES.INTERPOLATION && child.content) {
+ if (child.type === NodeTypes.INTERPOLATION && child.content) {
// content内の式を取り出す
+ if (child.content.type == NodeTypes.COMPOUND_EXPRESSION) throw new Error("unexpected COMPOUND_EXPRESSION");
const expression = child.content.content ||
(child.content.type === 4 ? child.content.content : null) ||
JSON.stringify(child.content);
@@ -796,14 +750,14 @@ function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null,
}
}
// 式ノード
- else if (child.type === NODE_TYPES.EXPRESSION && child.content && isI18nReference(child.content)) {
+ else if (child.type === NodeTypes.TEXT && child.content && isI18nReference(child.content)) {
const keyword = child.content.trim();
keywords.push(keyword);
logger.info(`Found i18n keyword in expression: ${keyword}`);
break;
}
// テキストノードでもMustache構文を探す
- else if (child.type === NODE_TYPES.TEXT && child.content) {
+ else if (child.type === NodeTypes.COMMENT && child.content) {
const mustacheMatch = child.content.trim().match(/^\s*{{\s*(.*?)\s*}}\s*$/);
if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) {
const keyword = mustacheMatch[1].trim();
@@ -834,23 +788,22 @@ function extractLabelsAndKeywords(nodes: VueAstNode[]): { label: string | null,
function extractUsageInfoFromTemplateAst(
- templateAst: any,
+ templateAst: RootNode | undefined,
id: string,
-): SearchIndexItem[] {
- const allMarkers: SearchIndexItem[] = [];
- const markerMap = new Map<string, SearchIndexItem>();
+): SearchIndexStringItem[] {
+ const allMarkers: SearchIndexStringItem[] = [];
+ const markerMap = new Map<string, SearchIndexItemLink<string>>();
const childrenIds = new Set<string>();
const normalizedId = id.replace(/\\/g, '/');
if (!templateAst) return allMarkers;
// マーカーの基本情報を収集
- function collectMarkers(node: VueAstNode, parentId: string | null = null) {
- if (node.type === 1 && node.tag === 'SearchMarker') {
+ function collectMarkers(node: TemplateChildNode | RootNode, parentId: string | null = null) {
+ if (node.type === NodeTypes.ELEMENT && node.tag === 'SearchMarker') {
// マーカーID取得
- const markerIdProp = node.props?.find((p: any) => p.name === 'markerId');
- const markerId = markerIdProp?.value?.content ||
- node.__markerId;
+ const markerIdProp = node.props?.find(p => p.name === 'markerId');
+ const markerId = markerIdProp?.type == NodeTypes.ATTRIBUTE ? markerIdProp.value?.content : null;
// SearchMarkerにマーカーIDがない場合はエラー
if (markerId == null) {
@@ -859,7 +812,7 @@ function extractUsageInfoFromTemplateAst(
}
// マーカー基本情報
- const markerInfo: SearchIndexItem = {
+ const markerInfo: SearchIndexStringItem = {
id: markerId,
children: [],
label: '', // デフォルト値
@@ -882,7 +835,7 @@ function extractUsageInfoFromTemplateAst(
if (bindings.path) markerInfo.path = bindings.path;
if (bindings.icon) markerInfo.icon = bindings.icon;
if (bindings.label) markerInfo.label = bindings.label;
- if (bindings.children) markerInfo.children = bindings.children;
+ if (bindings.children) markerInfo.children = processMarkerProperty(bindings.children, 'children') as string[];
if (bindings.inlining) {
markerInfo.inlining = bindings.inlining;
logger.info(`Added inlining ${JSON.stringify(bindings.inlining)} to marker ${markerId}`);
@@ -946,19 +899,19 @@ function extractUsageInfoFromTemplateAst(
}
// 子ノードを処理
- if (node.children && Array.isArray(node.children)) {
- node.children.forEach((child: VueAstNode) => {
- collectMarkers(child, markerId);
- });
+ for (const child of node.children) {
+ collectMarkers(child, markerId);
}
return markerId;
}
// SearchMarkerでない場合は再帰的に子ノードを処理
- else if (node.children && Array.isArray(node.children)) {
- node.children.forEach((child: VueAstNode) => {
- collectMarkers(child, parentId);
- });
+ else if ('children' in node && Array.isArray(node.children)) {
+ for (const child of node.children) {
+ if (typeof child == 'object' && child.type !== NodeTypes.SIMPLE_EXPRESSION) {
+ collectMarkers(child, parentId);
+ }
+ }
}
return null;
@@ -969,16 +922,22 @@ function extractUsageInfoFromTemplateAst(
return allMarkers;
}
+type SpecialBindings = {
+ inlining: string[];
+ keywords: string[] | string;
+};
+type Bindings = Partial<Omit<Record<keyof SearchIndexItem, string>, keyof SpecialBindings> & SpecialBindings>;
// バインドプロパティの処理を修正する関数
-function extractNodeBindings(node: VueAstNode): Record<keyof SearchIndexItem, any> {
- const bindings: Record<string, any> = {};
+function extractNodeBindings(node: TemplateChildNode | RootNode): Bindings {
+ const bindings: Bindings = {};
- if (!node.props || !Array.isArray(node.props)) return bindings;
+ if (node.type !== NodeTypes.ELEMENT) return bindings;
// バインド式を収集
for (const prop of node.props) {
- if (prop.type === 7 && prop.name === 'bind' && prop.arg?.content) {
+ if (prop.type === NodeTypes.DIRECTIVE && prop.name === 'bind' && prop.arg && 'content' in prop.arg) {
const propName = prop.arg.content;
+ if (prop.exp?.type === NodeTypes.COMPOUND_EXPRESSION) throw new Error('unexpected COMPOUND_EXPRESSION');
const propContent = prop.exp?.content || '';
logger.info(`Processing bind prop ${propName}: ${propContent}`);
@@ -1055,7 +1014,7 @@ function extractNodeBindings(node: VueAstNode): Record<keyof SearchIndexItem, an
}
// 配列式をパースする補助関数(文字列リテラル処理を改善)
-function parseArrayExpression(expr: string): any[] {
+function parseArrayExpression(expr: string): string[] {
try {
// 単純なケースはJSON5でパースを試みる
return JSON5.parse(expr.replace(/'/g, '"'));
@@ -1067,7 +1026,7 @@ function parseArrayExpression(expr: string): any[] {
const content = expr.substring(1, expr.length - 1).trim();
if (!content) return [];
- const result: any[] = [];
+ const result: string[] = [];
let currentItem = '';
let depth = 0;
let inString = false;
@@ -1138,37 +1097,16 @@ function parseArrayExpression(expr: string): any[] {
}
}
-export async function analyzeVueProps(options: Options & {
- transformedCodeCache: Record<string, string>,
-}): Promise<void> {
- initLogger(options);
-
- const allMarkers: SearchIndexItem[] = [];
-
- // 対象ファイルパスを glob で展開
- const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
- const matchedFiles = glob.sync(filePathPattern);
- return [...acc, ...matchedFiles];
- }, []);
-
- logger.info(`Found ${filePaths.length} matching files to analyze`);
-
- for (const filePath of filePaths) {
- const absolutePath = path.join(process.cwd(), filePath);
- const id = absolutePath.replace(/\\/g, '/'); // 絶対パスに変換
- const code = options.transformedCodeCache[id]; // options 経由でキャッシュ参照
- if (!code) { // キャッシュミスの場合
- logger.error(`Error: No cached code found for: ${id}.`); // エラーログ
- throw new Error(`No cached code found for: ${id}.`); // エラーを投げる
- }
-
+export function collectFileMarkers(files: [id: string, code: string][]): AnalysisResult<SearchIndexStringItem> {
+ const allMarkers: SearchIndexStringItem[] = [];
+ for (const [id, code] of files) {
try {
- const { descriptor, errors } = vueSfcParse(options.transformedCodeCache[id], {
- filename: filePath,
+ const { descriptor, errors } = vueSfcParse(code, {
+ filename: id,
});
if (errors.length > 0) {
- logger.error(`Compile Error: ${filePath}, ${errors}`);
+ logger.error(`Compile Error: ${id}, ${errors}`);
continue; // エラーが発生したファイルはスキップ
}
@@ -1176,83 +1114,76 @@ export async function analyzeVueProps(options: Options & {
if (fileMarkers && fileMarkers.length > 0) {
allMarkers.push(...fileMarkers); // すべてのマーカーを収集
- logger.info(`Successfully extracted ${fileMarkers.length} markers from ${filePath}`);
+ logger.info(`Successfully extracted ${fileMarkers.length} markers from ${id}`);
} else {
- logger.info(`No markers found in ${filePath}`);
+ logger.info(`No markers found in ${id}`);
}
} catch (error) {
- logger.error(`Error analyzing file ${filePath}:`, error);
+ logger.error(`Error analyzing file ${id}:`, error);
}
}
// 収集したすべてのマーカー情報を使用
- const analysisResult: AnalysisResult[] = [
- {
- filePath: "combined-markers", // すべてのファイルのマーカーを1つのエントリとして扱う
- usage: allMarkers,
- }
- ];
-
- outputAnalysisResultAsTS(options.exportFilePath, analysisResult); // すべてのマーカー情報を渡す
-}
-
-interface MarkerRelation {
- parentId?: string;
- markerId: string;
- node: VueAstNode;
+ return {
+ filePath: "combined-markers", // すべてのファイルのマーカーを1つのエントリとして扱う
+ usage: allMarkers,
+ };
}
-async function processVueFile(
- code: string,
- id: string,
- options: Options,
- transformedCodeCache: Record<string, string>
-): Promise<{
+type TransformedCode = {
code: string,
- map: any,
- transformedCodeCache: Record<string, string>
-}> {
- const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化
+ map: SourceMap,
+};
- // 開発モード時はコード内容に変更があれば常に再処理する
- // コード内容が同じ場合のみキャッシュを使用
- const isDevMode = process.env.NODE_ENV === 'development';
+export class MarkerIdAssigner {
+ // key: file id
+ private cache: Map<string, TransformedCode>;
- const s = new MagicString(code); // magic-string のインスタンスを作成
+ constructor() {
+ this.cache = new Map();
+ }
- if (!isDevMode && transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) {
- logger.info(`Using cached version for ${id}`);
- return {
- code: transformedCodeCache[normalizedId],
- map: s.generateMap({ source: id, includeContent: true }),
- transformedCodeCache
- };
+ public onInvalidate(id: string) {
+ this.cache.delete(id);
}
- // すでに処理済みのファイルでコードに変更がない場合はキャッシュを返す
- if (transformedCodeCache[normalizedId] === code) {
- logger.info(`Code unchanged for ${id}, using cached version`);
- return {
- code: transformedCodeCache[normalizedId],
- map: s.generateMap({ source: id, includeContent: true }),
- transformedCodeCache
- };
+ public processFile(id: string, code: string): TransformedCode {
+ // try cache first
+ if (this.cache.has(id)) {
+ return this.cache.get(id)!;
+ }
+ const transformed = this.#processImpl(id, code);
+ this.cache.set(id, transformed);
+ return transformed;
}
- const parsed = vueSfcParse(code, { filename: id });
- if (!parsed.descriptor.template) {
- return {
- code,
- map: s.generateMap({ source: id, includeContent: true }),
- transformedCodeCache
+ #processImpl(id: string, code: string): TransformedCode {
+ const s = new MagicString(code); // magic-string のインスタンスを作成
+
+ const parsed = vueSfcParse(code, { filename: id });
+ if (!parsed.descriptor.template) {
+ return {
+ code,
+ map: s.generateMap({ source: id, includeContent: true }),
+ };
+ }
+ const ast = parsed.descriptor.template.ast; // テンプレート AST を取得
+ const markerRelations: MarkerRelation[] = []; // MarkerRelation 配列を初期化
+
+ if (!ast) {
+ return {
+ code: s.toString(), // 変更後のコードを返す
+ map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
+ };
+ }
+
+ type SearchMarkerElementNode = ElementNode & {
+ __markerId?: string,
+ __children?: string[],
};
- }
- const ast = parsed.descriptor.template.ast; // テンプレート AST を取得
- const markerRelations: MarkerRelation[] = []; // MarkerRelation 配列を初期化
- if (ast) {
- function traverse(node: any, currentParent?: any) {
- if (node.type === 1 && node.tag === 'SearchMarker') {
+ function traverse(node: RootNode | TemplateChildNode | SimpleExpressionNode | CompoundExpressionNode, currentParent?: SearchMarkerElementNode) {
+ if (node.type === NodeTypes.ELEMENT && node.tag === 'SearchMarker') {
// 行番号はコード先頭からの改行数で取得
const lineNumber = code.slice(0, node.loc.start.offset).split('\n').length;
// ファイルパスと行番号からハッシュ値を生成
@@ -1261,14 +1192,14 @@ async function processVueFile(
const generatedMarkerId = toBase62(hash(`${idKey}:${lineNumber}`));
const props = node.props || [];
- const hasMarkerIdProp = props.some((prop: any) => prop.type === 6 && prop.name === 'markerId');
+ const hasMarkerIdProp = props.some((prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === 'markerId');
const nodeMarkerId = hasMarkerIdProp
- ? props.find((prop: any) => prop.type === 6 && prop.name === 'markerId')?.value?.content as string
+ ? props.find((prop): prop is AttributeNode => prop.type === NodeTypes.ATTRIBUTE && prop.name === 'markerId')?.value?.content as string
: generatedMarkerId;
- node.__markerId = nodeMarkerId;
+ (node as SearchMarkerElementNode).__markerId = nodeMarkerId;
// 子マーカーの場合、親ノードに __children を設定しておく
- if (currentParent && currentParent.type === 1 && currentParent.tag === 'SearchMarker') {
+ if (currentParent) {
currentParent.__children = currentParent.__children || [];
currentParent.__children.push(nodeMarkerId);
}
@@ -1313,9 +1244,13 @@ async function processVueFile(
}
}
- const newParent = node.type === 1 && node.tag === 'SearchMarker' ? node : currentParent;
- if (node.children && Array.isArray(node.children)) {
- node.children.forEach(child => traverse(child, newParent));
+ const newParent: SearchMarkerElementNode | undefined = node.type === NodeTypes.ELEMENT && node.tag === 'SearchMarker' ? node : currentParent;
+ if ('children' in node) {
+ for (const child of node.children) {
+ if (typeof child == 'object') {
+ traverse(child, newParent);
+ }
+ }
}
}
@@ -1341,7 +1276,11 @@ async function processVueFile(
if (!parentRelation || !parentRelation.node) continue;
const parentNode = parentRelation.node;
- const childrenProp = parentNode.props?.find((prop: any) => prop.type === 7 && prop.name === 'bind' && prop.arg?.content === 'children');
+ const childrenProp = parentNode.props?.find((prop): prop is DirectiveNode =>
+ prop.type === NodeTypes.DIRECTIVE &&
+ prop.name === 'bind' &&
+ prop.arg?.type === NodeTypes.SIMPLE_EXPRESSION &&
+ prop.arg.content === 'children');
// 親ノードの開始位置を特定
const parentNodeStart = parentNode.loc!.start.offset;
@@ -1416,53 +1355,64 @@ async function processVueFile(
}
}
}
+
+ return {
+ code: s.toString(), // 変更後のコードを返す
+ map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
+ };
}
- const transformedCode = s.toString(); // 変換後のコードを取得
- transformedCodeCache[normalizedId] = transformedCode; // 変換後のコードをキャッシュに保存
+ async getOrLoad(id: string) {
+ // if there already exists a cache, return it
+ // note cahce will be invalidated on file change so the cache must be up to date
+ let code = this.getCached(id)?.code;
+ if (code != null) {
+ return code;
+ }
- return {
- code: transformedCode, // 変更後のコードを返す
- map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
- transformedCodeCache // キャッシュも返す
- };
-}
+ // if no cache found, read and parse the file
+ const originalCode = await fs.promises.readFile(id, 'utf-8');
-export async function generateSearchIndex(options: Options, transformedCodeCache: Record<string, string> = {}) {
- const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
- const matchedFiles = glob.sync(filePathPattern);
- return [...acc, ...matchedFiles];
- }, []);
+ // Other code may already parsed the file while we were waiting for the file to be read so re-check the cache
+ code = this.getCached(id)?.code;
+ if (code != null) {
+ return code;
+ }
- for (const filePath of filePaths) {
- const id = path.resolve(filePath); // 絶対パスに変換
- const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
- const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
- transformedCodeCache = newCache; // キャッシュを更新
+ // parse the file
+ code = this.processFile(id, originalCode)?.code;
+ return code;
}
- await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
-
- return transformedCodeCache; // キャッシュを返す
+ getCached(id: string) {
+ return this.cache.get(id);
+ }
}
// Rollup プラグインとして export
-export default function pluginCreateSearchIndex(options: Options): Plugin {
- let transformedCodeCache: Record<string, string> = {}; // キャッシュオブジェクトをプラグインスコープで定義
- const isDevServer = process.env.NODE_ENV === 'development'; // 開発サーバーかどうか
+export default function pluginCreateSearchIndex(options: Options): PluginOption {
+ const assigner = new MarkerIdAssigner();
+ return [
+ createSearchIndex(options, assigner),
+ pluginCreateSearchIndexVirtualModule(options, assigner),
+ ]
+}
+function createSearchIndex(options: Options, assigner: MarkerIdAssigner): Plugin {
initLogger(options); // ロガーを初期化
+ const root = normalizePath(process.cwd());
+
+ function isTargetFile(id: string): boolean {
+ const relativePath = path.posix.relative(root, id);
+ return options.targetFilePaths.some(pat => minimatch(relativePath, pat))
+ }
return {
- name: 'createSearchIndex',
+ name: 'autoAssignMarkerId',
enforce: 'pre',
- async buildStart() {
- if (!isDevServer) {
- return;
- }
-
- transformedCodeCache = await generateSearchIndex(options, transformedCodeCache);
+ watchChange(id) {
+ assigner.onInvalidate(id);
},
async transform(code, id) {
@@ -1470,43 +1420,88 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
return;
}
- // targetFilePaths にマッチするファイルのみ処理を行う
- // glob パターンでマッチング
- let isMatch = false; // isMatch の初期値を false に設定
- for (const pattern of options.targetFilePaths) { // パターンごとにマッチング確認
- const globbedFiles = glob.sync(pattern);
- for (const globbedFile of globbedFiles) {
- const normalizedGlobbedFile = path.resolve(globbedFile); // glob 結果を絶対パスに
- const normalizedId = path.resolve(id); // id を絶対パスに
- if (normalizedGlobbedFile === normalizedId) { // 絶対パス同士で比較
- isMatch = true;
- break; // マッチしたらループを抜ける
- }
- }
- if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける
- }
-
- if (!isMatch) {
+ if (!isTargetFile(id)) {
return;
}
- // ファイルの内容が変更された場合は再処理を行う
- const normalizedId = id.replace(/\\/g, '/');
- const hasContentChanged = !transformedCodeCache[normalizedId] || transformedCodeCache[normalizedId] !== code;
+ return assigner.processFile(id, code);
+ },
+ };
+}
+
+export function pluginCreateSearchIndexVirtualModule(options: Options, asigner: MarkerIdAssigner): Plugin {
+ const searchIndexPrefix = options.fileVirtualModulePrefix ?? 'search-index-individual:';
+ const searchIndexSuffix = options.fileVirtualModuleSuffix ?? '.ts';
+ const allSearchIndexFile = options.mainVirtualModule;
+ const root = normalizePath(process.cwd());
+
+ function isTargetFile(id: string): boolean {
+ const relativePath = path.posix.relative(root, id);
+ return options.targetFilePaths.some(pat => minimatch(relativePath, pat))
+ }
+
+ function parseSearchIndexFileId(id: string): string | null {
+ const noQuery = id.split('?')[0];
+ if (noQuery.startsWith(searchIndexPrefix) && noQuery.endsWith(searchIndexSuffix)) {
+ const filePath = id.slice(searchIndexPrefix.length).slice(0, -searchIndexSuffix.length);
+ if (isTargetFile(filePath)) {
+ return filePath;
+ }
+ }
+ return null;
+ }
- const transformed = await processVueFile(code, id, options, transformedCodeCache);
- transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新
+ return {
+ name: 'generateSearchIndexVirtualModule',
+ // hotUpdate hook を vite:vue よりもあとに実行したいため enforce: post
+ enforce: 'post',
- if (isDevServer && hasContentChanged) {
- await analyzeVueProps({ ...options, transformedCodeCache }); // ファイルが変更されたときのみ分析を実行
+ async resolveId(id) {
+ if (id == allSearchIndexFile) {
+ return '\0' + allSearchIndexFile;
}
- return transformed;
+ const searchIndexFilePath = parseSearchIndexFileId(id);
+ if (searchIndexFilePath != null) {
+ return id;
+ }
+ return undefined;
},
- async writeBundle() {
- await analyzeVueProps({ ...options, transformedCodeCache }); // ビルド時にも analyzeVueProps を実行
+ async load(id) {
+ if (id == '\0' + allSearchIndexFile) {
+ const files = await Promise.all(options.targetFilePaths.map(async (filePathPattern) => await glob(filePathPattern))).then(paths => paths.flat());
+ let generatedFile = '';
+ let arrayElements = '';
+ for (let file of files) {
+ const normalizedRelative = normalizePath(file);
+ const absoluteId = normalizePath(path.join(process.cwd(), normalizedRelative)) + searchIndexSuffix;
+ const variableName = normalizedRelative.replace(/[\/.-]/g, '_');
+ generatedFile += `import { searchIndexes as ${variableName} } from '${searchIndexPrefix}${absoluteId}';\n`;
+ arrayElements += ` ...${variableName},\n`;
+ }
+ generatedFile += `export let searchIndexes = [\n${arrayElements}];\n`;
+ return generatedFile;
+ }
+
+ const searchIndexFilePath = parseSearchIndexFileId(id);
+ if (searchIndexFilePath != null) {
+ // call load to update the index file when the file is changed
+ this.addWatchFile(searchIndexFilePath);
+
+ const code = await asigner.getOrLoad(searchIndexFilePath);
+ return generateJavaScriptCode(collectSearchItemIndexes([collectFileMarkers([[id, code]])]));
+ }
+ return null;
},
+
+ hotUpdate(this: { environment: { moduleGraph: EnvironmentModuleGraph } }, { file, modules }) {
+ if (isTargetFile(file)) {
+ const updateMods = options.modulesToHmrOnUpdate.map(id => this.environment.moduleGraph.getModuleById(path.posix.join(root, id))).filter(x => x != null);
+ return [...modules, ...updateMods];
+ }
+ return modules;
+ }
};
}
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 01dcf09d47..156e6abea2 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -5,7 +5,6 @@
"scripts": {
"watch": "vite",
"build": "vite build",
- "build-search-index": "vite-node --config \"./vite-node.config.ts\" \"./scripts/generate-search-index.ts\"",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
@@ -115,6 +114,7 @@
"@typescript-eslint/eslint-plugin": "8.27.0",
"@typescript-eslint/parser": "8.27.0",
"@vitest/coverage-v8": "3.0.9",
+ "@vue/compiler-core": "3.5.13",
"@vue/runtime-core": "3.5.13",
"acorn": "8.14.1",
"cross-env": "7.0.3",
@@ -125,6 +125,7 @@
"happy-dom": "17.4.4",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
+ "minimatch": "10.0.1",
"msw": "2.7.3",
"msw-storybook-addon": "2.0.4",
"nodemon": "3.1.9",
diff --git a/packages/frontend/scripts/generate-search-index.ts b/packages/frontend/scripts/generate-search-index.ts
deleted file mode 100644
index cbb4bb8c51..0000000000
--- a/packages/frontend/scripts/generate-search-index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { searchIndexes } from '../vite.config.js';
-import { generateSearchIndex } from '../lib/vite-plugin-create-search-index.js';
-
-async function main() {
- for (const searchIndex of searchIndexes) {
- await generateSearchIndex(searchIndex);
- }
-}
-
-main();
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index a094718382..4156fa2732 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -94,7 +94,7 @@ export type SuperMenuDef = {
<script lang="ts" setup>
import { useTemplateRef, ref, watch, nextTick } from 'vue';
-import type { SearchIndexItem } from '@/utility/autogen/settings-search-index.js';
+import type { SearchIndexItem } from '@/utility/settings-search-index.js';
import MkInput from '@/components/MkInput.vue';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@@/js/scroll.js';
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 4ed4cdc773..5921a8c812 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -42,7 +42,7 @@ import { instance } from '@/instance.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import * as os from '@/os.js';
import { useRouter } from '@/router.js';
-import { searchIndexes } from '@/utility/autogen/settings-search-index.js';
+import { searchIndexes } from '@/utility/settings-search-index.js';
import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
import { store } from '@/store.js';
import { signout } from '@/signout.js';
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
deleted file mode 100644
index 7f800d2b70..0000000000
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ /dev/null
@@ -1,993 +0,0 @@
-
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-// This file was automatically generated by create-search-index.
-// Do not edit this file.
-
-import { i18n } from '@/i18n.js';
-
-export type SearchIndexItem = {
- id: string;
- path?: string;
- label: string;
- keywords: string[];
- icon?: string;
- children?: SearchIndexItem[];
-};
-
-export const searchIndexes: SearchIndexItem[] = [
- {
- id: 'flXd1LC7r',
- children: [
- {
- id: 'hB11H5oul',
- label: i18n.ts.syncDeviceDarkMode,
- keywords: ['sync', 'device', 'dark', 'light', 'mode'],
- },
- {
- id: 'fDbLtIKeo',
- label: i18n.ts.themeForLightMode,
- keywords: ['light', 'theme'],
- },
- {
- id: 'CsSVILKpX',
- label: i18n.ts.themeForDarkMode,
- keywords: ['dark', 'theme'],
- },
- {
- id: 'jwW5HULqA',
- label: i18n.ts._settings.enableSyncThemesBetweenDevices,
- keywords: ['sync', 'themes', 'devices'],
- },
- ],
- label: i18n.ts.theme,
- keywords: ['theme'],
- path: '/settings/theme',
- icon: 'ti ti-palette',
- },
- {
- id: '6fFIRXUww',
- children: [
- {
- id: 'EcwZE7dCl',
- label: i18n.ts.notUseSound,
- keywords: ['mute'],
- },
- {
- id: '9MxYVIf7k',
- label: i18n.ts.useSoundOnlyWhenActive,
- keywords: ['active', 'mute'],
- },
- {
- id: '94afQxKat',
- label: i18n.ts.masterVolume,
- keywords: ['volume', 'master'],
- },
- ],
- label: i18n.ts.sounds,
- keywords: ['sounds', i18n.ts._settings.soundsBanner],
- path: '/settings/sounds',
- icon: 'ti ti-music',
- },
- {
- id: '5BjnxMfYV',
- children: [
- {
- id: '75QPEg57v',
- children: [
- {
- id: 'CiHijRkGG',
- label: i18n.ts.changePassword,
- keywords: [],
- },
- ],
- label: i18n.ts.password,
- keywords: ['password'],
- },
- {
- id: '2fa',
- children: [
- {
- id: 'qCXM0HtJ7',
- label: i18n.ts.totp,
- keywords: ['totp', 'app', i18n.ts.totpDescription],
- },
- {
- id: '3g1RePuD9',
- label: i18n.ts.securityKeyAndPasskey,
- keywords: ['security', 'key', 'passkey'],
- },
- {
- id: 'pFRud5u8k',
- label: i18n.ts.passwordLessLogin,
- keywords: ['password', 'less', 'key', 'passkey', 'login', 'signin', i18n.ts.passwordLessLoginDescription],
- },
- ],
- label: i18n.ts['2fa'],
- keywords: ['2fa'],
- },
- ],
- label: i18n.ts.security,
- keywords: ['security', i18n.ts._settings.securityBanner],
- path: '/settings/security',
- icon: 'ti ti-lock',
- },
- {
- id: 'w4L6myH61',
- children: [
- {
- id: 'ru8DrOn3J',
- label: i18n.ts._profile.changeBanner,
- keywords: ['banner', 'change'],
- },
- {
- id: 'CCnD8Apnu',
- label: i18n.ts._profile.changeAvatar,
- keywords: ['avatar', 'icon', 'change'],
- },
- {
- id: 'yFEVCJxFX',
- label: i18n.ts._profile.name,
- keywords: ['name'],
- },
- {
- id: '2O1S5reaB',
- label: i18n.ts._profile.description,
- keywords: ['description', 'bio'],
- },
- {
- id: 'pWi4OLS8g',
- label: i18n.ts.location,
- keywords: ['location', 'locale'],
- },
- {
- id: 'oLO5X6Wtw',
- label: i18n.ts.birthday,
- keywords: ['birthday', 'birthdate', 'age'],
- },
- {
- id: 'm2trKwPgq',
- label: i18n.ts.language,
- keywords: ['language', 'locale'],
- },
- {
- id: 'kfDZxCDp9',
- label: i18n.ts._profile.metadataEdit,
- keywords: ['metadata'],
- },
- {
- id: 'uPt3MFymp',
- label: i18n.ts._profile.followedMessage,
- keywords: ['follow', 'message', i18n.ts._profile.followedMessageDescription],
- },
- {
- id: 'wuGg0tBjw',
- label: i18n.ts.reactionAcceptance,
- keywords: ['reaction'],
- },
- {
- id: 'EezPpmMnf',
- children: [
- {
- id: 'f2cRLh8ad',
- label: i18n.ts.flagAsCat,
- keywords: ['cat'],
- },
- {
- id: 'eVoViiF3h',
- label: i18n.ts.flagAsBot,
- keywords: ['bot'],
- },
- ],
- label: i18n.ts.advancedSettings,
- keywords: [],
- },
- ],
- label: i18n.ts.profile,
- keywords: ['profile'],
- path: '/settings/profile',
- icon: 'ti ti-user',
- },
- {
- id: '2rp9ka5Ht',
- children: [
- {
- id: 'BhAQiHogN',
- label: i18n.ts.makeFollowManuallyApprove,
- keywords: ['follow', 'lock', i18n.ts.lockedAccountInfo],
- },
- {
- id: '4DeWGsPaD',
- label: i18n.ts.autoAcceptFollowed,
- keywords: ['follow', 'auto', 'accept'],
- },
- {
- id: 'iaM6zUmO9',
- label: i18n.ts.makeReactionsPublic,
- keywords: ['reaction', 'public', i18n.ts.makeReactionsPublicDescription],
- },
- {
- id: '5Q6uhghzV',
- label: i18n.ts.followingVisibility,
- keywords: ['following', 'visibility'],
- },
- {
- id: 'pZ9q65FX5',
- label: i18n.ts.followersVisibility,
- keywords: ['follower', 'visibility'],
- },
- {
- id: 'DMS4yvAGg',
- label: i18n.ts.hideOnlineStatus,
- keywords: ['online', 'status', i18n.ts.hideOnlineStatusDescription],
- },
- {
- id: '8rEsGuN8w',
- label: i18n.ts.noCrawle,
- keywords: ['crawle', 'index', 'search', i18n.ts.noCrawleDescription],
- },
- {
- id: 's7LdSpiLn',
- label: i18n.ts.preventAiLearning,
- keywords: ['crawle', 'ai', i18n.ts.preventAiLearningDescription],
- },
- {
- id: 'l2Wf1s2ad',
- label: i18n.ts.makeExplorable,
- keywords: ['explore', i18n.ts.makeExplorableDescription],
- },
- {
- id: 'xEYlOghao',
- label: i18n.ts._chat.chatAllowedUsers,
- keywords: ['chat'],
- },
- {
- id: 'BnOtlyaAh',
- children: [
- {
- id: 'BzMIVBpL0',
- label: i18n.ts._accountSettings.requireSigninToViewContents,
- keywords: ['login', 'signin'],
- },
- {
- id: 'jJUqPqBAv',
- label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore,
- keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription],
- },
- {
- id: 'ra10txIFV',
- label: i18n.ts._accountSettings.makeNotesHiddenBefore,
- keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription],
- },
- ],
- label: i18n.ts.lockdown,
- keywords: ['lockdown'],
- },
- ],
- label: i18n.ts.privacy,
- keywords: ['privacy', i18n.ts._settings.privacyBanner],
- path: '/settings/privacy',
- icon: 'ti ti-lock-open',
- },
- {
- id: '3yCAv0IsZ',
- children: [
- {
- id: 'AKvDrxSj5',
- children: [
- {
- id: 'a5b9RjEvq',
- label: i18n.ts.uiLanguage,
- keywords: ['language'],
- },
- {
- id: '9ragaff40',
- label: i18n.ts.overridedDeviceKind,
- keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
- },
- {
- id: 'lfI3yMX9g',
- label: i18n.ts.showAvatarDecorations,
- keywords: ['avatar', 'icon', 'decoration', 'show'],
- },
- {
- id: '31Y4IcGEf',
- label: i18n.ts.alwaysConfirmFollow,
- keywords: ['follow', 'confirm', 'always'],
- },
- {
- id: '78q2asrLS',
- label: i18n.ts.highlightSensitiveMedia,
- keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
- },
- {
- id: 'zydOfGYip',
- label: i18n.ts.confirmWhenRevealingSensitiveMedia,
- keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
- },
- {
- id: 'wqpOC22Zm',
- label: i18n.ts.enableAdvancedMfm,
- keywords: ['mfm', 'enable', 'show', 'advanced'],
- },
- {
- id: 'c98gbF9c6',
- label: i18n.ts.enableInfiniteScroll,
- keywords: ['auto', 'load', 'auto', 'more', 'scroll'],
- },
- {
- id: '6ANRSOaNg',
- label: i18n.ts.emojiStyle,
- keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
- },
- ],
- label: i18n.ts.general,
- keywords: ['general'],
- },
- {
- id: '5G6O6qdis',
- children: [
- {
- id: 'khT3n6byY',
- label: i18n.ts.showFixedPostForm,
- keywords: ['post', 'form', 'timeline'],
- },
- {
- id: 'q5ElfNSou',
- label: i18n.ts.showFixedPostFormInChannel,
- keywords: ['post', 'form', 'timeline', 'channel'],
- },
- {
- id: '3GcWIaZf8',
- label: i18n.ts.collapseRenotes,
- keywords: ['renote', i18n.ts.collapseRenotesDescription],
- },
- {
- id: 'd2H4E5ys6',
- label: i18n.ts.showGapBetweenNotesInTimeline,
- keywords: ['note', 'timeline', 'gap'],
- },
- {
- id: '1LHOhDKGW',
- label: i18n.ts.disableStreamingTimeline,
- keywords: ['disable', 'streaming', 'timeline'],
- },
- {
- id: 'DSzwvTp7i',
- label: i18n.ts.pinnedList,
- keywords: ['pinned', 'list'],
- },
- {
- id: 'ykifk3NHS',
- label: i18n.ts.showNoteActionsOnlyHover,
- keywords: ['hover', 'show', 'footer', 'action'],
- },
- {
- id: 'tLGyaQagB',
- label: i18n.ts.showClipButtonInNoteFooter,
- keywords: ['footer', 'action', 'clip', 'show'],
- },
- {
- id: '7W6g8Dcqz',
- label: i18n.ts.showReactionsCount,
- keywords: ['reaction', 'count', 'show'],
- },
- {
- id: 'uAOoH3LFF',
- label: i18n.ts.confirmOnReact,
- keywords: ['reaction', 'confirm'],
- },
- {
- id: 'eCiyZLC8n',
- label: i18n.ts.loadRawImages,
- keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
- },
- {
- id: '68u9uRmFP',
- label: i18n.ts.useReactionPickerForContextMenu,
- keywords: ['reaction', 'picker', 'contextmenu', 'open'],
- },
- {
- id: 'yxehrHZ6x',
- label: i18n.ts.reactionsDisplaySize,
- keywords: ['reaction', 'size', 'scale', 'display'],
- },
- {
- id: 'gi8ILaE2Z',
- label: i18n.ts.limitWidthOfReaction,
- keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
- },
- {
- id: 'cEQJZ7DQG',
- label: i18n.ts.mediaListWithOneImageAppearance,
- keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
- },
- {
- id: 'haX4QVulD',
- label: i18n.ts.instanceTicker,
- keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
- },
- {
- id: 'pneYnQekL',
- label: i18n.ts.displayOfSensitiveMedia,
- keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
- },
- ],
- label: i18n.ts._settings.timelineAndNote,
- keywords: ['timeline', 'note'],
- },
- {
- id: 'eJ2jme16W',
- children: [
- {
- id: 'ErMQr6LQk',
- label: i18n.ts.keepCw,
- keywords: ['remember', 'keep', 'note', 'cw'],
- },
- {
- id: 'zrJicawH9',
- label: i18n.ts.rememberNoteVisibility,
- keywords: ['remember', 'keep', 'note', 'visibility'],
- },
- {
- id: 'BaQfrVO82',
- label: i18n.ts.enableQuickAddMfmFunction,
- keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
- },
- {
- id: 'C2WYcVM1d',
- label: i18n.ts.defaultNoteVisibility,
- keywords: ['default', 'note', 'visibility'],
- },
- ],
- label: i18n.ts.postForm,
- keywords: ['post', 'form'],
- },
- {
- id: 'sQXSA6gik',
- children: [
- {
- id: 'rICn8stqk',
- label: i18n.ts.useGroupedNotifications,
- keywords: ['group'],
- },
- {
- id: 'xFmAg2tDe',
- label: i18n.ts.position,
- keywords: ['position'],
- },
- {
- id: 'Ek4Cw3VPq',
- label: i18n.ts.stackAxis,
- keywords: ['stack', 'axis', 'direction'],
- },
- ],
- label: i18n.ts.notifications,
- keywords: ['notification'],
- },
- {
- id: 'gDVCqZfxm',
- children: [
- {
- id: 'ei8Ix3s4S',
- label: i18n.ts._settings._chat.showSenderName,
- keywords: ['show', 'sender', 'name'],
- },
- {
- id: '2E7vdIUQd',
- label: i18n.ts._settings._chat.sendOnEnter,
- keywords: ['send', 'enter', 'newline'],
- },
- ],
- label: i18n.ts.chat,
- keywords: ['chat', 'messaging'],
- },
- {
- id: '96LnS1sxB',
- children: [
- {
- id: 'vPQPvmntL',
- label: i18n.ts.reduceUiAnimation,
- keywords: ['animation', 'motion', 'reduce'],
- },
- {
- id: 'wfJ91vwzq',
- label: i18n.ts.disableShowingAnimatedImages,
- keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'],
- },
- {
- id: '42b1L4xdq',
- label: i18n.ts.enableAnimatedMfm,
- keywords: ['mfm', 'enable', 'show', 'animated'],
- },
- {
- id: 'dLkRNHn3k',
- label: i18n.ts.enableHorizontalSwipe,
- keywords: ['swipe', 'horizontal', 'tab'],
- },
- {
- id: 'BvooTWFW5',
- label: i18n.ts.keepScreenOn,
- keywords: ['keep', 'screen', 'display', 'on'],
- },
- {
- id: 'yzbghkAq0',
- label: i18n.ts.useNativeUIForVideoAudioPlayer,
- keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
- },
- {
- id: 'aSbKFHbOy',
- label: i18n.ts._settings.makeEveryTextElementsSelectable,
- keywords: ['text', 'selectable'],
- },
- {
- id: 'bTcAsPvNz',
- label: i18n.ts.menuStyle,
- keywords: ['menu', 'style', 'popup', 'drawer'],
- },
- {
- id: 'lSVBaLnyW',
- label: i18n.ts._contextMenu.title,
- keywords: ['contextmenu', 'system', 'native'],
- },
- {
- id: 'pec0uMPq5',
- label: i18n.ts.fontSize,
- keywords: ['font', 'size'],
- },
- {
- id: 'Eh7vTluDO',
- label: i18n.ts.useSystemFont,
- keywords: ['font', 'system', 'native'],
- },
- ],
- label: i18n.ts.accessibility,
- keywords: ['accessibility', i18n.ts._settings.accessibilityBanner],
- },
- {
- id: 'vTRSKf1JA',
- children: [
- {
- id: '2VjlA02wB',
- label: i18n.ts.turnOffToImprovePerformance,
- keywords: ['blur'],
- },
- {
- id: 'f6J0lmg1g',
- label: i18n.ts.turnOffToImprovePerformance,
- keywords: ['blur', 'modal'],
- },
- {
- id: 'hQqXhfNg8',
- label: i18n.ts.turnOffToImprovePerformance,
- keywords: ['sticky'],
- },
- ],
- label: i18n.ts.performance,
- keywords: ['performance'],
- },
- {
- id: 'utM8dEobb',
- label: i18n.ts.dataSaver,
- keywords: ['datasaver'],
- },
- {
- id: 'gOUvwkE9t',
- children: [
- {
- id: 'iUMUvFURf',
- label: i18n.ts.squareAvatars,
- keywords: ['avatar', 'icon', 'square'],
- },
- {
- id: 'ceyPO9Ywi',
- label: i18n.ts.seasonalScreenEffect,
- keywords: ['effect', 'show'],
- },
- {
- id: 'ztwIlsXhP',
- label: i18n.ts.openImageInNewTab,
- keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
- },
- {
- id: 'vLSsQbZEo',
- label: i18n.ts.withRepliesByDefaultForNewlyFollowed,
- keywords: ['follow', 'replies'],
- },
- {
- id: 'hQt85bBIX',
- label: i18n.ts.whenServerDisconnected,
- keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
- },
- {
- id: 'C9SyK2m0',
- label: i18n.ts.numberOfPageCache,
- keywords: ['cache', 'page'],
- },
- {
- id: '2U0iVUtfW',
- label: i18n.ts.forceShowAds,
- keywords: ['ad', 'show'],
- },
- {
- id: '1rA7ADEXY',
- label: i18n.ts.hemisphere,
- keywords: [],
- },
- {
- id: 'vRayx89Rt',
- label: i18n.ts.additionalEmojiDictionary,
- keywords: ['emoji', 'dictionary', 'additional', 'extra'],
- },
- ],
- label: i18n.ts.other,
- keywords: ['other'],
- },
- ],
- label: i18n.ts.preferences,
- keywords: ['general', 'preferences', i18n.ts._settings.preferencesBanner],
- path: '/settings/preferences',
- icon: 'ti ti-adjustments',
- },
- {
- id: 'mwkwtw83Y',
- label: i18n.ts.plugins,
- keywords: ['plugin', 'addon', 'extension', i18n.ts._settings.pluginBanner],
- path: '/settings/plugin',
- icon: 'ti ti-plug',
- },
- {
- id: 'F1uK9ssiY',
- children: [
- {
- id: 'E0ndmaP6Q',
- label: i18n.ts._role.policies,
- keywords: ['account', 'info'],
- },
- {
- id: 'r5SjfwZJc',
- label: i18n.ts.rolesAssignedToMe,
- keywords: ['roles'],
- },
- {
- id: 'cm7LrjgaW',
- label: i18n.ts.accountMigration,
- keywords: ['account', 'move', 'migration'],
- },
- {
- id: 'ozfqNviP3',
- label: i18n.ts.closeAccount,
- keywords: ['account', 'close', 'delete', i18n.ts._accountDelete.requestAccountDelete],
- },
- {
- id: 'tpywgkpxy',
- label: i18n.ts.experimentalFeatures,
- keywords: ['experimental', 'feature', 'flags'],
- },
- {
- id: 'zWbGKohZ2',
- label: i18n.ts.developer,
- keywords: ['developer', 'mode', 'debug'],
- },
- ],
- label: i18n.ts.other,
- keywords: ['other'],
- path: '/settings/other',
- icon: 'ti ti-dots',
- },
- {
- id: '9bNikHWzQ',
- children: [
- {
- id: 't6XtfnRm9',
- label: i18n.ts._settings.showNavbarSubButtons,
- keywords: ['navbar', 'sidebar', 'toggle', 'button', 'sub'],
- },
- ],
- label: i18n.ts.navbar,
- keywords: ['navbar', 'menu', 'sidebar'],
- path: '/settings/navbar',
- icon: 'ti ti-list',
- },
- {
- id: '3icEvyv2D',
- children: [
- {
- id: 'lO3uFTkPN',
- children: [
- {
- id: '5JKaXRqyt',
- label: i18n.ts.showMutedWord,
- keywords: ['show'],
- },
- ],
- label: i18n.ts.wordMute,
- keywords: ['note', 'word', 'soft', 'mute', 'hide'],
- },
- {
- id: 'fMkjL3dK4',
- label: i18n.ts.hardWordMute,
- keywords: ['note', 'word', 'hard', 'mute', 'hide'],
- },
- {
- id: 'cimSzQXN0',
- label: i18n.ts.instanceMute,
- keywords: ['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide'],
- },
- {
- id: 'gq8rPy3Du',
- label: `${i18n.ts.mutedUsers} (${ i18n.ts.renote })`,
- keywords: ['renote', 'mute', 'hide', 'user'],
- },
- {
- id: 'mh2r7EUbF',
- label: i18n.ts.mutedUsers,
- keywords: ['note', 'mute', 'hide', 'user'],
- },
- {
- id: 'AUS1OgHrn',
- label: i18n.ts.blockedUsers,
- keywords: ['block', 'user'],
- },
- ],
- label: i18n.ts.muteAndBlock,
- keywords: ['mute', 'block', i18n.ts._settings.muteAndBlockBanner],
- path: '/settings/mute-block',
- icon: 'ti ti-ban',
- },
- {
- id: 'yR1OSyLiT',
- children: [
- {
- id: 'yMJzyzOUk',
- label: i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes,
- keywords: ['sync', 'palettes', 'devices'],
- },
- {
- id: 'wCE09vgZr',
- label: i18n.ts._emojiPalette.paletteForMain,
- keywords: ['main', 'palette'],
- },
- {
- id: 'uCzRPrSNx',
- label: i18n.ts._emojiPalette.paletteForReaction,
- keywords: ['reaction', 'palette'],
- },
- {
- id: 'hgQr28WUk',
- children: [
- {
- id: 'fY04NIHSQ',
- label: i18n.ts.size,
- keywords: ['emoji', 'picker', 'scale', 'size'],
- },
- {
- id: '3j7vlaL7t',
- label: i18n.ts.numberOfColumn,
- keywords: ['emoji', 'picker', 'width', 'column', 'size'],
- },
- {
- id: 'zPX8z1Bcy',
- label: i18n.ts.height,
- keywords: ['emoji', 'picker', 'height', 'size'],
- },
- {
- id: '2CSkZa4tl',
- label: i18n.ts.style,
- keywords: ['emoji', 'picker', 'style'],
- },
- ],
- label: i18n.ts.emojiPickerDisplay,
- keywords: ['emoji', 'picker', 'display'],
- },
- ],
- label: i18n.ts.emojiPalette,
- keywords: ['emoji', 'palette'],
- path: '/settings/emoji-palette',
- icon: 'ti ti-mood-happy',
- },
- {
- id: '3Tcxw4Fwl',
- children: [
- {
- id: 'iIai9O65I',
- label: i18n.ts.emailAddress,
- keywords: ['email', 'address'],
- },
- {
- id: 'i6cC6oi0m',
- label: i18n.ts.receiveAnnouncementFromInstance,
- keywords: ['announcement', 'email'],
- },
- {
- id: 'C1YTinP11',
- label: i18n.ts.emailNotification,
- keywords: ['notification', 'email'],
- },
- ],
- label: i18n.ts.email,
- keywords: ['email'],
- path: '/settings/email',
- icon: 'ti ti-mail',
- },
- {
- id: 'tnYoppRiv',
- children: [
- {
- id: 'cN3dsGNxu',
- label: i18n.ts.usageAmount,
- keywords: ['capacity', 'usage'],
- },
- {
- id: 'rOAOU2P6C',
- label: i18n.ts.statistics,
- keywords: ['statistics', 'usage'],
- },
- {
- id: 'uXGlQXATx',
- label: i18n.ts.uploadFolder,
- keywords: ['default', 'upload', 'folder'],
- },
- {
- id: 'goQdtf3dD',
- label: i18n.ts.keepOriginalFilename,
- keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
- },
- {
- id: '83xRo0XJl',
- label: i18n.ts.alwaysMarkSensitive,
- keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
- },
- {
- id: 'BrBqZL35E',
- label: i18n.ts.enableAutoSensitive,
- keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
- },
- ],
- label: i18n.ts.drive,
- keywords: ['drive', i18n.ts._settings.driveBanner],
- path: '/settings/drive',
- icon: 'ti ti-cloud',
- },
- {
- id: 'FfZdOs8y',
- children: [
- {
- id: 'B1ZU6Ur54',
- label: i18n.ts._deck.enableSyncBetweenDevicesForProfiles,
- keywords: ['sync', 'profiles', 'devices'],
- },
- {
- id: 'wWH4pxMQN',
- label: i18n.ts._deck.useSimpleUiForNonRootPages,
- keywords: ['ui', 'root', 'page'],
- },
- {
- id: '3LR509BvD',
- label: i18n.ts.defaultNavigationBehaviour,
- keywords: ['default', 'navigation', 'behaviour', 'window'],
- },
- {
- id: 'ybU8RLXgm',
- label: i18n.ts._deck.alwaysShowMainColumn,
- keywords: ['always', 'show', 'main', 'column'],
- },
- {
- id: 'xRasZyAVl',
- label: i18n.ts._deck.columnAlign,
- keywords: ['column', 'align'],
- },
- {
- id: '6qcyPd0oJ',
- label: i18n.ts._deck.deckMenuPosition,
- keywords: ['menu', 'position'],
- },
- {
- id: '4zk2Now4S',
- label: i18n.ts._deck.navbarPosition,
- keywords: ['navbar', 'position'],
- },
- {
- id: 'CGNtJ2I3n',
- label: i18n.ts._deck.columnGap,
- keywords: ['column', 'gap', 'margin'],
- },
- {
- id: 'rxPDMo7bE',
- label: i18n.ts.setWallpaper,
- keywords: ['wallpaper'],
- },
- ],
- label: i18n.ts.deck,
- keywords: ['deck', 'ui'],
- path: '/settings/deck',
- icon: 'ti ti-columns',
- },
- {
- id: 'BlJ2rsw9h',
- children: [
- {
- id: '9bLU1nIjt',
- label: i18n.ts._settings.api,
- keywords: ['api', 'app', 'token', 'accessToken'],
- },
- {
- id: '5VSGOVYR0',
- label: i18n.ts._settings.webhook,
- keywords: ['webhook'],
- },
- ],
- label: i18n.ts._settings.serviceConnection,
- keywords: ['app', 'service', 'connect', 'webhook', 'api', 'token', i18n.ts._settings.serviceConnectionBanner],
- path: '/settings/connect',
- icon: 'ti ti-link',
- },
- {
- id: 'gtaOSdIJB',
- label: i18n.ts.avatarDecorations,
- keywords: ['avatar', 'icon', 'decoration'],
- path: '/settings/avatar-decoration',
- icon: 'ti ti-sparkles',
- },
- {
- id: 'zK6posor9',
- label: i18n.ts.accounts,
- keywords: ['accounts'],
- path: '/settings/accounts',
- icon: 'ti ti-users',
- },
- {
- id: '330Q4mf8E',
- children: [
- {
- id: 'eGSjUDIKu',
- label: i18n.ts._exportOrImport.allNotes,
- keywords: ['notes'],
- },
- {
- id: 'iMDgUVgRu',
- label: i18n.ts._exportOrImport.favoritedNotes,
- keywords: ['favorite', 'notes'],
- },
- {
- id: '3y6KgkVbT',
- label: i18n.ts._exportOrImport.clips,
- keywords: ['clip', 'notes'],
- },
- {
- id: 'cKiHkj8HE',
- label: i18n.ts._exportOrImport.followingList,
- keywords: ['following', 'users'],
- },
- {
- id: '3zzmQXn0t',
- label: i18n.ts._exportOrImport.userLists,
- keywords: ['user', 'lists'],
- },
- {
- id: '3ZGXcEqWZ',
- label: i18n.ts._exportOrImport.muteList,
- keywords: ['mute', 'users'],
- },
- {
- id: '84oL7B1Dr',
- label: i18n.ts._exportOrImport.blockingList,
- keywords: ['block', 'users'],
- },
- {
- id: 'ckqi48Kbl',
- label: i18n.ts.antennas,
- keywords: ['antennas'],
- },
- ],
- label: i18n.ts._settings.accountData,
- keywords: ['import', 'export', 'data', 'archive', i18n.ts._settings.accountDataBanner],
- path: '/settings/account-data',
- icon: 'ti ti-package',
- },
-] as const;
-
-export type SearchIndex = typeof searchIndexes;
diff --git a/packages/frontend/src/utility/settings-search-index.ts b/packages/frontend/src/utility/settings-search-index.ts
new file mode 100644
index 0000000000..22e2407b15
--- /dev/null
+++ b/packages/frontend/src/utility/settings-search-index.ts
@@ -0,0 +1,43 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { searchIndexes as generated } from 'search-index:settings';
+import type { GeneratedSearchIndexItem } from 'search-index:settings';
+
+export type SearchIndexItem = {
+ id: string;
+ path?: string;
+ label: string;
+ keywords: string[];
+ icon?: string;
+ children?: SearchIndexItem[];
+};
+
+const rootMods = new Map(generated.map(item => [item.id, item]));
+
+function walk(item: GeneratedSearchIndexItem) {
+ if (item.inlining) {
+ for (const id of item.inlining) {
+ const inline = rootMods.get(id);
+ if (inline) {
+ (item.children ??= []).push(inline);
+ rootMods.delete(id);
+ } else {
+ console.log('[Settings Search Index] Failed to inline', id);
+ }
+ }
+ }
+
+ for (const child of item.children ?? []) {
+ walk(child);
+ }
+}
+
+for (const item of generated) {
+ walk(item);
+}
+
+export const searchIndexes: SearchIndexItem[] = generated;
+
diff --git a/packages/frontend/src/utility/virtual.d.ts b/packages/frontend/src/utility/virtual.d.ts
new file mode 100644
index 0000000000..59470a1f5e
--- /dev/null
+++ b/packages/frontend/src/utility/virtual.d.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+declare module 'search-index:settings' {
+ export type GeneratedSearchIndexItem = {
+ id: string;
+ path?: string;
+ label: string;
+ keywords: string[];
+ icon?: string;
+ inlining?: string[];
+ children?: GeneratedSearchIndexItem[];
+ };
+
+ export const searchIndexes: GeneratedSearchIndexItem[];
+}
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index ec80e71ae4..aa7bf24174 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -24,7 +24,8 @@ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.s
*/
export const searchIndexes = [{
targetFilePaths: ['src/pages/settings/*.vue'],
- exportFilePath: './src/utility/autogen/settings-search-index.ts',
+ mainVirtualModule: 'search-index:settings',
+ modulesToHmrOnUpdate: ['src/pages/settings/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}] satisfies SearchIndexOptions[];