88 */
99
1010import { mkdirSync , writeFileSync } from 'fs' ;
11- import { dirname , resolve } from 'path' ;
11+ import { dirname , resolve , join } from 'path' ;
1212import { pathToFileURL } from 'url' ;
1313
14+ import asyncLib from 'neo-async' ;
15+
16+ import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency' ;
17+ import NullDependency from 'webpack/lib/dependencies/NullDependency' ;
18+ import AsyncDependenciesBlock from 'webpack/lib/AsyncDependenciesBlock' ;
19+ import Template from 'webpack/lib/Template' ;
20+
21+ class ClientReferenceDependency extends ModuleDependency {
22+ constructor ( request ) {
23+ super ( request ) ;
24+ }
25+
26+ get type ( ) {
27+ return 'client-reference' ;
28+ }
29+ }
30+
31+ // This is the module that will be used to anchor all client references to.
32+ // I.e. it will have all the client files as async deps from this point on.
33+ // We use the Flight client implementation because you can't get to these
34+ // without the client runtime so it's the first time in the loading sequence
35+ // you might want them.
36+ const clientFileName = require . resolve ( '../' ) ;
37+
38+ type ClientReferenceSearchPath = {
39+ directory : string ,
40+ recursive ?: boolean ,
41+ include : RegExp ,
42+ exclude ?: RegExp ,
43+ } ;
44+
45+ type ClientReferencePath = string | ClientReferenceSearchPath ;
46+
47+ type Options = {
48+ isServer : boolean ,
49+ clientReferences ?: ClientReferencePath | $ReadOnlyArray < ClientReferencePath > ,
50+ chunkName ?: string ,
51+ } ;
52+
53+ const PLUGIN_NAME = 'React Transport Plugin' ;
54+
1455export default class ReactFlightWebpackPlugin {
15- constructor ( options : { isServer : boolean } ) { }
56+ clientReferences : $ReadOnlyArray < ClientReferencePath > ;
57+ chunkName: string;
58+ constructor(options: Options) {
59+ if ( ! options || typeof options . isServer !== 'boolean' ) {
60+ throw new Error (
61+ PLUGIN_NAME + ': You must specify the isServer option as a boolean.' ,
62+ ) ;
63+ }
64+ if (options.isServer) {
65+ throw new Error ( 'TODO: Implement the server compiler.' ) ;
66+ }
67+ if (!options.clientReferences) {
68+ this . clientReferences = [
69+ {
70+ directory : '.' ,
71+ recursive : true ,
72+ include : / \. c l i e n t \. ( j s | t s | j s x | t s x ) $ / ,
73+ } ,
74+ ] ;
75+ } else if (
76+ typeof options.clientReferences === 'string' ||
77+ !Array.isArray(options.clientReferences)
78+ ) {
79+ this . clientReferences = [ ( options . clientReferences : $FlowFixMe ) ] ;
80+ } else {
81+ this . clientReferences = options . clientReferences ;
82+ }
83+ if (typeof options.chunkName === 'string') {
84+ this . chunkName = options . chunkName ;
85+ if ( ! / \[ ( i n d e x | r e q u e s t ) \] / . test ( this . chunkName ) ) {
86+ this . chunkName += '[index]' ;
87+ }
88+ } else {
89+ this . chunkName = 'client[index]' ;
90+ }
91+ }
1692
1793 apply ( compiler : any ) {
18- compiler . hooks . emit . tap ( 'React Transport Plugin' , compilation => {
94+ const run = ( params , callback ) => {
95+ // First we need to find all client files on the file system. We do this early so
96+ // that we have them synchronously available later when we need them. This might
97+ // not be needed anymore since we no longer need to compile the module itself in
98+ // a special way. So it's probably better to do this lazily and in parallel with
99+ // other compilation.
100+ const contextResolver = compiler . resolverFactory . get ( 'context' , { } ) ;
101+ this . resolveAllClientFiles (
102+ compiler . context ,
103+ contextResolver ,
104+ compiler . inputFileSystem ,
105+ compiler . createContextModuleFactory ( ) ,
106+ ( err , resolvedClientReferences ) => {
107+ if ( err ) {
108+ callback ( err ) ;
109+ return ;
110+ }
111+ compiler . hooks . compilation . tap (
112+ PLUGIN_NAME ,
113+ ( compilation , { normalModuleFactory} ) => {
114+ compilation . dependencyFactories . set (
115+ ClientReferenceDependency ,
116+ normalModuleFactory ,
117+ ) ;
118+ compilation . dependencyTemplates . set (
119+ ClientReferenceDependency ,
120+ new NullDependency . Template ( ) ,
121+ ) ;
122+
123+ compilation . hooks . buildModule . tap ( PLUGIN_NAME , module => {
124+ // We need to add all client references as dependency of something in the graph so
125+ // Webpack knows which entries need to know about the relevant chunks and include the
126+ // map in their runtime. The things that actually resolves the dependency is the Flight
127+ // client runtime. So we add them as a dependency of the Flight client runtime.
128+ // Anything that imports the runtime will be made aware of these chunks.
129+ // TODO: Warn if we don't find this file anywhere in the compilation.
130+ if ( module . resource !== clientFileName ) {
131+ return ;
132+ }
133+ if ( resolvedClientReferences ) {
134+ for ( let i = 0 ; i < resolvedClientReferences . length ; i ++ ) {
135+ const dep = resolvedClientReferences [ i ] ;
136+ const chunkName = this . chunkName
137+ . replace ( / \[ i n d e x \] / g, '' + i )
138+ . replace (
139+ / \[ r e q u e s t \] / g,
140+ Template . toPath ( dep . userRequest ) ,
141+ ) ;
142+
143+ const block = new AsyncDependenciesBlock (
144+ {
145+ name : chunkName ,
146+ } ,
147+ module ,
148+ null ,
149+ dep . require ,
150+ ) ;
151+ block . addDependency ( dep ) ;
152+ module . addBlock ( block ) ;
153+ }
154+ }
155+ } ) ;
156+ } ,
157+ ) ;
158+
159+ callback ( ) ;
160+ } ,
161+ ) ;
162+ } ;
163+
164+ compiler . hooks . run . tapAsync ( PLUGIN_NAME , run ) ;
165+ compiler . hooks . watchRun . tapAsync ( PLUGIN_NAME , run ) ;
166+
167+ compiler . hooks . emit . tap ( PLUGIN_NAME , compilation => {
19168 const json = { } ;
20169 compilation . chunks . forEach ( chunk => {
21170 chunk . getModules ( ) . forEach ( mod => {
171+ // TOOD: Hook into deps instead of the target module.
172+ // That way we know by the type of dep whether to include.
173+ // It also resolves conflicts when the same module is in multiple chunks.
22174 if ( ! / \. c l i e n t \. j s $ / . test ( mod . resource ) ) {
23175 return ;
24176 }
@@ -42,7 +194,83 @@ export default class ReactFlightWebpackPlugin {
42194 'react-transport-manifest.json' ,
43195 ) ;
44196 mkdirSync ( dirname ( filename ) , { recursive : true } ) ;
197+ // TODO: Use webpack's emit API and read from the devserver.
45198 writeFileSync ( filename , output ) ;
46199 } ) ;
47200 }
201+
202+ // This attempts to replicate the dynamic file path resolution used for other wildcard
203+ // resolution in Webpack is using.
204+ resolveAllClientFiles(
205+ context: string,
206+ contextResolver: any,
207+ fs: any,
208+ contextModuleFactory: any,
209+ callback: (
210+ err: null | Error,
211+ result?: $ReadOnlyArray< ClientReferenceDependency > ,
212+ ) => void ,
213+ ) {
214+ asyncLib . map (
215+ this . clientReferences ,
216+ (
217+ clientReferencePath : string | ClientReferenceSearchPath ,
218+ cb : (
219+ err : null | Error ,
220+ result ?: $ReadOnlyArray < ClientReferenceDependency > ,
221+ ) => void ,
222+ ) : void => {
223+ if ( typeof clientReferencePath === 'string' ) {
224+ cb ( null , [ new ClientReferenceDependency ( clientReferencePath ) ] ) ;
225+ return ;
226+ }
227+ const clientReferenceSearch : ClientReferenceSearchPath = clientReferencePath ;
228+ contextResolver . resolve (
229+ { } ,
230+ context ,
231+ clientReferencePath . directory ,
232+ { } ,
233+ ( err , resolvedDirectory ) => {
234+ if ( err ) return cb ( err ) ;
235+ const options = {
236+ resource : resolvedDirectory ,
237+ resourceQuery : '' ,
238+ recursive :
239+ clientReferenceSearch . recursive === undefined
240+ ? true
241+ : clientReferenceSearch . recursive ,
242+ regExp : clientReferenceSearch . include ,
243+ include : undefined ,
244+ exclude : clientReferenceSearch . exclude ,
245+ } ;
246+ contextModuleFactory . resolveDependencies (
247+ fs ,
248+ options ,
249+ ( err2 : null | Error , deps : Array < ModuleDependency > ) => {
250+ if ( err2 ) return cb ( err2 ) ;
251+ const clientRefDeps = deps . map ( dep => {
252+ const request = join ( resolvedDirectory , dep . request ) ;
253+ const clientRefDep = new ClientReferenceDependency ( request ) ;
254+ clientRefDep . userRequest = dep . userRequest ;
255+ return clientRefDep ;
256+ } ) ;
257+ cb ( null , clientRefDeps ) ;
258+ } ,
259+ ) ;
260+ } ,
261+ ) ;
262+ } ,
263+ (
264+ err : null | Error ,
265+ result : $ReadOnlyArray < $ReadOnlyArray < ClientReferenceDependency >> ,
266+ ) : void => {
267+ if ( err ) return callback ( err ) ;
268+ const flat = [ ] ;
269+ for ( let i = 0 ; i < result . length ; i ++ ) {
270+ flat . push . apply ( flat , result [ i ] ) ;
271+ }
272+ callback(null, flat);
273+ } ,
274+ ) ;
275+ }
48276}
0 commit comments