Skip to content
This repository was archived by the owner on Aug 7, 2021. It is now read-only.

Commit a4ac32b

Browse files
committed
feat: add initial HMR support for plain JS/TS apps (#645)
1 parent bbc335d commit a4ac32b

13 files changed

+332
-23
lines changed

Diff for: bundle-config-loader.js

+11
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@ module.exports = function (source) {
88
`;
99

1010
if (!angular && registerModules) {
11+
const hmr = `
12+
if (module.hot) {
13+
global.__hmrLivesyncBackup = global.__onLiveSync;
14+
global.__onLiveSync = function () {
15+
console.log("LiveSyncing...");
16+
require("nativescript-dev-webpack/hot")("", {});
17+
};
18+
}
19+
`;
20+
1121
source = `
22+
${hmr}
1223
const context = require.context("~/", true, ${registerModules});
1324
global.registerWebpackModules(context);
1425
${source}

Diff for: hot-loader-helper.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports.reload = `
2+
if (module.hot) {
3+
module.hot.accept();
4+
module.hot.dispose(() => {
5+
setTimeout(() => {
6+
global.__hmrLivesyncBackup();
7+
});
8+
})
9+
}
10+
`;
11+

Diff for: hot.js

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
const log = console;
2+
const refresh = 'Please refresh the page.';
3+
const hotOptions = {
4+
ignoreUnaccepted: true,
5+
ignoreDeclined: true,
6+
ignoreErrored: true,
7+
onUnaccepted(data) {
8+
const chain = [].concat(data.chain);
9+
const last = chain[chain.length - 1];
10+
11+
if (last === 0) {
12+
chain.pop();
13+
}
14+
15+
log.warn(`Ignored an update to unaccepted module ${chain.join(' ➭ ')}`);
16+
},
17+
onDeclined(data) {
18+
log.warn(`Ignored an update to declined module ${data.chain.join(' ➭ ')}`);
19+
},
20+
onErrored(data) {
21+
log.warn(
22+
`Ignored an error while updating module ${data.moduleId} <${data.type}>`
23+
);
24+
log.warn(data.error);
25+
},
26+
};
27+
28+
let lastHash;
29+
30+
function upToDate() {
31+
return lastHash.indexOf(__webpack_hash__) >= 0;
32+
}
33+
34+
function result(modules, appliedModules) {
35+
const unaccepted = modules.filter(
36+
(moduleId) => appliedModules && appliedModules.indexOf(moduleId) < 0
37+
);
38+
39+
if (unaccepted.length > 0) {
40+
let message = 'The following modules could not be updated:';
41+
42+
for (const moduleId of unaccepted) {
43+
message += `\n ⦻ ${moduleId}`;
44+
}
45+
log.warn(message);
46+
}
47+
48+
if (!(appliedModules || []).length) {
49+
console.info('No Modules Updated.');
50+
} else {
51+
const message = ['The following modules were updated:'];
52+
53+
for (const moduleId of appliedModules) {
54+
message.push(` ↻ ${moduleId}`);
55+
}
56+
57+
console.info(message.join('\n'));
58+
59+
const numberIds = appliedModules.every(
60+
(moduleId) => typeof moduleId === 'number'
61+
);
62+
if (numberIds) {
63+
console.info(
64+
'Please consider using the NamedModulesPlugin for module names.'
65+
);
66+
}
67+
}
68+
}
69+
70+
function check(options) {
71+
module.hot
72+
.check()
73+
.then((modules) => {
74+
if (!modules) {
75+
log.warn(
76+
`Cannot find update. The server may have been restarted. ${refresh}`
77+
);
78+
return null;
79+
}
80+
81+
return module.hot
82+
.apply(hotOptions)
83+
.then((appliedModules) => {
84+
if (!upToDate()) {
85+
log.warn("Hashes don't match. Ignoring second update...");
86+
// check(options);
87+
}
88+
89+
result(modules, appliedModules);
90+
91+
if (upToDate()) {
92+
console.info('App is up to date.');
93+
}
94+
})
95+
.catch((err) => {
96+
const status = module.hot.status();
97+
if (['abort', 'fail'].indexOf(status) >= 0) {
98+
log.warn(`Cannot apply update. ${refresh}`);
99+
log.warn(err.stack || err.message);
100+
if (options.reload) {
101+
window.location.reload();
102+
}
103+
} else {
104+
log.warn(`Update failed: ${err.stack}` || err.message);
105+
}
106+
});
107+
})
108+
.catch((err) => {
109+
const status = module.hot.status();
110+
if (['abort', 'fail'].indexOf(status) >= 0) {
111+
log.warn(`Cannot check for update. ${refresh}`);
112+
log.warn(err.stack || err.message);
113+
} else {
114+
log.warn(`Update check failed: ${err.stack}` || err.message);
115+
}
116+
});
117+
}
118+
119+
if (module.hot) {
120+
console.info('Hot Module Replacement Enabled. Waiting for signal.');
121+
} else {
122+
console.error('Hot Module Replacement is disabled.');
123+
}
124+
125+
module.exports = function update(currentHash, options) {
126+
lastHash = currentHash;
127+
if (!upToDate()) {
128+
const status = module.hot.status();
129+
130+
if (status === 'idle') {
131+
console.info('Checking for updates to the bundle.');
132+
check(options);
133+
} else if (['abort', 'fail'].indexOf(status) >= 0) {
134+
log.warn(
135+
`Cannot apply update. A previous update ${status}ed. ${refresh}`
136+
);
137+
}
138+
}
139+
};
140+

Diff for: lib/before-prepareJS.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const { runWebpackCompiler } = require("./compiler");
22

3-
module.exports = function ($logger, $liveSyncService, hookArgs) {
3+
module.exports = function ($logger, $liveSyncService, $options, hookArgs) {
44
const env = hookArgs.config.env || {};
5+
env.hmr = !!$options.hmr;
56
const platform = hookArgs.config.platform;
67
const appFilesUpdaterOptions = hookArgs.config.appFilesUpdaterOptions;
78
const config = {

Diff for: lib/before-watch.js

+19-18
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
const { runWebpackCompiler } = require("./compiler");
22

3-
module.exports = function ($logger, $liveSyncService, hookArgs) {
4-
if (hookArgs.config) {
5-
const appFilesUpdaterOptions = hookArgs.config.appFilesUpdaterOptions;
6-
if (appFilesUpdaterOptions.bundle) {
7-
const platforms = hookArgs.config.platforms;
8-
return Promise.all(platforms.map(platform => {
9-
const env = hookArgs.config.env || {};
10-
const config = {
11-
env,
12-
platform,
13-
bundle: appFilesUpdaterOptions.bundle,
14-
release: appFilesUpdaterOptions.release,
15-
watch: true
16-
};
3+
module.exports = function ($logger, $liveSyncService, $options, hookArgs) {
4+
if (hookArgs.config) {
5+
const appFilesUpdaterOptions = hookArgs.config.appFilesUpdaterOptions;
6+
if (appFilesUpdaterOptions.bundle) {
7+
const platforms = hookArgs.config.platforms;
8+
return Promise.all(platforms.map(platform => {
9+
const env = hookArgs.config.env || {};
10+
env.hmr = !!$options.hmr;
11+
const config = {
12+
env,
13+
platform,
14+
bundle: appFilesUpdaterOptions.bundle,
15+
release: appFilesUpdaterOptions.release,
16+
watch: true
17+
};
1718

18-
return runWebpackCompiler(config, hookArgs.projectData, $logger, $liveSyncService, hookArgs);
19-
}));
20-
}
21-
}
19+
return runWebpackCompiler(config, hookArgs.projectData, $logger, $liveSyncService, hookArgs);
20+
}));
21+
}
22+
}
2223
}

Diff for: markup-hot-loader.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { reload } = require("./hot-loader-helper");
2+
3+
module.exports = function (source) {
4+
return `${source};${reload}`;
5+
};

Diff for: page-hot-loader.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { reload } = require("./hot-loader-helper");
2+
3+
module.exports = function (source) {
4+
return `${source};${reload}`;
5+
};

Diff for: plugins/WatchStateLoggerPlugin.ts

+85-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { join } from "path";
2+
import { writeFileSync, readFileSync } from "fs";
23

34
export 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 = /^(.+)\.(.+)\.hot-update/gm;
119+
const matches = matcher.exec(name);
120+
121+
return {
122+
name: matches[1] || "",
123+
hash: matches[2] || "",
124+
};
125+
}
43126
}

Diff for: style-hot-loader.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { reload } = require("./hot-loader-helper");
2+
3+
module.exports = function (source) {
4+
return `${source};${reload}`;
5+
};

Diff for: templates/webpack.angular.js

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ module.exports = env => {
4242
uglify, // --env.uglify
4343
report, // --env.report
4444
sourceMap, // --env.sourceMap
45+
hmr, // --env.hmr
4546
} = env;
4647

4748
const appFullPath = resolve(projectRoot, appPath);
@@ -265,5 +266,9 @@ module.exports = env => {
265266
}));
266267
}
267268

269+
if (hmr) {
270+
config.plugins.push(new webpack.HotModuleReplacementPlugin());
271+
}
272+
268273
return config;
269274
};

0 commit comments

Comments
 (0)