@nestjs/core 12.0.0-alpha.3 → 12.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/Readme.md +1 -0
  2. package/application-config.d.ts +8 -2
  3. package/application-config.js +14 -0
  4. package/errors/exceptions/index.d.ts +1 -0
  5. package/errors/exceptions/index.js +1 -0
  6. package/errors/exceptions/invalid-class-module.exception.d.ts +1 -1
  7. package/errors/exceptions/invalid-class-module.exception.js +2 -2
  8. package/errors/exceptions/invalid-module.exception.d.ts +1 -1
  9. package/errors/exceptions/invalid-module.exception.js +2 -2
  10. package/errors/exceptions/route-conflict.exception.d.ts +4 -0
  11. package/errors/exceptions/route-conflict.exception.js +7 -0
  12. package/errors/messages.d.ts +5 -2
  13. package/errors/messages.js +37 -4
  14. package/injector/injector.d.ts +4 -3
  15. package/injector/injector.js +28 -3
  16. package/injector/instance-wrapper.d.ts +2 -0
  17. package/injector/instance-wrapper.js +25 -0
  18. package/nest-application.d.ts +1 -0
  19. package/nest-application.js +77 -1
  20. package/package.json +3 -3
  21. package/router/interfaces/index.d.ts +3 -0
  22. package/router/interfaces/index.js +3 -0
  23. package/router/interfaces/resolved-route.interface.d.ts +32 -0
  24. package/router/interfaces/resolved-route.interface.js +1 -0
  25. package/router/interfaces/resolver.interface.d.ts +5 -1
  26. package/router/interfaces/route-conflict.interface.d.ts +14 -0
  27. package/router/interfaces/route-conflict.interface.js +1 -0
  28. package/router/interfaces/route-resolution-options.interface.d.ts +24 -0
  29. package/router/interfaces/route-resolution-options.interface.js +1 -0
  30. package/router/route-conflict-detector.d.ts +71 -0
  31. package/router/route-conflict-detector.js +276 -0
  32. package/router/route-specificity-sorter.d.ts +23 -0
  33. package/router/route-specificity-sorter.js +59 -0
  34. package/router/router-explorer.d.ts +11 -2
  35. package/router/router-explorer.js +61 -19
  36. package/router/routes-resolver.d.ts +7 -4
  37. package/router/routes-resolver.js +9 -7
  38. package/scanner.js +9 -5
package/Readme.md CHANGED
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
129
129
  <tr>
130
130
  <td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
131
131
  <td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
132
+ <td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
132
133
  </tr>
133
134
  </table>
134
135
 
@@ -1,7 +1,7 @@
1
- import type { CanActivate, ExceptionFilter, NestInterceptor, PipeTransform, PreRequestHook, VersioningOptions, WebSocketAdapter } from '@nestjs/common';
1
+ import type { CanActivate, ExceptionFilter, NestInterceptor, PipeTransform, PreRequestHook, RouteConflictPolicy, RouteResolutionStrategy, VersioningOptions, WebSocketAdapter } from '@nestjs/common';
2
+ import type { GlobalPrefixOptions } from '@nestjs/common/internal';
2
3
  import { InstanceWrapper } from './injector/instance-wrapper.js';
3
4
  import { ExcludeRouteMetadata } from './router/interfaces/exclude-route-metadata.interface.js';
