Files
marco/.agents/skills/ember-best-practices/rules/exports-named-with-default-fallback.md

4.7 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Prefer Named Exports, Fallback to Default for Implicit Template Lookup LOW Clear export contracts across .hbs and template-tag codebases 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):

// app/utils/format-date.js
export default function formatDate(date) {
  return new Date(date).toLocaleDateString();
}

Correct (named export in a shared utility module):

// app/utils/format-date.js
export function formatDate(date) {
  return new Date(date).toLocaleDateString();
}

Correct (hybrid .gjs/.hbs named export + default alias):

// 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):

// 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:

// app/services/auth.js
import Service from '@ember/service';

export default class AuthService extends Service {
  // ...
}

Route:

// 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):

// app/modifiers/focus.js
import { modifier } from 'ember-modifier';

export default modifier((element) => {
  element.focus();
});

Template (.gjs):

// app/templates/dashboard.gjs
<template>
  <h1>Dashboard</h1>
</template>

Template (.gts):

// 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:

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