11import { join } from "path" ;
2+ import { writeFileSync , readFileSync } from "fs" ;
23
34export enum messages {
45 compilationComplete = "Webpack compilation complete." ,
@@ -24,6 +25,7 @@ export class WatchStateLoggerPlugin {
2425 } ) ;
2526 compiler . hooks . afterEmit . tapAsync ( "WatchStateLoggerPlugin" , function ( compilation , callback ) {
2627 callback ( ) ;
28+
2729 if ( plugin . isRunningWatching ) {
2830 console . log ( messages . startWatching ) ;
2931 } else {
@@ -32,12 +34,93 @@ export class WatchStateLoggerPlugin {
3234
3335 const emittedFiles = Object
3436 . keys ( compilation . assets )
35- . filter ( assetKey => compilation . assets [ assetKey ] . emitted )
37+ . filter ( assetKey => compilation . assets [ assetKey ] . emitted ) ;
38+
39+ if ( compilation . errors . length > 0 ) {
40+ WatchStateLoggerPlugin . rewriteHotUpdateChunk ( compiler , compilation , emittedFiles ) ;
41+ }
42+
43+ // provide fake paths to the {N} CLI - relative to the 'app' folder
44+ // in order to trigger the livesync process
45+ const emittedFilesFakePaths = emittedFiles
3646 . map ( file => join ( compiler . context , file ) ) ;
3747
3848 process . send && process . send ( messages . compilationComplete , error => null ) ;
3949 // Send emitted files so they can be LiveSynced if need be
40- process . send && process . send ( { emittedFiles } , error => null ) ;
50+ process . send && process . send ( { emittedFiles : emittedFilesFakePaths } , error => null ) ;
4151 } ) ;
4252 }
53+
54+ /**
55+ * Rewrite an errored chunk to make the hot module replace successful.
56+ * @param compiler the webpack compiler
57+ * @param emittedFiles the emitted files from the current compilation
58+ */
59+ private static rewriteHotUpdateChunk ( compiler , compilation , emittedFiles : string [ ] ) {
60+ const chunk = this . findHotUpdateChunk ( emittedFiles ) ;
61+ if ( ! chunk ) {
62+ return ;
63+ }
64+
65+ const { name } = this . parseHotUpdateChunkName ( chunk ) ;
66+ if ( ! name ) {
67+ return ;
68+ }
69+
70+ const absolutePath = join ( compiler . outputPath , chunk ) ;
71+
72+ const newContent = this . getWebpackHotUpdateReplacementContent ( compilation . errors , absolutePath , name ) ;
73+ writeFileSync ( absolutePath , newContent ) ;
74+ }
75+
76+ private static findHotUpdateChunk ( emittedFiles : string [ ] ) {
77+ return emittedFiles . find ( file => file . endsWith ( "hot-update.js" ) ) ;
78+ }
79+
80+ /**
81+ * Gets only the modules object after 'webpackHotUpdate("bundle",' in the chunk
82+ */
83+ private static getModulesObjectFromChunk ( chunkPath ) {
84+ let content = readFileSync ( chunkPath , "utf8" )
85+ const startIndex = content . indexOf ( "," ) + 1 ;
86+ let endIndex = content . length - 1 ;
87+ if ( content . endsWith ( ';' ) ) {
88+ endIndex -- ;
89+ }
90+ return content . substring ( startIndex , endIndex ) ;
91+ }
92+
93+ /**
94+ * Gets the webpackHotUpdate call with updated modules not to include the ones with errors
95+ */
96+ private static getWebpackHotUpdateReplacementContent ( compilationErrors , filePath , moduleName ) {
97+ const errorModuleIds = compilationErrors . filter ( x => x . module ) . map ( x => x . module . id ) ;
98+ if ( ! errorModuleIds || errorModuleIds . length == 0 ) {
99+ // could not determine error modiles so discard everything
100+ return `webpackHotUpdate('${ moduleName } ', {});` ;
101+ }
102+ const updatedModules = this . getModulesObjectFromChunk ( filePath ) ;
103+
104+ // we need to filter the modules with a function in the file as it is a relaxed JSON not valid to be parsed and manipulated
105+ return `const filter = function(updatedModules, modules) {
106+ modules.forEach(moduleId => delete updatedModules[moduleId]);
107+ return updatedModules;
108+ }
109+ webpackHotUpdate('${ moduleName } ', filter(${ updatedModules } , ${ JSON . stringify ( errorModuleIds ) } ));` ;
110+ }
111+
112+ /**
113+ * Parse the filename of the hot update chunk.
114+ * @param name bundle.deccb264c01d6d42416c.hot-update.js
115+ * @returns { name: string, hash: string } { name: 'bundle', hash: 'deccb264c01d6d42416c' }
116+ */
117+ private static parseHotUpdateChunkName ( name ) {
118+ const matcher = / ^ ( .+ ) \. ( .+ ) \. h o t - u p d a t e / gm;
119+ const matches = matcher . exec ( name ) ;
120+
121+ return {
122+ name : matches [ 1 ] || "" ,
123+ hash : matches [ 2 ] || "" ,
124+ } ;
125+ }
43126}
0 commit comments