/* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ 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[]; } export interface ComponentUsageInfo { parentFile: string; staticProps: Record; bindProps: Record; componentName: string; } function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { // (outputAnalysisResultAsTS 関数の実装は前回と同様) const varName = 'searchIndexes'; // 変数名 const jsonString = JSON5.stringify(analysisResults, { space: "\t", quote: "'" }); // JSON.stringify で JSON 文字列を生成 // bindProps の値を文字列置換で修正する関数 function modifyBindPropsInString(jsonString: string): string { // (modifyBindPropsInString 関数の実装は前回と同様) const modifiedString = jsonString.replace( /bindProps:\s*\{([^}]*)\}/g, // bindProps: { ... } にマッチ (g フラグで複数箇所を置換) (match, bindPropsBlock) => { // bindPropsBlock ( { ... } 内) の各プロパティをさらに置換 const modifiedBlock = bindPropsBlock.replace( /(.*):\s*\'(.*)\'/g, // propName: 'propValue' にマッチ (propMatch, propName, propValue) => { return `${propName}: ${propValue}`; // propValue のクォートを除去 } ).replaceAll("\\'", "'"); return `bindProps: {${modifiedBlock}}`; // 置換後の block で bindProps: { ... } を再構成 } ); return modifiedString; } const tsOutput = ` /* * 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'; // i18n のインポート export const ${varName} = ${modifyBindPropsInString(jsonString)} as const; export type AnalysisResults = typeof ${varName}; export type ComponentUsageInfo = AnalysisResults[number]['usage'][number]; `; try { fs.writeFileSync(outputPath, tsOutput, 'utf-8'); console.log(`[create-search-index]: output done. ${outputPath}`); } catch (error) { console.error('[create-search-index]: error: ', error); } } function extractUsageInfoFromTemplateAst( templateAst: any, currentFilePath: string, targetComponents: string[] ): ComponentUsageInfo[] { const usageInfoList: ComponentUsageInfo[] = []; if (!templateAst) { return usageInfoList; } 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 = {}; // 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; // prop.exp.content (文字列) を格納 } } }); } usageInfoList.push({ parentFile: currentFilePath, staticProps, bindProps, componentName: componentTag, }); } else if (node.children && Array.isArray(node.children)) { node.children.forEach(child => traverse(child)); } } 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 関数を呼び出す }, }; }