Runtime Compiler Options post Ember 3.16

Hello. I am currently in the process of upgrading an Ember app from 3.16 to 3.24 and am hitting a wall. The goal is to immediately go to 3.28, but that requires updating a bunch of add ons and switching to ember-auto-import 2, webpack, etc… but if a better path is 3.16 → 3.28 for the issue described below let me know.

My Problem

The way our app works is that users can sign up for searchable “integrations”, they can subscribe and unsubscribe to these on the fly. Then when they enter a search term, the server returns results based on the subscribed-to integrations.

Each integration supplies the client with an Ember component (JS and template). This is done through an integation-loader service. For example the wikipedia integration:

// wikipedia.js
'use strict';

polarity.export = PolarityComponent.extend({
  details: Ember.computed.alias('block.data.details')
});

and a template:

<h1 class="p-title">{{fa-icon "search" fixedWidth=true}} Search Result</h1>
<div>
  <span class="p-key">Article: </span>
  <span class="p-value"><a class="p-link" href="{{details.match.link}}">{{details.match.label}} {{fa-icon "external-link" class="external-link-icon"}}</a></span>
</div>
{{#if (gt details.relatedList.length 0)}}
  <h1 class="p-title">{{fa-icon "link" fixedWidth=true}} Related Wikipedia Articles</h1>
  <ul class="link-list">
    {{#each details.relatedList as |item|}}
      <li><a class="p-link" href="{{item.link}}">{{item.label}}</a></li>
    {{/each}}
  </ul>
{{/if}}

This template is compiled on the server and sent to the client in the “wire” format.

{
    "id": "5sJCSUQF",
    "block": "{\"symbols\":[\"item\"],\"statements\":[[7,\"h1\",true.............,
    "meta": {}
}

The server @ember-source matches the client @ember-source, so the wire format is 3.16 for 3.16 and 3.24 for 3.24.

Our current implementation uses createTemplateFactory like so:

app.register('component:/wikipedia',  evaluatedJS);
app.register('template:/wikipedia',  createTemplateFactory(compiled));

In Ember 3.16 the createTemplateFactory uses this function (via @ember/-internals/glimmer/index):

  var TEMPLATE_COMPILER_MAIN = (0, _container.privatize)`template-compiler:main`;

  function template(json) {
    var glimmerFactory = (0, _opcodeCompiler.templateFactory)(json);
    var cache = new WeakMap();

    var factory = owner => {
      var result = cache.get(owner);

      if (result === undefined) {
        counters.cacheMiss++;
        var compiler = owner.lookup(TEMPLATE_COMPILER_MAIN);
        result = glimmerFactory.create(compiler, {
          owner
        });
        cache.set(owner, result);
      } else {
        counters.cacheHit++;
      }

      return result;
    };

    factory.__id = glimmerFactory.id;
    factory.__meta = glimmerFactory.meta;
    return factory;
  }

This has changed to the following in 3.24 (via @glimmer/opcode-compiler)

function templateFactory({
    id: templateId,
    moduleName,
    block
  }) {
    // TODO(template-refactors): This should be removed in the near future, as it
    // appears that id is unused. It is currently kept for backwards compat reasons.
    var id = templateId || `client-${clientId++}`; // TODO: This caches JSON serialized output once in case a template is
    // compiled by multiple owners, but we haven't verified if this is actually
    // helpful. We should benchmark this in the future.

    var parsedBlock;
    var ownerlessTemplate = null;
    var templateCache = new WeakMap();
    var factory = owner => {
      if (parsedBlock === undefined) {
        parsedBlock = JSON.parse(block);
      }
      if (owner === undefined) {
        if (ownerlessTemplate === null) {
          templateCacheCounters.cacheMiss++;
          ownerlessTemplate = new TemplateImpl({
            id,
            block: parsedBlock,
            moduleName,
            owner: null
          });
        } else {
          templateCacheCounters.cacheHit++;
        }
        return ownerlessTemplate;
      }
      var result = templateCache.get(owner);
      if (result === undefined) {
        templateCacheCounters.cacheMiss++;
        result = new TemplateImpl({
          id,
          block: parsedBlock,
          moduleName,
          owner
        });
        templateCache.set(owner, result);
      } else {
        templateCacheCounters.cacheHit++;
      }
      return result;
    };
    factory.__id = id;
    factory.__meta = {
      moduleName
    };
    return factory;
  }

So now I am receiving the error:

Error occurred:

- While rendering:
  -top-level
    application
      integrations/wikipedia

I have tried using the following babel plugins:

babel-plugin-htmlbars-inline-precompile

babel-plugin-ember-template-compilation

But have not had much luck. These seem to be focused on the hbs functionality and when using the compile function, I am greeted with Ember.HTMLbars.compile is not a function. Also I do not want the configuration in my ember-cli-build.js to affect my build time templates only my runtime ones.

The server does also send the non-wire template. i.e:

// wikipedia.hbs
<h1 class="p-title">{{fa-icon "search" fixedWidth=true}} Search Result</h1>
<div>
  <span class="p-key">Article: </span>
  <span class="p-value"><a class="p-link" href="{{details.match.link}}">{{details.match.label}} {{fa-icon "external-link" class="external-link-icon"}}</a></span>
</div>
{{#if (gt details.relatedList.length 0)}}
  <h1 class="p-title">{{fa-icon "link" fixedWidth=true}} Related Wikipedia Articles</h1>
  <ul class="link-list">
    {{#each details.relatedList as |item|}}
      <li><a class="p-link" href="{{item.link}}">{{item.label}}</a></li>
    {{/each}}
  </ul>
{{/if}}

and if I just call hbs(the above code) it works, but hbs(${stringPlaceholder}) does not work and has been documented to not be possible which is unfortunate.

My Question What can I use to register a template either in hbs or wire format at runtime post Ember 3.16?

More specifically what can I pass as the second argument to the app.register( ) function for a template at runtime?

Ember.HTMLbars.compile is not a function

ye, it’s not meant to be a runtime API.

but hbs(${stringPlaceholder}) does not work and has been documented to not be possible which is unfortunate

ye, this is because the babel transform that operates on hbs can’t guarantee that that string placeholders do what the user wants or would expect (for example, if you place a reactive reference in the ${}


More specifically what can I pass as the second argument to the app.register( ) function for a template at runtime?

I don’t think you want to mess with the registry, actually – you may be interested in another approach which would allow you to delete a bunch of your existing code.

Idk if this is an option for you, but if you skip to 3.28 (which may require some deprecation silencing via ember-cli-deprecation-workflow while you incrementally work on upgrading stuff), you can use ember-repl - npm

which gives you a nicer runtime api for compiling templates:

// app/components/my-component.js
import Component from '@glimmer/component'; 
import { compileHBS } from 'ember-repl'; 

export class Renderer extends Component { 
  compileResult = compileHBS(this.args.input); 
}
{{! app/components/my-component.hbs }}
<this.compileResult.component />

of note, however, that ember-repl is strict mode only, which means if your dynamically compiled components need to access other components or helpers or anything like that, they’ll need be referenced as identifiers in the template, and then passed via the “scope bag”

for example (from the readme)

import Component from '@glimmer/component';
import { compileHBS } from 'ember-repl';
import BarComponent from './bar'

export class Renderer extends Component {
  compileResult = compileHBS(
    '<Bar />',
    {
      scope: {
        Bar: BarComponent
      }
    }
  );
}

There is also a beta of ember-repl, which uses resources, if that’s of interest to you

5 Likes

Dude I love you! I follow you on twitter, and have listened to you on the WW&W podcast and am so grateful you are so active in the Ember community.

I’ll try this. :pray:

5 Likes

@NullVoxPopuli So I have been able to get our app to build and run using 3.28, and am close to getting ember-repl working.

The problem I’m having currently is attaching my js and hbs?

It seems that compileHBS is for template only components, and all of the components we are getting from the server have js and hbs together sent like this:

{
    "component": {
        "content": "'use strict';\n\npolarity.export = PolarityComponent.extend({\n  details: Ember.computed.alias('block.data.details')\n});\n",
        "file": "./components/wiki.js"
    },
    "template": {
        "compiled": "{\"id\":\"5sJCSUQF\",\"block\":\"{\\\"symbols\\\":[\\\"item\\\"],\\\"statements\\\":[[7,\\\"h1\\\",true],[10,\\\"class\\\",\\\"p-title\\\"],[8],[1,[28,\\\"fa-icon\\\",[\\\"search\\\"],[[\\\"fixedWidth\\\"],[true]]],false],[0,\\\" Search Result\\\"],[9],[0,\\\"\\\\n\\\"],[7,\\\"div\\\",true],[8],[0,\\\"\\\\n  \\\"],[7,\\\"span\\\",true],[10,\\\"class\\\",\\\"p-key\\\"],[8],[0,\\\"Article: \\\"],[9],[0,\\\"\\\\n  \\\"],[7,\\\"span\\\",true],[10,\\\"class\\\",\\\"p-value\\\"],[8],[7,\\\"a\\\",true],[10,\\\"class\\\",\\\"p-link\\\"],[11,\\\"href\\\",[29,[[24,[\\\"details\\\",\\\"match\\\",\\\"link\\\"]]]]],[8],[1,[24,[\\\"details\\\",\\\"match\\\",\\\"label\\\"]],false],[0,\\\" \\\"],[1,[28,\\\"fa-icon\\\",[\\\"external-link\\\"],[[\\\"class\\\"],[\\\"external-link-icon\\\"]]],false],[9],[9],[0,\\\"\\\\n\\\"],[9],[0,\\\"\\\\n\\\"],[4,\\\"if\\\",[[28,\\\"gt\\\",[[24,[\\\"details\\\",\\\"relatedList\\\",\\\"length\\\"]],0],null]],null,{\\\"statements\\\":[[0,\\\"  \\\"],[7,\\\"h1\\\",true],[10,\\\"class\\\",\\\"p-title\\\"],[8],[1,[28,\\\"fa-icon\\\",[\\\"link\\\"],[[\\\"fixedWidth\\\"],[true]]],false],[0,\\\" Related Wikipedia Articles\\\"],[9],[0,\\\"\\\\n  \\\"],[7,\\\"ul\\\",true],[10,\\\"class\\\",\\\"link-list\\\"],[8],[0,\\\"\\\\n\\\"],[4,\\\"each\\\",[[24,[\\\"details\\\",\\\"relatedList\\\"]]],null,{\\\"statements\\\":[[0,\\\"      \\\"],[7,\\\"li\\\",true],[8],[7,\\\"a\\\",true],[10,\\\"class\\\",\\\"p-link\\\"],[11,\\\"href\\\",[29,[[23,1,[\\\"link\\\"]]]]],[8],[1,[23,1,[\\\"label\\\"]],false],[9],[9],[0,\\\"\\\\n\\\"]],\\\"parameters\\\":[1]},null],[0,\\\"  \\\"],[9],[0,\\\"\\\\n\\\"]],\\\"parameters\\\":[]},null],[0,\\\"\\\\n\\\"]],\\\"hasEval\\\":false}\",\"meta\":{}}",
        "content": "<h1 class=\"p-title\">{{fa-icon \"search\" fixedWidth=true}} Search Result</h1>\n<div>\n  <span class=\"p-key\">Article: </span>\n  <span class=\"p-value\"><a class=\"p-link\" href=\"{{details.match.link}}\">{{details.match.label}} {{fa-icon \"external-link\" class=\"external-link-icon\"}}</a></span>\n</div>\n{{#if (gt details.relatedList.length 0)}}\n  <h1 class=\"p-title\">{{fa-icon \"link\" fixedWidth=true}} Related Wikipedia Articles</h1>\n  <ul class=\"link-list\">\n    {{#each details.relatedList as |item|}}\n      <li><a class=\"p-link\" href=\"{{item.link}}\">{{item.label}}</a></li>\n    {{/each}}\n  </ul>\n{{/if}}\n\n",
        "file": "./template/wiki.hbs"
    }
}

I am assuming I can call compileJS to create the component at runtime but am not sure how to attach the JS and the hbs together?

Nah, separate js and hbs are not a concept in ember-repl, it’s too hard to support the old way.

Ember-repl only supports <template> syntax, which you can learn about at https://tutorial.glimdown.com

So I suppose the answer to my original question is No. There are no runtime compiler options post Ember 3.16?

Thanks for the help.

There are no runtime compiler options post Ember 3.16?

it’s more complicated than that. You could reverse-engineer ember-repl and build what you need.

Also, per JS Representation of Template Tag by ef4 · Pull Request #931 · emberjs/rfcs · GitHub,

we’ll have:

import { template } from "@ember/template-compiler/runtime";

const Headline = template("<h1>{{yield}}</h1>");
const Section = template(
  "<Headline>{{@title}}</Headline>", 
  { 
    scope: () => ({ Headline}) 
  }
);

and for class-based components:

import { template } from "@ember/template-compiler/runtime";
class Example extends Component {
  static {
    template(
      "Hello {{message}}",
      {
        component: this,
        scope: () => ({ message }),
      },
    );
  }
}

the RFC calls out:

Unlike precompileTemplate, our strict param defaults to true instead of false if it’s not provided. This is aligned with the expectation that our main programming model is moving everyone toward handlebars strict mode by default.

however if you have JS, you still have to use eval, like ember-repl does.