Good afternoon! I'm Matthew Beale and I'm excited to speaking with you again here at EmberConf. I've been increadibly inspired by the work Tilde (til-duh), the conference organizers, my fellow speakers, and many others have done to make this conference a success despite overwhelming circumstances. My heart and thanks go out to each of you for making this feel like a real community moment. Last year I started a new role in engineering at Addepar. We work on one of the longest-maintained Ember codebases, and additionally on a couple new app using Ember Octane. In a big project like ours, you quickly learn to prioritize writing code that is easy to read, and thats one of the reasons this topic today it important to me. Ok, so, there is a joke on the core teams, it goes...
Maybe some of you have an idea why.
Just before EmberConf last year we decided to withdraw this RFC, the Module Unification RFC, from consideration. It might seem late to be talking about this topic and I won't be talking about Module Unification in detail. What you should understand, if you don't already, is that Module Unification was a last attempt at cleaning up some loose ends in how we organize and reference files in Ember.
Lets say we're reading this template: Most Ember developers would intuit that they could look for the definition of `Welcome`'s component here:
and that is often correct. But if you want to build an implementation of jump to definition..
Maybe you're already familiar with the concept of a local maximum? When you're looking for a solution to a problem there are probably may possible solutions. And the path to get from solution to solution may not be linear, making it easy to over focus on what you already think is best. You'll often read about this idea if you dig into neural network systems.
To illustrate the problem with Ember's resolver system lets build a little fork of Ember I'll call Matt's Elegant Ember Microlib. Here is a component in my framework, you can see it just returns a string.
So here is my framework. I import my components, add them to a named list in setup, and resolve looks them up from that list. Further down I've got a template inspired by Glimmer's opcode system, and a render function above that processes that. When the opcode 1 is present, that component is resolved and called. The data passed with opcode 1 is the string `welcome`. This is the crux of a dynamic system: Instead of referencing the component directly, or template is using a name for the component. That introduces ambiguity about what the string is used for, ambiguity a lot of tooling can't penetrate. Let's see how that ambiguity presents itself in two common ways.
But lets change the template. Instead of calling opcode 1 to print the component, lets just render more text with opcode 0.
Ok, lets compile this.
In this output you wouldn't expect the component logic to be present, since we stopped referencing it in the templates. But because that relationship was something resolved at runtime and because rollup and terser don't know what the program will actually do, they aren't able to strip it out. Tool like Embroider close this gap by teaching build tools to assume things about the runtime during build-time analysis, but I can give you another example of how the resolver's dynamic implementation frustrates analysis.
Finding a second example of how the abigutity of our resolver-based library has practical impacts is as easy as looking at the most popular IDE for Ember users: VSCode. Here I've opened the micro lib and I've attempted to jump the definition of the component as it is referenced in the template. It isn't surprising this doesn't work: The data in the template is only a string. Again, in order to resolve this ambiguity about the meaning of the string "welcome" we would need to write a custom language server and encode certain assumptions about where to look for definitions.
The first draft of my microlib used dynamic resolution to look up components. Then the application boots, the available components are put in to a map then templates reference them by strings. In order for our eyes to know where to find a definition, or for our tooling to know, we need to teach those systems what the rules for resolution are. That's why when I ask *you* where to find the Welcome component, you can give a reasonable answer. You've internalized the rules. In contrast a static system, one based on ES modules, will always be explicit about where to find a definition. Lets build a second draft of the microlib, this time a static version.
In this version of the template I've referenced the welome component directly. There is no setup, no list of components, no resolver. Further more if you want to see where the welcome component comes from there is little code to think about, right? You simple look to where the variable was introduced. Here, as an import.
And when the bundlers process this, the implementation of the component is actually written directly into the template a compilation time. Cool. We've made the link between the program and the component static. The tooling can not only understand where the component is being used, it can also understand if it isn't used at all.
For example if we change the second render step back to "a fond farewell" instead of a component invocation, the bundler understands that the variable is not referenced and the entire component implementation can be dropped. Great. So Ember's templates are more featureful than my microlib. How can we bring the benefits of a staticly linked system to Ember itself?
What makes a strict mode template? Really it is a list of things we remove from the framework. Lint warnings and deprecations already encourage developers to follow the first two constraints. But the second two constraints seem bizarre when you think about them in isolation.
So here is an example of a handlebars strict mode template. It shouldn't look very different than an Ember template you might work in today. It contains...
However so that this template can have a static relationship to any components it might invoke, it also can't do some regular things..
There are several plausible approaches to getting imports into templates. To keep our design unsurprising to both new developers and advanced users, we think two constraints are important to keep in mind:
Given that we accept these constraints, it begs a question: If we're going to constrain ourselves so closly to re-implementing the way ES modules already work, why not simply adopt ES syntax into templates? In fact, any design which isn't simply module syntax seems to bend commmon sense past the breaking point. So that is what we're going to do. Let's take a look.
A strict mode template doesn't need to have any imports. If it doesn't then the template can skip any preamble. If it does, we introduce a block for writing imports above the template itself. In this block we're going to permit only import syntax in our first pass.
Just as with the hand-written compilation from earlier, these three components are passed to the compiled template as references. The template import syntax will allow us to write static templates which are easier for both our eyes and our tooling to understand.
What are our next steps in the process of delivering template imports? Before Glimmer Components were fully stabilized, we shipped them as an addon for users on the cutting edge. We could do this because we added a simple primitive, component managers, to the framework in a stable release. The feedback we got from those early adopters helped polish the API before glimmer components were promoted to a stable release. We want to use the same strategy with template imports.... First land a primitive API, and that is Handlebars Strict Mode... The build an addon supporting template imports themselves so we can get feedback from early adopters eager to try this feature... And in the meantime we will have some last-mile work, such as figuring out ES module paths for many framework built-ins.
I've talked a lot about the technical nature of a static template in this talk. Despite the fact that static templates will have performance and payload size impacts, I want to remind everyone that performance isn't the headline motivation. Static templates with ES imports are going to make it simple to understand where a component is coming from for our tooling, but also for our eyes. Additionally Module Unification had some interesting features you might have heard of like "local lookup". Explicit imports will make those features unnecessary. An import syntax will allow components to be grouped naturally in your projects without losing common conventions as suggested by linting and generators.