@nestjs/core 12.0.0-alpha.4 → 12.0.0-alpha.6

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 (65) hide show
  1. package/Readme.md +25 -45
  2. package/adapters/http-adapter.d.ts +2 -0
  3. package/adapters/http-adapter.js +3 -0
  4. package/application-config.d.ts +8 -2
  5. package/application-config.js +14 -0
  6. package/errors/exceptions/index.d.ts +1 -0
  7. package/errors/exceptions/index.js +1 -0
  8. package/errors/exceptions/invalid-class-module.exception.d.ts +1 -1
  9. package/errors/exceptions/invalid-class-module.exception.js +2 -2
  10. package/errors/exceptions/invalid-module.exception.d.ts +1 -1
  11. package/errors/exceptions/invalid-module.exception.js +2 -2
  12. package/errors/exceptions/route-conflict.exception.d.ts +4 -0
  13. package/errors/exceptions/route-conflict.exception.js +7 -0
  14. package/errors/messages.d.ts +5 -2
  15. package/errors/messages.js +37 -4
  16. package/exceptions/base-exception-filter.js +8 -3
  17. package/helpers/barrier.js +4 -1
  18. package/helpers/handler-metadata-storage.d.ts +1 -0
  19. package/helpers/router-method-factory.d.ts +1 -0
  20. package/helpers/router-method-factory.js +1 -0
  21. package/hooks/before-app-shutdown.hook.js +11 -2
  22. package/hooks/on-app-shutdown.hook.js +11 -2
  23. package/hooks/on-module-destroy.hook.js +11 -2
  24. package/injector/container.js +2 -2
  25. package/injector/helpers/transient-instances.d.ts +12 -0
  26. package/injector/helpers/transient-instances.js +28 -0
  27. package/injector/injector.d.ts +4 -3
  28. package/injector/injector.js +34 -13
  29. package/injector/instance-wrapper.d.ts +2 -0
  30. package/injector/instance-wrapper.js +25 -0
  31. package/injector/internal-core-module/internal-core-module-factory.js +1 -1
  32. package/injector/module.d.ts +6 -0
  33. package/injector/module.js +8 -0
  34. package/interceptors/interceptors-consumer.js +32 -7
  35. package/middleware/builder.js +5 -1
  36. package/nest-application-context.js +1 -1
  37. package/nest-application.d.ts +3 -1
  38. package/nest-application.js +96 -6
  39. package/package.json +3 -14
  40. package/router/interfaces/index.d.ts +3 -0
  41. package/router/interfaces/index.js +3 -0
  42. package/router/interfaces/resolved-route.interface.d.ts +32 -0
  43. package/router/interfaces/resolved-route.interface.js +1 -0
  44. package/router/interfaces/resolver.interface.d.ts +5 -1
  45. package/router/interfaces/route-conflict.interface.d.ts +14 -0
  46. package/router/interfaces/route-conflict.interface.js +1 -0
  47. package/router/interfaces/route-resolution-options.interface.d.ts +24 -0
  48. package/router/interfaces/route-resolution-options.interface.js +1 -0
  49. package/router/legacy-route-converter.d.ts +1 -1
  50. package/router/legacy-route-converter.js +24 -13
  51. package/router/route-conflict-detector.d.ts +71 -0
  52. package/router/route-conflict-detector.js +276 -0
  53. package/router/route-specificity-sorter.d.ts +23 -0
  54. package/router/route-specificity-sorter.js +59 -0
  55. package/router/router-execution-context.d.ts +1 -0
  56. package/router/router-execution-context.js +26 -3
  57. package/router/router-explorer.d.ts +11 -2
  58. package/router/router-explorer.js +61 -19
  59. package/router/router-response-controller.d.ts +1 -0
  60. package/router/router-response-controller.js +126 -49
  61. package/router/routes-resolver.d.ts +7 -4
  62. package/router/routes-resolver.js +9 -7
  63. package/router/sse-stream.d.ts +1 -0
  64. package/router/sse-stream.js +24 -13
  65. package/scanner.js +13 -6
@@ -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
  }
@@ -325,16 +329,12 @@ export class Injector {
325
329
  this.printFoundInModuleLog(name, relatedModule);
326
330
  instanceWrapperRef = providers.get(name);
327
331
  this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapperRef);
328
- const inquirerId = this.getContextInquirerId(resolutionContext);
329
- const instanceHost = instanceWrapperRef.getInstanceByContextId(this.getContextId(resolutionContext.contextId, instanceWrapperRef), inquirerId);
330
- if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
331
- /*
332
- * Provider will be loaded shortly in resolveComponentHost() once we pass the current
333
- * Barrier. We cannot load it here because doing so could incorrectly evaluate the
334
- * staticity of the dependency tree and lead to undefined / null injection.
335
- */
336
- break;
337
- }
332
+ /*
333
+ * Stop at the first direct export match. Continuing when the provider is
334
+ * already resolved would let a later import (e.g. a global forRoot module)
335
+ * override an explicit forFeature import for the same token.
336
+ */
337
+ break;
338
338
  }
