Ember.js 2019 Roadmap

June 04, 2019
emberjs

The soon-to-be-finalized Octane edition of Ember brings with it a number of modernizations and enhancements that make the framework feel lighter, more tightly tied to "The Platform", and simply more joyful to use. This next year presents a wonderful opportunity to double down on the progress from Octane and continue to modernize Ember itself and the apps we write with the framework.

This post is an answer to the Call for Blog Posts intended to inform Ember's 2019 Roadmap. As a member of the Framework Core Team, I feel that I have a particular responsibility to answer this call. My suggestion this year is to focus on modularity and packaging, both at the framework and application levels. Some aspects are technical, some are related to learning, and some are even related to marketing. All suggestions are made with ❤️ to help Ember continue its positive trajectory! 🚀

Framework Modularity and Packaging

Ember has long been perceived as an all-or-nothing framework, and there's no more obvious manifestation of this perception than the ember-source package. If there's a single change I'd like to see in Ember over the next year, it would be to stop shipping ember-source and move to multiple packages in the @ember scope. This would represent a firm commitment to the public modularization of Ember and allow us to deliver on the promise laid out in the 2017 EmberConf keynote to "npm install your way to Ember". Although, at this point, I would tweak that vision to "npm install Ember your way" so that it's clear that the starting and ending points can both be considered "Ember".

True Package Separation

Ember's JavaScript modules API was released in October 2017 as part of Ember.js 2.16. This release enabled us to stop accessing modules on the Ember global in favor of targeted, @ember-scoped modules, such as @ember/routing and @ember/service.

To take the example from the v2.16 blog post, your component modules could move from this v2.15 version:

1
2
3
4
5
6
import Ember from "ember";

export default Ember.Component.extend({
  session: Ember.inject.service(),
  title: "The Curious Case"
});

To this v2.16 version:

1
2
3
4
5
6
7
import Component from "@ember/component";
import { inject as service } from "@ember/service";

export default Component.extend({
  session: service(),
  title: "The Curious Case"
});

Even with this great leap forward in DX, these packages were not "true" packages published to npm. And this has not changed - they continue to be synthetic packages fully published in ember-source.

From the outside looking in, it may appear that it's a trivial move to take one more step and just break apart ember-source into packages, which are clearly already defined in a namespaced form. You may retain this belief after looking into Ember's source and seeing those packages already listed in the @ember namespace.

However, as you can probably suspect, there will be more to this process than meets the eye. Much of Ember's source remains in internal modules that represent cross-cutting or not fully factored concerns. Ideally, routing internals would be shipped together with @ember/routing and not contained in a massive @ember/-internals package (which has never been intended to be published as a package at all). The scope of the refactoring needed to do this right is why I believe this belongs on an annual roadmap rather than simply being a switch we can flip at a team meeting.

Let's delve into why I believe that true package separation will be worth the effort.

Align Learning with Packaging

Incremental adoption of capabilities provides an approachable path to learning that keeps control in the hands of the learner. Instead of diving (or being thrown) into the deep end, developers should be able to dip their toes into Ember, gradually acclimate, and then decide how much further they want to go.

Ember should be taught as a component-first framework that doesn't require routing, controllers, data, or much of anything else to get started.

I can easily imagine the guides incorporating this approach through phrases such as:

Routing is an important part of multi-page web applications. You can introduce routing into your Ember application by running: npm install @ember/routing.

The guides could go on to explain that installing @ember/routing will bring in other packages as dependencies, such as @ember/controllers. This will clarify the role of controllers as a bridge between routes and components. By teaching that Ember's core capabilities can be provided via core @ember-scoped packages, we are both telling and showing that Ember is modular.

Minimize Dependencies, Maximize Appeal

Historically, Ember has chosen to include capabilities by default that meet roughly 80% of the needs of web applications. However, if we apply this logic to every capability, there will be a further diminishing percentage for which this applies. After all, 80% x 80% x 80% x 80% < 50%.

Let's look at a couple examples.

While I strongly believe that Ember Data should be designed to meet the data needs of 80% of web applications, I don't think it should be included in the default blueprint. It would be much less surprising for developers new to Ember to begin using fetch and then "upgrade" to Ember Data when they feel the need.

