From a77d83ed6e5f1fc9e410cb9dd8236349bd2bd737 Mon Sep 17 00:00:00 2001 From: fatme Date: Thu, 26 Sep 2019 15:32:22 +0300 Subject: [PATCH] fix: handle correctly the compilation errors from webpack More info can be found here https://github.com/NativeScript/nativescript-dev-webpack/pull/1051. Rel to: https://github.com/NativeScript/nativescript-cli/issues/3785 --- .../webpack/webpack-compiler-service.ts | 25 +++++-- lib/services/webpack/webpack.d.ts | 1 + .../webpack/webpack-compiler-service.ts | 71 +++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 test/services/webpack/webpack-compiler-service.ts diff --git a/lib/services/webpack/webpack-compiler-service.ts b/lib/services/webpack/webpack-compiler-service.ts index 80457719b3..b0f88119bb 100644 --- a/lib/services/webpack/webpack-compiler-service.ts +++ b/lib/services/webpack/webpack-compiler-service.ts @@ -7,6 +7,7 @@ import { WEBPACK_COMPILATION_COMPLETE } from "../../constants"; export class WebpackCompilerService extends EventEmitter implements IWebpackCompilerService { private webpackProcesses: IDictionary = {}; + private expectedHash: string = null; constructor( private $childProcess: IChildProcess, @@ -38,13 +39,14 @@ export class WebpackCompilerService extends EventEmitter implements IWebpackComp if (message.emittedFiles) { if (isFirstWebpackWatchCompilation) { isFirstWebpackWatchCompilation = false; + this.expectedHash = message.hash; return; } let result; if (prepareData.hmr) { - result = this.getUpdatedEmittedFiles(message.emittedFiles, message.chunkFiles); + result = this.getUpdatedEmittedFiles(message.emittedFiles, message.chunkFiles, message.hash); } else { result = { emittedFiles: message.emittedFiles, fallbackFiles: [], hash: "" }; } @@ -228,11 +230,24 @@ export class WebpackCompilerService extends EventEmitter implements IWebpackComp return args; } - private getUpdatedEmittedFiles(allEmittedFiles: string[], chunkFiles: string[]) { - const hotHash = this.getCurrentHotUpdateHash(allEmittedFiles); - const emittedHotUpdateFiles = _.difference(allEmittedFiles, chunkFiles); + public getUpdatedEmittedFiles(allEmittedFiles: string[], chunkFiles: string[], nextHash: string) { + const currentHash = this.getCurrentHotUpdateHash(allEmittedFiles); - return { emittedFiles: emittedHotUpdateFiles, fallbackFiles: chunkFiles, hash: hotHash }; + // This logic is needed as there are already cases when webpack doesn't emit any files physically. + // We've set noEmitOnErrors in webpack.config.js based on noEmitOnError from tsconfig.json, + // so webpack doesn't emit any files when noEmitOnErrors: true is set in webpack.config.js and + // there is a compilation error in the source code. On the other side, hmr generates new hot-update files + // on every change and the hash of the next hmr update is written inside hot-update.json file. + // Although webpack doesn't emit any files, hmr hash is still generated. The hash is generated per compilation no matter + // if files will be emitted or not. This way, the first successful compilation after fixing the compilation error generates + // a hash that is not the same as the one expected in the latest emitted hot-update.json file. + // As a result, the hmr chain is broken and the changes are not applied. + const isHashValid = nextHash ? this.expectedHash === currentHash : true; + this.expectedHash = nextHash; + + const emittedHotUpdatesAndAssets = isHashValid ? _.difference(allEmittedFiles, chunkFiles) : allEmittedFiles; + + return { emittedFiles: emittedHotUpdatesAndAssets, fallbackFiles: chunkFiles, hash: currentHash }; } private getCurrentHotUpdateHash(emittedFiles: string[]) { diff --git a/lib/services/webpack/webpack.d.ts b/lib/services/webpack/webpack.d.ts index 9f1b68e977..3c283a3870 100644 --- a/lib/services/webpack/webpack.d.ts +++ b/lib/services/webpack/webpack.d.ts @@ -35,6 +35,7 @@ declare global { interface IWebpackEmitMessage { emittedFiles: string[]; chunkFiles: string[]; + hash: string; } interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectServiceBase { diff --git a/test/services/webpack/webpack-compiler-service.ts b/test/services/webpack/webpack-compiler-service.ts new file mode 100644 index 0000000000..ca845023f0 --- /dev/null +++ b/test/services/webpack/webpack-compiler-service.ts @@ -0,0 +1,71 @@ +import { Yok } from "../../../lib/common/yok"; +import { WebpackCompilerService } from "../../../lib/services/webpack/webpack-compiler-service"; +import { assert } from "chai"; + +const chunkFiles = ["bundle.js", "runtime.js", "vendor.js"]; + +function getAllEmittedFiles(hash: string) { + return ["bundle.js", "runtime.js", `bundle.${hash}.hot-update.js`, `${hash}.hot-update.json`]; +} + +function createTestInjector(): IInjector { + const testInjector = new Yok(); + testInjector.register("webpackCompilerService", WebpackCompilerService); + testInjector.register("childProcess", {}); + testInjector.register("hooksService", {}); + testInjector.register("hostInfo", {}); + testInjector.register("logger", {}); + testInjector.register("mobileHelper", {}); + testInjector.register("cleanupService", {}); + + return testInjector; +} + +describe("WebpackCompilerService", () => { + let testInjector: IInjector = null; + let webpackCompilerService: WebpackCompilerService = null; + + beforeEach(() => { + testInjector = createTestInjector(); + webpackCompilerService = testInjector.resolve(WebpackCompilerService); + }); + + describe("getUpdatedEmittedFiles", () => { + // backwards compatibility with old versions of nativescript-dev-webpack + it("should return only hot updates when nextHash is not provided", async () => { + const result = webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash1"), chunkFiles, null); + const expectedEmittedFiles = ['bundle.hash1.hot-update.js', 'hash1.hot-update.json']; + + assert.deepEqual(result.emittedFiles, expectedEmittedFiles); + }); + // 2 successful webpack compilations + it("should return only hot updates when nextHash is provided", async () => { + webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash1"), chunkFiles, "hash2"); + const result = webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash2"), chunkFiles, "hash3"); + + assert.deepEqual(result.emittedFiles, ['bundle.hash2.hot-update.js', 'hash2.hot-update.json']); + }); + // 1 successful webpack compilation, n compilations with no emitted files + it("should return all files when there is a webpack compilation with no emitted files", () => { + webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash1"), chunkFiles, "hash2"); + const result = webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash4"), chunkFiles, "hash5"); + + assert.deepEqual(result.emittedFiles, ['bundle.js', 'runtime.js', 'bundle.hash4.hot-update.js', 'hash4.hot-update.json']); + }); + // 1 successful webpack compilation, n compilations with no emitted files, 1 successful webpack compilation + it("should return only hot updates after fixing the compilation error", () => { + webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash1"), chunkFiles, "hash2"); + webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash5"), chunkFiles, "hash6"); + const result = webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash6"), chunkFiles, "hash7"); + + assert.deepEqual(result.emittedFiles, ['bundle.hash6.hot-update.js', 'hash6.hot-update.json']); + }); + // 1 webpack compilation with no emitted files + it("should return all files when first compilation on livesync change is not successful", () => { + (webpackCompilerService).expectedHash = "hash1"; + const result = webpackCompilerService.getUpdatedEmittedFiles(getAllEmittedFiles("hash1"), chunkFiles, "hash2"); + + assert.deepEqual(result.emittedFiles, ["bundle.hash1.hot-update.js", "hash1.hot-update.json"]); + }); + }); +});