EmberCamp Module Unification Update

Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.

Yesterday morning at EmberCamp London I gave a talk on Glimmer, Ember.js and the relationship between the projects.

As part of that talk I covered the latest news on Ember’s new application filesystem layout, a project we refer to as “module unification”.

As of a few days ago, all the code needed to run a module unification app has landed in the various requisite repositories. In this post I’ll give you a little background on the work we’re doing and tell you how to start a new app or migrate an existing one.

Reviewing the plan

As part of creating the Glimmer application library, Dan Gebhardt created the @glimmer/resolver package. This library implements the resolution rules for module unification based on a configuration. For example here is the resolver config for Glimmer apps.

How can we leverage the @glimmer/resolver functionality in an Ember app? We’ve chosen to approach its adoption in two phases.

  • In phase 1 @glimmer/resolver will be wrapped behind the existing ember-resolver API. The ember-resolver can provide its own configuration of @glimmer/resolver appropriate for Ember apps.
  • In phase 2, @glimmer/di will replace the Ember container. At this point, @glimmer/resolver’s API would become the new public Ember resolver API. Of course to land phase 2 we need a significant RFC.

Phase 1 is the part of this work that is close to feature complete today. It includes:

Let’s look at how to use module unification today.

Creating a new app

To create a new Ember application using module unification run the following (presuming you have Ember-CLI >2.14.0 installed):

ember new my-app -b ember-module-unification-blueprint

This will create a new application all ready to go. It uses the ember-module-unification-blueprint package to set up the app.

For a new app, that’s basically all you need to do. If you attempt to use an addon with an app/ directory, you may need to use some of the addon support steps I’ll describe in the next section: How to migrate an existing app.

Migrating an existing app

The Ember project continues to value migration paths to new features for existing apps. The ember-module-migrator will handle moving your files from the old file layout to the new one, but there are a few practical details you should know about if you want to run an app today.

Prerequisites to setup

Before going any further, I suggest upgrading the following libraries in your app:

  • ember-resolver >4.3.0
  • Ember.js Canary (which means using Ember via bower)
  • Ember-CLI Canary

If you use npm, this will look something like:

npm install ember-resolver@^4.3.0 ember-cli@github:ember-cli/ember-cli --save-dev
npm uninstall ember-source --save
bower install --save components/ember#canary

Additionally components with / in the name are explicitly disallowed by the rules of module unification. To address this, remove any component nesting. For example the following would be invalid:

{{foo/bar-baz}}
{{foo-bar/baz}}
{{component 'foo/bar-baz'}}
{{component someComponentNameWithASlash}}

If you’re groaning because your application will lose some nesting that was nice for organization, don’t worry too much. The ember-module-migrator is smart enough to identify components and helpers that are only used in one place and make those into local lookup components. For example if you have files like the following:

app/components/my-form.js
app/components/my-input.js
app/templates/components/my-form.hbs
app/templates/components/my-input.hbs

And {{my-input}} is only used in my-form.hbs, the migrated app will contain:

src/ui/components/my-form/-components/my-input/component.js
src/ui/components/my-form/-components/my-input/template.hbs
src/ui/components/my-form/component.js
src/ui/components/my-form/template.hbs

This migration behavior means any loss of organization should be fairly temporary.

Running the migrator

Migrate the files in your app by running the ember-module-migrator:

npm install -g ember-module-migrator jscodeshift
cd your/project/path
ember-module-migrator

After this step, your app/ directory should now be gone and a src/ directory created in its place.

Several files must be updated at this point for your app to boot, most specifically the src/resolver.js file and src/main.js file. The easiest way to move forward is to run the ember-module-unification-blueprint and review the changes as you would any addon. Run:

ember init -b ember-module-unification-blueprint

Many apps should boot at this point. There are three caveats you should consider before opening an issue if your app does not boot:

{{component}} helper usage

Note that the module migrator understands .hbs templates, but is limited in the static analysis it can perform on JavaScript files. Any usage of the dynamic {{component}} helper should be audited for correctness after migrating.

Addons providing an app/ tree

There isn’t yet a migration path for addons to use module unification. This is something looming as a next step for our efforts. As such, it isn’t surprising if your app uses an addon that provides an app/ tree and expects you to resolve components, services, and initializers in it.

To enable resolution of files in app/ as a fallback from src/, edit src/resolver.js to use the fallback resolver. Change the import of:

import Resolver from 'ember-resolver/resolvers/glimmer-wrapper';

to

import Resolver from 'ember-resolver/resolvers/fallback';

To load initializers from app/, you should additionally edit src/main.js to include the following:

/* Add this after the existing `loadInitializers` call. */
loadInitializers(App, config.modulePrefix);

At this point many addons with an app/ directory should work.

Custom collections and types

The classic ember resolver allowed you to create a file named app/animals/cat.js and then resolve it in the container as 'animal:cat'. In application code or addon app/ code, this may exist in your codebase.

Within the rules of module unification all factory collections and types must be explicitly defined. You can add types by directly mutating the module configuration passed to the resolver.

For example if you have a file at:

src/component-managers/glimmer.js

You would allow it to be resolved as 'component-manager:glimmer' by adding this config in src/resolver.js:

let moduleConfig = buildResolverConfig(config.modulePrefix);
/*
 * If your application has custom types and collections, modify moduleConfig
 here
 * to add support for them.
 */
moduleConfig.types['component-manager'] = {
  definitiveCollection: 'component-managers'
};
moduleConfig.collections['component-managers'] = {
  types: ['component-manager'],
  privateCollections: ['utils'],
  defaultType: 'component-manager'
};

If you are using the fallback resolver, you may need to define a type just so the module unification lookup can fail before the classic resolver takes over. Another cause for this change would be your app or an addon manually registering a factory with an unknown type and expecting it to look up successfully (an initializer calling application.register('animal:cat', Cat)).

For example if you have a file (in your app/ or in an addon’s app/):

app/animals/cat.js

You must define the type and some dummy collection config in src/resolver.js:

let moduleConfig = buildResolverConfig(config.modulePrefix);
/*
 * If your application has custom types and collections, modify moduleConfig
 here
 * to add support for them.
 */
moduleConfig.types['animal'] = {
  definitiveCollection: 'animals'
};
moduleConfig.collections['animals'] = {
  types: ['animal']
};

For more information on what values are appropriate in this configuration, see the types definition in @glimmer/resolver and Ember’s default configuration in ember-resolver.

Next steps

Feedback on using module unification in its current state is essential for work on the feature to continue. At this point our biggest known issue is related to the story around addons.

Small issues we’re already tracking are:

If you run into one of these issues please add a comment. If you run into something unexpected open an issue. If you have an issue but aren’t sure where to file it, please don’t hesitate to join us in #st-module-unification on the Ember.js Community Slack and we can help you out.

Past this work, we’re eager to start on phase 2 (replacing Ember’s container with @glimmer/di) in parallel. The major blocker on that work has been the test conversation epic emberjs/ember.js#15058, however that work is nearly complete.

Interested in supporting our work on module unification? Contact us at 201 Created and let’s chat. If you want to follow our progress or have a question about module unification, join us in #st-module-unification on the Ember.js Community Slack.