Default Strict Resolver
Summary
This RFC proposes shipping a built-in (opt-in) strict resolver as the default in Ember applications, replacing the current ember-resolver
package. The strict resolver uses explicit module registration through import.meta.glob('...', { eager: true })
(or manually) instead of dynamic string-based lookups, providing better tree-shaking, build-time optimization, and improved developer experience with static analysis tools.
Motivation
The current ember-resolver
requires that an Application
has a modulePrefix, and that every resolver registration begins with that modulePrefix
. As we start to move towards strict ESM applications, this requirement is unneeded. Previously, if folks wanted to use import.meta.glob()
in their vite apps (or test-apps (or minimal apps)), they would have to iterate the result of import.meta.glob
to prepend the modulePrefix
-- which is entirely boilerplate.
[!NOTE] The strict resolver does not affect existing apps using broccoli or
@embroider/virtual/compat-modules
, though users of those environments could opt in to this new behavior.
Detailed design
The strict resolver will be built into Ember applications and imported from @ember/application
instead of requiring a separate ember-resolver
package. Applications will register modules explicitly using a modules
property containing import map objects.
[!NOTE] This keeps all existing default ember-resolver semantics -- locking down the behaviors as the stable legacy thing and won't change and will no longer be extensible. (e.g.: no custom resolvers will be allowed with the
modules = {}
object.
Example:
// Before | with ember-resolver
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import config from 'my-app/config/environment';
export default class App extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver.withModules({
[`${config.modulePrefix}/router`]: { default: Router },
[`${config.modulePrefix}/services/current-user`]: { default: CurrentUserService },
// potentially thousands of manual entries
// and all imports need to be at the top of the file as well -- doubling the lines per resolver entries
});
}
// After:
import Application from '@ember/application';
import Router from './router';
import config from 'my-app/config/environment';
export default class App extends Application {
modules = {
'./router': { default: Router },
'./services/current-user': { default: CurrentUserService },
// author-time is easy
...import.meta.glob('./services/**/*', { eager: true }),
...import.meta.glob('./routes/*', { eager: true }),
...import.meta.glob('./controllers/*', { eager: true }),
...import.meta.glob('./templates/**/*', { eager: true }),
};
}
Module Registration Format
The modules
object maps module paths to either:
- Module objects with named exports:
{ default: ExportableType, namedExport: AnotherType }
- Direct exports for default-only modules:
ExportableType
modules = {
// Full module object
'./services/current-user': { default: CurrentUserService },
'./components/user-avatar': { default: UserAvatarComponent },
// Shorthand for default exports
'./services/session': SessionService,
// Automatic splat via import.meta.glob
...import.meta.glob('./services/**/*', { eager: true }),
}
How we teach this
API Docs will explain the above usages.
We aren't ready to add this to the guides as the community may not be ready for "minimal app" (which may need to be a different app blueprint from the current @ember/app-blueprint
).
Also, the existing guides have no information on resolving or ember-resolver.
For folks that wish to adopt this pattern today, they can use the polyfill
Appendix
Libraries can provide helper functions for registering their modules, services, etc:
// Library provides a registry builder
import { buildRegistry } from 'ember-strict-application-resolver/build-registry';
import libraryRegistry from 'some-library/registry';
export default class App extends Application {
modules = {
...import.meta.glob('./services/**/*', { eager: true }),
...libraryRegistry(), // Library-provided modules
'./services/custom': CustomService,
};
}
See: the polyfill for more information.
However, in existing projects, it's perfectly safe to use compat-modules
from @embroider/virtual
[^pending-embroider-support]
import compatModules from '@embroider/virtual/compat-modules';
export default class App extends Application {
modules = {
...compatModules,
}
}
[^pending-embroider-support]: embroider currently prefixes the compat modules with the modulePrefix
. That won't exist, so embroider needs to ship a feature that detects this usage of Application
, and omits the modulePrefix
Drawbacks
n/a - folks can keep using ember-resolver. ember-resolver is not deprecated.
Unresolved questions
n/a