339
339
  return instanceWrapperRef;
340
340
  }
@@ -501,7 +501,6 @@ export class Injector {
501
501
  wrapper.isExplicitlyRequested(resolutionContext.contextId, resolutionContext.inquirer));
502
502
  }
503
503
  shouldSkipProviderLoading(wrapper, resolutionContext) {
504
- const isSnapshotGraphCompilation = !!this.options?.snapshot;
505
504
  const isStaticContext = resolutionContext.contextId === STATIC_CONTEXT;
506
505
  const hasNoInquirer = !resolutionContext.inquirer;
507
506
  const isTopLevelStaticTransientOrRequestProvider = hasNoInquirer && (wrapper.isTransient || wrapper.scope === Scope.REQUEST);
@@ -510,7 +509,7 @@ export class Injector {
510
509
  const shouldSkipForStaticBootstrap = isStaticContext &&
511
510
  (isTopLevelStaticTransientOrRequestProvider ||
512
511
  isStaticInquirerOutsideResolutionContext);
513
- return !isSnapshotGraphCompilation && shouldSkipForStaticBootstrap;
512
+ return shouldSkipForStaticBootstrap;
514
513
  }
515
514
  /**
516
515
  * For nested TRANSIENT dependencies (TRANSIENT -> TRANSIENT) in non-static contexts,
@@ -554,6 +553,28 @@ export class Injector {
554
553
  getEffectiveResolutionContext(dependency, resolutionContext, parentInquirer) {
555
554
  return this.createResolutionContext(resolutionContext.contextId, this.getEffectiveInquirer(dependency, resolutionContext, parentInquirer), this.getEffectiveInquirerId(dependency, resolutionContext, parentInquirer));
556
555
  }
556
+ hasDenseCtorMetadata(wrapper, inject, metadata) {
557
+ if (!metadata) {
558
+ return false;
559
+ }
560
+ // The fast path requires a fully populated metadata array.
561
+ // While another request is still registering dependency metadata,
562
+ // sparse entries here would feed request-scoped factories `undefined`.
563
+ const expectedDepsLength = !isNil(inject)
564
+ ? inject.length
565
+ : wrapper.metatype
566
+ ? this.reflectConstructorParams(wrapper.metatype).length
567
+ : 0;
568
+ if (metadata.length !== expectedDepsLength) {
569
+ return false;
570
+ }
571
+ for (let index = 0; index < expectedDepsLength; index++) {
572
+ if (metadata[index] === undefined) {
573
+ return false;
574
+ }
575
+ }
576
+ return true;
577
+ }
557
578
  resolveScopedComponentHost(item, contextId, inquirer, parentInquirer) {
558
579
  return this.isInquirerRequest(item, parentInquirer)
559
580
  ? 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);
@@ -15,7 +15,7 @@ export class InternalCoreModuleFactory {
15
15
  timestamp: false,
16
16
  });
17
17
  const injector = new Injector({
18
- preview: container.contextOptions?.preview,
18
+ preview: container.contextOptions?.preview ?? false,
19
19
  instanceDecorator: container.contextOptions?.instrument?.instanceDecorator,
20
20
  });
21
21
  const instanceLoader = new InstanceLoader(container, injector, graphInspector, logger);
@@ -35,6 +35,12 @@ export declare class Module {
35
35
  get entryProviders(): Array<InstanceWrapper<Injectable>>;
36
36
  get exports(): Set<InjectionToken>;
37
37
  get instance(): NestModule;
38
+ /**
39
+ * Modules are registered in the container before they are scanned (dynamic
40
+ * `imports` are pre-registered by `NestContainer#addDynamicMetadata`), so
41
+ * registration alone does not imply the module was ever instantiated.
42
+ */
43
+ get isInstantiated(): boolean;
38
44
  get metatype(): Type<any>;
39
45
  get distance(): number;
40
46
  set distance(value: number);
@@ -83,6 +83,14 @@ export class Module {
83
83
  const moduleRef = this._providers.get(this._metatype);
84
84
  return moduleRef.instance;
85
85
  }
86
+ /**
87
+ * Modules are registered in the container before they are scanned (dynamic
88
+ * `imports` are pre-registered by `NestContainer#addDynamicMetadata`), so
89
+ * registration alone does not imply the module was ever instantiated.
90
+ */
91
+ get isInstantiated() {
92
+ return !isNil(this._providers.get(this._metatype)?.instance);
93
+ }
86
94
  get metatype() {
87
95
  return this._metatype;
88
96
  }
@@ -1,11 +1,11 @@
1
1
  import { AsyncResource } from 'async_hooks';
2
- import { Observable, defer, from as fromPromise } from 'rxjs';
3
- import { mergeAll, switchMap } from 'rxjs/operators';
2
+ import { Observable, defer, from } from 'rxjs';
3
+ import { mergeAll } from 'rxjs/operators';
4
4
  import { ExecutionContextHost } from '../helpers/execution-context-host.js';
5
5
  import { isEmptyArray } from '@nestjs/common/internal';
6
6
  export class InterceptorsConsumer {
7
7
  async intercept(interceptors, args, instance, callback, next, type) {
8
- if (isEmptyArray(interceptors)) {
8
+ if (!interceptors || isEmptyArray(interceptors)) {
9
9
  return next();
10
10
  }
11
11
  const context = this.createContext(args, instance, callback);
@@ -25,9 +25,34 @@ export class InterceptorsConsumer {
25
25
  return new ExecutionContextHost(args, instance.constructor, callback);
26
26
  }
27
27
  transformDeferred(next) {
28
- return fromPromise(next()).pipe(switchMap(res => {
29
- const isDeferred = res instanceof Promise || res instanceof Observable;
30
- return isDeferred ? res : Promise.resolve(res);
31
- }));
28
+ // Call next() eagerly here — this method is invoked inside
29
+ // defer(AsyncResource.bind(...)), so the async context (e.g. AsyncLocalStorage)
30
+ // is correctly inherited. Deferring next() into the subscriber function would
31
+ // lose that context because the subscriber is called outside the bound scope.
32
+ const nextPromise = next();
33
+ return new Observable(subscriber => {
34
+ let innerSub;
35
+ nextPromise
36
+ .then(res => {
37
+ if (subscriber.closed) {
38
+ // The outer subscription was torn down (e.g. an SSE client disconnect)
39
+ // before the async handler resolved. Do not subscribe the producer
40
+ // Observable after the consumer has already gone away — subscribing
41
+ // only to unsubscribe in the same tick would start producer side
42
+ // effects just to immediately abort them.
43
+ return;
44
+ }
45
+ const isDeferred = res instanceof Promise || res instanceof Observable;
46
+ innerSub = from(isDeferred ? res : Promise.resolve(res)).subscribe(subscriber);
47
+ })
48
+ .catch(err => {
49
+ if (!subscriber.closed) {
50
+ subscriber.error(err);
51
+ }
52
+ });
53
+ return () => {
54
+ innerSub?.unsubscribe();
55
+ };
56
+ });
32
57
  }
33
58
  }
@@ -74,7 +74,11 @@ export class MiddlewareBuilder {
74
74
  .map(route => ({
75
75
  method: route.method,
76
76
  path: route.path,
77
- regex: new RegExp('^(' + route.path.replace(regexMatchParams, wildcard) + ')$', 'g'),
77
+ // No `g` flag: each regex is reused across every route below, and a
78
+ // global regex advances `lastIndex` on a match, so the next `test()`
79
+ // would resume mid-string and fail the `^` anchor. The pattern is
80
+ // anchored and only used with `test()`, so `g` buys nothing anyway.
81
+ regex: new RegExp('^(' + route.path.replace(regexMatchParams, wildcard) + ')$'),
78
82
  }));
79
83
  return routes.filter(route => {
80
84
  const isOverlapped = (item) => {
@@ -164,7 +164,7 @@ export class NestApplicationContext extends AbstractInstanceResolver {
164
164
  * @returns {this} The Nest application context instance
165
165
  */
166
166
  enableShutdownHooks(signals = [], options = {}) {
167
- if (isEmptyArray(signals)) {
167
+ if (!signals || isEmptyArray(signals)) {
168
168
  signals = Object.values(ShutdownSignal);
169
169
  }
170
170
  else {
@@ -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>;
@@ -31,7 +32,7 @@ export declare class NestApplication extends NestApplicationContext<NestApplicat
31
32
  applyOptions(): void;
32
33
  createServer<T = any>(): T;
33
34
  registerModules(): Promise<void>;
34
- registerWsModule(): void;
35
+ registerWsModule(): Promise<void>;
35
36
  init(): Promise<this>;
36
37
  registerParserMiddleware(): void;
37
38
  registerRouter(): Promise<void>;
@@ -67,6 +68,7 @@ export declare class NestApplication extends NestApplicationContext<NestApplicat
67
68
  private getProtocol;
68
69
  private registerMiddleware;
69
70
  private applyInstanceDecoratorIfRegistered;
71
+ private applyFunctionDecoratorIfRegistered;
70
72
  private loadSocketModule;
71
73
  private loadMicroservicesModule;
72
74
  }
@@ -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({
@@ -80,18 +85,19 @@ export class NestApplication extends NestApplicationContext {
80
85
  return this.httpAdapter.getHttpServer();
81
86
  }
82
87
  async registerModules() {
83
- this.registerWsModule();
88
+ await this.registerWsModule();
84
89
  if (this.microservicesModule) {
85
90
  this.microservicesModule.register(this.container, this.graphInspector, this.config, this.appOptions);
86
91
  this.microservicesModule.setupClients(this.container);
87
92
  }
88
93
  await this.middlewareModule.register(this.middlewareContainer, this.container, this.config, this.injector, this.httpAdapter, this.graphInspector, this.appOptions);
89
94
  }
90
- registerWsModule() {
95
+ async registerWsModule() {
91
96
  if (!this.socketModule) {
92
97
  return;
93
98
  }
94
- this.socketModule.register(this.container, this.config, this.graphInspector, this.appOptions, this.httpServer);
99
+ await 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,14 +130,81 @@ 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();
131
204
  this.routesResolver.registerExceptionHandler();
132
205
  }
133
206
  connectMicroservice(microserviceOptions, hybridAppOptions = {}) {
134
- const { NestMicroservice } = loadPackageCached('@nestjs/microservices');
207
+ const { NestMicroservice } = loadPackageCached('@nestjs/microservices', 'NestFactory');
135
208
  const { inheritAppConfig } = hybridAppOptions;
136
209
  const applicationConfig = inheritAppConfig
137
210
  ? this.config
@@ -157,7 +230,7 @@ export class NestApplication extends NestApplicationContext {
157
230
  return this;
158
231
  }
159
232
  use(...args) {
160
- this.httpAdapter.use(...args);
233
+ this.httpAdapter.use(...this.applyFunctionDecoratorIfRegistered(args));
161
234
  return this;
162
235
  }
163
236
  useBodyParser(...args) {
@@ -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
  }
@@ -339,6 +415,20 @@ export class NestApplication extends NestApplicationContext {
339
415
  }
340
416
  return instances;
341
417
  }
418
+ applyFunctionDecoratorIfRegistered(args) {
419
+ if (!this.appOptions.instrument?.instanceDecorator) {
420
+ return args;
421
+ }
422
+ const [firstArg, secondArg] = args;
423
+ return [
424
+ isFunction(firstArg)
425
+ ? this.appOptions.instrument.instanceDecorator(firstArg)
426
+ : firstArg,
427
+ isFunction(secondArg)
428
+ ? this.appOptions.instrument.instanceDecorator(secondArg)
429
+ : secondArg,
430
+ ];
431
+ }
342
432
  async loadSocketModule() {
343
433
  if (!this.socketModule) {
344
434
  const socketModule = await optionalRequire('@nestjs/websockets/socket-module', () => import('@nestjs/websockets/socket-module.js'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/core",
3
- "version": "12.0.0-alpha.4",
3
+ "version": "12.0.0-alpha.6",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@core)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -28,18 +28,7 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
- "scripts": {
32
- "postinstall": "opencollective || exit 0"
33
- },
34
- "collective": {
35
- "type": "opencollective",
36
- "url": "https://opencollective.com/nest",
37
- "donation": {
38
- "text": "Become a partner:"
39
- }
40
- },
41
31
  "dependencies": {
42
- "@nuxt/opencollective": "0.4.1",
43
32
  "fast-safe-stringify": "2.1.1",
44
33
  "iterare": "1.2.1",
45
34
  "path-to-regexp": "8.4.2",
@@ -47,7 +36,7 @@
47
36
  "uid": "2.0.2"
48
37
  },
49
38
  "devDependencies": {
50
- "@nestjs/common": "^12.0.0-alpha.4"
39
+ "@nestjs/common": "^12.0.0-alpha.6"
51
40
  },
52
41
  "peerDependencies": {
53
42
  "@nestjs/common": "^11.0.0",
@@ -68,5 +57,5 @@
68
57
  "optional": true
69
58
  }
70
59
  },
71
- "gitHead": "415e4d33707e74998d8456be7bd305cedb6508e3"
60
+ "gitHead": "4c38a5ab100f1d2a0d50602998968a25509525c7"
72
61
  }
@@ -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
+ }
@@ -12,5 +12,5 @@ export declare class LegacyRouteConverter {
12
12
  logs?: boolean;
13
13
  }): string;
14
14
  static printError(route: string): void;
15
- static printWarning(route: string): void;
15
+ static printWarning(route: string, convertedRoute?: string): void;
16
16
  }