Add AGENTS.md and Ember Best Practices skill
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: Prefer Named Exports, Fallback to Default for Implicit Template Lookup
|
||||
impact: LOW
|
||||
impactDescription: Clear export contracts across .hbs and template-tag codebases
|
||||
tags: exports, hbs, gjs, interop, code-organization
|
||||
---
|
||||
|
||||
## Prefer Named Exports, Fallback to Default for Implicit Template Lookup
|
||||
|
||||
Use named exports for shared modules imported directly in JS/TS (utilities, constants, pure functions). If a module should be invokable from `.hbs` templates via implicit lookup, provide a default export. In hybrid `.gjs`/`.hbs` projects, a practical pattern is a named export plus a default export alias.
|
||||
|
||||
**Incorrect (default export in a shared utility module):**
|
||||
|
||||
```javascript
|
||||
// app/utils/format-date.js
|
||||
export default function formatDate(date) {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (named export in a shared utility module):**
|
||||
|
||||
```javascript
|
||||
// app/utils/format-date.js
|
||||
export function formatDate(date) {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (hybrid `.gjs`/`.hbs` named export + default alias):**
|
||||
|
||||
```javascript
|
||||
// app/helpers/format-date.js
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export const formatDate = helper(([value]) => {
|
||||
return new Date(value).toLocaleDateString();
|
||||
});
|
||||
|
||||
export default formatDate;
|
||||
```
|
||||
|
||||
## Where Named Exports Are Preferred
|
||||
|
||||
Use named exports when the module is imported directly by other modules and is not resolved via implicit template lookup.
|
||||
|
||||
**Example (utility module with multiple named exports):**
|
||||
|
||||
```javascript
|
||||
// app/utils/validators.js
|
||||
export function isEmail(value) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||||
}
|
||||
|
||||
export function isPhoneNumber(value) {
|
||||
return /^\d{3}-\d{3}-\d{4}$/.test(value);
|
||||
}
|
||||
```
|
||||
|
||||
Benefits:
|
||||
|
||||
1. Explicit import contracts
|
||||
2. Better refactor safety (symbol rename tracking)
|
||||
3. Better tree-shaking for utility modules
|
||||
4. Easier multi-export module organization
|
||||
|
||||
## Where Default Exports Are Required
|
||||
|
||||
Use default exports for modules consumed through resolver/template lookup.
|
||||
If your project uses `.hbs`, invokables that should be accessible from templates should provide `export default`.
|
||||
In hybrid `.gjs`/`.hbs` codebases, use named exports plus a default export alias where you want both explicit imports and template compatibility.
|
||||
|
||||
**Service:**
|
||||
|
||||
```javascript
|
||||
// app/services/auth.js
|
||||
import Service from '@ember/service';
|
||||
|
||||
export default class AuthService extends Service {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Route:**
|
||||
|
||||
```javascript
|
||||
// app/routes/dashboard.js
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class DashboardRoute extends Route {
|
||||
@service store;
|
||||
|
||||
model() {
|
||||
return this.store.findAll('dashboard-item');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modifier (when invoked from `.hbs`):**
|
||||
|
||||
```javascript
|
||||
// app/modifiers/focus.js
|
||||
import { modifier } from 'ember-modifier';
|
||||
|
||||
export default modifier((element) => {
|
||||
element.focus();
|
||||
});
|
||||
```
|
||||
|
||||
**Template (`.gjs`):**
|
||||
|
||||
```glimmer-js
|
||||
// app/templates/dashboard.gjs
|
||||
<template>
|
||||
<h1>Dashboard</h1>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Template (`.gts`):**
|
||||
|
||||
```glimmer-ts
|
||||
// app/templates/dashboard.gts
|
||||
import type { TOC } from '@ember/component/template-only';
|
||||
|
||||
interface Signature {
|
||||
Args: {
|
||||
model: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export default <template>
|
||||
<h1>Dashboard</h1>
|
||||
</template> satisfies TOC<Signature>;
|
||||
```
|
||||
|
||||
Template-tag files must resolve via a module default export in convention-based and `import.meta.glob` flows.
|
||||
For `app/templates/*.gjs`, the default export is implicit after compilation.
|
||||
|
||||
## Strict Resolver Nuance
|
||||
|
||||
With `ember-strict-application-resolver`, you can register explicit module values in `App.modules`:
|
||||
|
||||
**Strict resolver explicit modules registration:**
|
||||
|
||||
```ts
|
||||
modules = {
|
||||
'./services/manual': { default: ManualService },
|
||||
'./services/manual-shorthand': ManualService,
|
||||
};
|
||||
```
|
||||
|
||||
In that explicit shorthand case, a direct value works without a default-exported module object.
|
||||
This is an explicit registration escape hatch and does not replace default-export requirements for `.hbs`-invokable modules.
|
||||
|
||||
## Rule of Thumb
|
||||
|
||||
1. If a module should be invokable from `.hbs`, provide a default export.
|
||||
2. In hybrid `.gjs`/`.hbs` projects, use named export + default alias for resolver-facing modules.
|
||||
3. Strict resolver explicit `modules` entries may use direct shorthand values where appropriate.
|
||||
4. Plain shared modules (`app/utils`, shared constants, reusable pure functions): prefer named exports.
|
||||
5. Template-tag components (`.gjs`/`.gts`): follow the component file-conventions rule and use named class exports.
|
||||
|
||||
## References
|
||||
|
||||
- [ES Modules Best Practices](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
|
||||
- [ember-strict-application-resolver](https://github.com/ember-cli/ember-strict-application-resolver)
|
||||
- [ember-resolver](https://github.com/ember-cli/ember-resolver)
|
||||
Reference in New Issue
Block a user