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

Commit

Permalink
feat: add initial HMR support for plain JS/TS apps (#645)
Browse files Browse the repository at this point in the history
  • Loading branch information
sis0k0 committed Sep 28, 2018
1 parent bbc335d commit a4ac32b
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 23 deletions.
11 changes: 11 additions & 0 deletions bundle-config-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@ module.exports = function (source) {
`;

if (!angular && registerModules) {
const hmr = `
if (module.hot) {
global.__hmrLivesyncBackup = global.__onLiveSync;
global.__onLiveSync = function () {
console.log("LiveSyncing...");
require("nativescript-dev-webpack/hot")("", {});
};
}
`;

source = `
${hmr}
const context = require.context("~/", true, ${registerModules});
global.registerWebpackModules(context);
${source}
Expand Down
11 changes: 11 additions & 0 deletions hot-loader-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports.reload = `
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => {
setTimeout(() => {
global.__hmrLivesyncBackup();
});
})
}
`;

140 changes: 140 additions & 0 deletions hot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
const log = console;
const refresh = 'Please refresh the page.';
const hotOptions = {
ignoreUnaccepted: true,
ignoreDeclined: true,
ignoreErrored: true,
onUnaccepted(data) {
const chain = [].concat(data.chain);
const last = chain[chain.length - 1];

if (last === 0) {
chain.pop();
}

log.warn(`Ignored an update to unaccepted module ${chain.join(' ➭ ')}`);
},
onDeclined(data) {
log.warn(`Ignored an update to declined module ${data.chain.join(' ➭ ')}`);
},
onErrored(data) {
log.warn(
`Ignored an error while updating module ${data.moduleId} <${data.type}>`
);
log.warn(data.error);
},
};

let lastHash;

function upToDate() {
return lastHash.indexOf(__webpack_hash__) >= 0;
}

function result(modules, appliedModules) {
const unaccepted = modules.filter(
(moduleId) => appliedModules && appliedModules.indexOf(moduleId) < 0
);

if (unaccepted.length > 0) {
let message = 'The following modules could not be updated:';

for (const moduleId of unaccepted) {
message += `\n ⦻ ${moduleId}`;
}
log.warn(message);
}

if (!(appliedModules || []).length) {
console.info('No Modules Updated.');
} else {
const message = ['The following modules were updated:'];

for (const moduleId of appliedModules) {
message.push(` ↻ ${moduleId}`);
}

console.info(message.join('\n'));

const numberIds = appliedModules.every(
(moduleId) => typeof moduleId === 'number'
);
if (numberIds) {
console.info(
'Please consider using the NamedModulesPlugin for module names.'
);
}
}
}

function check(options) {
module.hot
.check()
.then((modules) => {
if (!modules) {
log.warn(
`Cannot find update. The server may have been restarted. ${refresh}`
);
return null;
}

return module.hot
.apply(hotOptions)
.then((appliedModules) => {
if (!upToDate()) {
log.warn("Hashes don't match. Ignoring second update...");
// check(options);
}

result(modules, appliedModules);

if (upToDate()) {
console.info('App is up to date.');
}
})
.catch((err) => {
const status = module.hot.status();
if (['abort', 'fail'].indexOf(status) >= 0) {
log.warn(`Cannot apply update. ${refresh}`);
log.warn(err.stack || err.message);
if (options.reload) {
window.location.reload();
}
} else {
log.warn(`Update failed: ${err.stack}` || err.message);
}
});
})
.catch((err) => {
const status = module.hot.status();
if (['abort', 'fail'].indexOf(status) >= 0) {
log.warn(`Cannot check for update. ${refresh}`);
log.warn(err.stack || err.message);
} else {
log.warn(`Update check failed: ${err.stack}` || err.message);
}
});
}

if (module.hot) {
console.info('Hot Module Replacement Enabled. Waiting for signal.');
} else {
console.error('Hot Module Replacement is disabled.');
}

module.exports = function update(currentHash, options) {
lastHash = currentHash;
if (!upToDate()) {
const status = module.hot.status();

if (status === 'idle') {
console.info('Checking for updates to the bundle.');
check(options);
} else if (['abort', 'fail'].indexOf(status) >= 0) {
log.warn(
`Cannot apply update. A previous update ${status}ed. ${refresh}`
);
}
}
};

3 changes: 2 additions & 1 deletion lib/before-prepareJS.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { runWebpackCompiler } = require("./compiler");

module.exports = function ($logger, $liveSyncService, hookArgs) {
module.exports = function ($logger, $liveSyncService, $options, hookArgs) {
const env = hookArgs.config.env || {};
env.hmr = !!$options.hmr;
const platform = hookArgs.config.platform;
const appFilesUpdaterOptions = hookArgs.config.appFilesUpdaterOptions;
const config = {
Expand Down
37 changes: 19 additions & 18 deletions lib/before-watch.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
const { runWebpackCompiler } = require("./compiler");

module.exports = function ($logger, $liveSyncService, hookArgs) {
if (hookArgs.config) {
const appFilesUpdaterOptions = hookArgs.config.appFilesUpdaterOptions;
if (appFilesUpdaterOptions.bundle) {
const platforms = hookArgs.config.platforms;
return Promise.all(platforms.map(platform => {
const env = hookArgs.config.env || {};
const config = {
env,
platform,
bundle: appFilesUpdaterOptions.bundle,
release: appFilesUpdaterOptions.release,
watch: true
};
module.exports = function ($logger, $liveSyncService, $options, hookArgs) {
if (hookArgs.config) {
const appFilesUpdaterOptions = hookArgs.config.appFilesUpdaterOptions;
if (appFilesUpdaterOptions.bundle) {
const platforms = hookArgs.config.platforms;
return Promise.all(platforms.map(platform => {
const env = hookArgs.config.env || {};
env.hmr = !!$options.hmr;
const config = {
env,
platform,
bundle: appFilesUpdaterOptions.bundle,
release: appFilesUpdaterOptions.release,
watch: true
};

return runWebpackCompiler(config, hookArgs.projectData, $logger, $liveSyncService, hookArgs);
}));
}
}
return runWebpackCompiler(config, hookArgs.projectData, $logger, $liveSyncService, hookArgs);
}));
}
}
}
5 changes: 5 additions & 0 deletions markup-hot-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { reload } = require("./hot-loader-helper");

module.exports = function (source) {
return `${source};${reload}`;
};
5 changes: 5 additions & 0 deletions page-hot-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { reload } = require("./hot-loader-helper");

module.exports = function (source) {
return `${source};${reload}`;
};
87 changes: 85 additions & 2 deletions plugins/WatchStateLoggerPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { join } from "path";
import { writeFileSync, readFileSync } from "fs";

export enum messages {
compilationComplete = "Webpack compilation complete.",
Expand All @@ -24,6 +25,7 @@ export class WatchStateLoggerPlugin {
});
compiler.hooks.afterEmit.tapAsync("WatchStateLoggerPlugin", function (compilation, callback) {
callback();

if (plugin.isRunningWatching) {
console.log(messages.startWatching);
} else {
Expand All @@ -32,12 +34,93 @@ export class WatchStateLoggerPlugin {

const emittedFiles = Object
.keys(compilation.assets)
.filter(assetKey => compilation.assets[assetKey].emitted)
.filter(assetKey => compilation.assets[assetKey].emitted);

if (compilation.errors.length > 0) {
WatchStateLoggerPlugin.rewriteHotUpdateChunk(compiler, compilation, emittedFiles);
}

// provide fake paths to the {N} CLI - relative to the 'app' folder
// in order to trigger the livesync process
const emittedFilesFakePaths = emittedFiles
.map(file => join(compiler.context, file));

process.send && process.send(messages.compilationComplete, error => null);
// Send emitted files so they can be LiveSynced if need be
process.send && process.send({ emittedFiles }, error => null);
process.send && process.send({ emittedFiles: emittedFilesFakePaths }, error => null);
});
}

/**
* Rewrite an errored chunk to make the hot module replace successful.
* @param compiler the webpack compiler
* @param emittedFiles the emitted files from the current compilation
*/
private static rewriteHotUpdateChunk(compiler, compilation, emittedFiles: string[]) {
const chunk = this.findHotUpdateChunk(emittedFiles);
if (!chunk) {
return;
}

const { name } = this.parseHotUpdateChunkName(chunk);
if (!name) {
return;
}

const absolutePath = join(compiler.outputPath, chunk);

const newContent = this.getWebpackHotUpdateReplacementContent(compilation.errors, absolutePath, name);
writeFileSync(absolutePath, newContent);
}

private static findHotUpdateChunk(emittedFiles: string[]) {
return emittedFiles.find(file => file.endsWith("hot-update.js"));
}

/**
* Gets only the modules object after 'webpackHotUpdate("bundle",' in the chunk
*/
private static getModulesObjectFromChunk(chunkPath) {
let content = readFileSync(chunkPath, "utf8")
const startIndex = content.indexOf(",") + 1;
let endIndex = content.length - 1;
if(content.endsWith(';')) {
endIndex--;
}
return content.substring(startIndex, endIndex);
}

/**
* Gets the webpackHotUpdate call with updated modules not to include the ones with errors
*/
private static getWebpackHotUpdateReplacementContent(compilationErrors, filePath, moduleName) {
const errorModuleIds = compilationErrors.filter(x => x.module).map(x => x.module.id);
if (!errorModuleIds || errorModuleIds.length == 0) {
// could not determine error modiles so discard everything
return `webpackHotUpdate('${moduleName}', {});`;
}
const updatedModules = this.getModulesObjectFromChunk(filePath);

// 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
return `const filter = function(updatedModules, modules) {
modules.forEach(moduleId => delete updatedModules[moduleId]);
return updatedModules;
}
webpackHotUpdate('${moduleName}', filter(${updatedModules}, ${JSON.stringify(errorModuleIds)}));`;
}

/**
* Parse the filename of the hot update chunk.
* @param name bundle.deccb264c01d6d42416c.hot-update.js
* @returns { name: string, hash: string } { name: 'bundle', hash: 'deccb264c01d6d42416c' }
*/
private static parseHotUpdateChunkName(name) {
const matcher = /^(.+)\.(.+)\.hot-update/gm;
const matches = matcher.exec(name);

return {
name: matches[1] || "",
hash: matches[2] || "",
};
}
}
5 changes: 5 additions & 0 deletions style-hot-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { reload } = require("./hot-loader-helper");

module.exports = function (source) {
return `${source};${reload}`;
};
5 changes: 5 additions & 0 deletions templates/webpack.angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = env => {
uglify, // --env.uglify
report, // --env.report
sourceMap, // --env.sourceMap
hmr, // --env.hmr
} = env;

const appFullPath = resolve(projectRoot, appPath);
Expand Down Expand Up @@ -265,5 +266,9 @@ module.exports = env => {
}));
}

if (hmr) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}

return config;
};
Loading

0 comments on commit a4ac32b

Please sign in to comment.