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

Module Unification Packages (MU with Ember Addons) #367

Closed
wants to merge 3 commits into from

Conversation

mixonic
Copy link
Sponsor Member

@mixonic mixonic commented Aug 27, 2018

An iteration on and replacement of #309

Rendered


RFC #143 introduced a new filesystem layout for Ember applications we've called "Module Unification".

Ember addons can use the module unification filesystem layout (i.e. an addon with a src/ directory) in place of the legacy app/ or addon/ folders. The src/ folder encapsulates a module unification "package", and Ember applications can access components, helpers, and services from that package.

This RFC rejects some parts of the Module Unification RFC, such as :: component invocation syntax and implicit invocation of an addon's main. These changes are largely motivated by conflicts between these designs and new features in Ember and NPM.

This RFC describes a new helper {{use}} for explicit importing of a component or helper from a package. This replaces the :: syntax in templates.

Service injections do not currently have source information encoded. Here we propose adding that information via an AST transform and argument to
inject.service. This additional information provided by Ember CLI at build
time mirrors how source is already a compile-time addition for templates.

@webark
Copy link

webark commented Aug 27, 2018

Since use is a module import statement in a handlebars file, why not just use import as the reserved word?

@ef4
Copy link
Contributor

ef4 commented Aug 27, 2018

I think the problem with import is that it has different syntax and semantics, so trying to make it too much like JS import could put it in an uncanny valley.

If we wanted to make it truly match import, it would need to change from:

{{use thing from "somewhere"}}

to

{{use { thing } from "somewhere"}}i

Because we're always accessing named exports. And the two levels of { look very WAT to me.

I guess this is worth calling out in the alternatives section. I can imagine just living with the gap between JS import and an hbs import, but even just typing this sentence makes me appreciate being able to distinguish between import and use.

@webark
Copy link

webark commented Aug 27, 2018

That makes sense to me. Also, if you used import you’d want to put in the fully qualified path, rather then just the name of the component, which would get annoying and overly verbose. Was just concerned about introducing something that is so similar but yet subtly different could lead to confusion and “that’s clever eyerolls”

But i think the amount that is different is easily argued as a need for another word, with using a similar convention.

@mikkopaderes
Copy link

So the store is saved from the proposed API. Did I understand that properly? If so, I'd prefer not to. That's bound to be one of those "aha!" moments when you're trying to solve why the store doesn't need a source but session needs it. It's another learning curve IMO.

@mixonic
Copy link
Sponsor Member Author

mixonic commented Aug 27, 2018

@rmmmp I'm not sure how to read this statement:

So the store is saved from the proposed API. Did I understand that properly?

When crossing any package boundary (via templates or JS) you must specify the package. For example I suggest an Ember Data API of the following:

In an app, explicit package (in RFC: Explicit packages for service injections)

// my-app/src/ui/components/foo-bar/component.js
import Component from '@ember/component';
import { inject } from '@ember/service';

export default Component.extend({
  store: inject({ package: 'ember-data' })
});

In ember-data itself, implicit package (in RFC: Implicit packages for services)

// ember-data/src/services/something.js
import Service, { inject } from '@ember/service';

export default Service.extend({
  store: inject() // babel transform will add the `source` arg for you
});

Many people have commented that they rather like store: inject() without a package. This RFC does not propose a way that a full MU version of Ember Data could continue to offer that API. If Ember Data really wanted to continue to provide store: inject() under MU layout, it would need to do one of the following non-standard things:

  • Add a broccoli transform that adds a file to the application's src/ tree at src/services/store.js
  • Have users manually install a re-export file at src/services/store.js
  • Have the post-install hook add the file
  • Use one of those three methods with an initializer to do some fancy stuff
  • Use a babel transform to add the package: arg to all uses of inject

Basically it would be non-standard and I don't suggest that there is a good solution here.

@luxzeitlos
Copy link

Can I pass a string to the usehelper? Like {{use string from bla}} and then bla being dynamic? It doesnt sound like it is possible.

Is there a way to basically {{use}} all components from a package and then, at runtime decide which one to use? Dynamic lookup can be very useful. And I do understand that some static {{use}} is needed, but I think it should be possible to use an entire package and then dynamically select the component.

