@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.
- package/Readme.md +25 -45
- package/adapters/http-adapter.d.ts +2 -0
- package/adapters/http-adapter.js +3 -0
- package/application-config.d.ts +8 -2
- package/application-config.js +14 -0
- package/errors/exceptions/index.d.ts +1 -0
- package/errors/exceptions/index.js +1 -0
- package/errors/exceptions/invalid-class-module.exception.d.ts +1 -1
- package/errors/exceptions/invalid-class-module.exception.js +2 -2
- package/errors/exceptions/invalid-module.exception.d.ts +1 -1
- package/errors/exceptions/invalid-module.exception.js +2 -2
- package/errors/exceptions/route-conflict.exception.d.ts +4 -0
- package/errors/exceptions/route-conflict.exception.js +7 -0
- package/errors/messages.d.ts +5 -2
- package/errors/messages.js +37 -4
- package/exceptions/base-exception-filter.js +8 -3
- package/helpers/barrier.js +4 -1
- package/helpers/handler-metadata-storage.d.ts +1 -0
- package/helpers/router-method-factory.d.ts +1 -0
- package/helpers/router-method-factory.js +1 -0
- package/hooks/before-app-shutdown.hook.js +11 -2
- package/hooks/on-app-shutdown.hook.js +11 -2
- package/hooks/on-module-destroy.hook.js +11 -2
- package/injector/container.js +2 -2
- package/injector/helpers/transient-instances.d.ts +12 -0
- package/injector/helpers/transient-instances.js +28 -0
- package/injector/injector.d.ts +4 -3
- package/injector/injector.js +34 -13
- package/injector/instance-wrapper.d.ts +2 -0
- package/injector/instance-wrapper.js +25 -0
- package/injector/internal-core-module/internal-core-module-factory.js +1 -1
- package/injector/module.d.ts +6 -0
- package/injector/module.js +8 -0
- package/interceptors/interceptors-consumer.js +32 -7
- package/middleware/builder.js +5 -1
- package/nest-application-context.js +1 -1
- package/nest-application.d.ts +3 -1
- package/nest-application.js +96 -6
- package/package.json +3 -14
- package/router/interfaces/index.d.ts +3 -0
- package/router/interfaces/index.js +3 -0
- package/router/interfaces/resolved-route.interface.d.ts +32 -0
- package/router/interfaces/resolved-route.interface.js +1 -0
- package/router/interfaces/resolver.interface.d.ts +5 -1
- package/router/interfaces/route-conflict.interface.d.ts +14 -0
- package/router/interfaces/route-conflict.interface.js +1 -0
- package/router/interfaces/route-resolution-options.interface.d.ts +24 -0
- package/router/interfaces/route-resolution-options.interface.js +1 -0
- package/router/legacy-route-converter.d.ts +1 -1
- package/router/legacy-route-converter.js +24 -13
- package/router/route-conflict-detector.d.ts +71 -0
- package/router/route-conflict-detector.js +276 -0
- package/router/route-specificity-sorter.d.ts +23 -0
- package/router/route-specificity-sorter.js +59 -0
- package/router/router-execution-context.d.ts +1 -0
- package/router/router-execution-context.js +26 -3
- package/router/router-explorer.d.ts +11 -2
- package/router/router-explorer.js +61 -19
- package/router/router-response-controller.d.ts +1 -0
- package/router/router-response-controller.js +126 -49
- package/router/routes-resolver.d.ts +7 -4
- package/router/routes-resolver.js +9 -7
- package/router/sse-stream.d.ts +1 -0
- package/router/sse-stream.js +24 -13
- package/scanner.js +13 -6
package/injector/injector.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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);
|
package/injector/module.d.ts
CHANGED
|
@@ -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);
|
package/injector/module.js
CHANGED
|
@@ -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
|
|
3
|
-
import { mergeAll
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
}
|
package/middleware/builder.js
CHANGED
|
@@ -74,7 +74,11 @@ export class MiddlewareBuilder {
|
|
|
74
74
|
.map(route => ({
|
|
75
75
|
method: route.method,
|
|
76
76
|
path: route.path,
|
|
77
|
-
|
|
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 {
|
package/nest-application.d.ts
CHANGED
|
@@ -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
|
}
|
package/nest-application.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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": "
|
|
60
|
+
"gitHead": "4c38a5ab100f1d2a0d50602998968a25509525c7"
|
|
72
61
|
}
|
|
@@ -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(
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|