mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Leon Senft
parent
b395173cf2
commit
8f3d0b9d97
+6
@@ -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;
|
||||
}
|
||||
|
||||
+59
@@ -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 {}
|
||||
+9
@@ -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); })();
|
||||
+4
@@ -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!;
|
||||
}
|
||||
}
|
||||
+9
@@ -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); })();
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import {Service} from '@angular/core';
|
||||
|
||||
@Service({autoProvided: false})
|
||||
export class MyService {}
|
||||
+11
@@ -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); })();
|
||||
+6
@@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user