This commit is contained in:
tai-cha 2025-02-17 14:51:46 +09:00
parent 5f4cc2463e
commit 28c6b40f84
No known key found for this signature in database
GPG Key ID: 1D5EE39F870DC283
3 changed files with 183 additions and 118 deletions

View File

@ -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<string, string>;
bindProps: Record<string, string>;
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<string, string>;
bindProps: Record<string, string>;
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<string, string> = {};
const bindProps: Record<string, string> = {};
const staticProps: Record<string, string> = {};
const bindProps: Record<string, string> = {}; // 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<void> {
const targetComponents = options.targetComponents || [];
const analysisResults: AnalysisResult[] = [];
// 対象ファイルパスを glob で展開
const filePaths = options.targetFilePaths.reduce<string[]>((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 関数を呼び出す
},
};
}

View File

@ -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];

View File

@ -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(),