Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Engines #10

Merged
merged 4 commits into from
Apr 11, 2016
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions active/0000-engines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
- Start Date: 2014-10-24
- RFC PR:
- Ember Issue:

# Summary

Engines allow multiple logical applications to be composed together into
a single application from the user's perspective. They also allow addons
to provide encapsulated functionality normally associated with apps,
such as routes and templates that integrate seamlessly with the primary
app.

# Motivation

Large companies are increasingly adopting Ember.js to power their entire
product lines. Often, this means separate teams (sometimes distributed
around the world) working on the same app. Typically, responsibility is
shared by dividing the application into one or more "sections". How this
division is actually implemented varies from team to team.

Sometimes, each "section" will be a completely separate Ember app, with
a shared navigation bar allowing users to switch between each app. This
allows teams to work quickly without stepping on each others' toes, but
switching apps feels slow (especially compared to the normally speedy
route transitions in Ember) because the entire page must be thrown out,
then an entirely new set of the same assets downloaded and parsed.
Additionally, code sharing is largely accomplished via copy-and-paste.

Other times, the separation is enforced socially, with each team
claiming a section of the same app in the same repository.
Unfortunately, this approach leads to frequent conflicts around shared
resources, and feedback from tests gets slower and slower as test suites
grow in size.

Engines allow each section of an application to be developed in
isolation, including separate test suites and naming conventions. These
isolated units can be composed together into a single app from the
perspective of the user.

Additionally, engines allow addons to augment applications with
off-the-shelf fully-functional UI. For example, imagine an
authentication library that offers a prefab login screen that
application developers can opt in to, dramatically increasing the speed
at which prototypes can be developed.

Or imagine a product like Discourse allowing you to mount their Ember
app at `/forums/`, so the user needn't leave your app to read your
discussion forum.

# Detailed Design

Your app can gain access to an engine in one of two ways:

