feat(core): introduce @Service decorator

These changes introduce the new `@Service` decorator which is a more ergonomic alternative to `@Injectable`. The reason we're adding a new decorator is that `@Injectable` has been around since the beginning of Angular and it has a lot of baggage that adds unnecessary overhead for users that generally want to define a singleton service, available in their entire app. The key differences between `@Service` and `@Injectable` are:
1. `@Service` is `providedIn: 'root'` by default. You can opt into providing the service yourself by setting `autoProvided: false` on it.
2. `@Service` doesn't allow constructor-based injection, only the `inject` function.
3. `@Service` doesn't support the complex type signature of `@Injectable` (`useClass`, `useValue` etc.). Instead it supports a single `factory` function.

Example:

```ts
import {Service} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AuthService} from './auth';

@Service()
export class PostService {
  private readonly httpClient = inject(HttpClient);
  private readonly authService = inject(AuthService);

  getUserPosts() {
    return this.httpClient.get('/api/posts/' + this.authService.userId);
  }
}
```
This commit is contained in:
Kristiyan Kostadinov
2026-04-14 10:43:14 +02:00
committed by Leon Senft
parent b395173cf2
commit 8f3d0b9d97
42 changed files with 1502 additions and 10 deletions
@@ -23,6 +23,7 @@ import {PartialNgModuleLinkerVersion1} from './partial_ng_module_linker_1';
import {PartialPipeLinkerVersion1} from './partial_pipe_linker_1';
import {PLACEHOLDER_VERSION} from './util';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system/src/types';
import {PartialServiceLinkerVersion1} from './partial_service_linker_1';
export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective';
export const ɵɵngDeclareClassMetadata = 'ɵɵngDeclareClassMetadata';
@@ -33,6 +34,7 @@ export const ɵɵngDeclareInjector = 'ɵɵngDeclareInjector';
export const ɵɵngDeclareNgModule = 'ɵɵngDeclareNgModule';
export const ɵɵngDeclarePipe = 'ɵɵngDeclarePipe';
export const ɵɵngDeclareClassMetadataAsync = 'ɵɵngDeclareClassMetadataAsync';
export const ɵɵngDeclareService = 'ɵɵngDeclareService';
export const declarationFunctions = [
ɵɵngDeclareDirective,
ɵɵngDeclareClassMetadata,
@@ -43,6 +45,7 @@ export const declarationFunctions = [
ɵɵngDeclareNgModule,
ɵɵngDeclarePipe,
ɵɵngDeclareClassMetadataAsync,
ɵɵngDeclareService,
];
export interface LinkerRange<TExpression> {
@@ -119,6 +122,9 @@ export function createLinkerMap<TStatement, TExpression, TType>(
linkers.set(ɵɵngDeclarePipe, [
{range: LATEST_VERSION_RANGE, linker: new PartialPipeLinkerVersion1()},
]);
linkers.set(ɵɵngDeclareService, [
{range: LATEST_VERSION_RANGE, linker: new PartialServiceLinkerVersion1()},
]);
return linkers;
}
@@ -0,0 +1,59 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
compileService,
ConstantPool,
R3DeclareServiceMetadata,
R3PartialDeclaration,
R3ServiceMetadata,
} from '@angular/compiler';
import {AstObject} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {LinkedDefinition, PartialLinker} from './partial_linker';
import {wrapReference} from './util';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareService()` call expressions.
*/
export class PartialServiceLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>,
): LinkedDefinition {
const meta = toR3ServiceMeta(metaObj);
return compileService(meta, /* resolveForwardRefs */ false);
}
}
/**
* Derives the `R3ServiceMetadata` structure from the AST object.
*/
export function toR3ServiceMeta<TExpression>(
metaObj: AstObject<R3DeclareServiceMetadata, TExpression>,
): R3ServiceMetadata {
const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName();
if (typeName === null) {
throw new FatalLinkerError(
typeExpr.expression,
'Unsupported type, its name could not be determined',
);
}
const meta: R3ServiceMetadata = {
name: typeName,
type: wrapReference(typeExpr.getOpaque()),
typeArgumentCount: 0,
autoProvided: metaObj.has('autoProvided') ? metaObj.getBoolean('autoProvided') : undefined,
factory: metaObj.has('factory') ? metaObj.getOpaque('factory') : undefined,
};
return meta;
}
@@ -0,0 +1,318 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
compileClassMetadata,
CompileClassMetadataFn,
compileDeclareClassMetadata,
compileDeclareServiceFromMetadata,
compileService,
FactoryTarget,
R3ClassMetadata,
R3CompiledExpression,
R3ServiceMetadata,
WrappedNodeExpr,
} from '@angular/compiler';
import ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics';
import {PartialEvaluator} from '../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../perf';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {
AnalysisOutput,
CompilationMode,
CompileResult,
DecoratorHandler,
DetectResult,
HandlerPrecedence,
ResolveResult,
} from '../../transform';
import {
compileDeclareFactory,
CompileFactoryFn,
compileNgFactoryDefField,
extractClassMetadata,
findAngularDecorator,
readBaseClass,
toFactoryMetadata,
wrapTypeReference,
} from '../common';
export interface ServiceHandlerData {
meta: R3ServiceMetadata;
classMetadata: R3ClassMetadata | null;
}
/**
* Adapts the `compileService` compiler for `@Service` decorators to the Ivy compiler.
*/
export class ServiceDecoratorHandler implements DecoratorHandler<
Decorator,
ServiceHandlerData,
null,
unknown
> {
constructor(
private reflector: ReflectionHost,
private evaluator: PartialEvaluator,
private isCore: boolean,
private perf: PerfRecorder,
private includeClassMetadata: boolean,
private readonly compilationMode: CompilationMode,
) {}
readonly precedence = HandlerPrecedence.SHARED;
readonly name = 'ServiceDecoratorHandler';
detect(
node: ClassDeclaration,
decorators: Decorator[] | null,
): DetectResult<Decorator> | undefined {
if (!decorators) {
return undefined;
}
const decorator = findAngularDecorator(decorators, 'Service', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,
decorator: decorator,
metadata: decorator,
};
} else {
return undefined;
}
}
analyze(
node: ClassDeclaration,
decorator: Readonly<Decorator>,
): AnalysisOutput<ServiceHandlerData> {
this.perf.eventCount(PerfEvent.AnalyzeService);
const decorators = this.reflector.getDecoratorsOfDeclaration(node);
let diagnostics: ts.Diagnostic[] | undefined;
if (decorators !== null && decorators.length > 1) {
const nonServiceDecorator = decorators.find(
(decorator) => decorator.import?.from === '@angular/core' && decorator.name !== 'Service',
);
if (nonServiceDecorator) {
diagnostics ??= [];
diagnostics.push(
makeDiagnostic(
ErrorCode.DECORATOR_COLLISION,
nonServiceDecorator.node,
'Cannot apply more than one Angular decorator on an @Service class.',
),
);
}
}
return {
analysis: {
meta: this.extractServiceMetadata(node, decorator),
classMetadata: this.includeClassMetadata
? extractClassMetadata(node, this.reflector, this.isCore)
: null,
},
diagnostics,
};
}
symbol(): null {
return null;
}
resolve(node: ClassDeclaration): ResolveResult<unknown> {
const diagnostics = this.getDependencyInjectionDiagnostics(node);
return diagnostics === null ? {} : {diagnostics};
}
compileFull(node: ClassDeclaration, analysis: Readonly<ServiceHandlerData>): CompileResult[] {
return this.compile(
compileNgFactoryDefField,
(meta) => compileService(meta, false),
compileClassMetadata,
node,
analysis,
);
}
compilePartial(node: ClassDeclaration, analysis: Readonly<ServiceHandlerData>): CompileResult[] {
return this.compile(
compileDeclareFactory,
compileDeclareServiceFromMetadata,
compileDeclareClassMetadata,
node,
analysis,
);
}
compileLocal(node: ClassDeclaration, analysis: Readonly<ServiceHandlerData>): CompileResult[] {
return this.compile(
compileNgFactoryDefField,
(meta) => compileService(meta, false),
compileClassMetadata,
node,
analysis,
);
}
private compile(
compileFactoryFn: CompileFactoryFn,
compileServiceFn: (meta: R3ServiceMetadata) => R3CompiledExpression,
compileClassMetadataFn: CompileClassMetadataFn,
node: ClassDeclaration,
analysis: Readonly<ServiceHandlerData>,
): CompileResult[] {
const results: CompileResult[] = [];
const meta = analysis.meta;
const factoryRes = compileFactoryFn(
toFactoryMetadata({...meta, deps: []}, FactoryTarget.Service),
);
if (analysis.classMetadata !== null) {
factoryRes.statements.push(compileClassMetadataFn(analysis.classMetadata).toStmt());
}
results.push(factoryRes);
const ɵprov = this.reflector.getMembersOfClass(node).find((member) => member.name === 'ɵprov');
if (ɵprov !== undefined) {
throw new FatalDiagnosticError(
ErrorCode.INJECTABLE_DUPLICATE_PROV,
ɵprov.nameNode || ɵprov.node || node,
'Services cannot contain a static ɵprov property, because the compiler is going to generate one.',
);
}
if (ɵprov === undefined) {
// Only add a new ɵprov if there is not one already
const res = compileServiceFn(analysis.meta);
results.push({
name: 'ɵprov',
initializer: res.expression,
statements: res.statements,
type: res.type,
deferrableImports: null,
});
}
return results;
}
/**
* Read metadata from the `@Service` decorator and produce the metadata needed to run
* `compileService`.
*
* A `null` return value indicates this is `@Service` has invalid data.
*/
private extractServiceMetadata(clazz: ClassDeclaration, decorator: Decorator): R3ServiceMetadata {
const name = clazz.name.text;
const type = wrapTypeReference(clazz);
const typeArgumentCount = this.reflector.getGenericArityOfClass(clazz) || 0;
if (decorator.args === null) {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_NOT_CALLED,
decorator.node,
'@Service must be called',
);
}
if (decorator.args.length === 0) {
return {
name,
type,
typeArgumentCount,
};
} else if (decorator.args.length === 1) {
const metaNode = decorator.args[0];
if (!ts.isObjectLiteralExpression(metaNode)) {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARG_NOT_LITERAL,
metaNode,
'@Service argument must be an object literal',
);
}
// Resolve the fields of the literal into a map of field name to expression.
const meta = reflectObjectLiteral(metaNode);
let autoProvided: boolean | undefined;
if (meta.has('autoProvided')) {
const value = meta.get('autoProvided')!;
if (value.kind !== ts.SyntaxKind.TrueKeyword && value.kind !== ts.SyntaxKind.FalseKeyword) {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE,
metaNode,
'`autoProvided` property must be a boolean literal',
);
}
autoProvided = value.kind === ts.SyntaxKind.TrueKeyword;
}
const result: R3ServiceMetadata = {name, type, typeArgumentCount, autoProvided};
if (meta.has('factory')) {
result.factory = new WrappedNodeExpr(meta.get('factory')!);
}
return result;
} else {
throw new FatalDiagnosticError(
ErrorCode.DECORATOR_ARITY_WRONG,
decorator.args[2],
'Too many arguments to @Service',
);
}
}
private getDependencyInjectionDiagnostics(node: ClassDeclaration): ts.Diagnostic[] | null {
const ownCtorParams = this.reflector.getConstructorParameters(node);
if (ownCtorParams !== null) {
return ownCtorParams.length > 0 ? [makeConstructorDependencyInjectionDiagnostic(node)] : null;
}
// Don't traverse parent classes in local mode.
if (this.compilationMode === CompilationMode.LOCAL) {
return null;
}
let baseClass = readBaseClass(node, this.reflector, this.evaluator);
while (baseClass !== null) {
if (baseClass === 'dynamic') {
return null;
}
const baseCtorParams = this.reflector.getConstructorParameters(baseClass.node);
if (baseCtorParams !== null) {
return baseCtorParams.length > 0
? [makeConstructorDependencyInjectionDiagnostic(node)]
: null;
}
baseClass = readBaseClass(baseClass.node, this.reflector, this.evaluator);
}
return null;
}
}
function makeConstructorDependencyInjectionDiagnostic(node: ClassDeclaration): ts.Diagnostic {
return makeDiagnostic(
ErrorCode.SERVICE_CONSTRUCTOR_DI,
node.name,
'@Service class cannot use constructor dependency injection. Use the `inject` function instead.',
);
}
@@ -127,6 +127,7 @@ import {DiagnosticCategoryLabel, NgCompilerAdapter, NgCompilerOptions} from '../
import {untagAllTsFiles} from '../../shims';
import {angularJitApplicationTransform} from '../../transform/jit';
import {coreVersionSupportsFeature} from './feature_detection';
import {ServiceDecoratorHandler} from '../../annotations/src/service';
/**
* State information about a compilation which is only generated once some data is requested from
@@ -1579,6 +1580,14 @@ export class NgCompiler {
!!this.options.strictStandalone,
this.implicitStandaloneValue,
),
new ServiceDecoratorHandler(
reflector,
evaluator,
isCore,
this.delegatingPerfRecorder,
supportTestBed,
compilationMode,
),
new InjectableDecoratorHandler(
reflector,
evaluator,
@@ -189,6 +189,11 @@ export enum ErrorCode {
*/
COMPONENT_ANIMATIONS_CONFLICT = 2027,
/**
* Raised when an `@Service` class is using constructor dependency injection.
*/
SERVICE_CONSTRUCTOR_DI = 2028,
SYMBOL_NOT_EXPORTED = 3001,
/**
* Raised when a relationship between directives and/or pipes would cause a cyclic import to be
@@ -234,6 +234,11 @@ export enum PerfEvent {
*/
AnalyzePipe,
/**
* An `@Service` class was analyzed.
*/
AnalyzeService,
/**
* A trait was analyzed.
*
@@ -298,7 +303,7 @@ export enum PerfEvent {
EmitSourceFile,
/**
* Tracks the number of `PrefEvent`s, and must appear at the end of the list.
* Tracks the number of `PerfEvent`s, and must appear at the end of the list.
*/
LAST,
}
@@ -0,0 +1,123 @@
/****************************************************************************************************
* PARTIAL FILE: basic_service.js
****************************************************************************************************/
import { Service } from '@angular/core';
import * as i0 from "@angular/core";
export class MyService {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Service });
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, decorators: [{
type: Service
}] });
/****************************************************************************************************
* PARTIAL FILE: basic_service.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyService {
static ɵfac: i0.ɵɵFactoryDeclaration<MyService, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<MyService>;
}
/****************************************************************************************************
* PARTIAL FILE: service_with_factory.js
****************************************************************************************************/
import { Service } from '@angular/core';
import * as i0 from "@angular/core";
class Alternate {
}
export class MyService {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Service });
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, factory: () => new Alternate() });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, decorators: [{
type: Service,
args: [{ factory: () => new Alternate() }]
}] });
/****************************************************************************************************
* PARTIAL FILE: service_with_factory.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyService {
static ɵfac: i0.ɵɵFactoryDeclaration<MyService, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<MyService>;
}
/****************************************************************************************************
* PARTIAL FILE: not_provided_service.js
****************************************************************************************************/
import { Service } from '@angular/core';
import * as i0 from "@angular/core";
export class MyService {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Service });
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, autoProvided: false });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, decorators: [{
type: Service,
args: [{ autoProvided: false }]
}] });
/****************************************************************************************************
* PARTIAL FILE: not_provided_service.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyService {
static ɵfac: i0.ɵɵFactoryDeclaration<MyService, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<MyService>;
}
/****************************************************************************************************
* PARTIAL FILE: explicitly_provided_service.js
****************************************************************************************************/
import { Service } from '@angular/core';
import * as i0 from "@angular/core";
export class MyService {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Service });
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, decorators: [{
type: Service,
args: [{ autoProvided: true }]
}] });
/****************************************************************************************************
* PARTIAL FILE: explicitly_provided_service.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyService {
static ɵfac: i0.ɵɵFactoryDeclaration<MyService, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<MyService>;
}
/****************************************************************************************************
* PARTIAL FILE: generic_service.js
****************************************************************************************************/
import { Service } from '@angular/core';
import * as i0 from "@angular/core";
export class MyService {
getOne() {
return null;
}
getTwo() {
return null;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, deps: [], target: i0.ɵɵFactoryTarget.Service });
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyService, decorators: [{
type: Service
}] });
/****************************************************************************************************
* PARTIAL FILE: generic_service.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyService<T extends number, V = T> {
getOne(): T;
getTwo(): V;
static ɵfac: i0.ɵɵFactoryDeclaration<MyService<any, any>, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<MyService<any, any>>;
}
@@ -0,0 +1,80 @@
{
"$schema": "../test_case_schema.json",
"cases": [
{
"description": "should compile a basic service",
"inputFiles": ["basic_service.ts"],
"expectations": [
{
"files": [
{
"expected": "basic_service.js",
"generated": "basic_service.js"
}
],
"failureMessage": "Incorrect service"
}
]
},
{
"description": "should compile a service with a factory",
"inputFiles": ["service_with_factory.ts"],
"expectations": [
{
"files": [
{
"expected": "service_with_factory.js",
"generated": "service_with_factory.js"
}
],
"failureMessage": "Incorrect service"
}
]
},
{
"description": "should compile a service with `autoProvided: false`",
"inputFiles": ["not_provided_service.ts"],
"expectations": [
{
"files": [
{
"expected": "not_provided_service.js",
"generated": "not_provided_service.js"
}
],
"failureMessage": "Incorrect service"
}
]
},
{
"description": "should compile a service with `autoProvided: true`",
"inputFiles": ["explicitly_provided_service.ts"],
"expectations": [
{
"files": [
{
"expected": "explicitly_provided_service.js",
"generated": "explicitly_provided_service.js"
}
],
"failureMessage": "Incorrect service"
}
]
},
{
"description": "should compile a service with generic arguments",
"inputFiles": ["generic_service.ts"],
"expectations": [
{
"files": [
{
"expected": "generic_service.js",
"generated": "generic_service.js"
}
],
"failureMessage": "Incorrect service"
}
]
}
]
}
@@ -0,0 +1,8 @@
export class MyService {
static ɵfac = function MyService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MyService)(); };
static ɵprov = /*@__PURE__*/ $r3$.ɵɵdefineService({ token: MyService, factory: MyService.ɵfac });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && $r3$.ɵsetClassMetadata(MyService, [{
type: Service
}], null, null); })();
@@ -0,0 +1,4 @@
import {Service} from '@angular/core';
@Service()
export class MyService {}
@@ -0,0 +1,9 @@
export class MyService {
static ɵfac = function MyService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MyService)(); };
static ɵprov = /*@__PURE__*/ $r3$.ɵɵdefineService({ token: MyService, factory: MyService.ɵfac });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && $r3$.ɵsetClassMetadata(MyService, [{
type: Service,
args: [{ autoProvided: true }]
}], null, null); })();
@@ -0,0 +1,4 @@
import {Service} from '@angular/core';
@Service({autoProvided: true})
export class MyService {}
@@ -0,0 +1,6 @@
export class MyService {
…
static ɵfac = function MyService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MyService)(); };
static ɵprov = /*@__PURE__*/ i0.ɵɵdefineService({ token: MyService, factory: MyService.ɵfac });
}
@@ -0,0 +1,12 @@
import {Service} from '@angular/core';
@Service()
export class MyService<T extends number, V = T> {
getOne(): T {
return null!;
}
getTwo(): V {
return null!;
}
}
@@ -0,0 +1,9 @@
export class MyService {
static ɵfac = function MyService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MyService)(); };
static ɵprov = /*@__PURE__*/ $r3$.ɵɵdefineService({ token: MyService, factory: MyService.ɵfac, autoProvided: false });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && $r3$.ɵsetClassMetadata(MyService, [{
type: Service,
args: [{ autoProvided: false }]
}], null, null); })();
@@ -0,0 +1,4 @@
import {Service} from '@angular/core';
@Service({autoProvided: false})
export class MyService {}
@@ -0,0 +1,11 @@
class Alternate {}
export class MyService {
static ɵfac = function MyService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MyService)(); };
static ɵprov = /*@__PURE__*/ $r3$.ɵɵdefineService({ token: MyService, factory: () => (() => new Alternate())() });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && $r3$.ɵsetClassMetadata(MyService, [{
type: Service,
args: [{ factory: () => new Alternate() }]
}], null, null); })();
@@ -0,0 +1,6 @@
import {Service} from '@angular/core';
class Alternate {}
@Service({factory: () => new Alternate()})
export class MyService {}
@@ -0,0 +1,127 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
const testFiles = loadStandardTestFiles();
runInEachFileSystem(() => {
describe('@Service decorator', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig();
});
// More thorough compilation tests are in `packages/compiler-cli/test/compliance/test_cases/service_decorator`.
it('should compile an @Service class', () => {
env.write(
'test.ts',
`
import {Service} from '@angular/core';
@Service()
export class TestService {}
`,
);
env.driveMain();
const jsContents = env.getContents('test.js');
const dtsContents = env.getContents('test.d.ts');
expect(jsContents).not.toContain('__decorate');
expect(jsContents).toContain('TestService.ɵfac =');
expect(jsContents).toContain('TestService.ɵprov =');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration<TestService, never>;');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration<TestService>;');
});
it('should report if an @Service class has another Angular decorator', () => {
env.write(
'test.ts',
`
import {Service, Pipe} from '@angular/core';
@Service()
@Pipe({name: 'foo'})
export class TestService {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
'Cannot apply more than one Angular decorator on an @Service class.',
);
});
it('should report if an @Service class uses constructor-based DI from its own constructor', () => {
env.write(
'test.ts',
`
import {Service, ApplicationRef} from '@angular/core';
@Service()
export class TestService {
constructor(appRef: ApplicationRef) {}
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
'@Service class cannot use constructor dependency injection. Use the `inject` function instead.',
);
});
it('should report if an @Service class uses constructor-based DI from an inherited constructor', () => {
env.write(
'grandparent.ts',
`
import {Injectable, ApplicationRef} from '@angular/core';
@Injectable()
export class GrandparentService {
constructor(appRef: ApplicationRef) {}
}
`,
);
env.write(
'parent.ts',
`
import {Injectable} from '@angular/core';
import {GrandparentService} from './grandparent';
@Injectable()
export class ParentService extends GrandparentService {}
`,
);
env.write(
'test.ts',
`
import {Service} from '@angular/core';
import {ParentService} from './parent';
@Service()
export class TestService extends ParentService {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(
'@Service class cannot use constructor dependency injection. Use the `inject` function instead.',
);
});
});
});