Similarly, while I believe that Ember's router may meet the routing needs of well over 80% of web applications, I also don't believe that @ember/routing should be included in the default blueprint. But doesn't every "real app" need routing? I would say that's the kind of question that leads to selection bias and missed opportunities.

If we want to break the stereotype that Ember is only suitable for desktop applications with traditional routing needs that communicate primarily with REST APIs, then we should stop shipping a default blueprint that reflects that use case so well.

Experiments at Every Level

One of the benefits to package modularization is that it will open experiments at every layer of the framework.

Someone will build a lightweight router for simple widgets. Someone else will build a stack-based router driven by state pushed from the server (see Alex Matchneer's ember-rideshare thought experiment).

Wouldn't it be better to continue to add features into @ember/routing so that it can do it all? I certainly believe in the power of shared abstractions, but these take a long time to get right. And you can never figure out The Right Abstraction™️ based upon a sample size of one.

I need to be clear that there's no doubt that one of Ember's primary strengths are its strong conventions. I'm sure that I'm not the only one who enjoys the comfort and productivity that comes from being able to drop into any Ember app and find my way around without guidance. So I'm in no way proposing that we abandon the important tenet of "convention over configuration". Rather, I strongly believe that any fragmentations that appear in the community due to experimentation will be driven by divergences in actual needs. This has been proven out in the spaces that already support experimentation. Plenty of Ember developers use JSON:API, while many others use GraphQL. Ember developers work on desktop apps and mobile web apps, browser and native. We have a pretty big tent, and I think it will get bigger and better through more openness and experimentation.

The Pitch

I personally believe that the "batteries included" marketing pitch is a bit off. I'd prefer something that sounds less like we're sending you the kitchen sink whether you want it or not. In my opinion, a better pitch would emphasize that, between Ember's core packages and its ecosystem of addons, a massive matrix of capabilities is available to choose from depending upon your needs. Perhaps "Ember has an answer". Or "The composable framework".

Application Modularity and Packaging

I consider Ember's developer experience to be second-to-none. We are spoiled by Ember CLI, its generators, blueprints, test framework, and ecosystem of addons. Yet there are some aspects of building Ember applications that could no doubt be improved.

New File System

William Faulkner wrote that "In writing, you must kill all your darlings". And I would say that, in software, we are surprisingly gleeful about killing our own code. The reason for our glee is that we always believe we are replacing it with something better or (best of all) no code at all.

Thus, it is with a light heart that I bid adieau to Module Unification, an RFC that I toiled mightily upon after first joining the Core Team. The reasons I am untroubled by this are outlined by Tom Dale in his Update on Module Unification. Simply put, the acceptance of template imports removes swaths of use cases that drove the original RFC's design.

I believe that we now have the opportunity to shape a "New File Layout" that is simpler and clearer than the one proposed as Module Unification. Hopefully, we can take the best aspects of that design that are still relevant and move forward quickly. A shared project structure is one of Ember's strongest conventions, and I consider its evolution to be one of our highest priorities.

DI is Dead, Long Live DI

The role of dependency injection ("DI") in Ember did not also die with the introduction of template imports. Rather, we are trimming the sails and bit and not using DI where it is not needed. In other words, we should not add dynamic degrees of freedom in places where simple static dependencies will do. We've come to a pretty strong consensus that dynamism is not needed when resolving components and helpers, and in fact causes more harm than good.

I believe that DI remains a powerful tool for Ember that could be improved. I think the time is right to re-evaluate where and how it is used.

Embroider

Last but not least, this is undoubtedly the year of Embroider! 🎉

Ed Faulkner (no relation to William, AFAIK) has been working tirelessly on this modern rethink of Ember's build system. Embroider introduces a pipeline that includes a packaging step that can be fulfilled by any industry standard tool - Webpack, Rollup, or Parcel. This unlocks automatic npm imports, tree-shaking, and lazy-loading of segments of your application.

I'm thankful for all the modernizations and simplications Embroider will bring. Personally, I'm most looking forward to finally deleting all the custom build processing and lazy-loading code written for ember-engines.

Summary

Octane brought a number of massive ergonomic improvements to the day-to-day developer experience within individual modules, from Glimmer templates and components to native classes. Now it's time for us to focus on how we structure and bind and package those modules, both at the framework and application levels.