diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index acb79061a6..354013283a 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1888,6 +1888,8 @@ _role: descriptionOfIsExplorable: "La línia de temps d'aquest rol i la llista d'usuaris seran públics si s'activa." displayOrder: "Posició " descriptionOfDisplayOrder: "Com més gran és el número, més dalt la seva posició a la interfície." + preserveAssignmentOnMoveAccount: "L'estat de l'assignació també es trasllada amb el compte migrat" + preserveAssignmentOnMoveAccount_description: "Si s'activa quan es migra un compte amb aquest rol, el compte migrat també heretarà aquest rol." canEditMembersByModerator: "Permetre que els moderadors editin la llista d'usuaris en aquest rol" descriptionOfCanEditMembersByModerator: "Quan s'activa, els moderadors, així com els administradors, podran afegir i treure usuaris d'aquest rol. Si es troba desactivat, només els administradors poden assignar usuaris." priority: "Prioritat" diff --git a/locales/en-US.yml b/locales/en-US.yml index cc0c219a60..5c82e4176c 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1888,6 +1888,8 @@ _role: descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled." displayOrder: "Position" descriptionOfDisplayOrder: "The higher the number, the higher its UI position." + preserveAssignmentOnMoveAccount: "Preserve role assignment during migration" + preserveAssignmentOnMoveAccount_description: "When turned on, this role will be carried over to the destination account when an account with this role is migrated." canEditMembersByModerator: "Allow moderators to edit the list of members for this role" descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." priority: "Priority" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 403f9c7718..9b62141fdc 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1888,6 +1888,8 @@ _role: descriptionOfIsExplorable: "Selezionandolo, la timeline del ruolo diventerà accessibile pubblicamente. Tranne se il ruolo non è pubblico." displayOrder: "Ordine di visualizzazione" descriptionOfDisplayOrder: "I valori più alti vengono visualizzati per primi" + preserveAssignmentOnMoveAccount: "Mantenere l'assegnazione alla migrazione del profilo" + preserveAssignmentOnMoveAccount_description: "Attivando, il ruolo verrà portato sul profilo destinatario, durante la migrazione." canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." priority: "Priorità" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 67f2692f03..57a78471db 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1887,6 +1887,8 @@ _role: descriptionOfIsExplorable: "打开后将公开角色时间线。如果角色不是公开的,就无法公开时间线。" displayOrder: "显示顺序" descriptionOfDisplayOrder: "数字越大,显示位置越靠前。" + preserveAssignmentOnMoveAccount: "将分配状态继承到目标账户" + preserveAssignmentOnMoveAccount_description: "启用后,当迁移具有该角色的账户时,目标账户也会继承该角色。" canEditMembersByModerator: "允许监察员编辑成员" descriptionOfCanEditMembersByModerator: "如果选中,监察员和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" priority: "优先级" diff --git a/package.json b/package.json index cc6bea1d11..0cbe009f5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2025.4.0-rc.0", + "version": "2025.4.0-rc.1", "codename": "shonk", "repository": { "type": "git", @@ -24,7 +24,6 @@ "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", - "build-frontend-search-index": "pnpm --filter frontend build-search-index", "start": "pnpm check:connect && cd packages/backend && MK_WARNED_ABOUT_CONFIG=true node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "init": "pnpm migrate", diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 index 4625b9dec4..57ec7b98b1 100644 --- a/packages/frontend-shared/themes/_dark.json5 +++ b/packages/frontend-shared/themes/_dark.json5 @@ -15,8 +15,6 @@ focus: ':alpha<0.3<@accent', bg: '#000', fg: '#dadada', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', fgHighlighted: ':lighten<3<@fg', fgOnAccent: '#fff', fgOnWhite: '#333', @@ -26,7 +24,6 @@ panelHighlight: ':lighten<3<@panel', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: '" solid 1px var(--MI_THEME-divider)', thread: ':lighten<12<@panel', windowHeader: ':alpha<0.85<@panel', diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5 index 1588470bd3..23c8f39af7 100644 --- a/packages/frontend-shared/themes/_light.json5 +++ b/packages/frontend-shared/themes/_light.json5 @@ -15,8 +15,6 @@ focus: ':alpha<0.3<@accent', bg: '#fff', fg: '#5f5f5f', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', fgHighlighted: ':darken<3<@fg', fgOnAccent: '#fff', fgOnWhite: '#333', @@ -26,7 +24,6 @@ panelHighlight: ':darken<3<@panel', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: '" solid 1px var(--MI_THEME-divider)', thread: ':darken<12<@panel', windowHeader: ':alpha<0.85<@panel', diff --git a/packages/frontend-shared/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5 index 5e721534d3..6d34665528 100644 --- a/packages/frontend-shared/themes/d-astro.json5 +++ b/packages/frontend-shared/themes/d-astro.json5 @@ -46,7 +46,6 @@ fgOnWhite: '@accent', panelHighlight: ':lighten<3<@panel', scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', }, } diff --git a/packages/frontend-shared/themes/d-botanical.json5 b/packages/frontend-shared/themes/d-botanical.json5 index 507880aab1..5a57a14f13 100644 --- a/packages/frontend-shared/themes/d-botanical.json5 +++ b/packages/frontend-shared/themes/d-botanical.json5 @@ -14,7 +14,6 @@ fgOnWhite: '@accent', divider: 'rgba(255, 255, 255, 0.14)', panel: 'rgb(47, 47, 44)', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', header: ':alpha<0.7<@panel', navBg: '#363636', renote: '@accent', diff --git a/packages/frontend-shared/themes/d-dark.json5 b/packages/frontend-shared/themes/d-dark.json5 index ae4f7d53f5..67d49aa861 100644 --- a/packages/frontend-shared/themes/d-dark.json5 +++ b/packages/frontend-shared/themes/d-dark.json5 @@ -14,7 +14,6 @@ fgOnWhite: '@accent', divider: 'rgba(255, 255, 255, 0.14)', panel: '#2d2d2d', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', header: ':alpha<0.7<@panel', navBg: '#363636', renote: '@accent', diff --git a/packages/frontend-shared/themes/d-future.json5 b/packages/frontend-shared/themes/d-future.json5 index f2c1f3eb86..6a66f2eca9 100644 --- a/packages/frontend-shared/themes/d-future.json5 +++ b/packages/frontend-shared/themes/d-future.json5 @@ -15,7 +15,6 @@ fgOnWhite: '@accent', divider: 'rgba(255, 255, 255, 0.1)', panel: '#18181c', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', renote: '@accent', mention: '#f2c97d', mentionMe: '@accent', diff --git a/packages/frontend-shared/themes/d-green-lime.json5 b/packages/frontend-shared/themes/d-green-lime.json5 index ca4e688fdb..fcd6651197 100644 --- a/packages/frontend-shared/themes/d-green-lime.json5 +++ b/packages/frontend-shared/themes/d-green-lime.json5 @@ -15,7 +15,6 @@ fgOnWhite: '@accent', divider: '#e7fffb24', panel: '#192320', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', popup: '#293330', renote: '@accent', mentionMe: '#ffaa00', diff --git a/packages/frontend-shared/themes/d-green-orange.json5 b/packages/frontend-shared/themes/d-green-orange.json5 index c2539816e2..aef3897329 100644 --- a/packages/frontend-shared/themes/d-green-orange.json5 +++ b/packages/frontend-shared/themes/d-green-orange.json5 @@ -15,7 +15,6 @@ fgOnWhite: '@accent', divider: '#e7fffb24', panel: '#192320', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', popup: '#293330', renote: '@accent', mentionMe: '#b4e900', diff --git a/packages/frontend-shared/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5 index ddce06649c..4f6c04b906 100644 --- a/packages/frontend-shared/themes/d-u0.json5 +++ b/packages/frontend-shared/themes/d-u0.json5 @@ -49,7 +49,6 @@ navIndicator: '@indicator', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', buttonGradateA: '@accent', @@ -58,8 +57,6 @@ panelHighlight: ':lighten<3<@panel', scrollbarHandle: 'rgba(255, 255, 255, 0.2)', inputBorderHover: 'rgba(255, 255, 255, 0.2)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', deckBg: '#142022', }, diff --git a/packages/frontend-shared/themes/l-botanical.json5 b/packages/frontend-shared/themes/l-botanical.json5 index 4ad539e8a0..2fbae4fbae 100644 --- a/packages/frontend-shared/themes/l-botanical.json5 +++ b/packages/frontend-shared/themes/l-botanical.json5 @@ -15,7 +15,6 @@ divider: '#cfcfcf', panel: '#ebe7e5', panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', header: ':alpha<0.7<@panel', navBg: '#ebe7e5', renote: '#229e92', diff --git a/packages/frontend-shared/themes/l-light.json5 b/packages/frontend-shared/themes/l-light.json5 index 63c2e6d278..55f2d2f004 100644 --- a/packages/frontend-shared/themes/l-light.json5 +++ b/packages/frontend-shared/themes/l-light.json5 @@ -15,7 +15,6 @@ header: ':alpha<0.7<@panel', navBg: '#fff', panel: '#fff', - panelHeaderDivider: '@divider', mentionMe: 'rgb(0, 179, 70)', }, } diff --git a/packages/frontend-shared/themes/l-rainy.json5 b/packages/frontend-shared/themes/l-rainy.json5 index e7d1d5af00..d7c31bda8d 100644 --- a/packages/frontend-shared/themes/l-rainy.json5 +++ b/packages/frontend-shared/themes/l-rainy.json5 @@ -13,7 +13,6 @@ fgOnWhite: '@accent', panel: '#fff', divider: 'rgb(230 233 234)', - panelHeaderDivider: '@divider', renote: '@accent', link: '@accent', mention: '@accent', diff --git a/packages/frontend-shared/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5 index d8e403c961..35241986df 100644 --- a/packages/frontend-shared/themes/l-u0.json5 +++ b/packages/frontend-shared/themes/l-u0.json5 @@ -51,7 +51,6 @@ buttonHoverBg: '#0000001a', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', buttonGradateA: '@accent', @@ -60,8 +59,6 @@ panelHighlight: ':lighten<3<@panel', scrollbarHandle: '#74747433', inputBorderHover: 'rgba(255, 255, 255, 0.2)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', }, } diff --git a/packages/frontend-shared/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5 index d69f024a6b..5ad8d60728 100644 --- a/packages/frontend-shared/themes/l-vivid.json5 +++ b/packages/frontend-shared/themes/l-vivid.json5 @@ -41,15 +41,12 @@ navIndicator: '@accent', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':darken<3<@fg', - fgTransparent: ':alpha<0.5<@fg', fgOnWhite: '@accent', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', htmlThemeColor: '@bg', panelHighlight: ':darken<3<@panel', scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: '@divider', scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', }, } 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 = { filePath: string; - usage: SearchIndexItem[]; + usage: T[]; } -export type SearchIndexItem = { +export type SearchIndexItem = SearchIndexItemLink; +export type SearchIndexStringItem = SearchIndexItemLink; +export interface SearchIndexItemLink { 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[]): SearchIndexItem[] { logger.info(`Processing ${analysisResults.length} files for output`); // 新しいツリー構造を構築 - const allMarkers = new Map(); + const allMarkers = new Map(); // 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): Set { +function collectChildIds(allMarkers: Map): Set { const childIds = new Set(); allMarkers.forEach((marker, id) => { @@ -232,10 +218,10 @@ function collectChildIds(allMarkers: Map): Set * ルートマーカー(他の子でないマーカー)を特定する */ function identifyRootMarkers( - allMarkers: Map, + allMarkers: Map, childIds: Set -): 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 + rootMarkers: SearchIndexStringItem[], + allMarkers: Map ): SearchIndexItem[] { - function resolveChildrenForMarker(marker: SearchIndexItem): SearchIndexItem { + function resolveChildrenForMarker(marker: SearchIndexStringItem): SearchIndexItem { // マーカーのディープコピーを作成 - const resolvedMarker = { ...marker }; + const resolvedMarker: SearchIndexItem = { ...marker, children: [] }; // 明示的に子マーカー配列を作成 const resolvedChildren: SearchIndexItem[] = []; @@ -351,55 +337,19 @@ function countMarkers(markers: SearchIndexItem[]): { totalMarkers: number, total return { totalMarkers, totalChildren }; } -/** - * 最終的な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(); +): SearchIndexStringItem[] { + const allMarkers: SearchIndexStringItem[] = []; + const markerMap = new Map>(); const childrenIds = new Set(); 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, keyof SpecialBindings> & SpecialBindings>; // バインドプロパティの処理を修正する関数 -function extractNodeBindings(node: VueAstNode): Record { - const bindings: Record = {}; +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, -}): Promise { - initLogger(options); - - const allMarkers: SearchIndexItem[] = []; - - // 対象ファイルパスを glob で展開 - const filePaths = options.targetFilePaths.reduce((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 { + 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, + return { + filePath: "combined-markers", // すべてのファイルのマーカーを1つのエントリとして扱う + usage: allMarkers, + }; +} + +type TransformedCode = { + code: string, + map: SourceMap, +}; + +export class MarkerIdAssigner { + // key: file id + private cache: Map; + + constructor() { + this.cache = new Map(); + } + + public onInvalidate(id: string) { + this.cache.delete(id); + } + + public processFile(id: string, code: string): TransformedCode { + // try cache first + if (this.cache.has(id)) { + return this.cache.get(id)!; } - ]; - - outputAnalysisResultAsTS(options.exportFilePath, analysisResult); // すべてのマーカー情報を渡す -} - -interface MarkerRelation { - parentId?: string; - markerId: string; - node: VueAstNode; -} - -async function processVueFile( - code: string, - id: string, - options: Options, - transformedCodeCache: Record -): Promise<{ - code: string, - map: any, - transformedCodeCache: Record -}> { - const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化 - - // 開発モード時はコード内容に変更があれば常に再処理する - // コード内容が同じ場合のみキャッシュを使用 - const isDevMode = process.env.NODE_ENV === 'development'; - - const s = new MagicString(code); // magic-string のインスタンスを作成 - - 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 - }; + const transformed = this.#processImpl(id, code); + this.cache.set(id, transformed); + return transformed; } - // すでに処理済みのファイルでコードに変更がない場合はキャッシュを返す - 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 - }; - } + #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 }), - transformedCodeCache - }; - } - const ast = parsed.descriptor.template.ast; // テンプレート AST を取得 - const markerRelations: MarkerRelation[] = []; // MarkerRelation 配列を初期化 + 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) { - function traverse(node: any, currentParent?: any) { - if (node.type === 1 && node.tag === 'SearchMarker') { + if (!ast) { + return { + code: s.toString(), // 変更後のコードを返す + map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要) + }; + } + + type SearchMarkerElementNode = ElementNode & { + __markerId?: string, + __children?: string[], + }; + + 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 = {}) { - const filePaths = options.targetFilePaths.reduce((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 = {}; // キャッシュオブジェクトをプラグインスコープで定義 - 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); + }, + }; +} - const transformed = await processVueFile(code, id, options, transformedCodeCache); - transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新 +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()); - if (isDevServer && hasContentChanged) { - await analyzeVueProps({ ...options, transformedCodeCache }); // ファイルが変更されたときのみ分析を実行 + 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; + } + + return { + name: 'generateSearchIndexVirtualModule', + // hotUpdate hook を vite:vue よりもあとに実行したいため enforce: post + enforce: 'post', + + 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 907c8d68f0..489dab733f 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", @@ -120,6 +119,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", @@ -129,6 +129,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/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index c3242a5914..766d1535ce 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -140,7 +140,7 @@ watch(v, newValue => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index 9cf75d6528..9c6ccac8db 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -60,7 +60,7 @@ const onInput = () => { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 3362139c3d..934d31318f 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -181,12 +181,17 @@ onUnmounted(() => { left: 0; color: var(--MI_THEME-panelHeaderFg); background: var(--MI_THEME-panelHeaderBg); - border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider); z-index: 2; line-height: 1.4em; background: color-mix(in srgb, var(--MI_THEME-panelHeaderBg) 35%, transparent); } +@container style(--MI_THEME-panelHeaderBg: var(--MI_THEME-panel)) { + .header { + box-shadow: 0 0.5px 0 0 light-dark(#0002, #fff2); + } +} + .title { margin: 0; padding: 12px 16px; diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 47eb15c68c..5231a577d7 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -175,7 +175,7 @@ onMounted(() => { } .headerLower { - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); font-size: .85em; padding-left: 4px; } @@ -209,13 +209,13 @@ onMounted(() => { } .headerTextSub { - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); font-size: .85em; } .headerRight { margin-left: auto; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); white-space: nowrap; } diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 26c361c122..f07a1ac33e 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -201,7 +201,7 @@ defineExpose({ .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index 559399d1d4..884890bf70 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -78,7 +78,7 @@ export default defineComponent({ > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 6097848518..d668fa342e 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -213,7 +213,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 361d80c68b..cf4e4eda74 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -268,7 +268,7 @@ function show() { .caption { font-size: 0.85em; padding: 8px 0 0 0; - color: var(--MI_THEME-fgTransparentWeak); + color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); &:empty { display: none; diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 272646f338..9a03869437 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -94,7 +94,7 @@ export type SuperMenuDef = {