From 9f786b772b511dfb1cd391db1b1e646c75c0cf2a Mon Sep 17 00:00:00 2001 From: tai-cha Date: Mon, 17 Feb 2025 17:18:38 +0900 Subject: [PATCH] =?UTF-8?q?wip:=20markerId=E3=82=92=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E4=BB=98=E4=B8=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/rollup-plugin-create-search-index.ts | 121 ++++++++++++++---- packages/frontend/package.json | 1 + .../src/{ => scripts}/autogen/search-index.ts | 7 +- packages/frontend/vite.config.ts | 2 +- pnpm-lock.yaml | 25 ++-- 5 files changed, 116 insertions(+), 40 deletions(-) rename packages/frontend/src/{ => scripts}/autogen/search-index.ts (84%) diff --git a/packages/frontend/lib/rollup-plugin-create-search-index.ts b/packages/frontend/lib/rollup-plugin-create-search-index.ts index 9b3ef223b8..cb2a3cd751 100644 --- a/packages/frontend/lib/rollup-plugin-create-search-index.ts +++ b/packages/frontend/lib/rollup-plugin-create-search-index.ts @@ -3,11 +3,14 @@ * 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 fs from 'node:fs'; import { glob } from 'glob'; import JSON5 from 'json5'; +import { randomUUID } from 'crypto'; +import MagicString from 'magic-string'; +import path from 'node:path' export interface AnalysisResult { filePath: string; @@ -15,7 +18,6 @@ export interface AnalysisResult { } export interface ComponentUsageInfo { - parentFile: string; staticProps: Record; bindProps: Record; componentName: string; @@ -23,24 +25,23 @@ export interface ComponentUsageInfo { function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisResult[]): void { // (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 { - // (modifyBindPropsInString 関数の実装は前回と同様) const modifiedString = jsonString.replace( - /bindProps:\s*\{([^}]*)\}/g, // bindProps: { ... } にマッチ (g フラグで複数箇所を置換) + /bindProps:\s*\{([^}]*)\}/g, //  bindProps: { ... } にマッチ (g フラグで複数箇所を置換) (match, bindPropsBlock) => { - // bindPropsBlock ( { ... } 内) の各プロパティをさらに置換 + //  bindPropsBlock ( { ... } 内) の各プロパティをさらに置換 const modifiedBlock = bindPropsBlock.replace( - /(.*):\s*\'(.*)\'/g, // propName: 'propValue' にマッチ + /(.*):\s*\'(.*)\'/g, //  propName: 'propValue' にマッチ (propMatch, propName, propValue) => { return `${propName}: ${propValue}`; // propValue のクォートを除去 } ).replaceAll("\\'", "'"); - return `bindProps: {${modifiedBlock}}`; // 置換後の block で bindProps: { ... } を再構成 + return `bindProps: {${modifiedBlock}}`; //  置換後の block で bindProps: { ... } を再構成 } ); return modifiedString; @@ -56,7 +57,7 @@ function outputAnalysisResultAsTS(outputPath: string, analysisResults: AnalysisR // This file was automatically generated by create-search-index. // Do not edit this file. -import { i18n } from '@/i18n'; // i18n のインポート +import { i18n } from '@/i18n.js'; export const ${varName} = ${modifyBindPropsInString(jsonString)} as const; @@ -72,10 +73,8 @@ export type ComponentUsageInfo = AnalysisResults[number]['usage'][number]; } } - function extractUsageInfoFromTemplateAst( templateAst: any, - currentFilePath: string, targetComponents: string[] ): ComponentUsageInfo[] { const usageInfoList: ComponentUsageInfo[] = []; @@ -96,7 +95,7 @@ function extractUsageInfoFromTemplateAst( 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) { + if (prop.exp?.content && prop.arg.content !== 'class') { bindProps[prop.arg.content] = prop.exp.content; // prop.exp.content (文字列) を格納 } } @@ -104,7 +103,6 @@ function extractUsageInfoFromTemplateAst( } usageInfoList.push({ - parentFile: currentFilePath, staticProps, bindProps, componentName: componentTag, @@ -122,12 +120,14 @@ function extractUsageInfoFromTemplateAst( export async function analyzeVueProps(options: { targetComponents: string[], targetFilePaths: string[], - exportFilePath: string + exportFilePath: string, + transformedCodeCache: Record }): Promise { + const targetComponents = options.targetComponents || []; const analysisResults: AnalysisResult[] = []; - // 対象ファイルパスを glob で展開 + //  対象ファイルパスを glob で展開 const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => { const matchedFiles = glob.sync(filePathPattern); return [...acc, ...matchedFiles]; @@ -135,7 +135,13 @@ export async function analyzeVueProps(options: { 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, { filename: filePath, }); @@ -146,7 +152,7 @@ export async function analyzeVueProps(options: { } // テンプレートASTを走査してコンポーネント使用箇所とpropsの値を取得 - const usageInfo = extractUsageInfoFromTemplateAst(descriptor.template?.ast, filePath, targetComponents); + const usageInfo = extractUsageInfoFromTemplateAst(descriptor.template?.ast, targetComponents); if (!usageInfo) continue; if (usageInfo.length > 0) { @@ -161,16 +167,87 @@ export async function analyzeVueProps(options: { } // Rollup プラグインとして export -export default function vuePropsAnalyzer(options: { +export default function pluginCreateSearchIndex(options: { targetComponents: string[], targetFilePaths: string[], exportFilePath: string }): Plugin { + const transformedCodeCache: Record = {}; // キャッシュオブジェクトを定義 + 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 属性を に追加 + 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() { - await analyzeVueProps(options); // writeBundle フックで analyzeVueProps 関数を呼び出す + await analyzeVueProps({ ...options, transformedCodeCache }); // writeBundle フックで analyzeVueProps 関数を呼び出す (変更なし) }, }; } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 9ed6aa708f..68ffb91155 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -49,6 +49,7 @@ "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", + "magic-string": "0.30.17", "matter-js": "0.20.0", "mfm-js": "0.24.0", "misskey-bubble-game": "workspace:*", diff --git a/packages/frontend/src/autogen/search-index.ts b/packages/frontend/src/scripts/autogen/search-index.ts similarity index 84% rename from packages/frontend/src/autogen/search-index.ts rename to packages/frontend/src/scripts/autogen/search-index.ts index b0cf61dc9a..4207aae35b 100644 --- a/packages/frontend/src/autogen/search-index.ts +++ b/packages/frontend/src/scripts/autogen/search-index.ts @@ -7,14 +7,13 @@ // This file was automatically generated by create-search-index. // Do not edit this file. -import { i18n } from '@/i18n'; // i18n のインポート +import { i18n } from '@/i18n.js'; export const searchIndexes = [ { filePath: 'src/pages/settings/profile.vue', usage: [ { - parentFile: 'src/pages/settings/profile.vue', staticProps: { markerId: '727cc9e8-ad67-474a-9241-b5a9a6475e47', }, @@ -22,7 +21,6 @@ export const searchIndexes = [ componentName: 'MkSearchMarker', }, { - parentFile: 'src/pages/settings/profile.vue', staticProps: { markerId: '1a06c7f9-e85e-46cb-bf5f-b3efa8e71b93', }, @@ -35,9 +33,9 @@ export const searchIndexes = [ filePath: 'src/pages/settings/privacy.vue', usage: [ { - parentFile: 'src/pages/settings/privacy.vue', staticProps: { icon: 'ti ti-lock-open', + markerId: 'db7de893-e299-40af-a515-8954da435f4b', }, bindProps: { locationLabel: [i18n.ts.privacy, i18n.ts.makeFollowManuallyApprove], @@ -51,7 +49,6 @@ export const searchIndexes = [ filePath: 'src/pages/settings/mute-block.vue', usage: [ { - parentFile: 'src/pages/settings/mute-block.vue', staticProps: { markerId: 'test', icon: 'ti ti-ban', diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 8a6b1d285b..771530092e 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -87,7 +87,7 @@ export function getConfig(): UserConfig { pluginCreateSearchIndex({ targetComponents: ['MkSearchMarker'], targetFilePaths: ['src/pages/settings/*.vue'], - exportFilePath: './src/autogen/search-index.ts' + exportFilePath: './src/scripts/autogen/search-index.ts' }), pluginVue(), pluginUnwindCssModuleClassName(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1b8d5e1ae..f963afa408 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -784,6 +784,9 @@ importers: json5: specifier: 2.2.3 version: 2.2.3 + magic-string: + specifier: 0.30.17 + version: 0.30.17 matter-js: specifier: 0.20.0 version: 0.20.0 @@ -7895,9 +7898,6 @@ packages: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -10729,6 +10729,9 @@ packages: vue-component-type-helpers@2.2.0: 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: resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} engines: {node: '>=12'} @@ -13408,7 +13411,7 @@ snapshots: '@rollup/plugin-replace@6.0.2(rollup@4.34.7)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.34.7) - magic-string: 0.30.11 + magic-string: 0.30.17 optionalDependencies: 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/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 - magic-string: 0.30.11 + magic-string: 0.30.17 react: 19.0.0 react-docgen: 7.0.1 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/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 - 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) 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) @@ -14332,7 +14335,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 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)': dependencies: @@ -15146,7 +15149,7 @@ snapshots: '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 estree-walker: 2.0.2 - magic-string: 0.30.11 + magic-string: 0.30.17 postcss: 8.5.2 source-map-js: 1.2.1 @@ -19037,10 +19040,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.11: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -22137,6 +22136,8 @@ snapshots: 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)): dependencies: vue: 3.5.13(typescript@5.7.3)