Fixed hot reload for v4

This commit is contained in:
Patrick Steele-Idem 2016-12-08 11:52:21 -08:00
parent 6f7981d7aa
commit 6403cd549e
12 changed files with 168 additions and 108 deletions

View File

@ -1,19 +1,3 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var enabled = false;
var browserRefreshClient = require('browser-refresh-client');

View File

@ -1,26 +1,18 @@
require('raptor-polyfill/string/endsWith');
const extend = require('raptor-util/extend');
const nodePath = require('path');
const fs = require('fs');
var compiler;
var marko;
var runtime;
var widgets;
var modifiedId = 1;
var nextTemplateId = 0;
var runtime;
var HOT_RELOAD_KEY = Symbol('HOT_RELOAD');
/**
* Lazily require the Marko runtime because there is a circular dependency.
* We need to export our `enable` function before actually requiring the
* Marko runtime.
*/
function _getMarkoRuntime() {
return runtime || (runtime = require('../runtime/html'));
}
function tryReload(path, runtime) {
function tryReloadTemplate(path) {
try {
return marko.load(path);
} catch(e) {
@ -29,82 +21,101 @@ function tryReload(path, runtime) {
}
exports.enable = function() {
var runtime = _getMarkoRuntime();
if (runtime.__hotReloadEnabled) {
// Marko has already been monkey-patched. Nothing to do!
return;
}
var Template = runtime.Template;
runtime.__hotReloadEnabled = true;
// We set an environment variable so that _all_ marko modules
// installed in the project will have hot reload enabled.
process.env.MARKO_HOT_RELOAD = 'true';
// Patch the Template prototype to proxy all render methods...
Object.keys(Template.prototype).forEach(function(k) {
if (k === 'c') {
return;
}
var v = Template.prototype[k];
if (typeof v === 'function') {
var oldMethod = v;
Template.prototype[k] = function() {
if (this.__hotReloadModifiedId !== modifiedId) {
var path = this.__hotReloadPath;
if (path) {
// Reload the template
var template = tryReload(path, runtime);
if (template && template.__hotReloadTemplateId !== this.__hotReloadTemplateId) {
extend(this, template);
}
}
this.__hotReloadModifiedId = modifiedId;
}
return oldMethod.apply(this, arguments);
};
}
});
var oldCreateTemplate = runtime.c;
function patchMethods(obj, methodNames, reloadFunc) {
var hotReloadData = obj[HOT_RELOAD_KEY] || (obj[HOT_RELOAD_KEY] = {});
hotReloadData._modifiedId = modifiedId;
hotReloadData._latest = obj;
methodNames.forEach(function(methodName) {
hotReloadData[methodName] = obj[methodName];
obj[methodName] = function hotReloadWrapper() {
if (hotReloadData.modifiedId !== modifiedId) {
hotReloadData.modifiedId = modifiedId;
hotReloadData._latest = reloadFunc() || obj;
}
var latest = hotReloadData._latest;
return latest[HOT_RELOAD_KEY][methodName].apply(latest, arguments);
};
});
}
runtime.c = function hotReloadCreateTemplate(path) {
if (!path) {
throw new Error('Invalid path');
}
var originalTemplate = oldCreateTemplate.apply(runtime, arguments);
path = path.replace(/\.js$/, '');
var templatePath = path;
var actualRenderFunc;
if (typeof templatePath !== 'string') {
templatePath = path.path;
}
var firstSet = true;
if (typeof templatePath === 'string') {
templatePath = templatePath.replace(/\.js$/, '');
}
Object.defineProperty(originalTemplate, '_', {
configurable: true,
var template = oldCreateTemplate.apply(runtime, arguments);
get: function() {
return actualRenderFunc;
},
// Store the current last modified with the template
template.__hotReloadModifiedId = modifiedId;
set: function(renderFunc) {
actualRenderFunc = renderFunc;
if (firstSet) {
firstSet = false;
patchMethods(originalTemplate, ['_'], function reloadTemplate() {
var latestTemplate = tryReloadTemplate(path);
if (latestTemplate) {
if (latestTemplate !== originalTemplate) {
console.log('[marko/hot-reload] Reloaded template: ' + path);
originalTemplate.meta = latestTemplate.meta;
}
// Store the path of the loaded template so that we can reload it if
// necessary
template.__hotReloadPath = templatePath;
if (latestTemplate.template) {
// The template might export a component that has a template property.
return latestTemplate.template;
} else {
return latestTemplate;
}
}
});
}
}
});
// Assign a unique ID to the loaded template so that we can know if
// a new version of the template is loaded
template.__hotReloadTemplateId = nextTemplateId++;
return originalTemplate;
};
return template;
var oldCreateComponent = widgets.c;
widgets.c = function hotReloadCreateComponent(componentDef, template) {
var path = template.path;
path = path.replace(/\.js$/, '');
var originalComponent = oldCreateComponent.apply(runtime, arguments);
patchMethods(originalComponent, ['renderer', 'render', 'renderSync'], function reloadTemplate() {
var latestComponent = tryReloadTemplate(path);
if (latestComponent) {
if (latestComponent !== originalComponent) {
console.log('[marko/hot-reload] Reloaded template: ' + path);
}
return latestComponent;
}
});
return originalComponent;
};
};
@ -114,36 +125,39 @@ exports.handleFileModified = function(path) {
return;
}
var runtime = _getMarkoRuntime();
var basename = nodePath.basename(path);
if (path.endsWith('.marko') ||
path.endsWith('.marko.html') ||
path.endsWith('.marko.xml') ||
basename === 'marko-tag.json' ||
basename === 'marko.json') {
function handleFileModified() {
console.log('[marko/hot-reload] File modified: ' + path);
if (path.endsWith('.marko') || path.endsWith('.marko.html')) {
// Uncache just the modified template
delete require.cache[path];
delete require.cache[path + '.js'];
} else {
// If we taglib was modified then uncache *all* templates so that they will
// all be reloaded
Object.keys(require.cache).forEach((filename) => {
if (filename.endsWith('.marko') || filename.endsWith('.marko.js')) {
delete require.cache[filename];
}
});
}
runtime.cache = {};
compiler.clearCaches();
modifiedId++;
}
if (basename === 'marko-tag.json' || basename === 'marko.json') {
handleFileModified();
// If we taglib was modified then uncache *all* templates so that they will
// all be reloaded
Object.keys(require.cache).forEach((filename) => {
if (filename.endsWith('.marko') || filename.endsWith('.marko.js')) {
delete require.cache[filename];
}
});
} else if (path.endsWith('.marko') || path.endsWith('.marko.html') || path.endsWith('.marko.xml')) {
handleFileModified();
delete require.cache[path];
delete require.cache[path + '.js'];
} else if (basename === 'component.js') {
handleFileModified();
var dir = nodePath.dirname(path);
var templatePath = nodePath.join(dir, 'index.marko');
delete require.cache[path];
delete require.cache[templatePath];
delete require.cache[templatePath + '.js'];
}
};
compiler = require('../compiler');
marko = require('../');
marko = require('../');
runtime = require('../runtime/html');
widgets = require('../widgets');

View File

@ -53,7 +53,7 @@ function createDeferredRenderer(handler) {
}
function resolveRenderer(handler) {
var renderer = handler.renderer;
var renderer = handler.renderer || handler._;
if (renderer) {
return renderer;

View File

@ -0,0 +1 @@
template.temp.marko

View File

@ -0,0 +1,7 @@
module.exports = {
getTemplateData() {
return {
name: 'Frank'
};
}
};

View File

@ -0,0 +1,3 @@
<div class="a">
Hello ${data.name}
</div>

View File

@ -0,0 +1,7 @@
module.exports = {
getTemplateData() {
return {
name: 'John'
};
}
};

View File

@ -0,0 +1,3 @@
<div class="b">
Hello ${data.name}
</div>

View File

@ -0,0 +1,7 @@
module.exports = {
getTemplateData() {
return {
name: 'John'
};
}
};

View File

@ -0,0 +1,3 @@
<div class="b">
Hello ${data.name}
</div>

View File

@ -0,0 +1,29 @@
var fs = require('fs');
var nodePath = require('path');
var tempDir = nodePath.join(__dirname, 'temp');
function copyFiles(dir) {
var files = fs.readdirSync(dir);
files.forEach((file) => {
var src = fs.readFileSync(nodePath.join(dir, file));
fs.writeFileSync(nodePath.join(tempDir, file), src);
});
}
exports.check = function(marko, hotReload, expect) {
try {
fs.mkdirSync(nodePath.join(__dirname, 'temp'));
} catch(e) {}
var tempTemplatePath = nodePath.join(__dirname, 'temp/index.marko');
copyFiles(nodePath.join(__dirname, 'a'));
var component = require(tempTemplatePath);
expect(component.renderSync().toString()).to.equal('<div class="a" id="w0">Hello Frank</div>');
hotReload.handleFileModified(tempTemplatePath);
copyFiles(nodePath.join(__dirname, 'b'));
expect(component.renderSync().toString()).to.equal('<div class="b" id="w0">Hello Frank</div>');
};

View File

@ -95,9 +95,11 @@ exports.defineWidget = require('./defineWidget');
exports.defineRenderer = require('./defineRenderer');
exports.makeRenderable = exports.renderable = require('../runtime/renderable');
exports.c = function(component, template) {
exports.c = function(componentDef, template) {
componentDef.template = template;
var component = exports.defineComponent(componentDef);
component.template = template;
return exports.defineComponent(component);
return component;
};
exports.r = function(renderer, template) {