@@ -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 < string , string > ;
bindProps : Record < string , string > ;
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 < string , string >
} ) : Promise < void > {
const targetComponents = options . targetComponents || [ ] ;
const analysisResults : AnalysisResult [ ] = [ ] ;
// 対象ファイルパスを glob で展開
// 対象ファイルパスを glob で展開
const filePaths = options . targetFilePaths . reduce < string [ ] > ( ( 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 < string , string > = { } ; // キャッシュオブジェクトを定義
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() {
await analyzeVueProps ( options ) ; // writeBundle フックで analyzeVueProps 関数を呼び出す
await analyzeVueProps ( { . . . options , transformedCodeCache } ) ; // writeBundle フックで analyzeVueProps 関数を呼び出す (変更なし)
} ,
} ;
}