@mikkopaderes
Copy link

@mixonic got it. I think I was the one who misunderstood things. This paragraphs has a lot of fancy words in it that it confused me 😛

The concept of a data store is a generic software design tool. That Ember Data has claimed the word "store" so effectively in the Ember user's mind that another library providing a "store" seems unimaginable limits the potential of the framework.

This RFC proposes no design-level mitigation for the extra argument injecting a store. Ember Data itself could continue to support store: inject() via special-case broccoli tooling should it choose to.

@mixonic
Copy link
Sponsor Member Author

mixonic commented Aug 28, 2018

Can I pass a string to the use helper? Like {{use string from bla}} and then bla being dynamic? It doesnt sound like it is possible.

Correct, this is not suggested. The syntax section for {{use}} (under The {{use}} helper, scroll down) specifically calls out the ModuleSpecifier as a StringLiteral. "The module unification package from which a component or helper is being imported. Usually an NPM package name."

I would like these import statements to be hints for improving Ember's template compilation and application packaging. If the source of the import is dynamic we lose lots of information.

Is there a way to basically {{use}} all components from a package and then, at runtime decide which one to use?

Sure, you could explicitly import all the components from the package and the use the {{#if}} helper to decide which one was rendered. That would be explicit in the template, but still be dynamic at runtime. 😉 :shipit:

What is not provided are two things I believe you are implying. First, something like JavaScript's "import all named exports" feature:

import * as name from "module-name";

This would then perhaps allow you to invoke a component with {{component name.Foo}}. The problem is that Ember's resolve is traditionally lazy, so even at build time we really can't be sure that 100% of loadable things are in the module map. At build, we can't guarantee the * at build is really the same as * would be at runtime.

Sidestepping this and requiring statically named imports seems like a good idea for now. Maybe in the future Ember can decide build-time imports are reliable at runtime, but it just isn't true today.

The second thing implied is that this renders the component foo-bar:

{{use foo-bar from 'my-addon'}}
{{component computedPropertyWithStringOfFooBar}}

However the component helper doesn't do this today. Strings passed to the component helper are resolved in the current package. A string passed to the {{component}} helper does not mean "check for a local variable with that name".

There was discussion about if the component helper should be changed to support this. I decided against advocating for that change for a few reasons:

  • It is not actually a backwards compatible change. Today {{#with (component 'foo-bar') as |c|}}{{component 'c'}}{{/with}} would render nothing. If we made this change, it would render the foo-bar component. How big of a breaking change this is would be up for debate.
  • It is a weird change to make. The {{component}} helper is already pretty unique in the Glimmer template language. This change would also give it a macro-esq ability to take runtime information (the string) and combine it with build-time information (the variable names).
  • Although this RFC does not allow the "dynamically invoke anything from another package" approach, it certainly continues to allow apps and addons to have dynamic {{component}} usage in their own package-space. So if an addon is indeed highly dynamic, it can certainly {{yield (component someDynamicString)}}. The available components for dynamic invocation would be limited by the template with the yield statement, and only the components from that one addon would be defeated in tree-shaking. An addon can provide a highly dynamic API if it desires.
  • I'm not convinced there are many (any?) real-world cases where you want to dynamically invoke some component from another package. If there are any, the workarounds of {{#if}} statements and using {{yield}} to pass a dynamic component out of a package seem like ample workarounds. I would welcome another perspective here. Happy to kick around practical cases in #st-module-unification on community Slack.

@rtablada
Copy link
Contributor

{{use}} is required to bring a component or helper from another package (and not from a directory or file) into the template's scope as a symbol

This is limiting (not in a good way).

For local packages there’s a use for aliasing or renaming components. I think that having the use helper would be a great fit for this.

Having the use helper work like import for modules in browser by requiring absolute paths makes sense and makes local import and dependency import feel a bit more natural.

For clarification I don’t mean that all local component declaration should require a use statement. But I’m only recommending that use MyComponetnt as NewAlias from 'my-app' should work.

@sandstrom
Copy link
Contributor

sandstrom commented Aug 28, 2018

Overall very positive! 🥇

Two thoughts:

  1. Good that you went with use instead of import. Since they are different things (although with similarities), using different names is better.
  2. I expected the imported use value to be quoted (example below).
// I expected this:
{{use 'Select' from 'ember-power-select'}}
// instead of: 
{{use Select from 'ember-power-select'}}

// To me, the latter is more similar to: 
{{component variableThatResolvesToAstring}}

Or have I completely misunderstood something?

@luxzeitlos
Copy link

@mixonic thank you very much for clarification.
I agree that building a wrapper component for dynamic component calls is totally feasible. So the package would provide a component with this template:

{{component @name}}

I totally agree that this works for now. Can't wait for MU now.

@bryanaka
Copy link

bryanaka commented Aug 30, 2018

@sandstrom I had that initial reaction too when watching mixonic's NYC meetup talk. I came around though for two reasons

  1. When adding in Angle bracket components, this doesn't actually feel too much of a departure from component creation in React/JSX. The win isn't necessarily the syntax, but the portability of knowledge.

  2. By not having a string, there is one less "magic transform" step. Non-quoted words signal code level comprehension (i.e. vars, helpers, components, etc.), which this is. Don't know if the VM/complier also gets a benefit from this, but I could see it happening conceptually.

anyway, just my two cents. Thanks for the work on this @mixonic 💯

@toranb
Copy link

toranb commented Sep 3, 2018

@mixonic I'd love to see a migration path example that takes into account "shipping with sensible defaults" as I've described in great detail below

ember-cli/ember-cli#7680

@Kerrick
Copy link

Kerrick commented Sep 4, 2018

It seems that as specified, prelude.hbs could include arbitrary text and/or HTML, and that could lead to that text rendering all over apps. Imagine this prelude.hbs file:

{{use Select from 'ember-power-select'}}}

Now suddenly the user has }s rendering in dozens of places across the screen.

Perhaps it would be best to say that the only contents prelude.hbs can have are {{use}} statements, and any text nodes, DOM nodes, or component/helper invocations cause a compilation error?

@rwjblue
Copy link
Member

rwjblue commented Sep 4, 2018

Great point @Kerrick! In addition to limiting prelude.hbs to {{use statements and text nodes, I think we should also specify that white space text nodes before and after use statements is stripped.

@webark
Copy link

webark commented Sep 5, 2018

if the prelude is limited to “use” import statements, does it being a special one off .hbs file cause more confusion then it being a config block or js file?

Also, where does this file live? can it be scoped to a “level” i.e. the values in two/level/prelude.hbs get added/overwritten by three/more/levels/prelude.hbs?

It’s mentioned that only one use can be done, and that “Once an addon has claimed a name in a prelude the app will not, in its own templates, be able to write {{use}} for that name without a re-assignment.” Shouldn’t the use from the individual template over write the prelude rather then the other way around? Isn’t that the point? Otherwise, if an addon developer adds a generic item to the prelude, we are kind of back in the same boat we currently are if we can’t override it on an individual basis. Or did i read that passage wrong?

@mikkopaderes
Copy link

I think the prelude.hbs has a potential to cause confusion with application.hbs

How should we describe things so this wouldn't confuse newcomers?

@dfreeman dfreeman mentioned this pull request Sep 10, 2018
@dfreeman
Copy link
Contributor

dfreeman commented Sep 12, 2018

This occurred to me over on the Modifier Managers RFC when I read a setModifierManager snippet there, but it equally applies to setComponentManager, which already exists.

In the context of this RFC, how do these functions work when referencing a manager from an addon? Would it be something like setComponentManager('sparkles', { package: 'sparkles-component' }, SparklesComponent)?

@cibernox
Copy link
Contributor

cibernox commented Sep 13, 2018

I like to explicitly import the components and helpers I want to use in my templates, so I'm generally in favor or this.

I even think that we could take this one step further and also require to explicitly import even components from your very app, but I think this is a good compromise for now. If next year we realize that explicitly importing components from your app can help with tree shaking it seems that it wouldn't be terribly hard to opt-in into a "strict mode" that enforces explicit imports. At the end in React apps imports are explicit and it's not a big deal.

Also, this could open a door to lazy-load components when they are imported with a very similar syntax some day in the future.

Also, regarding the prelude, it's supposed to be used for components or helpers that are used suuuuuper often, like the eq, not helpers from ember-truth-helpers. Recently there has been some conversations about moving some of those helpers to core (I bet that over 90% of the apps use them already). The need for using the prelude will be low, maybe low enough to remove it.

@webark
Copy link

webark commented Sep 13, 2018

Also, regarding the prelude, it's supposed to be used for components or helpers that are used suuuuuper often

My main concern is that there is no way to enforce this, and if an addon developer can add all of their components to this file, which definitions take presedent over your own in your app, we are basically where we started.

@cibernox
Copy link
Contributor

@webark Personally other than the helpers in ember-truth-helpers, I struggle to find any other example of something I use sooooo often that explicitly importing it would become annoying. Maybe {{href-to}} comes to mind. I wouldn't be too pissed if the prelude.hbs doesn't exist at all if the truth-helpers are moved to core.

But I can see people feeling otherwise. As long as addons cannot add things to your prelude implicitly (they obviously can if they use the afterInstall hook to edit the file) I can live with it.

@chriskrycho
Copy link
Contributor

Yeah, I'd argue that the behavior should be both discouraged and explicit; as long as it is both of those things, the user can trivially undo it if they so choose.

@webark
Copy link

webark commented Sep 13, 2018

Once an addon has claimed a name in a prelude the app will not, in its own templates, be able to write {{use}} for that name without a re-assignment.

As i read that, if an addon author defined a “not” helper, that would be something you could not redefine, and arevstick with there’s. And if two addon authors do the same, not sure what would happen.

I get that this behavior should be discouraged, but without any enforcement, you lend yourself to the some of the same issues that drove the creation of this addon.

I the pretending of use statements to some, or all template files, is something that if the community decides is a priority, a extra community addon could be created that would do just that, and people could opt in that way. I feel (personally) that the “prelude” feature shouldn’t be included as part of this effort.

And if we hold to local helpers and components don’t need to be “use”d, then you could just create your own “t” helper that just inherits from i18n’s for example.

@chriskrycho
Copy link
Contributor

Right, but the explicitness of the prelude is the other save: if you don't want something that an addon added to your prelude – since it's in the actual file my-app/src/prelude.hbs on disk – you just remove it. Delete that post-install-added line from the file and it's gone. Or remap it to what you do want it globally available as, or whatever else. The fact that it's just a file in your app's src folder means the addon can add it automatically, but it can't require it to be there.

@webark
Copy link

webark commented Sep 13, 2018

hmm.. I did not read it as lines that where continually added to the consuming apps prelude. That makes more sense, and would be more manageable.

Does this concatenation happen automatically? or would someone have to write there own custom post install hook in their addon to opt in.

@chriskrycho
Copy link
Contributor

Yeah, re-reading I see where the ambiguity comes from there. It might be worth a small update to the text to indicate that an addon can only do that by running a post-install hook that updates the src/prelude.hbs.

@knownasilya
Copy link
Contributor

knownasilya commented Sep 27, 2018

One of the main things that has been coming back up in the chat is loosing the ability to organize components in deeply nested trees. A few people have brought this up @Alonski, @lougreenwood, @NullVoxPopuli, sol and Robbo (both from Discord) and I think it's worth at least mentioning in this RFC.

I do see the value in nesting purely from an organizational perspective, where things go into categories or areas and make it easier to reason about and navigate components. People that have mentioned this concern have many components, some 400+ I think. I encourage those people to give some concrete examples of where nesting has helped them, with specific names of nested components and how they are used (private or outside of parent components).

Packages are a sort of solution, but they only support one level of grouping and require a few levels of folders to even get started, e.g. packages/somepackage/src/ui/components/*.

I'm for punting on nested groupings and revisiting in a followup RFC, especially since it was never advertised as a feature, but I do think it needs to be mentioned here if that's the way we are going to approach it. In that case, the classic mode should not be deprecated before there is a solution/consensus for nesting.

@jeffhertzler
Copy link

From Discord in response to the discussion mentioned above:

We have 300+ components, a significant chunk are organized in directories like this. Some of our uses can be solved by yielding out the children or only referencing the children from within the parent. Many many others could not. Like others here, we commonly use the directory structure for namespacing.

@rtablada
Copy link
Contributor

We use nested components for namespacing. Most of these namespaces match either route groups for parts of our application or could be easily moved to in-app-addons from what I see.

We'd probably migrate slowly for those as we migrate to angle brackets and MU folder structure.

@luxzeitlos
Copy link

@mixonic

An addon may: import config from 'config/environment'; export default Whatever.extend({ store: inject({ package: config.modulePrefix }); and use it's post-install step to install a re-exported service in the app's src/ directory. This is probably the easiest, best path forward.

how can the addon then use this code from addon code? Is there a way to inject a service from the app namespace without knowing the app namespaces name? And the same for factoryFor and lookup?

And the same for components? Is there any way to use a app component from addon code?


@knownasilya isn't this already part of the original RFC?

  1. No slashes in component names. The existing system allows this, but we don't want to support invocation of nested components in Glimmer Components.
    ...
  2. Local relative lookup for components and helpers needs to work.

Local relative lookup will work right?

@Alonski
Copy link
Member

Alonski commented Sep 28, 2018

@knownasilya Thanks for speaking our concerns.
I use nested component structures with a lot of inheritance for certain components. Especially in our UI Component Styleguide addon.
We do this for example: {{button-/primary/small}}
I have been living with the fact that Angle Bracket Invocation will mean we will have to migrate from using /. However maybe this can be solved as I and my team like using this nested folder structure pattern. We have multiple hundreds of components.

@NullVoxPopuli
Copy link
Sponsor Contributor

Local relative lookup will work right?

it already does: https://gitlab.com/NullVoxPopuli/emberclear/tree/master/packages/frontend/src/ui/components/chat-entry :)

<ChatEntry /> renders <EmbedsMenu />

@knownasilya
Copy link
Contributor

knownasilya commented Sep 28, 2018

@knownasilya isn't this already part of the original RFC?

No slashes in component names. The existing system allows this, but we don't want to support invocation of nested components in Glimmer Components.

@luxferresum yes it is mentioned, but I think since packages is it's own RFC and it promotes local single level grouping, it should consider the nesting scenario explicitly. I don't think it's enough to refer back to the original RFC because the '/' was avoided in the actual component name, e.g. <Namespace/SomeComponent />, but this RFC promotes {{use}} + packages as a sort of alternative to that.

The original RFC threw out the idea of grouping all together, but this RFC supports one level of grouping. So far it seems like one level isn't enough for many people.

@webark
Copy link

webark commented Sep 29, 2018

i must have missed the nested part. Are you not able to use {{use}} with nested components? or only top level ones?

@chriskrycho
Copy link
Contributor

chriskrycho commented Oct 3, 2018

Is there a blocker to supporting a {{use * as Alias from 'package'}} pattern?

Here's the motivating example: we have a family of components which we'll be grouping together into a package and possibly extracting to a shared addon. In the old proposal, I'd have used name-spacing for that: Olo::Button. In the new world proposed here—as the text stands—I can either import those all individually in the prelude (and, if I want name-spacing of any sort, use Button as OloButton for all of them), or I have to do use for them in every template which uses them. For a library of shared UI components, this is annoying. It's not the end of the world, but it's annoying.

If we had the ability to use * as Alias, however, we could do {{use * as Olo from 'olo-shared-components'}} in the prelude, and this would allow us to then do <Olo.Button /> in our application.

@ro0gr
Copy link

ro0gr commented Oct 3, 2018

Can we support a short form of use, which would expose all the public components of the package?

  {{use "ember-bootstrap"}}

  {{!-- we can use all the public package components with a short form use! --}}
  {{bs-tooltip}}

I think it also can be convenient in prelude.


Playing around @chriskrycho's proposal above, I can think of something like this for namespacing:

{{use 'olo-shared-components' as Olo}}

@knownasilya
Copy link
Contributor

knownasilya commented Oct 4, 2018

I'm totally for the @chriskrycho proposal, but only with a name. An import all from addon, like {{use "ember-bootstrap"}} is exactly the kind of ambiguity I think MU and this RFC is trying to avoid/prevent. There is no way to know what is being used or how one addon might affect the "imports" of another with this form of "import"

@samselikoff
Copy link
Contributor

@chriskrycho I think the fact that ECMA modules already support import * as Alias is another point in column for an ECMA-based approach, or at least an approach that desugars to ECMA imports.

@adamseckel
Copy link

If we had the ability to use * as Alias, however, we could do {{use * as Olo from 'olo-shared-components'}} in the prelude, and this would allow us to then do <Olo.Button /> in our application.

I just want to echo @chriskrycho's suggestion to allow * as Alias as a pattern. It would really unblock building powerful design systems in ember addons for consumption here at Intercom 👍

I think its also worth noting that this goes some way to address your concern @mixonic

Ember templates don't require any imports today. If you've written some React in a large codebase, you will have seen files with many, many lines of imports. I believe the ergonomic sweet spot is somewhere in the middle.

When you can export groups of components with an alias you dramatically reduce the length and number of import statements, while simultaneously adding a lot of context as to which components are grouped and indicate that they work together, or have the same source (I've noticed this pattern becoming more common in big react project component imports). Allowing * as Alias goes a long way to hitting that ergonomic sweet spot 👌

@NullVoxPopuli
Copy link
Sponsor Contributor

NullVoxPopuli commented Oct 4, 2018

* as Alias would enable stuff like

<Alias.Form>
  <Alias.Input />
</Alias.Form>

which I am a big fan of. :)

@lougreenwood
Copy link

Maybe I'm mis-understanding, but would * as Alias also allow arbitrary levels of nesting?

@chriskrycho
Copy link
Contributor

No, the rules around nesting would be unchanged; the only thing I'm suggesting with the addition of * as Alias (or the {{use 'package-name' as Alias}}, which I also quite like) is being able to provide an alias for the whole set of components exported at the top level, with exactly the same rules around top-level exports etc. as you have in the existing proposal.

@lougreenwood
Copy link

Ok, in that case, +1 me in :) - this could be very powerful for allowing nice semantic names for components in templates. I can see this being very useful for the general Ember teaching story & guides too.

Also, if we begin to see common alias names emerge for component aliases in the community, it's another point towards the "It's easy to onboard onto an Ember project" story.

@luxzeitlos
Copy link

@chriskrycho would this work?

{{use * as Alias from 'package'}}
{{component (get Alias 'dynamicLookupString')}}

@mixonic
Copy link
Sponsor Member Author

mixonic commented Oct 14, 2018

Is there a blocker to supporting a {{use * as Alias from 'package'}} pattern?

I think a follow-on RFC would be a good place to add that feature. One downside of this feature is that since Alias can be yielded or passed into JS space, you cannot tree-shake out un-used components from a template using this style. The same downside applies to alternative of simply yielding the dynamic component of course, but in that case the downside is opt-in for the addon author instead of the application author.

I'm not convinced there is a case where * as is required over having the addon provide a component that yields the hash.

Basically we're considering a {{use}} feature giving app authors:

{{use * as FormHelpers from '@org/form-helpers'}}
<FormHelpers.Form>
  <FormHelpers.Input />
</FormHelpers.Form>

vs. (and there are a few ways this could be done in practice)

{{use form-helpers from '@org/form-helpers'}}
{{form-helpers as |f|}}
  <f.Form>
    <f.Input />
  </f.Form>
{{/form-helpers}}

@samselikoff said:

Ultimately I just wanted to make sure that an ES6 module-based API was considered and discussed, even if we ultimately move in the direction of making our own abstraction.

I believe @wycats and @dgeb are exploring a design on that front. I think there are notable challenges but it is plausible. For example we need a system for services (and arbitrary lookups) to come from addon's as well, so perhaps services become class-based injections instead of name-based? That removes flexibility from the DI/container system. And IMO front-matter or some other syntax-switching would look weird once you try to build single-file components. The Vue system still means swapping to a class-based system, and implies the need for a JS file just to reference components from another package: I think that is a bit clumsy. If there is some uncertainty about using {{use}} for replacing nested components I expect the Vue system would be even worse for that case.

I've been on leave for a few weeks and missed an informal conversation at the most recent F2F that seems to have made an impact on some core peeps. I'm pretty happy with this RFC (I think clarifications to the Ember Data migration section would be nice but the conversations here have covered most of that). It would probably be helpful if some framework core people engaged here. I'll raise it at the next standup I attend regardless.

@dgeb
Copy link
Member

dgeb commented Oct 14, 2018

Just wanted to follow up on what @mixonic said ASAP. It's true that @wycats and I and several others on the core team discussed the implications of using ES imports for templates at the last F2F in Chicago. The more we discussed it, it seemed obvious to us that we won't have legitimately done our due diligence on this design without fully considering this potential solution.

At this point we're doing some exploratory work right now, which involves a preliminary design and pulling on threads in the MU design to understand its full implications. We will surface this work as soon as possible if it seems to us like it's got a chance of working well. Either way, we'll report back here so that we can engage with the community, make a final decision, and get some traction on the implementation ASAP.

Thanks to everyone for your patience in what has turned into a very long process.

@lougreenwood
Copy link

lougreenwood commented Nov 22, 2018

I'm leaving this thought here instead of discord for posterity...

One of my concerns around MU/Packages is that it seems to remove the ability to use directories for organising components. Personally, I like to group many components which are used in a similar context together, for example, I currently have a /components/forms/ dir where I place all of my forms - this helps me to:

  • quickly find a component by type
  • easily create new components based on previous implementations which may follow a similar implied interface
  • has benefits for introducing new team members to the project - less cognitive load needed to become familiar with the project shape

When I heard about MU I felt quite upset that this pattern is seemingly disallowed because of the no nested components and the switch to local/global components.

But since then I've started on a new project and I've been exposed a lot to contextual components through some work I did on ember-bootstrap - this work gave me a new perspective and what I think could be a nice way to avoid the problem of seemingly losing nested component organisation in MU-world...

My idea is to make use of the factory pattern for components.

So, in my form example above - currently I invoke a form by doing {{forms/some-fancy-form model=@model}}

However, with the factory pattern, I would create some new <FormFactory /> component. The JS for the component could contain a list of defined local form components which the factory can pull from a cleanly organised sub-dir, so invocation would now look something like:

<FormFactory @form="someFancyForm" @model=@model />
// components/form-factory.js

export default class FormFactoryComponent extends Component {
    someFancyForm="components/forms/some-fancy-form";
    someOtherJuicyForm="components/forms/some-other-juicy-form";

    @argument
    @type("string")
    form = null;

    @argument
    @required
    model = null;

    @computed("form")
    get formComponent() {
        // TODO: validation
        return this[this.form];
    }
}
// templates/form-factory.hbs
{{if hasBlock}}
    {{yield
        (hash form=(component this.formComponent model=@model))
    }}
{{else}}
    {{component this.formComponent model=@model}}
{{/if}}

Developing this idea further, these simple Factory-like components could use some type of custom component using RFC 213 to minimise performance impact - I expect in my case I'll make use of a fair number of these component factories across my application to allow me to keep my semantic component organisation - knowing that these are minimal components likely with no runtime hooks or bindings to add weight could be a bonus.

I'm going to test out this idea over the coming days, but I just wanted to share my idea and get any feedback - maybe I missed something obvious, or maybe people are already doing something similar?

@rtablada
Copy link
Contributor

@lougreenwood

One of my concerns around MU/Packages is that it seems to remove the ability to use directories for organising components.

The idea of the use helper is to allow these nested component trees to still work with the new angle bracket syntax (nested components work with the old curly brace component syntax and the component helper)

@lougreenwood
Copy link

@rtablada

I'm not sure that's the current intention, from all of the discussions I've seen here and in Discord {{use}} won't resolve nested components and any discussion of some kind of implementation for nested invocation should happen after this RFC is implemented and we are more aware of the problem space:

#367 (comment)
#367 (comment)

If I'm wrong, that's even better 🎉

@rtablada
Copy link
Contributor

Bringing some discussion from Discord and fresh 👀 to this RFC.

I think a lot of the discussion here has been about the particulars of the use helper and how it mitigates issues with syntax and module imports.

I think since the syntax of use is the sticking point and question of a lot of discussion here IMO it should be moved to a separate RFC since there seems to be desire for it to be used outside of package/addon invocation.

While the rest of this package RFC requires an import API for addon/package component resolution, IMO it makes more sense to split those out (and maybe indicate any details/changes needed here)

// inject src/services/geo.js
geo: inject(),

// inject node_modules/ember-stripe-service/src/services/store.js
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// inject node_modules/ember-stripe-service/src/services/store.js
// inject node_modules/ember-stripe-service/src/services/stripe.js

@mixonic
Copy link
Sponsor Member Author

mixonic commented Mar 2, 2019

Good morning! I'm closing this RFC today in favor of SFC & Template Import Primitives and other possibly-to-be-written RFCs focused on a new filesystem layout for Ember.

The {{use}} part of the this RFC effectively lost consensus in October of last year, but framework core had to do a lot of soul searching to figure out exactly where to go next. The path still isn't totally clear, but there is a strong desire to have template imports simply be ES imports.

Putting aside the exact design addressing imports: There was some suggestion that this RFC could simply strip out the {{use}} bits and be ready to go. I don't think that is true. For one, the comments here would be largely irrelevant making further discussion confusing. And second, the motivations and constraints this RFC (and the MU RFC) were based on change dramatically given an ES import based template import system.

An "explicit import" system means only explicit imports. In the same way this this RFC suggested removing the implicit resolution of an MU package (an addon's) "main", explicit imports would replace the need for local lookup and even top-level component lookup in a package. You import any component you need, all the time, and always based on the path on disk.

That design obliterates most of the clever bits in the original MU RFC:

  • As mentioned above this RFC already gutted the "main" resolution rule for packages.
  • You don't need local lookup as a resolution rule, instead you import from a subdirectory.
  • Without local lookup you don't need "private collections" in MU. We do lose something, the ability for a component to be private to a specific template, but we eliminate a bunch of design complexity and Ember-specific file layout rules.
  • You don't need implicit lookup at the top level as a resolution rule. If templates are 100% explicit, then a template in an addon doesn't need implicit resolution rules to find its own top-level components. If we remove that behavior then the implicit scoping for JS files (as described in this RFC) and service injections should also be removed. All service injections can explicitly state their package.
  • You obviously don't need {{use}} or any other package+component name invocation. Instead you import a component/helper/modifier from an addon based on it's path on disk in the NPM package.

Furthermore as far as creating a migration path for Ember app code: Templates with no implicit scope don't care about resolution rules, and so don't intersect with a new filesystem layout. Their design doesn't require we put components in src/ui/components/, for example, because everything is imported.

That leaves the migration path for Ember applications to a new filesystem layout wide open. We need to figure out a design for templates that considers layout on disk, and might use it as a carrot, but it decouples templates and component entirely from a resolvable system.

So what is left for MU, or any resolving system, to tackle?

A major fault in Ember's addon story is the merged app/ directory. Much of the MU design tried to build a robust system that could replace that with properly scoped modules. We still need to address that fault.

Addons in Ember often provide only two kinds of resolved modules: Services and components. I've seen models provided as well. I'm unsure about how public the API is, but Ember developers can already inject a service from an addon's addon/ dir with the @ sign:

export default EmberComponent.extend({
  session: inject('my-addon@session')
});

We could RFC the src/ dir design for services as in this RFC with explicit packages:

export default EmberComponent.extend({
  session: inject({ package: 'my-addon' })
});

...but if this is the only impact of that change I doubt it is worth adding src/ to addons. There is maybe some middle ground here to be explored.

In general src/ may end up as a tool to help Ember nudge and lint toward new best practices on the file system like co-located tests, single-file components, addon-providede styles, and co-located routes and templates. I think it may be possible to do some of that design work in parallel to the template imports design, but it certainly would not be a simple stripping of {{use}} from this RFC.

Thank you to the community members and contributors who helped hash out the nitty-gritty of this design! By fleshing out these ideas so concretely we demonstrated the best of the RFC process: We rejected the design before it caused churn for our apps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.