FEATURE: Use Glimmer compiler for widget templates

FEATURE: Use Glimmer compiler for widget templates

Widgets can now specify a template which is precompiled using Glimmer’s AST and then converted into our virtual dom code.

Example:

createWidget('post-link-arrow', {
  template: hbs`
    {{#if attrs.above}}
      <a class="post-info arrow" title={{i18n "topic.jump_reply_up"}}>
        {{fa-icon "arrow-up"}}
      </a>
    {{else}}
      <a class="post-info arrow" title={{i18n "topic.jump_reply_down"}}>
        {{fa-icon "arrow-down"}}
      </a>
    {{/if}}
  `,

  click() {
    DiscourseURL.routeTo(this.attrs.shareUrl);
  }
});
diff --git a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6
index 11036d8541..f3476faec4 100644
--- a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6
+++ b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6
@@ -2,21 +2,21 @@ import PostCooked from 'discourse/widgets/post-cooked';
 import DecoratorHelper from 'discourse/widgets/decorator-helper';
 import { createWidget } from 'discourse/widgets/widget';
 import { h } from 'virtual-dom';
-import { iconNode } from 'discourse-common/lib/icon-library';
 import DiscourseURL from 'discourse/lib/url';
+import hbs from 'discourse/widgets/hbs-compiler';
 
 createWidget('post-link-arrow', {
-  html(attrs) {
-   if (attrs.above) {
-     return h('a.post-info.arrow', {
-       attributes: { title: I18n.t('topic.jump_reply_up') }
-     }, iconNode('arrow-up'));
-   } else {
-     return h('a.post-info.arrow', {
-       attributes: { title: I18n.t('topic.jump_reply_down') }
-     }, iconNode('arrow-down'));
-   }
-  },
+  template: hbs`
+    {{#if attrs.above}}
+      <a class="post-info arrow" title={{i18n "topic.jump_reply_up"}}>
+        {{fa-icon "arrow-up"}}
+      </a>
+    {{else}}
+      <a class="post-info arrow" title={{i18n "topic.jump_reply_down"}}>
+        {{fa-icon "arrow-down"}}
+      </a>
+    {{/if}}
+  `,
 
   click() {
     DiscourseURL.routeTo(this.attrs.shareUrl);
diff --git a/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 b/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6
new file mode 100644
index 0000000000..ef58ffc796
--- /dev/null
+++ b/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6
@@ -0,0 +1,3 @@
+export default function hbs() {
+  console.log('Templates should be precompiled server side');
+}
diff --git a/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6
index 607da731a2..058b64607b 100644
--- a/app/assets/javascripts/discourse/widgets/menu-panel.js.es6
+++ b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6
@@ -1,3 +1,4 @@
+import hbs from 'discourse/widgets/hbs-compiler';
 import { createWidget } from 'discourse/widgets/widget';
 import { h } from 'virtual-dom';
 
@@ -23,14 +24,17 @@ createWidget('menu-links', {
 
 createWidget('menu-panel', {
   tagName: 'div.menu-panel',
+  template: hbs`
+    <div class='panel-body'>
+      <div class='panel-body-contents clearfix'>
+        {{yield}}
+      </div>
+    </div>
+  `,
 
   buildAttributes(attrs) {
     if (attrs.maxWidth) {
       return { 'data-max-width': attrs.maxWidth };
     }
   },
-
-  html(attrs) {
-    return h('div.panel-body', h('div.panel-body-contents.clearfix', attrs.contents()));
-  }
 });
diff --git a/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 b/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6
index f12fb26f34..1579413613 100644
--- a/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6
@@ -1,17 +1,18 @@
 import { createWidget } from 'discourse/widgets/widget';
-import { h } from 'virtual-dom';
+import hbs from 'discourse/widgets/hbs-compiler';
 
 export default createWidget('post-placeholder', {
   tagName: 'article.placeholder',
-
-  html() {
-    return h('div.row', [
-             h('div.topic-avatar', h('div.placeholder-avatar')),
-             h('div.topic-body', [
-                h('div.placeholder-text'),
-                h('div.placeholder-text'),
-                h('div.placeholder-text')
-               ])
-           ]);
-  }
+  template: hbs`
+    <div class='row'>
+      <div class='topic-avatar'>
+        <div class='placeholder-avatar'></div>
+      </div>
+      <div class='topic-body'>
+        <div class='placeholder-text'></div>
+        <div class='placeholder-text'></div>
+        <div class='placeholder-text'></div>
+      </div>
+    </div>
+  `
 });
diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6
index 60441867e1..2aa4ba4ef0 100644
--- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6
+++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6
@@ -2,13 +2,11 @@ import { iconNode } from 'discourse-common/lib/icon-library';
 import { createWidget } from 'discourse/widgets/widget';
 import { h } from 'virtual-dom';
 import { avatarFor } from 'discourse/widgets/post';
+import hbs from 'discourse/widgets/hbs-compiler';
 
 createWidget('pm-remove-group-link', {
   tagName: 'a.remove-invited',
-
-  html() {
-    return iconNode('times');
-  },
+  template: hbs`{{fa-icon "times"}}`,
 
   click() {
     bootbox.confirm(I18n.t("private_message_info.remove_allowed_group", {name: this.attrs.name}), (confirmed) => {
@@ -35,10 +33,7 @@ createWidget('pm-map-user-group', {
 
 createWidget('pm-remove-link', {
   tagName: 'a.remove-invited',
-
-  html() {
-    return iconNode('times');
-  },
+  template: hbs`{{fa-icon "times"}}`,
 
   click() {
     bootbox.confirm(I18n.t("private_message_info.remove_allowed_user", {name: this.attrs.username}), (confirmed) => {
diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6
index 2d0ea8d587..8b2c34e8ad 100644
--- a/app/assets/javascripts/discourse/widgets/widget.js.es6
+++ b/app/assets/javascripts/discourse/widgets/widget.js.es6
@@ -112,6 +112,10 @@ export function createWidget(name, opts) {
   opts.html = opts.html || emptyContent;
   opts.draw = drawWidget;
 
+  if (opts.template) {
+    opts.html = opts.template;
+  }
+
   Object.keys(opts).forEach(k => result.prototype[k] = opts[k]);
   return result;
 }
diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb
index 545f752280..704a95f1f9 100644
--- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb
+++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb
@@ -27,6 +27,7 @@ module Tilt
       # timeout any eval that takes longer than 15 seconds
       ctx = MiniRacer::Context.new(timeout: 15000)
       ctx.eval("var self = this; #{File.read("#{Rails.root}/vendor/assets/javascripts/babel.js")}")
+      ctx.eval(File.read(Ember::Source.bundled_path_for('ember-template-compiler.js')))
       ctx.eval("module = {}; exports = {};");
       ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) })
       ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) })
@@ -36,7 +37,13 @@ module Tilt
         log: function(msg){ rails.logger.info(console.prefix + msg); },
         error: function(msg){ rails.logger.error(console.prefix + msg); }
       }
+
 JS
+      source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js.es6")
+      js_source = ::JSON.generate(source, quirks_mode: true)
+      js = ctx.eval("Babel.transform(#{js_source}, { ast: false, plugins: ['check-es2015-constants', 'transform-es2015-arrow-functions', 'transform-es2015-block-scoped-functions', 'transform-es2015-block-scoping', 'transform-es2015-classes', 'transform-es2015-computed-properties', 'transform-es2015-destructuring', 'transform-es2015-duplicate-keys', 'transform-es2015-for-of', 'transform-es2015-function-name', 'transform-es2015-literals', 'transform-es2015-object-super', 'transform-es2015-parameters', 'transform-es2015-shorthand-properties', 'transform-es2015-spread', 'transform-es2015-sticky-regex', 'transform-es2015-template-literals', 'transform-es2015-typeof-symbol', 'transform-es2015-unicode-regex'] }).code")
+      ctx.eval(js)
+
       ctx
     end
 
@@ -105,7 +112,11 @@ JS
       klass = self.class
       klass.protect do
         klass.v8.eval("console.prefix = 'BABEL: babel-eval: ';")

[... diff too long, it was truncated ...]

GitHub sha: dffb1fc4

possibly check for console here so it explodes in the same way on both ie and chrome?

Not following – this is an error that should never happen in production, only in development mode. In fact it would mean discourse is setup improperly because these are compiled server side :slight_smile:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/jsx-instead-of-h-createelement-function-in-widgets-and-other-places/150040/2