import fs from 'fs' ; import path from 'path' ; import glob from 'glob' ; import { RawSource } from 'webpack-sources' ; import { ModuleUMDInfo } from '@arco-design/arco-material-preview-utils/es/interface' ; import parseComment from './parseComment' ; import parseRawComment , { Comment } from './parseRawComment' ; import parseDependencies from './parseDependencies' ; import parseModuleExport , { ModuleExportMap } from './parseModuleExport' ; import tryGetUMDInfo from '../utils/tryGetUMDInfo' ; import { PLACEHOLDER_ARCO_SITE_MODULE_INFO } from '../constant' ; import encodeInfo from '../utils/encodeInfo' ; import getTitleOfMarkdown from '../utils/getTitleOfMarkdown' ; export type ModuleInfo = { name : string ; info : Record < string , string > & { umd : ModuleUMDInfo ; } ; isDoc ? : boolean ; children ? : Array < { name : string ; info : Record < string , string > ; rawCode ? : string ; } > ; } ; type ChunkInfo = { name : string ; entry : string ; files ? : string [ ] ; } ; export default class ArcoSiteModuleInfoPlugin { private readonly options : { globs ? : { doc ? : string ; demo ? : string ; } ; } ; private paths : { doc : string [ ] ; demo : string [ ] ; } ; constructor ( options ) { options = options || { } ; const { globs } = options ; this . options = options ; this . paths = { doc : globs . doc ? glob . sync ( globs . doc ) . map ( ( item ) => path . resolve ( item ) ) : [ ] , demo : globs . demo ? glob . sync ( globs . demo ) . map ( ( item ) => path . resolve ( item ) ) : [ ] , } ; } parseModuleExport ( { context , filePathList , statsModules , needRawCode = false } ) { const fileDependencyMap = { } ; const { modules } = parseDependencies ( statsModules ) ; Object . entries ( modules ) . forEach ( ( [ modulePath , dependencies ] ) => { modulePath = path . resolve ( context , modulePath ) ; if ( filePathList . indexOf ( modulePath ) > - 1 ) { fileDependencyMap [ modulePath ] = dependencies . map ( ( filePath ) => path . resolve ( context , filePath ) ) . filter ( ( filePath ) => filePath . indexOf ( 'node_modules' ) === - 1 ) ; } } ) ; return parseModuleExport ( { context , statsModules , needRawCode , validPaths : filePathList , fileDependencyMap , } ) ; } getModuleInfoMap ( { chunkInfoList , moduleExportMap , } ) { const rawDemoCommentMap = { } ; this . paths . demo . forEach ( ( demoPath ) => { rawDemoCommentMap [ demoPath ] = parseRawComment ( fs . readFileSync ( demoPath , 'utf-8' ) ) ; } ) ; const moduleInfoMap = { } ; for ( const { name : chunkName , entry } of chunkInfoList ) { if ( ! moduleExportMap [ entry ] ) { continue ; } const demoCommentMap = { } ; Object . entries ( rawDemoCommentMap ) . forEach ( ( [ demoPath , commentList ] ) => { demoCommentMap [ demoPath ] = commentList . map ( ( comment ) => { if ( comment . memberof ) { comment . memberOf = comment . memberof ; } return parseComment ( { comment , targetLanguage : chunkName , } ) ; } ) ; } ) ; moduleInfoMap [ chunkName ] = moduleExportMap [ entry ] . map ( ( { name , moduleFilePath } ) => { if ( this . paths . doc . indexOf ( moduleFilePath ) > - 1 ) { return { name , info : { name : getTitleOfMarkdown ( moduleFilePath ) , } , isDoc : true , } ; } if ( this . paths . demo . indexOf ( moduleFilePath ) > - 1 ) { const demoList = moduleExportMap [ moduleFilePath ] ; if ( demoList ) { const commentList = demoCommentMap [ moduleFilePath ] ; const componentInfo = commentList ? commentList [ 0 ] : { } ; const demoInfoList = commentList ? commentList . slice ( - demoList . length ) : [ ] ; componentInfo . umd = tryGetUMDInfo ( moduleFilePath ) ; return { name , info : componentInfo , children : demoList . map ( ( { name , rawCode } , index ) => { return { name , rawCode , info : demoInfoList [ index ] || { } , } ; } ) , } ; } } } ) . filter ( ( info ) => info ) ; } return moduleInfoMap ; } apply ( compiler ) { const { globs } = this . options ; const paths = { doc : globs . doc ? glob . sync ( globs . doc ) . map ( ( item ) => path . resolve ( item ) ) : [ ] , demo : globs . demo ? glob . sync ( globs . demo ) . map ( ( item ) => path . resolve ( item ) ) : [ ] , } ; let moduleExportMap = { } ; let hasInjectedModuleInfo = false ; const chunkInfoList = [ ] ; const getNewSource = ( source , moduleInfo ) : string => { return source . replace ( new RegExp ( PLACEHOLDER_ARCO_SITE_MODULE_INFO , 'g' ) , encodeInfo ( moduleInfo ) ) ; } ; compiler . hooks . compilation . tap ( 'ArcoSiteModuleInfoPlugin' , ( compilation ) => { compilation . hooks . optimizeModules . tap ( 'ArcoSiteModuleInfoPlugin' , ( ) => { const chunkList = [ ... compilation . chunks ] ; chunkList . forEach ( ( chunk ) => { if ( compilation . chunkGraph !== undefined ) { for ( const module of compilation . chunkGraph . getChunkEntryModulesIterable ( chunk ) ) { if ( module . resource . indexOf ( '/node_modules/' ) === - 1 ) { chunkInfoList . push ( { name : chunk . name , entry : module . resource , } ) ; } } } else { const { name , entryModule } = chunk ; chunkInfoList . push ( { name , entry : entryModule . resource , } ) ; } } ) ; moduleExportMap = this . parseModuleExport ( { context : compiler . context , statsModules : compilation . getStats ( ) . toJson ( { source : true , providedExports : true , } ) . modules , filePathList : paths . demo . concat ( chunkInfoList . map ( ( { entry } ) => entry ) ) , needRawCode : true , } ) ; } ) ; if ( compilation . hooks . processAssets ) { compilation . hooks . processAssets . tap ( 'ArcoSiteModuleInfoPlugin' , ( assets ) => { const moduleInfoMap = this . getModuleInfoMap ( { moduleExportMap , chunkInfoList , } ) ; for ( const filename in assets ) { if ( assets . hasOwnProperty ( filename ) && filename . endsWith ( '.js' ) ) { const chunkName = Object . keys ( compiler . options . entry ) . find ( ( chunkName ) => compiler . options . output . filename . replace ( '.[name].' , `.${ chunkName }.` ) === filename ) ; if ( moduleInfoMap [ chunkName ] ) { compilation . updateAsset ( filename , new RawSource ( getNewSource ( assets [ filename ] . source ( ) , moduleInfoMap [ chunkName ] ) ) ) ; } } } hasInjectedModuleInfo = true ; } ) ; } } ) ; compiler . hooks . emit . tapPromise ( 'ArcoSiteModuleInfoPlugin' , async ( compilation ) => { if ( hasInjectedModuleInfo ) { return ; } compilation . chunks . forEach ( ( { name , files } ) => { const info = chunkInfoList . find ( ( i ) => i . name === name ) ; if ( info ) { info . files = files ; } } ) ; const moduleInfoMap = this . getModuleInfoMap ( { chunkInfoList , moduleExportMap , } ) ; chunkInfoList . forEach ( ( { name : chunkName , files } ) => { files && files . forEach ( ( file ) => { if ( moduleInfoMap [ chunkName ] ) { const asset = compilation . assets [ file ] ; const source = asset . source ( ) ; asset . source = ( ) => getNewSource ( source , moduleInfoMap [ chunkName ] ) ; } } ) ; } ) ; } ) ; } } module . exports = ArcoSiteModuleInfoPlugin ;