flow import { join } from 'path' ; import { pathToFileURL } from 'url' ; import asyncLib from 'neo-async' ; import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency' ; import NullDependency from 'webpack/lib/dependencies/NullDependency' ; import AsyncDependenciesBlock from 'webpack/lib/AsyncDependenciesBlock' ; import Template from 'webpack/lib/Template' ; class ClientReferenceDependency extends ModuleDependency { constructor ( request ) { super ( request ) ; } get type ( ) { return 'client-reference' ; } } const clientFileName = join ( __dirname , '../node_modules/react-server-dom-webpack/index.js' ) ; type ClientReferenceSearchPath = { directory : string ; recursive ? : boolean ; include : RegExp ; exclude ? : RegExp ; } ; type ClientReferencePath = string | ClientReferenceSearchPath ; type Options = { isServer : boolean ; clientReferences ? : ClientReferencePath | ReadonlyArray < ClientReferencePath > ; chunkName ? : string ; manifestFilename ? : string ; } ; const PLUGIN_NAME = 'React Server Plugin' ; export default class ReactFlightWebpackPlugin { clientReferences : ReadonlyArray < ClientReferencePath > ; chunkName : string ; manifestFilename : string ; constructor ( options ) { if ( ! options || typeof options . isServer !== 'boolean' ) { throw new Error ( PLUGIN_NAME + ': You must specify the isServer option as a boolean.' ) ; } if ( options . isServer ) { throw new Error ( 'TODO: Implement the server compiler.' ) ; } if ( ! options . clientReferences ) { this . clientReferences = [ { directory : '.' , recursive : true , include : /\.client\.(js|ts|jsx|tsx)$/ , } , ] ; } else if ( typeof options . clientReferences === 'string' || ! Array . isArray ( options . clientReferences ) ) { this . clientReferences = options . clientReferences ; } else { this . clientReferences = options . clientReferences ; } if ( typeof options . chunkName === 'string' ) { this . chunkName = options . chunkName ; if ( ! /\[(index|request)\]/ . test ( this . chunkName ) ) { this . chunkName += '[index]' ; } } else { this . chunkName = 'client[index]' ; } this . manifestFilename = options . manifestFilename || 'react-client-manifest.json' ; } apply ( compiler ) { let resolvedClientReferences ; const run = ( params , callback ) => { const contextResolver = compiler . resolverFactory . get ( 'context' , { } ) ; this . resolveAllClientFiles ( compiler . context , contextResolver , compiler . inputFileSystem , compiler . createContextModuleFactory ( ) , ( err , resolvedClientRefs ) => { if ( err ) { callback ( err ) ; return ; } resolvedClientReferences = resolvedClientRefs ; callback ( ) ; } ) ; } ; compiler . hooks . run . tapAsync ( PLUGIN_NAME , run ) ; compiler . hooks . watchRun . tapAsync ( PLUGIN_NAME , run ) ; compiler . hooks . compilation . tap ( PLUGIN_NAME , ( compilation , { normalModuleFactory } ) => { compilation . dependencyFactories . set ( ClientReferenceDependency , normalModuleFactory ) ; compilation . dependencyTemplates . set ( ClientReferenceDependency , new NullDependency . Template ( ) ) ; compilation . hooks . buildModule . tap ( PLUGIN_NAME , ( module ) => { if ( module . resource !== clientFileName ) { return ; } if ( resolvedClientReferences ) { for ( let i = 0 ; i < resolvedClientReferences . length ; i ++ ) { const dep = resolvedClientReferences [ i ] ; const chunkName = this . chunkName . replace ( /\[index\]/g , '' + i ) . replace ( /\[request\]/g , Template . toPath ( dep . userRequest ) ) ; const block = new AsyncDependenciesBlock ( { name : chunkName , } , module , null , dep . require ) ; block . addDependency ( dep ) ; module . addBlock ( block ) ; } } } ) ; } ) ; compiler . hooks . emit . tap ( PLUGIN_NAME , ( compilation ) => { const json = { } ; compilation . chunkGroups . forEach ( ( chunkGroup ) => { const chunkIds = chunkGroup . chunks . map ( ( c ) => c . id ) ; function recordModule ( id , mod ) { if ( ! /\.client\.(js|jsx|ts|tsx)$/ . test ( mod . resource ) ) { return ; } const moduleExports = { } ; [ '' , '*' ] . concat ( mod . buildMeta . providedExports ) . forEach ( ( name ) => { moduleExports [ name ] = { id : id , chunks : chunkIds , name : name , } ; } ) ; const href = pathToFileURL ( mod . resource ) . href ; if ( href !== undefined ) { json [ href ] = moduleExports ; } } chunkGroup . chunks . forEach ( ( chunk ) => { chunk . getModules ( ) . forEach ( ( mod ) => { recordModule ( mod . id , mod ) ; if ( mod . modules ) { mod . modules . forEach ( ( concatenatedMod ) => { recordModule ( mod . id , concatenatedMod ) ; } ) ; } } ) ; } ) ; } ) ; const output = JSON . stringify ( json , null , 2 ) ; compilation . assets [ this . manifestFilename ] = { source ( ) { return output ; } , size ( ) { return output . length ; } , } ; } ) ; } resolveAllClientFiles ( context , contextResolver , fs , contextModuleFactory , callback ) { asyncLib . map ( this . clientReferences , ( clientReferencePath , cb ) : void => { if ( typeof clientReferencePath === 'string' ) { cb ( null , [ new ClientReferenceDependency ( clientReferencePath ) ] ) ; return ; } const clientReferenceSearch = clientReferencePath ; contextResolver . resolve ( { } , context , clientReferencePath . directory , { } , ( err , resolvedDirectory ) => { if ( err ) return cb ( err ) ; const options = { resource : resolvedDirectory , resourceQuery : '' , recursive : clientReferenceSearch . recursive === undefined ? true : clientReferenceSearch . recursive , regExp : clientReferenceSearch . include , include : undefined , exclude : clientReferenceSearch . exclude , } ; contextModuleFactory . resolveDependencies ( fs , options , ( err2 , deps ) => { if ( err2 ) return cb ( err2 ) ; const clientRefDeps = deps . map ( ( dep ) => { const request = join ( resolvedDirectory , dep . request ) ; const clientRefDep = new ClientReferenceDependency ( request ) ; clientRefDep . userRequest = dep . userRequest ; return clientRefDep ; } ) ; cb ( null , clientRefDeps ) ; } ) ; } ) ; } , ( err , result ) : void => { if ( err ) return callback ( err ) ; const flat = [ ] ; for ( let i = 0 ; i < result . length ; i ++ ) { flat . push . apply ( flat , result [ i ] ) ; } callback ( null , flat ) ; } ) ; } }