From 5f4cc2463e5f02d2f04c954dc7cc6843c4f96198 Mon Sep 17 00:00:00 2001 From: tai-cha Date: Mon, 17 Feb 2025 04:21:16 +0000 Subject: [PATCH] =?UTF-8?q?wip=20rollup=20plugin=E3=81=A7searchIndex?= =?UTF-8?q?=E3=81=AE=E6=83=85=E5=A0=B1=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/rollup-plugin-create-search-index.ts | 168 ++++++++++++++++++ packages/frontend/vite.config.ts | 6 + 2 files changed, 174 insertions(+) create mode 100644 packages/frontend/lib/rollup-plugin-create-search-index.ts diff --git a/packages/frontend/lib/rollup-plugin-create-search-index.ts b/packages/frontend/lib/rollup-plugin-create-search-index.ts new file mode 100644 index 0000000000..2e3501c931 --- /dev/null +++ b/packages/frontend/lib/rollup-plugin-create-search-index.ts @@ -0,0 +1,168 @@ +import { parse as vueSfcParse } from '@vue/compiler-sfc'; +import type { Plugin } from 'rollup'; +import fs from 'node:fs'; + + +export interface AnalysisResult { + 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); + } +} + +function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { + const varName = 'searchIndexResults'; + + const jsonString = JSON.stringify(analysisResults, null, 2); // JSON.stringify で JSON 文字列を生成 + + // bindProps の値を文字列置換で修正する関数 + function modifyBindPropsInString(jsonString: string): string { + // bindProps: { ... } ブロックを正規表現で検索し、置換 + 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 のダブルクォートを除去 + } + ); + return `"bindProps": {${modifiedBlock} }`; // 置換後の block で "bindProps": { ... } を再構成 + } + ); + return modifiedString; + } + + + const tsOutput = ` +// vue-props-analyzer によって自動生成されたファイルです。 +// 編集はしないでください。 + +import { i18n } from '@/i18n.js'; + +export const ${varName} = ${modifyBindPropsInString(jsonString)} as const; + +export type AnalysisResults = typeof ${varName}; +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 に変更 + } catch (error) { + console.error('TypeScriptファイル出力エラー:', error); // エラーメッセージも TypeScriptファイル出力エラー に変更 + } +} + + +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[] +): 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 = {}; + + 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 部分を取得 (文字列) + } + } + }); + } + + 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; +} diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index d1b7c410dc..07f0c89342 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -10,6 +10,7 @@ import meta from '../../package.json'; import packageInfo from './package.json' with { type: 'json' }; import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js'; import pluginJson5 from './vite.json5.js'; +import pluginCreateSearchIndex from './lib/rollup-plugin-create-search-index.js'; const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; const host = url ? (new URL(url)).hostname : undefined; @@ -83,6 +84,11 @@ export function getConfig(): UserConfig { }, plugins: [ + pluginCreateSearchIndex({ + targetComponents: ['MkSearchMarker'], + targetFilePaths: ['/src/pages/settings'], + exportFilePath: './search-index.ts' + }), pluginVue(), pluginUnwindCssModuleClassName(), pluginJson5(),