Start Date Release Date Release Versions PR link Tracking Link Stage Teams
12/26/2023
Accepted
  • Framework
  • TypeScript

Deprecate named inject export from @ember/service

Summary

As of ember-source@4.1 (and RFC#752), inject is an old alias that's no longer needed

Motivation

import { service } from '@ember/service' makes more sense than import { inject as service } from '@ember/service'

This allows us to slim down our public API surface area to more of what's needed.

Transition Path

Most folks can do a mass find and replace switch from inject as service to just service.

An example codemod could look something like this

export const parser = 'ts'

export default function transformer(file, api) {
  const j = api.jscodeshift;

  const importNames = new Set();

  const root = j(file.source);

  // find things we want to get rid of
  root
    .find(j.ImportSpecifier)
    .forEach(path => {
      if (path.node.imported.name === 'inject') {
          importNames.add(path.node.local.name);
      }
    })

  // now it's time to replace
  root.find(j.ClassProperty).forEach(path => {
    let node = path.node;

    let hasInject = hasDecorators(node, [...importNames.values()]);

    if (!hasInject) return;

    node.decorators = node.decorators.map(decorator => {
      let { expression } = decorator;

      if (expression.type === 'Identifier') {
        if (importNames.has(expression.name)) {
          decorator.expression = j.identifier('service');
        }
      }

      if (expression.type === 'CallExpression') {
        decorator.expression.callee = j.identifier('service');
      }

      return decorator;
    });
  });

  return root.toSource();
}

// Copied from: https://github.com/NullVoxPopuli/ember-concurrency-codemods/tree/main
function firstMatchingDecorator(node, named = []) {
  if (!node.decorators) return;

  return node.decorators.find((decorator) => {
    let { expression } = decorator;

    switch (expression.type) {
      case 'MethodDefinition': {
      }
      case 'CallExpression': {
        let { callee } = expression;

        switch (callee.type) {
          case 'Identifier':
            return named.includes(callee.name);
          case 'MemberExpression': {
            let { object } = callee;

            return named.includes(object.callee.name);
          }
        }
      }
      case 'Identifier':
        return named.includes(expression.name);
    }
  });
}

function hasDecorators(node, named = []) {
  return Boolean(firstMatchingDecorator(node, named));
}

The test scenarios

import { inject } from '@ember/service';
import { inject as service } from '@ember/service';
// import Service from '@ember/service';
import BaseService from '@ember/service';
import { inject as serviceDecorator } from '@ember/service';
import { inject as x } from '@ember/service';
// import { service } from '@ember/service';
import { service as y } from '@ember/service';
// import Service, { inject, service } from '@ember/service';
import Service, { inject as s } from '@ember/service';


export default class Demo extends Service {

}

export default class Demo2 extends BaseService {
  // simple
  @inject router;
  @service router1;
  @x router2;
  @y router3;
  @serviceDecorator router4;
  @inject('router') router41;

  // TS-only
  @inject declare router5: Type;
  @inject('router') declare router51: Type;
  @service declare router6: Type;
  @x declare router7: Type;
  @y declare router8: Type;
  @serviceDecorator declare router9: Type;
}

How We Teach This

The docs / guides already use the new import path.

Drawbacks

As with any deprecation, we introduce an upgrade cliff for addons that are updated infrequently, and consequently their consuming apps. As a mitigation, we could, for v1 addons, add an additional transform to ember-cli-babel to automatically upgrade inject from @ember/service to service. This does narrow the range a bit, as service was introduced in ember-source@4.1, so libraries could not support from 3.28 to 6 (or whichever major ends up removing the inject) without adding @embroider/macros to conditionally import inject or service based on the consumer's ember-source version.

Alternatives

do nothing, the cost of an export alias is:

  • a few extra bytes
  • mental gymnastics for teaching
  • "another case to cover" for tooling

add a lint against inject

  • all the downsides of the above ("do nothing") may still be present

Unresolved questions

n/a