1. By placing an Ember CLI app inside another Ember CLI app's `engines/`
directory (a sibling of `app/`).
2. By installing an Ember CLI addon that contains an `app` directory.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to consider loading engines at runtime? I know some apps do something like

  1. map /admin/* to route:load_admin statically in the core app
  2. route:load_admin fetches the JS, CSS, and (compiled) templates for the Admin Engine
  3. route:load_admin defines new routes for admin/x, admin/y, etc. Because they're more specific than admin/*, they have higher routing priority.
  4. route:load_admin restarts the transition.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using that strategy a lot in Ember 1.10. It seems like it broke in Ember 1.11 (I've not been able to confirm if there's another workaround). In order for step 3 to work, we were calling Router.map a second time, however, when doing that in Ember 1.11 the new routes weren't recognized :(
I know there was a big refactor of the router in 1.11, so maybe this broke after that.


Engines are not loaded until the primary application mounts them
explicitly. The primary application uses the new `mount()` router DSL
method to mount an engine:

```js
Router.map(function() {
this.mount('an-engine');
});
```

Engines' routes are prefixed with the name of the engine. For example,
let's write an engine that adds authentication, and includes a default
login and logout page. Imaginatively, we'll call this engine
`authentication`.

```js
// engines/authentication/router.js

import Ember from 'ember';

var Router = Ember.Router.extend();

Router.map(function() {
this.resource('login');
this.resource('logout');
});

export default Router;
```

Now, in the primary app, we mount the `authentication` engine:

```js
// app/router.js

import Ember from 'ember';

var Router = Ember.Router.extend();

Router.map(function() {
this.mount('authentication');
});

export default Router;
```

Now the primary app gains two new URLs:

1. `/authentication/login`
2. `/authentication/logout`

If I want to customize the prefix used, I can pass options to `mount()`
that specify a path:

```js
this.mount('authentication', { path: 'auth' });
```

Now the URLs become:

1. `/auth/login`
2. `/auth/logout`

Engines otherwise behave like addons. Thanks to [the PR adding
namespaces to the Ember CLI resolver][namespaces], container keys can
specify an engine. For example, if the `authentication` engine contains
`engines/authentication/models/user.js`, this could be resolved via the
container with:

```js
container.lookup('authentication@model:user');
```

[namespaces]: https://github.com/stefanpenner/ember-resolver/pull/70

Other APIs in Ember would need to be extended to support namespaces to
take full advantage of this feature. For example, components that ship
with an engine could be accessed from the primary application like this:

```handlebars
{{authentication@login-form obscure-password=true}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downside of this syntax is that it has no html compliant tag form (yet)

In theory something like:

<ember-namespace namespace="authentication">
   <login-form>
</ember-namespace>

or a block form, may introduce nice things

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefanpenner How's that HTML-compliant tags align with (presumably) allowing tags with slashes after the Handlebars 2.0 update?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now we will support slashes only via {{foo/bar}} syntax, as <foo/bar></foo/bar> is kinda crazy and not supported by HTMLBars currently. I am unsure it should be, but likely further thought is needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually seems like a somewhat serious issue. We're relying on HTMLBars syntax for something like: <my-component on-click={{action "foo"}}>, and there is no equivalent old-component syntax.

We will need to figure out a solution for nested tag names pretty soon.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: document.createElement("foo:bar") works (holdover from XML days, I suppose).

I think our solutions are going to end up being:

  • Template-wide imports (* or named)
  • Scoped imports (like XML namespaces or what Stef suggested)
  • Changes to the default namespace (would require html:div if HTML was desired).

I think that many solutions will work, and that as long as we have a clear, "one true story" for doing this, and it doesn't introduce excessive duplication or boilerplate, people will be able to live with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its a bummer that @ wont work, as : already has meaning...

```

However, these changes are outside the scope of this RFC.

## Generator

Create a new engine by running `ember g engine-name`.

# Drawbacks

This RFC introduces the new concept of engines, which increases the
learning curve of the framework. However, I believe this issue is
mitigated by the fact that engines are an opt-in packaging around
existing concepts.

In the end, I believe that "engines" are just a small API for composing
existing concepts. And they can be introduced at the top of the
conceptual ladder, once users are comfortable with the basics of Ember,
and only for those working on large teams or distributing addons.

# Alternatives

One alternative is to simply allow addons to merge app-like assets
(templates, routes, etc.) with the primary app (e.g. via Broccoli's
`mergeTrees`). However, this is highly likely to lead to naming
collisions. By providing an isolated app that users can mount at a path
of their choosing, we can ensure that collisions are next to impossible.

# Unresolved questions

## Non-CLI Users

This RFC assumes Ember CLI. I would prefer to prove this out in Ember
CLI before locking down the public APIs/hooks the router exposes for
locating and mounting engines. Once this is done, however, we should
expose and document those hooks so users who cannot use Ember CLI for
whatever reason can still take advantage of composability.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be sure it de-sugars to something non CLI. In theory we can make engines able to pre-build themselves (with cli) for consumption without CLI.

This is also interesting and appealing, as engines being able to pre-build themselves will allow for much faster build times. (Something @chadhietala's work on CLI aims to support)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on all counts 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love this! It would also allow deploying engines separately, and then adding them dynamically to the page as plugins, so not increasing bloat for those users that don't have the plugin enabled.


## Dependencies (Shared vs. Isolated)

Because each engine is an isolated app, it will have its own set of
dependencies. Frequently, those dependencies will be the same as the
dependencies of the primary app. For example, it would be bad to ship
multiple copies of Ember, one for each engine. Or, imagine both the app
and an engine required a Bootstrap add-on that included CSS. It would
not do to include multiple (perhaps subtly different) copies of the same
CSS.

Ideally, dependencies would be deduplicated using a strategy described
by @wycats in [this Google doc](https://docs.google.com/a/tomdale.net/document/d/12CsR-zli5oP2TDWOef_-D28zjmbVD83hU4q9_VTk-9s/edit).

## App/Container/File Layout

Does each engine have its own container? Or does it register factories
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

point of clarification. in ember-cli there is no concept of registering, merely resolving on-demand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also feel the layout should mostly mimic a normal ember-cli app's app directory.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, absolutely.

In Rails, the primary difference is that Applications have the notion of booting, and certain app-global configuration. The TLDR is: Application extends Engine extends Plugin.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, i would like our main app (and our addons) to mimic this same. Engines all the way down.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefanpenner and I have talked about this in part. Currently in ember-cli apps and addons have two different compilation pipelines. Part of this effort might include moving ember-cli to consider the application itself as an addon/engine. This would consolidate the compilation pipelines and would demand that the build system revolve around engines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bcardarella yupyup :)

with the host app's container?

Do engines subclass `Ember.Application`, or do we introduce something
like `Ember.Engine`?

How do engines use initializers? Can they inject into their own
namespace, or into the primary app also? How do you disambiguate?

Where does configuration go? Do engines put all of their app code in
`app/` like full-blown apps?