177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
/*
|
|
* 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<string, string>;
|
|
bindProps: Record<string, string>;
|
|
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<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; // 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<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 関数を呼び出す
|
|
},
|
|
};
|
|
}
|