diff --git a/packages/frontend/lib/rollup-plugin-create-search-index.ts b/packages/frontend/lib/rollup-plugin-create-search-index.ts index 2e3501c931..92559611eb 100644 --- a/packages/frontend/lib/rollup-plugin-create-search-index.ts +++ b/packages/frontend/lib/rollup-plugin-create-search-index.ts @@ -1,50 +1,41 @@ import { parse as vueSfcParse } from '@vue/compiler-sfc'; import type { Plugin } from 'rollup'; import fs from 'node:fs'; - +import { glob } from 'glob'; +import JSON5 from 'json5'; export interface AnalysisResult { - filePath: string; - usage: ComponentUsageInfo[]; + filePath: string; + usage: ComponentUsageInfo[]; } export interface ComponentUsageInfo { - parentFile: string; - staticProps: Record; - bindProps: Record; - componentName: string; -} - -function outputAnalysisResultAsJson(outputPath: string, analysisResults: AnalysisResult[]): void { - const jsonOutput = JSON.stringify(analysisResults, null, 2); - - try { - fs.writeFileSync(outputPath, jsonOutput, 'utf-8'); - console.log(`静的解析結果を ${outputPath} に出力しました。`); - } catch (error) { - console.error('JSONファイル出力エラー:', error); - } + parentFile: string; + staticProps: Record; + bindProps: Record; + componentName: string; } function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { - const varName = 'searchIndexResults'; + // (outputAnalysisResultAsTS 関数の実装は前回と同様) + const varName = 'searchIndexes'; // 変数名 - const jsonString = JSON.stringify(analysisResults, null, 2); // JSON.stringify で JSON 文字列を生成 + const jsonString = JSON5.stringify(analysisResults, { space: "\t", quote: "'" }); // JSON.stringify で JSON 文字列を生成 // bindProps の値を文字列置換で修正する関数 function modifyBindPropsInString(jsonString: string): string { - // bindProps: { ... } ブロックを正規表現で検索し、置換 + // (modifyBindPropsInString 関数の実装は前回と同様) const modifiedString = jsonString.replace( - /"bindProps":\s*\{([^}]*)\}/g, // "bindProps": { ... } にマッチ (g フラグで複数箇所を置換) + /bindProps:\s*\{([^}]*)\}/g, // bindProps: { ... } にマッチ (g フラグで複数箇所を置換) (match, bindPropsBlock) => { // bindPropsBlock ( { ... } 内) の各プロパティをさらに置換 const modifiedBlock = bindPropsBlock.replace( - /"([^"]*)":\s*"(.*)"/g, // "propName": "propValue" にマッチ + /(.*):\s*\'(.*)\'/g, // propName: 'propValue' にマッチ (propMatch, propName, propValue) => { - return `"${propName}": ${propValue}`; // propValue のダブルクォートを除去 + return `${propName}: ${propValue}`; // propValue のクォートを除去 } - ); - return `"bindProps": {${modifiedBlock} }`; // 置換後の block で "bindProps": { ... } を再構成 + ).replaceAll("\\'", "'"); + return `bindProps: {${modifiedBlock}}`; // 置換後の block で bindProps: { ... } を再構成 } ); return modifiedString; @@ -52,10 +43,11 @@ function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisR const tsOutput = ` -// vue-props-analyzer によって自動生成されたファイルです。 -// 編集はしないでください。 +// To English +// This file was automatically generated by create-search-index. +// Do not edit this file. -import { i18n } from '@/i18n.js'; +import { i18n } from '@/i18n'; // i18n のインポート export const ${varName} = ${modifyBindPropsInString(jsonString)} as const; @@ -64,105 +56,112 @@ export type ComponentUsageInfo = AnalysisResults[number]['usage'][number]; `; try { - fs.writeFileSync(outputPath.replace('.json', '.ts'), tsOutput, 'utf-8'); // 拡張子を .ts に変更 - console.log(`静的解析結果を ${outputPath.replace('.json', '.ts')} に出力しました。`); // 出力メッセージも .ts に変更 + fs.writeFileSync(outputPath, tsOutput, 'utf-8'); + console.log(`[create-search-index]: output done. ${outputPath}`); } catch (error) { - console.error('TypeScriptファイル出力エラー:', error); // エラーメッセージも TypeScriptファイル出力エラー に変更 + console.error('[create-search-index]: error: ', error); } } -export default function vuePropsAnalyzer(options: { - targetComponents: string[], - targetFilePaths: string[], - exportFilePath: string -}): Plugin { - const targetComponents = options.targetComponents || []; - const analysisResults: AnalysisResult[] = []; // 解析結果を格納する配列をプラグイン内で定義 - - return { - name: 'vue-props-analyzer', - - async transform(code, id) { // transform に渡される code を使用 (ファイル直接読み込みはしない) - if (!id.endsWith('.vue')) { - return null; - } - - if (!options.targetFilePaths.some(targetFilePath => id.includes(targetFilePath))) { - return null; - } - - const { descriptor, errors } = vueSfcParse(code, { // transform の code を解析 - filename: id, - }); - - if (errors.length) { - console.error(`コンパイルエラー: ${id}`, errors); - return null; - } - - // テンプレートASTを走査してコンポーネント使用箇所とpropsの値を取得 - const usageInfo = extractUsageInfoFromTemplateAst(descriptor.template?.ast, id, targetComponents); - if (!usageInfo) return null; - - if (usageInfo.length > 0) { - analysisResults.push({ // グローバル変数ではなく、プラグイン内の配列に push - filePath: id, - usage: usageInfo, - }); - } - - return null; - }, - - async writeBundle() { - outputAnalysisResultAsTS(options.exportFilePath, analysisResults); // writeBundle でファイル出力、解析結果配列を渡す - }, - }; -} - function extractUsageInfoFromTemplateAst( - templateAst: any, - currentFilePath: string, - targetComponents: string[] + templateAst: any, + currentFilePath: string, + targetComponents: string[] ): ComponentUsageInfo[] { - const usageInfoList: ComponentUsageInfo[] = []; + const usageInfoList: ComponentUsageInfo[] = []; - if (!templateAst) { - return usageInfoList; - } + if (!templateAst) { + return usageInfoList; + } - function traverse(node: any) { - if (node.type === 1 /* ELEMENT */ && node.tag && targetComponents.includes(node.tag)) { - const componentTag = node.tag; + function traverse(node: any) { + if (node.type === 1 /* ELEMENT */ && node.tag && targetComponents.includes(node.tag)) { + const componentTag = node.tag; - const staticProps: Record = {}; - const bindProps: Record = {}; + const staticProps: Record = {}; + const bindProps: Record = {}; // bindProps の型を string に戻す - if (node.props && Array.isArray(node.props)) { - node.props.forEach((prop: any) => { - if (prop.type === 6 /* ATTRIBUTE */) { // type 6 は StaticAttribute - staticProps[prop.name] = prop.value?.content || ''; // 属性値を文字列として取得 - } else if (prop.type === 7 /* DIRECTIVE */ && prop.name === 'bind' && prop.arg?.content) { // type 7 は DirectiveNode, v-bind:propName の場合 - if (prop.exp?.content) { - bindProps[prop.arg.content] = prop.exp.content; // v-bind:propName="expression" の expression 部分を取得 (文字列) - } - } - }); - } + if (node.props && Array.isArray(node.props)) { + node.props.forEach((prop: any) => { + if (prop.type === 6 /* ATTRIBUTE */) { // type 6 は StaticAttribute + staticProps[prop.name] = prop.value?.content || ''; //  属性値を文字列として取得 + } else if (prop.type === 7 /* DIRECTIVE */ && prop.name === 'bind' && prop.arg?.content) { // type 7 は DirectiveNode, v-bind:propName の場合 + if (prop.exp?.content) { + bindProps[prop.arg.content] = prop.exp.content; // prop.exp.content (文字列) を格納 + } + } + }); + } - usageInfoList.push({ - parentFile: currentFilePath, - staticProps, - bindProps, - componentName: componentTag, - }); + usageInfoList.push({ + parentFile: currentFilePath, + staticProps, + bindProps, + componentName: componentTag, + }); - } else if (node.children && Array.isArray(node.children)) { - node.children.forEach(child => traverse(child)); - } - } + } else if (node.children && Array.isArray(node.children)) { + node.children.forEach(child => traverse(child)); + } + } - traverse(templateAst); - return usageInfoList; + traverse(templateAst); + return usageInfoList; +} + +export async function analyzeVueProps(options: { + targetComponents: string[], + targetFilePaths: string[], + exportFilePath: string +}): Promise { + const targetComponents = options.targetComponents || []; + const analysisResults: AnalysisResult[] = []; + + // 対象ファイルパスを glob で展開 + const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => { + const matchedFiles = glob.sync(filePathPattern); + return [...acc, ...matchedFiles]; + }, []); + + + for (const filePath of filePaths) { + const code = fs.readFileSync(filePath, 'utf-8'); + const { descriptor, errors } = vueSfcParse(code, { + filename: filePath, + }); + + if (errors.length) { + console.error(`[create-search-index] Compile Error: ${filePath}`, errors); + continue; // エラーが発生したファイルはスキップ + } + + // テンプレートASTを走査してコンポーネント使用箇所とpropsの値を取得 + const usageInfo = extractUsageInfoFromTemplateAst(descriptor.template?.ast, filePath, targetComponents); + if (!usageInfo) continue; + + if (usageInfo.length > 0) { + analysisResults.push({ + filePath: filePath, + usage: usageInfo, + }); + } + } + + outputAnalysisResultAsTS(options.exportFilePath, analysisResults); // outputAnalysisResultAsTS を呼び出す +} + +// Rollup プラグインとして export +export default function vuePropsAnalyzer(options: { + targetComponents: string[], + targetFilePaths: string[], + exportFilePath: string +}): Plugin { + return { + name: 'vue-props-analyzer', + + async writeBundle() { + await analyzeVueProps(options); // writeBundle フックで analyzeVueProps 関数を呼び出す + }, + }; } diff --git a/packages/frontend/src/autogen/search-index.ts b/packages/frontend/src/autogen/search-index.ts new file mode 100644 index 0000000000..aa7967dcf1 --- /dev/null +++ b/packages/frontend/src/autogen/search-index.ts @@ -0,0 +1,66 @@ + +// vue-props-analyzer によって自動生成されたファイルです。 +// 編集はしないでください。 + +import { i18n } from '@/i18n'; // i18n のインポート + +export const searchIndexes = [ + { + filePath: 'src/pages/settings/profile.vue', + usage: [ + { + parentFile: 'src/pages/settings/profile.vue', + staticProps: { + markerId: '727cc9e8-ad67-474a-9241-b5a9a6475e47', + }, + bindProps: {}, + componentName: 'MkSearchMarker', + }, + { + parentFile: 'src/pages/settings/profile.vue', + staticProps: { + markerId: '1a06c7f9-e85e-46cb-bf5f-b3efa8e71b93', + }, + bindProps: {}, + componentName: 'MkSearchMarker', + }, + ], + }, + { + filePath: 'src/pages/settings/privacy.vue', + usage: [ + { + parentFile: 'src/pages/settings/privacy.vue', + staticProps: { + icon: 'ti ti-lock-open', + }, + bindProps: { + locationLabel: [i18n.ts.privacy, i18n.ts.makeFollowManuallyApprove], + keywords: ['follow', 'lock', i18n.ts.lockedAccountInfo], + }, + componentName: 'MkSearchMarker', + }, + ], + }, + { + filePath: 'src/pages/settings/mute-block.vue', + usage: [ + { + parentFile: 'src/pages/settings/mute-block.vue', + staticProps: { + markerId: 'test', + icon: 'ti ti-ban', + }, + bindProps: { + locationLabel: [i18n.ts.muteAndBlock], + keywords: ['mute', i18n.ts.wordMute], + children: ['test2'], + }, + componentName: 'MkSearchMarker', + }, + ], + }, +] as const; + +export type AnalysisResults = typeof searchIndexes; +export type ComponentUsageInfo = AnalysisResults[number]['usage'][number]; diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 07f0c89342..8a6b1d285b 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -86,8 +86,8 @@ export function getConfig(): UserConfig { plugins: [ pluginCreateSearchIndex({ targetComponents: ['MkSearchMarker'], - targetFilePaths: ['/src/pages/settings'], - exportFilePath: './search-index.ts' + targetFilePaths: ['src/pages/settings/*.vue'], + exportFilePath: './src/autogen/search-index.ts' }), pluginVue(), pluginUnwindCssModuleClassName(),