4
- import type { GlobalPrefixOptions } from '@nestjs/common/internal';
5
5
  export declare class ApplicationConfig {
6
6
  private ioAdapter;
7
7
  private globalPrefix;
@@ -12,6 +12,8 @@ export declare class ApplicationConfig {
12
12
  private globalGuards;
13
13
  private globalPreRequestHooks;
14
14
  private versioningOptions;
15
+ private routeConflictPolicy;
16
+ private routeResolutionStrategy;
15
17
  private readonly globalRequestPipes;
16
18
  private readonly globalRequestFilters;
17
19
  private readonly globalRequestInterceptors;
@@ -47,4 +49,8 @@ export declare class ApplicationConfig {
47
49
  getGlobalPreRequestHooks(): PreRequestHook[];
48
50
  enableVersioning(options: VersioningOptions): void;
49
51
  getVersioning(): VersioningOptions | undefined;
52
+ setRouteConflictPolicy(policy: RouteConflictPolicy | undefined): void;
53
+ getRouteConflictPolicy(): RouteConflictPolicy | undefined;
54
+ setRouteResolutionStrategy(strategy: RouteResolutionStrategy | undefined): void;
55
+ getRouteResolutionStrategy(): RouteResolutionStrategy | undefined;
50
56
  }
@@ -8,6 +8,8 @@ export class ApplicationConfig {
8
8
  globalGuards = [];
9
9
  globalPreRequestHooks = [];
10
10
  versioningOptions;
11
+ routeConflictPolicy;
12
+ routeResolutionStrategy;
11
13
  globalRequestPipes = [];
12
14
  globalRequestFilters = [];
13
15
  globalRequestInterceptors = [];
@@ -109,4 +111,16 @@ export class ApplicationConfig {
109
111
  getVersioning() {
110
112
  return this.versioningOptions;
111
113
  }
114
+ setRouteConflictPolicy(policy) {
115
+ this.routeConflictPolicy = policy;
116
+ }
117
+ getRouteConflictPolicy() {
118
+ return this.routeConflictPolicy;
119
+ }
120
+ setRouteResolutionStrategy(strategy) {
121
+ this.routeResolutionStrategy = strategy;
122
+ }
123
+ getRouteResolutionStrategy() {
124
+ return this.routeResolutionStrategy;
125
+ }
112
126
  }
@@ -3,6 +3,7 @@ export * from './runtime.exception.js';
3
3
  export * from './unknown-element.exception.js';
4
4
  export * from './invalid-class-scope.exception.js';
5
5
  export * from './invalid-class.exception.js';
6
+ export * from './route-conflict.exception.js';
6
7
  export * from './unknown-export.exception.js';
7
8
  export * from './unknown-module.exception.js';
8
9
  export * from './undefined-forwardref.exception.js';
@@ -3,6 +3,7 @@ export * from './runtime.exception.js';
3
3
  export * from './unknown-element.exception.js';
4
4
  export * from './invalid-class-scope.exception.js';
5
5
  export * from './invalid-class.exception.js';
6
+ export * from './route-conflict.exception.js';
6
7
  export * from './unknown-export.exception.js';
7
8
  export * from './unknown-module.exception.js';
8
9
  export * from './undefined-forwardref.exception.js';
@@ -1,4 +1,4 @@
1
1
  import { RuntimeException } from './runtime.exception.js';
2
2
  export declare class InvalidClassModuleException extends RuntimeException {
3
- constructor(metatypeUsedAsAModule: any, scope: any[]);
3
+ constructor(metatypeUsedAsAModule: any, scope: any[], classKind: 'provider' | 'controller' | 'filter');
4
4
  }
@@ -1,7 +1,7 @@
1
1
  import { USING_INVALID_CLASS_AS_A_MODULE_MESSAGE } from '../messages.js';
2
2
  import { RuntimeException } from './runtime.exception.js';
3
3
  export class InvalidClassModuleException extends RuntimeException {
4
- constructor(metatypeUsedAsAModule, scope) {
5
- super(USING_INVALID_CLASS_AS_A_MODULE_MESSAGE(metatypeUsedAsAModule, scope));
4
+ constructor(metatypeUsedAsAModule, scope, classKind) {
5
+ super(USING_INVALID_CLASS_AS_A_MODULE_MESSAGE(metatypeUsedAsAModule, scope, classKind));
6
6
  }
7
7
  }
@@ -1,4 +1,4 @@
1
1
  import { RuntimeException } from './runtime.exception.js';
2
2
  export declare class InvalidModuleException extends RuntimeException {
3
- constructor(parentModule: any, index: number, scope: any[]);
3
+ constructor(parentModule: any, index: number, scope: any[], receivedValue: unknown);
4
4
  }
@@ -1,7 +1,7 @@
1
1
  import { INVALID_MODULE_MESSAGE } from '../messages.js';
2
2
  import { RuntimeException } from './runtime.exception.js';
3
3
  export class InvalidModuleException extends RuntimeException {
4
- constructor(parentModule, index, scope) {
5
- super(INVALID_MODULE_MESSAGE(parentModule, index, scope));
4
+ constructor(parentModule, index, scope, receivedValue) {
5
+ super(INVALID_MODULE_MESSAGE(parentModule, index, scope, receivedValue));
6
6
  }
7
7
  }
@@ -0,0 +1,4 @@
1
+ import { RuntimeException } from './runtime.exception.js';
2
+ export declare class RouteConflictException extends RuntimeException {
3
+ constructor(messages: string[]);
4
+ }
@@ -0,0 +1,7 @@
1
+ import { ROUTE_CONFLICT_MESSAGE } from '../messages.js';
2
+ import { RuntimeException } from './runtime.exception.js';
3
+ export class RouteConflictException extends RuntimeException {
4
+ constructor(messages) {
5
+ super(ROUTE_CONFLICT_MESSAGE(messages));
6
+ }
7
+ }
@@ -4,13 +4,16 @@ import { Module } from '../injector/module.js';
4
4
  export declare const UNKNOWN_DEPENDENCIES_MESSAGE: (type: string | symbol, unknownDependencyContext: InjectorDependencyContext, moduleRef: Module | undefined) => string;
5
5
  export declare const INVALID_MIDDLEWARE_MESSAGE: (text: TemplateStringsArray, name: string) => string;
6
6
  export declare const UNDEFINED_FORWARDREF_MESSAGE: (scope: Type<any>[]) => string;
7
- export declare const INVALID_MODULE_MESSAGE: (parentModule: any, index: number, scope: any[]) => string;
8
- export declare const USING_INVALID_CLASS_AS_A_MODULE_MESSAGE: (metatypeUsedAsAModule: Type | ForwardReference, scope: any[]) => string;
7
+ export declare const INVALID_MODULE_MESSAGE: (parentModule: any, index: number, scope: any[], receivedValue: unknown) => string;
8
+ export declare const USING_INVALID_CLASS_AS_A_MODULE_MESSAGE: (metatypeUsedAsAModule: Type | ForwardReference, scope: any[], classKind: "provider" | "controller" | "filter") => string;
9
9
  export declare const UNDEFINED_MODULE_MESSAGE: (parentModule: any, index: number, scope: any[]) => string;
10
10
  export declare const UNKNOWN_EXPORT_MESSAGE: (token: string | symbol | undefined, module: string) => string;
11
11
  export declare const INVALID_CLASS_MESSAGE: (text: TemplateStringsArray, value: any) => string;
12
12
  export declare const INVALID_CLASS_SCOPE_MESSAGE: (text: TemplateStringsArray, name: string | undefined) => string;
13
13
  export declare const UNKNOWN_REQUEST_MAPPING: (metatype: Type) => string;
14
+ export declare const ROUTE_CONFLICT_MESSAGE: (messages: string[]) => string;
15
+ export declare const DUPLICATE_ROUTE_MESSAGE: (method: string, path: string, firstHandlerLabel: string, secondHandlerLabel: string) => string;
16
+ export declare const SHADOWED_ROUTE_MESSAGE: (method: string, shadowedPath: string, shadowedHandlerLabel: string, winnerPath: string, winnerHandlerLabel: string) => string;
14
17
  export declare const INVALID_MIDDLEWARE_CONFIGURATION = "An invalid middleware configuration has been passed inside the module 'configure()' method.";
15
18
  export declare const UNHANDLED_RUNTIME_EXCEPTION = "Unhandled Runtime Exception.";
16
19
  export declare const INVALID_EXCEPTION_FILTER = "Invalid exception filters (@UseFilters()).";
@@ -111,17 +111,43 @@ export const UNDEFINED_FORWARDREF_MESSAGE = (scope) => `Nest cannot create the m
111
111
  (Read more: https://docs.nestjs.com/fundamentals/circular-dependency)
112
112
  Scope [${stringifyScope(scope)}]
113
113
  `;
114
- export const INVALID_MODULE_MESSAGE = (parentModule, index, scope) => {
114
+ export const INVALID_MODULE_MESSAGE = (parentModule, index, scope, receivedValue) => {
115
115
  const parentModuleName = parentModule?.name || 'module';
116
+ let formattedValue;
117
+ let receivedType;
118
+ if (receivedValue === null) {
119
+ formattedValue = 'null';
120
+ receivedType = 'null';
121
+ }
122
+ else if (typeof receivedValue === 'string') {
123
+ formattedValue = `"${receivedValue}"`;
124
+ receivedType = 'string';
125
+ }
126
+ else {
127
+ formattedValue = String(receivedValue);
128
+ receivedType = typeof receivedValue;
129
+ }
116
130
  return `Nest cannot create the ${parentModuleName} instance.
117
131
  Received an unexpected value at index [${index}] of the ${parentModuleName} "imports" array.
132
+ The received value \`${formattedValue}\` is of type "${receivedType}".
118
133
 
119
134
  Scope [${stringifyScope(scope)}]`;
120
135
  };
121
- export const USING_INVALID_CLASS_AS_A_MODULE_MESSAGE = (metatypeUsedAsAModule, scope) => {
136
+ export const USING_INVALID_CLASS_AS_A_MODULE_MESSAGE = (metatypeUsedAsAModule, scope, classKind) => {
122
137
  const metatypeNameQuote = `"${getInstanceName(metatypeUsedAsAModule)}"`;
123
- return `Classes annotated with @Injectable(), @Catch(), and @Controller() decorators must not appear in the "imports" array of a module.
124
- Please remove ${metatypeNameQuote} (including forwarded occurrences, if any) from all of the "imports" arrays.
138
+ let hint;
139
+ switch (classKind) {
140
+ case 'controller':
141
+ hint = `${metatypeNameQuote} is decorated with @Controller() and cannot appear in the "imports" array of a module. Please move ${metatypeNameQuote} to the "controllers" array of the importing module instead.`;
142
+ break;
143
+ case 'provider':
144
+ hint = `${metatypeNameQuote} is decorated with @Injectable() and cannot appear in the "imports" array of a module. Please move ${metatypeNameQuote} to the "providers" array of the importing module instead.`;
145
+ break;
146
+ case 'filter':
147
+ hint = `${metatypeNameQuote} is decorated with @Catch() and cannot appear in the "imports" array of a module. Please move ${metatypeNameQuote} to the "providers" array (using the APP_FILTER token to apply it globally) or apply it via @UseFilters() instead.`;
148
+ break;
149
+ }
150
+ return `${hint}
125
151
 
126
152
  Scope [${stringifyScope(scope)}]
127
153
  `;
@@ -155,6 +181,13 @@ export const UNKNOWN_REQUEST_MAPPING = (metatype) => {
155
181
  ? `An invalid controller has been detected. "${className}" does not have the @Controller() decorator but it is being listed in the "controllers" array of some module.`
156
182
  : `An invalid controller has been detected. Perhaps, one of your controllers is missing the @Controller() decorator.`;
157
183
  };
184
+ export const ROUTE_CONFLICT_MESSAGE = (messages) => [
185
+ 'Conflicting HTTP routes detected:',
186
+ ...messages.map(message => ` - ${message}`),
187
+ `Adjust route declarations or relax the 'routeConflictPolicy' option passed to NestFactory.create() to allow the application to start.`,
188
+ ].join('\n');
189
+ export const DUPLICATE_ROUTE_MESSAGE = (method, path, firstHandlerLabel, secondHandlerLabel) => `Duplicate route: ${method} ${path} is registered by both ${firstHandlerLabel} and ${secondHandlerLabel}.`;
190
+ export const SHADOWED_ROUTE_MESSAGE = (method, shadowedPath, shadowedHandlerLabel, winnerPath, winnerHandlerLabel) => `Route ${method} ${shadowedPath} (${shadowedHandlerLabel}) is shadowed by ${method} ${winnerPath} (${winnerHandlerLabel}). The first-registered route will match all matching requests on order-sensitive adapters.`;
158
191
  export const INVALID_MIDDLEWARE_CONFIGURATION = `An invalid middleware configuration has been passed inside the module 'configure()' method.`;
159
192
  export const UNHANDLED_RUNTIME_EXCEPTION = `Unhandled Runtime Exception.`;
160
193
  export const INVALID_EXCEPTION_FILTER = `Invalid exception filters (@UseFilters()).`;
@@ -73,9 +73,9 @@ export declare class Injector {
73
73
  resolveConstructorParams<T>(wrapper: InstanceWrapper<T>, moduleRef: Module, inject: InjectorDependency[] | undefined, callback: (args: unknown[], depth?: number) => void | Promise<void>, resolutionContext?: ResolutionContext, parentInquirer?: InstanceWrapper): Promise<void>;
74
74
  getClassDependencies<T>(wrapper: InstanceWrapper<T>): [InjectorDependency[], number[]];
75
75
  getFactoryProviderDependencies<T>(wrapper: InstanceWrapper<T>): [InjectorDependency[], number[]];
76
- reflectConstructorParams<T>(type: Type<T>): any[];
77
- reflectOptionalParams<T>(type: Type<T>): any[];
78
- reflectSelfParams<T>(type: Type<T>): any[];
76
+ reflectConstructorParams(type: Type<unknown> | Function): any[];
77
+ reflectOptionalParams(type: Type<unknown> | Function): any[];
78
+ reflectSelfParams(type: Type<unknown> | Function): any[];
79
79
  resolveSingleParam<T>(wrapper: InstanceWrapper<T>, param: Type<any> | string | symbol, dependencyContext: InjectorDependencyContext, moduleRef: Module, resolutionContext?: ResolutionContext, keyOrIndex?: symbol | string | number): Promise<InstanceWrapper<any>>;
80
80
  resolveParamToken<T>(wrapper: InstanceWrapper<T>, param: Type<any> | string | symbol | ForwardReference): any;
81
81
  resolveComponentWrapper<T>(moduleRef: Module, token: InjectionToken, dependencyContext: InjectorDependencyContext, wrapper: InstanceWrapper<T>, resolutionContext?: ResolutionContext, keyOrIndex?: symbol | string | number): Promise<InstanceWrapper>;
@@ -107,6 +107,7 @@ export declare class Injector {
107
107
  private getEffectiveInquirerId;
108
108
  private getStaticTransientResolutionContext;
109
109
  private getEffectiveResolutionContext;
110
+ private hasDenseCtorMetadata;
110
111
  private resolveScopedComponentHost;
111
112
  private isInquirerRequest;
112
113
  private isInquirer;
@@ -72,6 +72,9 @@ export class Injector {
72
72
  settlementSignal.complete();
73
73
  };
74
74
  await this.resolveConstructorParams(wrapper, moduleRef, inject, callback, localResolutionContext, resolutionContext.inquirer);
75
+ if (!instanceHost.isResolved) {
76
+ settlementSignal.complete();
77
+ }
75
78
  }
76
79
  catch (err) {
77
80
  wrapper.removeInstanceByContextId(this.getContextId(resolutionContext.contextId, wrapper), inquirerId);
@@ -114,7 +117,8 @@ export class Injector {
114
117
  }
115
118
  async resolveConstructorParams(wrapper, moduleRef, inject, callback, resolutionContext = { contextId: STATIC_CONTEXT }, parentInquirer) {
116
119
  const metadata = wrapper.getCtorMetadata();
117
- if (metadata && resolutionContext.contextId !== STATIC_CONTEXT) {
120
+ if (resolutionContext.contextId !== STATIC_CONTEXT &&
121
+ this.hasDenseCtorMetadata(wrapper, inject, metadata)) {
118
122
  const deps = await this.loadCtorMetadata(metadata, resolutionContext.contextId, resolutionContext.inquirer, parentInquirer);
119
123
  return callback(deps);
120
124
  }
@@ -501,7 +505,6 @@ export class Injector {
501
505
  wrapper.isExplicitlyRequested(resolutionContext.contextId, resolutionContext.inquirer));
502
506
  }
503
507
  shouldSkipProviderLoading(wrapper, resolutionContext) {
504
- const isSnapshotGraphCompilation = !!this.options?.snapshot;
505
508
  const isStaticContext = resolutionContext.contextId === STATIC_CONTEXT;
506
509
  const hasNoInquirer = !resolutionContext.inquirer;
507
510
  const isTopLevelStaticTransientOrRequestProvider = hasNoInquirer && (wrapper.isTransient || wrapper.scope === Scope.REQUEST);
@@ -510,7 +513,7 @@ export class Injector {
510
513
  const shouldSkipForStaticBootstrap = isStaticContext &&
511
514
  (isTopLevelStaticTransientOrRequestProvider ||
512
515
  isStaticInquirerOutsideResolutionContext);
513
- return !isSnapshotGraphCompilation && shouldSkipForStaticBootstrap;
516
+ return shouldSkipForStaticBootstrap;
514
517
  }
515
518
  /**
516
519
  * For nested TRANSIENT dependencies (TRANSIENT -> TRANSIENT) in non-static contexts,
@@ -554,6 +557,28 @@ export class Injector {
554
557
  getEffectiveResolutionContext(dependency, resolutionContext, parentInquirer) {
555
558
  return this.createResolutionContext(resolutionContext.contextId, this.getEffectiveInquirer(dependency, resolutionContext, parentInquirer), this.getEffectiveInquirerId(dependency, resolutionContext, parentInquirer));
556
559
  }
560
+ hasDenseCtorMetadata(wrapper, inject, metadata) {
561
+ if (!metadata) {
562
+ return false;
563
+ }
564
+ // The fast path requires a fully populated metadata array.
565
+ // While another request is still registering dependency metadata,
566
+ // sparse entries here would feed request-scoped factories `undefined`.
567
+ const expectedDepsLength = !isNil(inject)
568
+ ? inject.length
569
+ : wrapper.metatype
570
+ ? this.reflectConstructorParams(wrapper.metatype).length
571
+ : 0;
572
+ if (metadata.length !== expectedDepsLength) {
573
+ return false;
574
+ }
575
+ for (let index = 0; index < expectedDepsLength; index++) {
576
+ if (metadata[index] === undefined) {
577
+ return false;
578
+ }
579
+ }
580
+ return true;
581
+ }
557
582
  resolveScopedComponentHost(item, contextId, inquirer, parentInquirer) {
558
583
  return this.isInquirerRequest(item, parentInquirer)
559
584
  ? parentInquirer
@@ -94,6 +94,8 @@ export declare class InstanceWrapper<T = any> {
94
94
  getStaticTransientInstances(): (InstancePerContext<T> | undefined)[];
95
95
  mergeWith(provider: Provider): void;
96
96
  private isNewable;
97
+ private registerDependencyTreeParent;
98
+ private resetDependencyTreeState;
97
99
  private initialize;
98
100
  private printIntrospectedAsRequestScoped;
99
101
  private printIntrospectedAsDurable;
@@ -6,6 +6,7 @@ import { STATIC_CONTEXT } from './constants.js';
6
6
  import { isClassProvider, isFactoryProvider, isValueProvider, } from './helpers/provider-classifier.js';
7
7
  export const INSTANCE_METADATA_SYMBOL = Symbol.for('instance_metadata:cache');
8
8
  export const INSTANCE_ID_SYMBOL = Symbol.for('instance_metadata:id');
9
+ const dependencyTreeParents = new WeakMap();
9
10
  export class InstanceWrapper {
10
11
  name;
11
12
  token;
@@ -121,6 +122,8 @@ export class InstanceWrapper {
121
122
  this[INSTANCE_METADATA_SYMBOL].dependencies = [];
122
123
  }
123
124
  this[INSTANCE_METADATA_SYMBOL].dependencies[index] = wrapper;
125
+ this.registerDependencyTreeParent(wrapper);
126
+ this.resetDependencyTreeState();
124
127
  }
125
128
  getCtorMetadata() {
126
129
  return this[INSTANCE_METADATA_SYMBOL].dependencies;
@@ -133,6 +136,8 @@ export class InstanceWrapper {
133
136
  key,
134
137
  wrapper,
135
138
  });
139
+ this.registerDependencyTreeParent(wrapper);
140
+ this.resetDependencyTreeState();
136
141
  }
137
142
  getPropertiesMetadata() {
138
143
  return this[INSTANCE_METADATA_SYMBOL].properties;
@@ -142,6 +147,8 @@ export class InstanceWrapper {
142
147
  this[INSTANCE_METADATA_SYMBOL].enhancers = [];
143
148
  }
144
149
  this[INSTANCE_METADATA_SYMBOL].enhancers.push(wrapper);
150
+ this.registerDependencyTreeParent(wrapper);
151
+ this.resetDependencyTreeState();
145
152
  }
146
153
  getEnhancersMetadata() {
147
154
  return this[INSTANCE_METADATA_SYMBOL].enhancers;
@@ -333,6 +340,24 @@ export class InstanceWrapper {
333
340
  isNewable() {
334
341
  return isNil(this.inject) && this.metatype && this.metatype.prototype;
335
342
  }
343
+ registerDependencyTreeParent(wrapper) {
344
+ if (wrapper instanceof InstanceWrapper) {
345
+ const parents = dependencyTreeParents.get(wrapper) ?? new Set();
346
+ parents.add(this);
347
+ dependencyTreeParents.set(wrapper, parents);
348
+ }
349
+ }
350
+ resetDependencyTreeState(lookupRegistry = new Set()) {
351
+ if (lookupRegistry.has(this[INSTANCE_ID_SYMBOL])) {
352
+ return;
353
+ }
354
+ lookupRegistry.add(this[INSTANCE_ID_SYMBOL]);
355
+ this.isTreeStatic = undefined;
356
+ this.isTreeDurable = undefined;
357
+ dependencyTreeParents
358
+ .get(this)
359
+ ?.forEach(parent => parent.resetDependencyTreeState(lookupRegistry));
360
+ }
336
361
  initialize(metadata) {
337
362
  const { instance, isResolved, ...wrapperPartial } = metadata;
338
363
  Object.assign(this, wrapperPartial);
@@ -22,6 +22,7 @@ export declare class NestApplication extends NestApplicationContext<NestApplicat
22
22
  private readonly microservices;
23
23
  private httpServer;
24
24
  private isListening;
25
+ private isWsModuleRegistered;
25
26
  constructor(container: NestContainer, httpAdapter: HttpServer, config: ApplicationConfig, graphInspector: GraphInspector, appOptions?: NestApplicationOptions);
26
27
  protected prepareClose(): Promise<void>;
27
28
  protected dispose(): Promise<void>;
@@ -12,6 +12,8 @@ import { NestApplicationContext } from './nest-application-context.js';
12
12
  import { RoutesResolver } from './router/routes-resolver.js';
13
13
  import { Logger } from '@nestjs/common';
14
14
  import { loadPackage, loadPackageCached, tryLoadPackage, addLeadingSlash, isFunction, isObject, isString, } from '@nestjs/common/internal';
15
+ import { RouteConflictDetector } from './router/route-conflict-detector.js';
16
+ import { RouteSpecificitySorter } from './router/route-specificity-sorter.js';
15
17
  /**
16
18
  * @publicApi
17
19
  */
@@ -30,11 +32,14 @@ export class NestApplication extends NestApplicationContext {
30
32
  microservices = [];
31
33
  httpServer;
32
34
  isListening = false;
35
+ isWsModuleRegistered = false;
33
36
  constructor(container, httpAdapter, config, graphInspector, appOptions = {}) {
34
37
  super(container, appOptions);
35
38
  this.httpAdapter = httpAdapter;
36
39
  this.config = config;
37
40
  this.graphInspector = graphInspector;
41
+ this.config.setRouteConflictPolicy(appOptions.routeConflictPolicy);
42
+ this.config.setRouteResolutionStrategy(appOptions.routeResolutionStrategy);
38
43
  this.selectContextModule();
39
44
  this.registerHttpServer();
40
45
  this.injector = new Injector({
@@ -92,6 +97,7 @@ export class NestApplication extends NestApplicationContext {
92
97
  return;
93
98
  }
94
99
  this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
100
+ this.isWsModuleRegistered = true;
95
101
  }
96
102
  async init() {
97
103
  if (this.isInitialized) {
@@ -124,7 +130,74 @@ export class NestApplication extends NestApplicationContext {
124
130
  await this.registerMiddleware(this.httpAdapter);
125
131
  const prefix = this.config.getGlobalPrefix();
126
132
  const basePath = addLeadingSlash(prefix);
127
- this.routesResolver.resolve(this.httpAdapter, basePath);
133
+ const conflictPolicy = this.config.getRouteConflictPolicy();
134
+ const resolutionStrategy = this.config.getRouteResolutionStrategy();
135
+ const adapterIsOrderSensitive = this.httpAdapter.isRouteOrderSensitive?.() ?? true;
136
+ const shouldSortBySpecificity = resolutionStrategy === 'specificity' && adapterIsOrderSensitive;
137
+ // Adapters that are not order-sensitive (e.g. Fastify) currently
138
+ // also reject duplicate (method, URL) registrations synchronously
139
+ // from the underlying router. Treat the two properties as one
140
+ // signal until a separate capability flag is introduced.
141
+ const adapterRejectsDuplicates = !adapterIsOrderSensitive;
142
+ if (!conflictPolicy && !shouldSortBySpecificity) {
143
+ this.routesResolver.resolve(this.httpAdapter, basePath);
144
+ return;
145
+ }
146
+ // Defer registration whenever we collect routes for diagnostics or
147
+ // re-ordering. In particular, when a conflict policy is set we
148
+ // must run detection *before* the adapter sees any route, because
149
+ // duplicate-rejecting adapters like Fastify throw synchronously
150
+ // from `instance.route()` and would short-circuit both the resolve
151
+ // loop and the aggregated `RouteConflictException`.
152
+ const resolvedRoutes = [];
153
+ this.routesResolver.resolve(this.httpAdapter, basePath, {
154
+ onRouteResolved: route => resolvedRoutes.push(route),
155
+ deferRegistration: true,
156
+ });
157
+ // Sort before conflict detection so that winner/shadowed pairs in every
158
+ // conflict record reflect actual adapter registration order. Without this,
159
+ // the reported winner could be the declaration-first (less-specific) route
160
+ // even though the sorted-first (more-specific) route is what really wins.
161
+ const orderedRoutes = shouldSortBySpecificity
162
+ ? RouteSpecificitySorter.sort(resolvedRoutes)
163
+ : resolvedRoutes;
164
+ const routesToSkip = new Set();
165
+ if (conflictPolicy) {
166
+ const filteredPolicy = adapterIsOrderSensitive
167
+ ? conflictPolicy
168
+ : { duplicate: conflictPolicy.duplicate };
169
+ const conflicts = RouteConflictDetector.detect(orderedRoutes, this.config.getVersioning());
170
+ // On adapters that reject duplicate registrations the policy
171
+ // cannot be honoured by simply logging — the adapter would
172
+ // throw on the second `instance.route()` call. Drop the
173
+ // shadowed route of every duplicate conflict so the detector
174
+ // (not the adapter) decides which one wins. The detector
175
+ // always picks the earlier-registered route as the winner.
176
+ if (adapterRejectsDuplicates) {
177
+ conflicts.forEach(conflict => {
178
+ if (conflict.kind === 'duplicate') {
179
+ routesToSkip.add(conflict.shadowed);
180
+ }
181
+ });
182
+ }
183
+ // When specificity sorting is active, shadow conflicts where the sort
184
+ // promoted the winner (declared later but more specific) are resolved
185
+ // at runtime: the more-specific route is first-registered and handles
186
+ // its requests, the less-specific route handles the rest. Filtering
187
+ // these out prevents shadow: 'error' from aborting an app whose routes
188
+ // work correctly after specificity ordering. Genuine shadows — where the
189
+ // winner was already first in declaration order and the sort did not help
190
+ // — are kept and still apply the configured policy.
191
+ const effectiveConflicts = shouldSortBySpecificity
192
+ ? RouteConflictDetector.filterSortResolvedShadows(conflicts, resolvedRoutes)
193
+ : conflicts;
194
+ RouteConflictDetector.handle(effectiveConflicts, filteredPolicy, this.logger);
195
+ }
196
+ orderedRoutes.forEach(route => {
197
+ if (routesToSkip.has(route))
198
+ return;
199
+ this.routesResolver.registerResolvedRoute(this.httpAdapter, route);
200
+ });
128
201
  }
129
202
  async registerRouterHooks() {
130
203
  this.routesResolver.registerNotFoundHandler();
@@ -260,6 +333,9 @@ export class NestApplication extends NestApplicationContext {
260
333
  return this;
261
334
  }
262
335
  useWebSocketAdapter(adapter) {
336
+ if (this.isWsModuleRegistered) {
337
+ this.logger.warn('useWebSocketAdapter() was called after WebSocket gateways were already initialized. The provided adapter will be stored but will NOT be applied to existing gateways — they remain bound to the previously installed adapter. To install a custom adapter, call app.useWebSocketAdapter(...) BEFORE app.init() (or app.listen()).');
338
+ }
263
339
  this.config.setIoAdapter(adapter);
264
340
  return this;
265
341
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/core",
3
- "version": "12.0.0-alpha.3",
3
+ "version": "12.0.0-alpha.5",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@core)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "uid": "2.0.2"
48
48
  },
49
49
  "devDependencies": {
50
- "@nestjs/common": "^12.0.0-alpha.3"
50
+ "@nestjs/common": "^12.0.0-alpha.5"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@nestjs/common": "^11.0.0",
@@ -68,5 +68,5 @@
68
68
  "optional": true
69
69
  }
70
70
  },
71
- "gitHead": "1c9d5482a65a446ede8dd1195bfa1cbbc16e0857"
71
+ "gitHead": "d91b72cc9567e6a09e3ad6075b2fb801e71adfc8"
72
72
  }
@@ -1 +1,4 @@
1
+ export * from './resolved-route.interface.js';
2
+ export * from './route-conflict.interface.js';
3
+ export * from './route-resolution-options.interface.js';
1
4
  export * from './routes.interface.js';
@@ -1 +1,4 @@
1
+ export * from './resolved-route.interface.js';
2
+ export * from './route-conflict.interface.js';
3
+ export * from './route-resolution-options.interface.js';
1
4
  export * from './routes.interface.js';
@@ -0,0 +1,32 @@
1
+ import { RequestMethod } from '@nestjs/common';
2
+ import { type VersionValue } from '@nestjs/common/internal';
3
+ import { InstanceWrapper } from '../../injector/instance-wrapper.js';
4
+ import { RouterProxyCallback } from '../router-proxy.js';
5
+ /**
6
+ * Loose callable signature shared by the various handler wrappers that
7
+ * are composed before adapter registration (host filter, version
8
+ * filter, request-scoped handler, etc.). They all accept the (req, res,
9
+ * next) trio but may be invoked variadically by adapter shims.
10
+ */
11
+ export type ResolvedRouteHandler = (...args: unknown[]) => unknown;
12
+ /**
13
+ * Final route description produced during the "collect" phase of the
14
+ * router pipeline and consumed during the "register" phase. Holds the
15
+ * fully composed path, the pre-built handler chain (proxy + host filter
16
+ * + optional version filter), and the metadata needed to register the
17
+ * route on the HTTP adapter and to insert an entrypoint into the graph
18
+ * inspector.
19
+ */
20
+ export interface ResolvedRoute {
21
+ method: RequestMethod;
22
+ path: string;
23
+ rawPath?: string;
24
+ host: string | RegExp | Array<string | RegExp> | undefined;
25
+ version: VersionValue | undefined;
26
+ methodVersion: VersionValue | undefined;
27
+ controllerVersion: VersionValue | undefined;
28
+ handler: ResolvedRouteHandler;
29
+ targetCallback: RouterProxyCallback;
30
+ methodName: string;
31
+ instanceWrapper: InstanceWrapper;
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,9 @@
1
+ import { HttpServer } from '@nestjs/common';
2
+ import { ResolvedRoute } from './resolved-route.interface.js';
3
+ import { RouteResolutionOptions } from './route-resolution-options.interface.js';
1
4
  export interface Resolver {
2
- resolve(instance: any, basePath: string): void;
5
+ resolve(applicationRef: HttpServer, basePath: string, options?: RouteResolutionOptions): void;
6
+ registerResolvedRoute(applicationRef: HttpServer, route: ResolvedRoute): void;
3
7
  registerNotFoundHandler(): void;
4
8
  registerExceptionHandler(): void;
5
9
  }
@@ -0,0 +1,14 @@
1
+ import { ResolvedRoute } from './resolved-route.interface.js';
2
+ /**
3
+ * Distinguishes the two flavors of route overlap.
4
+ * - `duplicate` — identical method + path + version + host registered twice.
5
+ * - `shadow` — patterns can match the same request but are not identical.
6
+ */
7
+ export type ConflictKind = 'duplicate' | 'shadow';
8
+ export interface RouteConflict {
9
+ /** Route registered first; on order-sensitive adapters this wins. */
10
+ winner: ResolvedRoute;
11
+ /** Route registered later; on order-sensitive adapters this never matches. */
12
+ shadowed: ResolvedRoute;
13
+ kind: ConflictKind;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { ResolvedRoute } from './resolved-route.interface.js';
2
+ /**
3
+ * Options that control how `Resolver.resolve` walks the controller
4
+ * graph and registers routes on the HTTP adapter. Used internally to
5
+ * thread route-collection and deferred-registration concerns through
6
+ * the resolver chain without bloating individual method signatures.
7
+ */
8
+ export interface RouteResolutionOptions {
9
+ /**
10
+ * Invoked once for each route after its final path, host and version
11
+ * have been composed. Lets the caller observe resolved routes (for
12
+ * conflict detection, specificity sorting, etc.) without coupling
13
+ * those concerns to the resolver itself.
14
+ */
15
+ onRouteResolved?: (route: ResolvedRoute) => void;
16
+ /**
17
+ * When `true`, the resolver still walks every controller and emits
18
+ * `onRouteResolved` callbacks but skips the actual adapter
19
+ * registration step. The caller is then responsible for ordering and
20
+ * installing the collected routes via `registerResolvedRoute`.
21
+ * Defaults to `false`.
22
+ */
23
+ deferRegistration?: boolean;
24
+ }