From af2341363fd7361c3ef15725b50876bef5f37b2d Mon Sep 17 00:00:00 2001 From: brucejo Date: Mon, 11 Dec 2017 05:27:14 -0800 Subject: [PATCH 01/39] Fix HTML.isArray to work across frames. --- packages/htmljs/html.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/htmljs/html.js b/packages/htmljs/html.js index ce758772d..913e95d63 100644 --- a/packages/htmljs/html.js +++ b/packages/htmljs/html.js @@ -171,10 +171,7 @@ Raw.prototype.htmljsType = Raw.htmljsType = ['Raw']; HTML.isArray = function (x) { - // could change this to use the more convoluted Object.prototype.toString - // approach that works when objects are passed between frames, but does - // it matter? - return (x instanceof Array); + return x instanceof Array || Array.isArray(x); }; HTML.isConstructedObject = function (x) { From e503814f6f936f05cdf3b35899cd16a85c91415c Mon Sep 17 00:00:00 2001 From: brucejo Date: Wed, 20 Dec 2017 16:32:52 -0800 Subject: [PATCH 02/39] Use HTML.isArray where appropriate. --- packages/blaze/builtins.js | 2 +- packages/html-tools/parse.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blaze/builtins.js b/packages/blaze/builtins.js index 18e165c09..21fcd21eb 100644 --- a/packages/blaze/builtins.js +++ b/packages/blaze/builtins.js @@ -1,5 +1,5 @@ Blaze._calculateCondition = function (cond) { - if (cond instanceof Array && cond.length === 0) + if (HTML.isArray(cond) && cond.length === 0) cond = false; return !! cond; }; diff --git a/packages/html-tools/parse.js b/packages/html-tools/parse.js index 109e9fa2f..7b3296327 100644 --- a/packages/html-tools/parse.js +++ b/packages/html-tools/parse.js @@ -187,7 +187,7 @@ getContent = HTMLTools.Parse.getContent = function (scanner, shouldStopFunc) { // as in `FOO.apply(null, content)`. if (content == null) content = []; - else if (! (content instanceof Array)) + else if (! HTML.isArray(content)) content = [content]; items.push(HTML.getTag(tagName).apply( From e0f4b56320963e65723ac3bae50cbce4c183725b Mon Sep 17 00:00:00 2001 From: brucejo Date: Wed, 20 Dec 2017 16:34:13 -0800 Subject: [PATCH 03/39] use lodash.isPlainObject technique to determine if object is a plain object. --- packages/htmljs/html.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/htmljs/html.js b/packages/htmljs/html.js index 913e95d63..f1141a239 100644 --- a/packages/htmljs/html.js +++ b/packages/htmljs/html.js @@ -182,10 +182,16 @@ HTML.isConstructedObject = function (x) { // if you assign to a prototype when setting up the class as in: // `Foo = function () { ... }; Foo.prototype = { ... }`, then // `(new Foo).constructor` is `Object`, not `Foo`). - return (x && (typeof x === 'object') && - (x.constructor !== Object) && - (typeof x.constructor === 'function') && - (x instanceof x.constructor)); + if(!x || (typeof x !== 'object')) return false; + // Is this a plain object? + let proto = x; + while(Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + let plain = Object.getPrototypeOf(x) === proto; + return !plain && + (typeof x.constructor === 'function') && + (x instanceof x.constructor); }; HTML.isNully = function (node) { From 2acf08c4ffa26b4048d67a58dff107a37d1a5549 Mon Sep 17 00:00:00 2001 From: brucejo Date: Wed, 20 Dec 2017 16:56:43 -0800 Subject: [PATCH 04/39] Added one other case... --- packages/htmljs/html.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/htmljs/html.js b/packages/htmljs/html.js index f1141a239..55c8adac2 100644 --- a/packages/htmljs/html.js +++ b/packages/htmljs/html.js @@ -184,11 +184,17 @@ HTML.isConstructedObject = function (x) { // `(new Foo).constructor` is `Object`, not `Foo`). if(!x || (typeof x !== 'object')) return false; // Is this a plain object? - let proto = x; - while(Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); + let plain = false; + if(Object.getPrototypeOf(x) === null) { + plain = true; + } else { + let proto = x; + while(Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + plain = Object.getPrototypeOf(x) === proto; } - let plain = Object.getPrototypeOf(x) === proto; + return !plain && (typeof x.constructor === 'function') && (x instanceof x.constructor); From 245a5864007ea09d20b4ca1b1163eed1c8cf5583 Mon Sep 17 00:00:00 2001 From: zodern Date: Thu, 14 Jan 2021 13:33:40 -0600 Subject: [PATCH 05/39] Add basic HMR integration to Blaze --- packages/blaze-hot/hot.js | 119 ++++++++++++++++++ packages/blaze-hot/package.js | 19 +++ .../caching-html-compiler.js | 2 +- packages/templating-runtime/templating.js | 40 ++++++ packages/templating-tools/code-generation.js | 36 +++++- .../compile-tags-with-spacebars.js | 10 +- 6 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 packages/blaze-hot/hot.js create mode 100644 packages/blaze-hot/package.js diff --git a/packages/blaze-hot/hot.js b/packages/blaze-hot/hot.js new file mode 100644 index 000000000..1c389113f --- /dev/null +++ b/packages/blaze-hot/hot.js @@ -0,0 +1,119 @@ +import { Blaze } from 'meteor/blaze'; +import { Template as Templates} from 'meteor/templating-runtime'; + +let importedTemplating = new WeakMap(); +let currentModule = {id: null}; +const SourceModule = Symbol(); + +function patchTemplate(Template) { + const oldOnCreated = Template.prototype.onCreated; + Template.prototype.onCreated = function (cb) { + if (cb) { + cb[SourceModule] = currentModule.id; + } + + return oldOnCreated.call(this, cb); + } + + const oldOnRendered = Template.prototype.onRendered; + Template.prototype.onRendered = function (cb) { + if (cb) { + cb[SourceModule] = currentModule.id; + } + + return oldOnRendered.call(this, cb); + } + + const oldOnDestroyed = Template.prototype.onDestroyed; + Template.prototype.onDestroyed = function (cb) { + if (cb) { + cb[SourceModule] = currentModule.id; + } + + return oldOnDestroyed.call(this, cb); + } + + const oldHelpers = Template.prototype.helpers; + Template.prototype.helpers = function (dict) { + if (typeof dict === 'object') { + for (var k in dict) { + if (dict[k]) { + dict[k][SourceModule] = currentModule.id; + } + } + } + + return oldHelpers.call(this, dict); + } + + const oldEvents = Template.prototype.events; + Template.prototype.events = function (eventMap) { + const result = oldEvents.call(this, eventMap); + this.__eventMaps[this.__eventMaps.length - 1][SourceModule] = currentModule.id; + return result; + } +} + +function cleanTemplate(template, moduleId) { + if (!template || !Blaze.isTemplate(template)) { + return; + } + + function cleanArray(array) { + for (let i = array.length - 1; i >= 0; i--) { + let item = array[i]; + if (item && item[SourceModule] === moduleId) { + array.splice(i, 1); + } + } + } + + cleanArray(template._callbacks.created); + cleanArray(template._callbacks.rendered); + cleanArray(template._callbacks.destroyed); + cleanArray(template.__eventMaps); + + Object.keys(template.__helpers).forEach(key => { + if (template.__helpers[key] && template.__helpers[key][SourceModule]) { + delete template.__helpers[key]; + } + }); +} + +function shouldAccept(module) { + if (!importedTemplating.get(module)) { + return false; + } + if (!module.exports) { + return true; + } + + return Object.keys(module.exports).filter(key => key !== '__esModule').length === 0; +} + +if (module.hot) { + patchTemplate(Blaze.Template); + module.hot.onRequire({ + before(module) { + if (module.id === '/node_modules/meteor/blaze.js' || module.id === '/node_modules/meteor/templating.js') { + importedTemplating.set(currentModule, true); + } + + let previousModule = currentModule; + currentModule = module; + return previousModule; + }, + after(module, previousModule) { + if (shouldAccept(module)) { + module.hot.accept(); + module.hot.dispose(() => { + Object.values(Templates).forEach(template => { + cleanTemplate(template, module.id); + }); + Template._applyHmrChanges(); + }); + } + currentModule = previousModule + } + }); +} diff --git a/packages/blaze-hot/package.js b/packages/blaze-hot/package.js new file mode 100644 index 000000000..c93788639 --- /dev/null +++ b/packages/blaze-hot/package.js @@ -0,0 +1,19 @@ +Package.describe({ + name: 'blaze-hot', + summary: "Update files using Blaze's API with HMR", + version: '1.3.2', + git: 'https://github.com/meteor/blaze.git', + documentation: null, + debugOnly: true +}); + +Package.onUse(function (api) { + api.use('modules'); + api.use('ecmascript'); + api.use('blaze'); + api.use('underscore'); + api.use('templating-runtime'); + api.use('hot-module-replacement', { weak: true }); + + api.addFiles('hot.js', 'client'); +}); diff --git a/packages/caching-html-compiler/caching-html-compiler.js b/packages/caching-html-compiler/caching-html-compiler.js index f4819b324..cf82dfc53 100644 --- a/packages/caching-html-compiler/caching-html-compiler.js +++ b/packages/caching-html-compiler/caching-html-compiler.js @@ -61,7 +61,7 @@ CachingHtmlCompiler = class CachingHtmlCompiler extends CachingCompiler { tagNames: ["body", "head", "template"] }); - return this.tagHandlerFunc(tags); + return this.tagHandlerFunc(tags, inputFile.hmrAvailable && inputFile.hmrAvailable()); } catch (e) { if (e instanceof TemplatingTools.CompileError) { inputFile.error({ diff --git a/packages/templating-runtime/templating.js b/packages/templating-runtime/templating.js index e00646cd8..0712ff9e0 100644 --- a/packages/templating-runtime/templating.js +++ b/packages/templating-runtime/templating.js @@ -68,6 +68,46 @@ Template.body.renderToDocument = function () { Template.body.view = view; }; +Template._migrateTemplate = function (templateName, newTemplate) { + const oldTemplate = Template[templateName]; + + if (oldTemplate) { + newTemplate.__helpers = oldTemplate.__helpers; + newTemplate.__eventMaps = oldTemplate.__eventMaps; + newTemplate._callbacks.created = oldTemplate._callbacks.created; + newTemplate._callbacks.rendered = oldTemplate._callbacks.rendered; + newTemplate._callbacks.destroyed = oldTemplate._callbacks.destroyed; + delete Template[templateName]; + } + + Template.__checkName(templateName); + Template[templateName] = newTemplate; +}; + +let timeout = null; +Template._applyHmrChanges = function () { + if (timeout) { + return; + } + + timeout = setTimeout(() => { + Blaze.remove(Template.body.view); + delete Template.body.view; + + Object.keys(Template._removed || {}).forEach(key => { + if (Template[key] === Template._removed[key]) { + // This template was removed from the new version of its module + delete Template[key]; + } + }); + + delete Template._removed; + timeout = null; + + Template.body.renderToDocument(); + }); +}; + // XXX COMPAT WITH 0.9.0 UI.body = Template.body; diff --git a/packages/templating-tools/code-generation.js b/packages/templating-tools/code-generation.js index 6a7f8695b..6b5c2a619 100644 --- a/packages/templating-tools/code-generation.js +++ b/packages/templating-tools/code-generation.js @@ -1,8 +1,22 @@ TemplatingTools.generateTemplateJS = -function generateTemplateJS(name, renderFuncCode) { +function generateTemplateJS(name, renderFuncCode, useHMR) { const nameLiteral = JSON.stringify(name); const templateDotNameLiteral = JSON.stringify(`Template.${name}`); + if (useHMR) { + return ` +Template._migrateTemplate(${nameLiteral}, new Template(${templateDotNameLiteral}, ${renderFuncCode})); +if (typeof module === "object" && module.hot) { + module.hot.accept(); + module.hot.dispose(() => { + Template._removed = Template._removed || {}; + Template._removed[${nameLiteral}] = Template[${nameLiteral}]; + Template._applyHmrChanges(); + }); +} +` + } + return ` Template.__checkName(${nameLiteral}); Template[${nameLiteral}] = new Template(${templateDotNameLiteral}, ${renderFuncCode}); @@ -10,7 +24,25 @@ Template[${nameLiteral}] = new Template(${templateDotNameLiteral}, ${renderFuncC } TemplatingTools.generateBodyJS = -function generateBodyJS(renderFuncCode) { +function generateBodyJS(renderFuncCode, useHMR) { + if (useHMR) { + return ` +(function () { + var renderFunc = ${renderFuncCode}; + Template.body.addContent(renderFunc); + Meteor.startup(Template.body.renderToDocument); + if (typeof module === "object" && module.hot) { + module.hot.accept(); + module.hot.dispose(() => { + var index = Template.body.contentRenderFuncs.indexOf(renderFunc) + Template.body.contentRenderFuncs.splice(renderFunc, 1); + Template._applyHmrChanges(); + }); + } +})(); +` + } + return ` Template.body.addContent(${renderFuncCode}); Meteor.startup(Template.body.renderToDocument); diff --git a/packages/templating-tools/compile-tags-with-spacebars.js b/packages/templating-tools/compile-tags-with-spacebars.js index 7c3e2d67c..38fa4e550 100644 --- a/packages/templating-tools/compile-tags-with-spacebars.js +++ b/packages/templating-tools/compile-tags-with-spacebars.js @@ -1,8 +1,8 @@ -TemplatingTools.compileTagsWithSpacebars = function compileTagsWithSpacebars(tags) { +TemplatingTools.compileTagsWithSpacebars = function compileTagsWithSpacebars(tags, hmrAvailable) { var handler = new SpacebarsTagCompiler(); tags.forEach((tag) => { - handler.addTagToResults(tag); + handler.addTagToResults(tag, hmrAvailable); }); return handler.getResults(); @@ -22,7 +22,7 @@ class SpacebarsTagCompiler { return this.results; } - addTagToResults(tag) { + addTagToResults(tag, hmrAvailable) { this.tag = tag; // do we have 1 or more attributes? @@ -58,7 +58,7 @@ class SpacebarsTagCompiler { }); this.results.js += TemplatingTools.generateTemplateJS( - name, renderFuncCode); + name, renderFuncCode, hmrAvailable); } else if (this.tag.tagName === "body") { this.addBodyAttrs(this.tag.attribs); @@ -68,7 +68,7 @@ class SpacebarsTagCompiler { }); // We may be one of many `` tags. - this.results.js += TemplatingTools.generateBodyJS(renderFuncCode); + this.results.js += TemplatingTools.generateBodyJS(renderFuncCode, hmrAvailable); } else { this.throwCompileError("Expected