wip: markerIdを自動付与

This commit is contained in:
tai-cha 2025-02-17 17:18:38 +09:00
parent 30ff85babb
commit 9f786b772b
No known key found for this signature in database
GPG Key ID: 1D5EE39F870DC283
5 changed files with 116 additions and 40 deletions

View File

@ -3,11 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { parse as vueSfcParse } from '@vue/compiler-sfc'; import { parse as vueSfcParse } from 'vue/compiler-sfc';
import type { Plugin } from 'rollup'; import type { Plugin } from 'rollup';
import fs from 'node:fs'; import fs from 'node:fs';
import { glob } from 'glob'; import { glob } from 'glob';
import JSON5 from 'json5'; import JSON5 from 'json5';
import { randomUUID } from 'crypto';
import MagicString from 'magic-string';
import path from 'node:path'
export interface AnalysisResult { export interface AnalysisResult {
filePath: string; filePath: string;
@ -15,7 +18,6 @@ export interface AnalysisResult {
} }
export interface ComponentUsageInfo { export interface ComponentUsageInfo {
parentFile: string;
staticProps: Record<string, string>; staticProps: Record<string, string>;
bindProps: Record<string, string>; bindProps: Record<string, string>;
componentName: string; componentName: string;
@ -23,24 +25,23 @@ export interface ComponentUsageInfo {
function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void {
// (outputAnalysisResultAsTS 関数の実装は前回と同様) // (outputAnalysisResultAsTS 関数の実装は前回と同様)
const varName = 'searchIndexes'; // 変数名 const varName = 'searchIndexes'; //  変数名
const jsonString = JSON5.stringify(analysisResults, { space: "\t", quote: "'" }); // JSON.stringify で JSON 文字列を生成 const jsonString = JSON5.stringify(analysisResults, { space: "\t", quote: "'" }); //  JSON.stringify で JSON 文字列を生成
// bindProps の値を文字列置換で修正する関数 //  bindProps の値を文字列置換で修正する関数
function modifyBindPropsInString(jsonString: string): string { function modifyBindPropsInString(jsonString: string): string {
// (modifyBindPropsInString 関数の実装は前回と同様)
const modifiedString = jsonString.replace( const modifiedString = jsonString.replace(
/bindProps:\s*\{([^}]*)\}/g, // bindProps: { ... } にマッチ (g フラグで複数箇所を置換) /bindProps:\s*\{([^}]*)\}/g, //  bindProps: { ... } にマッチ (g フラグで複数箇所を置換)
(match, bindPropsBlock) => { (match, bindPropsBlock) => {
// bindPropsBlock ( { ... } 内) の各プロパティをさらに置換 //  bindPropsBlock ( { ... } 内) の各プロパティをさらに置換
const modifiedBlock = bindPropsBlock.replace( const modifiedBlock = bindPropsBlock.replace(
/(.*):\s*\'(.*)\'/g, // propName: 'propValue' にマッチ /(.*):\s*\'(.*)\'/g, //  propName: 'propValue' にマッチ
(propMatch, propName, propValue) => { (propMatch, propName, propValue) => {
return `${propName}: ${propValue}`; // propValue のクォートを除去 return `${propName}: ${propValue}`; // propValue のクォートを除去
} }
).replaceAll("\\'", "'"); ).replaceAll("\\'", "'");
return `bindProps: {${modifiedBlock}}`; // 置換後の block で bindProps: { ... } を再構成 return `bindProps: {${modifiedBlock}}`; //  置換後の block で bindProps: { ... } を再構成
} }
); );
return modifiedString; return modifiedString;
@ -56,7 +57,7 @@ function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisR
// This file was automatically generated by create-search-index. // This file was automatically generated by create-search-index.
// Do not edit this file. // Do not edit this file.
import { i18n } from '@/i18n'; // i18n のインポート import { i18n } from '@/i18n.js';
export const ${varName} = ${modifyBindPropsInString(jsonString)} as const; export const ${varName} = ${modifyBindPropsInString(jsonString)} as const;
@ -72,10 +73,8 @@ export type ComponentUsageInfo = AnalysisResults[number]['usage'][number];
} }
} }
function extractUsageInfoFromTemplateAst( function extractUsageInfoFromTemplateAst(
templateAst: any, templateAst: any,
currentFilePath: string,
targetComponents: string[] targetComponents: string[]
): ComponentUsageInfo[] { ): ComponentUsageInfo[] {
const usageInfoList: ComponentUsageInfo[] = []; const usageInfoList: ComponentUsageInfo[] = [];
@ -96,7 +95,7 @@ function extractUsageInfoFromTemplateAst(
if (prop.type === 6 /* ATTRIBUTE */) { // type 6 は StaticAttribute if (prop.type === 6 /* ATTRIBUTE */) { // type 6 は StaticAttribute
staticProps[prop.name] = prop.value?.content || ''; //  属性値を文字列として取得 staticProps[prop.name] = prop.value?.content || ''; //  属性値を文字列として取得
} else if (prop.type === 7 /* DIRECTIVE */ && prop.name === 'bind' && prop.arg?.content) { // type 7 は DirectiveNode, v-bind:propName の場合 } else if (prop.type === 7 /* DIRECTIVE */ && prop.name === 'bind' && prop.arg?.content) { // type 7 は DirectiveNode, v-bind:propName の場合
if (prop.exp?.content) { if (prop.exp?.content && prop.arg.content !== 'class') {
bindProps[prop.arg.content] = prop.exp.content; // prop.exp.content (文字列) を格納 bindProps[prop.arg.content] = prop.exp.content; // prop.exp.content (文字列) を格納
} }
} }
@ -104,7 +103,6 @@ function extractUsageInfoFromTemplateAst(
} }
usageInfoList.push({ usageInfoList.push({
parentFile: currentFilePath,
staticProps, staticProps,
bindProps, bindProps,
componentName: componentTag, componentName: componentTag,
@ -122,12 +120,14 @@ function extractUsageInfoFromTemplateAst(
export async function analyzeVueProps(options: { export async function analyzeVueProps(options: {
targetComponents: string[], targetComponents: string[],
targetFilePaths: string[], targetFilePaths: string[],
exportFilePath: string exportFilePath: string,
transformedCodeCache: Record<string, string>
}): Promise<void> { }): Promise<void> {
const targetComponents = options.targetComponents || []; const targetComponents = options.targetComponents || [];
const analysisResults: AnalysisResult[] = []; const analysisResults: AnalysisResult[] = [];
// 対象ファイルパスを glob で展開 //  対象ファイルパスを glob で展開
const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => { const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
const matchedFiles = glob.sync(filePathPattern); const matchedFiles = glob.sync(filePathPattern);
return [...acc, ...matchedFiles]; return [...acc, ...matchedFiles];
@ -135,7 +135,13 @@ export async function analyzeVueProps(options: {
for (const filePath of filePaths) { for (const filePath of filePaths) {
const code = fs.readFileSync(filePath, 'utf-8'); // ★ キャッシュから変換済みコードを取得 (修正): キャッシュに存在しない場合はエラーにする (キャッシュ必須)
const code = options.transformedCodeCache[path.resolve(filePath)]; // キャッシュからコードを取得 (キャッシュミス時は undefined)
if (!code) { // キャッシュミスの場合
console.error(`[create-search-index] Error: No cached code found for: ${filePath}.`); // エラーログ
continue;
}
console.log(`[create-search-index] analyzeVueProps: Processing file: ${filePath}, using cached code: true`); // ★ ログ: キャッシュ使用
const { descriptor, errors } = vueSfcParse(code, { const { descriptor, errors } = vueSfcParse(code, {
filename: filePath, filename: filePath,
}); });
@ -146,7 +152,7 @@ export async function analyzeVueProps(options: {
} }
// テンプレートASTを走査してコンポーネント使用箇所とpropsの値を取得 // テンプレートASTを走査してコンポーネント使用箇所とpropsの値を取得
const usageInfo = extractUsageInfoFromTemplateAst(descriptor.template?.ast, filePath, targetComponents); const usageInfo = extractUsageInfoFromTemplateAst(descriptor.template?.ast, targetComponents);
if (!usageInfo) continue; if (!usageInfo) continue;
if (usageInfo.length > 0) { if (usageInfo.length > 0) {
@ -161,16 +167,87 @@ export async function analyzeVueProps(options: {
} }
// Rollup プラグインとして export // Rollup プラグインとして export
export default function vuePropsAnalyzer(options: { export default function pluginCreateSearchIndex(options: {
targetComponents: string[], targetComponents: string[],
targetFilePaths: string[], targetFilePaths: string[],
exportFilePath: string exportFilePath: string
}): Plugin { }): Plugin {
const transformedCodeCache: Record<string, string> = {}; // キャッシュオブジェクトを定義
return { return {
name: 'vue-props-analyzer', name: 'createSearchIndex',
async transform(code, id) {
if (!id.endsWith('.vue')) {
return null;
}
// targetFilePaths にマッチするファイルのみ処理を行う
// glob パターンでマッチング
let fullFileName = '';
let isMatch = false; // isMatch の初期値を false に設定
for (const pattern of options.targetFilePaths) { // パターンごとにマッチング確認
const globbedFiles = glob.sync(pattern);
for (const globbedFile of globbedFiles) {
const normalizedGlobbedFile = path.resolve(globbedFile); // glob 結果を絶対パスに
const normalizedId = path.resolve(id); // id を絶対パスに
if (normalizedGlobbedFile === normalizedId) { // 絶対パス同士で比較
isMatch = true;
fullFileName = normalizedId;
break; // マッチしたらループを抜ける
}
}
if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける
}
if (!isMatch) {
return null;
}
console.log(`[create-search-index] Processing file: ${id}`); // ログ: マッチしたファイルを処理中
const s = new MagicString(code); // magic-string のインスタンスを作成
const ast = vueSfcParse(code, { filename: id }).descriptor.template?.ast; // テンプレート AST を取得
if (ast) {
function traverse(node: any) {
if (node.type === 1 /* ELEMENT */ && node.tag === 'MkSearchMarker') { // MkSearchMarker コンポーネントを検出
const markerId = randomUUID(); // UUID を生成
const props = node.props || [];
const hasMarkerIdProp = props.some((prop: any) => prop.type === 6 && prop.name === 'markerId'); // markerId 属性が既に存在するか確認
if (!hasMarkerIdProp) {
// magic-string を使って markerId 属性を <MkSearchMarker> に追加
const startTagEnd = code.indexOf('>', node.loc.start.offset); // 開始タグの閉じ > の位置を検索
if (startTagEnd !== -1) {
s.appendRight(startTagEnd, ` markerId="${markerId}"`); //  markerId 属性を追記
console.log(`[create-search-index] 付与 markerId="${markerId}" to MkSearchMarker in ${id}`); // 付与ログ
}
}
}
if (node.children && Array.isArray(node.children)) {
node.children.forEach(child => traverse(child)); // 子ノードを再帰的に traverse
}
}
traverse(ast); // AST を traverse
const transformedCode = s.toString(); // ★ 変換後のコードを取得
transformedCodeCache[id] = transformedCode; // ★ 変換後のコードをキャッシュに保存
return {
code: transformedCode, // 変更後のコードを返す
map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
};
}
return null; // テンプレート AST がない場合は null を返す
},
async writeBundle() { async writeBundle() {
await analyzeVueProps(options); // writeBundle フックで analyzeVueProps 関数を呼び出す await analyzeVueProps({ ...options, transformedCodeCache }); // writeBundle フックで analyzeVueProps 関数を呼び出す (変更なし)
}, },
}; };
} }

View File

@ -49,6 +49,7 @@
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"magic-string": "0.30.17",
"matter-js": "0.20.0", "matter-js": "0.20.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"misskey-bubble-game": "workspace:*", "misskey-bubble-game": "workspace:*",

View File

@ -7,14 +7,13 @@
// This file was automatically generated by create-search-index. // This file was automatically generated by create-search-index.
// Do not edit this file. // Do not edit this file.
import { i18n } from '@/i18n'; // i18n のインポート import { i18n } from '@/i18n.js';
export const searchIndexes = [ export const searchIndexes = [
{ {
filePath: 'src/pages/settings/profile.vue', filePath: 'src/pages/settings/profile.vue',
usage: [ usage: [
{ {
parentFile: 'src/pages/settings/profile.vue',
staticProps: { staticProps: {
markerId: '727cc9e8-ad67-474a-9241-b5a9a6475e47', markerId: '727cc9e8-ad67-474a-9241-b5a9a6475e47',
}, },
@ -22,7 +21,6 @@ export const searchIndexes = [
componentName: 'MkSearchMarker', componentName: 'MkSearchMarker',
}, },
{ {
parentFile: 'src/pages/settings/profile.vue',
staticProps: { staticProps: {
markerId: '1a06c7f9-e85e-46cb-bf5f-b3efa8e71b93', markerId: '1a06c7f9-e85e-46cb-bf5f-b3efa8e71b93',
}, },
@ -35,9 +33,9 @@ export const searchIndexes = [
filePath: 'src/pages/settings/privacy.vue', filePath: 'src/pages/settings/privacy.vue',
usage: [ usage: [
{ {
parentFile: 'src/pages/settings/privacy.vue',
staticProps: { staticProps: {
icon: 'ti ti-lock-open', icon: 'ti ti-lock-open',
markerId: 'db7de893-e299-40af-a515-8954da435f4b',
}, },
bindProps: { bindProps: {
locationLabel: [i18n.ts.privacy, i18n.ts.makeFollowManuallyApprove], locationLabel: [i18n.ts.privacy, i18n.ts.makeFollowManuallyApprove],
@ -51,7 +49,6 @@ export const searchIndexes = [
filePath: 'src/pages/settings/mute-block.vue', filePath: 'src/pages/settings/mute-block.vue',
usage: [ usage: [
{ {
parentFile: 'src/pages/settings/mute-block.vue',
staticProps: { staticProps: {
markerId: 'test', markerId: 'test',
icon: 'ti ti-ban', icon: 'ti ti-ban',

View File

@ -87,7 +87,7 @@ export function getConfig(): UserConfig {
pluginCreateSearchIndex({ pluginCreateSearchIndex({
targetComponents: ['MkSearchMarker'], targetComponents: ['MkSearchMarker'],
targetFilePaths: ['src/pages/settings/*.vue'], targetFilePaths: ['src/pages/settings/*.vue'],
exportFilePath: './src/autogen/search-index.ts' exportFilePath: './src/scripts/autogen/search-index.ts'
}), }),
pluginVue(), pluginVue(),
pluginUnwindCssModuleClassName(), pluginUnwindCssModuleClassName(),

View File

@ -784,6 +784,9 @@ importers:
json5: json5:
specifier: 2.2.3 specifier: 2.2.3
version: 2.2.3 version: 2.2.3
magic-string:
specifier: 0.30.17
version: 0.30.17
matter-js: matter-js:
specifier: 0.20.0 specifier: 0.20.0
version: 0.20.0 version: 0.20.0
@ -7895,9 +7898,6 @@ packages:
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
engines: {node: '>=12'} engines: {node: '>=12'}
magic-string@0.30.11:
resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
magic-string@0.30.17: magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@ -10729,6 +10729,9 @@ packages:
vue-component-type-helpers@2.2.0: vue-component-type-helpers@2.2.0:
resolution: {integrity: sha512-cYrAnv2me7bPDcg9kIcGwjJiSB6Qyi08+jLDo9yuvoFQjzHiPTzML7RnkJB1+3P6KMsX/KbCD4QE3Tv/knEllw==} resolution: {integrity: sha512-cYrAnv2me7bPDcg9kIcGwjJiSB6Qyi08+jLDo9yuvoFQjzHiPTzML7RnkJB1+3P6KMsX/KbCD4QE3Tv/knEllw==}
vue-component-type-helpers@2.2.2:
resolution: {integrity: sha512-6lLY+n2xz2kCYshl59mL6gy8OUUTmkscmDFMO8i7Lj+QKwgnIFUZmM1i/iTYObtrczZVdw7UakPqDTGwVSGaRg==}
vue-demi@0.14.7: vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -13408,7 +13411,7 @@ snapshots:
'@rollup/plugin-replace@6.0.2(rollup@4.34.7)': '@rollup/plugin-replace@6.0.2(rollup@4.34.7)':
dependencies: dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.34.7) '@rollup/pluginutils': 5.1.4(rollup@4.34.7)
magic-string: 0.30.11 magic-string: 0.30.17
optionalDependencies: optionalDependencies:
rollup: 4.34.7 rollup: 4.34.7
@ -14248,7 +14251,7 @@ snapshots:
'@storybook/builder-vite': 8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(vite@6.1.0(@types/node@22.13.4)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.2)) '@storybook/builder-vite': 8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(vite@6.1.0(@types/node@22.13.4)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.2))
'@storybook/react': 8.5.6(@storybook/test@8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(typescript@5.7.3) '@storybook/react': 8.5.6(@storybook/test@8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(typescript@5.7.3)
find-up: 5.0.0 find-up: 5.0.0
magic-string: 0.30.11 magic-string: 0.30.17
react: 19.0.0 react: 19.0.0
react-docgen: 7.0.1 react-docgen: 7.0.1
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
@ -14311,7 +14314,7 @@ snapshots:
'@storybook/builder-vite': 8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(vite@6.1.0(@types/node@22.13.4)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.2)) '@storybook/builder-vite': 8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(vite@6.1.0(@types/node@22.13.4)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.2))
'@storybook/vue3': 8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(vue@3.5.13(typescript@5.7.3)) '@storybook/vue3': 8.5.6(storybook@8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5))(vue@3.5.13(typescript@5.7.3))
find-package-json: 1.2.0 find-package-json: 1.2.0
magic-string: 0.30.11 magic-string: 0.30.17
storybook: 8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5) storybook: 8.5.6(bufferutil@4.0.9)(prettier@3.5.1)(utf-8-validate@6.0.5)
typescript: 5.7.3 typescript: 5.7.3
vite: 6.1.0(@types/node@22.13.4)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.2) vite: 6.1.0(@types/node@22.13.4)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.2)
@ -14332,7 +14335,7 @@ snapshots:
ts-dedent: 2.2.0 ts-dedent: 2.2.0
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.5.13(typescript@5.7.3) vue: 3.5.13(typescript@5.7.3)
vue-component-type-helpers: 2.2.0 vue-component-type-helpers: 2.2.2
'@stylistic/eslint-plugin@2.13.0(eslint@9.20.1)(typescript@5.7.3)': '@stylistic/eslint-plugin@2.13.0(eslint@9.20.1)(typescript@5.7.3)':
dependencies: dependencies:
@ -15146,7 +15149,7 @@ snapshots:
'@vue/compiler-ssr': 3.5.13 '@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13 '@vue/shared': 3.5.13
estree-walker: 2.0.2 estree-walker: 2.0.2
magic-string: 0.30.11 magic-string: 0.30.17
postcss: 8.5.2 postcss: 8.5.2
source-map-js: 1.2.1 source-map-js: 1.2.1
@ -19037,10 +19040,6 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
magic-string@0.30.11:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
magic-string@0.30.17: magic-string@0.30.17:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
@ -22137,6 +22136,8 @@ snapshots:
vue-component-type-helpers@2.2.0: {} vue-component-type-helpers@2.2.0: {}
vue-component-type-helpers@2.2.2: {}
vue-demi@0.14.7(vue@3.5.13(typescript@5.7.3)): vue-demi@0.14.7(vue@3.5.13(typescript@5.7.3)):
dependencies: dependencies:
vue: 3.5.13(typescript@5.7.3) vue: 3.5.13(typescript@5.7.3)