Getting addons ready to support First-Class Component Templates

A public service announcement for addon authors:

First-class component templates is a merged RFC and already has a working implementation. Users are going to want to consume your components from this new style of templates.

Mostly that will Just Work, but there are a few things you may not have considered.

  1. Use component template co-location. This isn’t just nice for developer ergonomics – it makes components work as first-class values. Without it, your component can’t be passed around as a value in Javascript without losing its template.

    The older pattern of setting layout on an @ember/component also works correctly as a first-class value, since it carries its template within it. But this only works on the older style Ember components, not Octane-style Glimmer components, which is why I suggest co-location for all components of either kind.

    The case that does not work is separately providing app/components/thing.js and app/templates/components/thing.hbs from your addon. For an example of fixing this pattern, see Co-locate component templates by ef4 · Pull Request #661 · cibernox/ember-basic-dropdown · GitHub.

  2. Make sure the components (and helpers and modifiers) in your /addon folder have reasonable filenames, because those are going to be what people import from to use them.

    It used to really not matter how you organized these, because only the reexports in the /app folder governed what consumers of your addon would have access to. But we’re moving toward not needing /app folder merging anymore. Think of your addon more as a normal NPM library whose modules will be available to consuming packages.

    If you would like strong control over which modules in your package are public API and which are private, consider adopting V2 addon format and using the exports key in package.json to control it explicitly. V2 addons fully support that feature, V1 (traditional) addons do not.

    While you are free to organize your files within /addon however you want and it will work, it’s still a good idea to stick to the convention of your-addon/components/thing being the import path for the Thing component from your-addon, because it’s predictable.

    While it’s possible to export or reexport many components from a single file, this will defeat certain optimizations as we move toward a world where browsers natively load ES modules. They will load whole modules, so if users are likely to only want some of your components, you should put those components into separate ES modules.

First-class templates plus Glint is looking really nice. If you haven’t had a chance to play with it give it a go.

9 Likes

I certainly agree with everything said here, but say someone (consuming app) is in an embroider environment / already using a modern packager – how would one go about enabling tree-shaking?, or dealing with extra-exports packages that includes maybe 90% of stuff that isn’t used?

I know addon-authors can, by “just not providing a mono-re-exports file”, can prevent the need for the above, but it’s not always possible in big organizations where nearly anyone can make (or add to) an addon (and PR reviewers are not up to date on the current best patterns), or maybe the _best DX for the situation is to setup your import {} from 'my-library' and let intellisense tell you everything in the library? – in these scenarios, tree-shaking seems like an option (I assume trading production build speed for smaller JS size)

Webpack’s docs have this page: Tree Shaking | webpack

I haven’t tried setting

 optimization: {
   usedExports: true,
 },

in an ember app though. Will try to remember to try this and report back.

Looks like on that same docs page though, addon-athors can specify in their package.json:

{
  "name": "your-project",
  "sideEffects": false
}

sideEffects is much more effective since it allows to skip whole modules/files and the complete subtree.

For me, since I have a couple addons that register managers, I think I’d need to set:

{
  "name": "your-project",
  "sideEffects": ["./dist/initializers/setup-helper-manager.js"]
}

(because a side-effect is something that executes in module-space, such as setHelperManager)

sideEffects false doesn’t work reliably for us yet.

The problem is that in many cases both embroider and ember-auto-import have to take advantage of webpack’s CJS support to get compatibility with older ember APIs. That means instead of importing the dependency, we require it. This defeats the in-module tree shaking.

1 Like