Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Aug 7, 2021. It is now read-only.

Commit fe43c90

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

13 files changed

+332
-23
lines changed

‎bundle-config-loader.js‎

Lines changed: 11 additions & 0 deletions
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}

‎hot-loader-helper.js‎

Lines changed: 11 additions & 0 deletions
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+

‎hot.js‎

Lines changed: 140 additions & 0 deletions
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+

‎lib/before-prepareJS.js‎

Lines changed: 2 additions & 1 deletion
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 = {

‎lib/before-watch.js‎

Lines changed: 19 additions & 18 deletions
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
}

‎markup-hot-loader.js‎

Lines changed: 5 additions & 0 deletions
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+
};

‎page-hot-loader.js‎

Lines changed: 5 additions & 0 deletions
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+
};

‎plugins/WatchStateLoggerPlugin.ts‎

Lines changed: 85 additions & 2 deletions
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
}

‎style-hot-loader.js‎

Lines changed: 5 additions & 0 deletions
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+
};

‎templates/webpack.angular.js‎

Lines changed: 5 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /