Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): added unique dynamic module factory #12898

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/core/injector/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ export class ModuleCompiler {
metatype: Type<any> | ForwardReference | DynamicModule,
): {
type: Type<any>;
dynamicMetadata?: Partial<DynamicModule> | undefined;
dynamicMetadata?: DynamicModule | undefined;
} {
if (!this.isDynamicModule(metatype)) {
return {
type: (metatype as ForwardReference)?.forwardRef
? (metatype as ForwardReference).forwardRef()
: metatype,
dynamicMetadata: undefined,
};
}
const { module: type, ...dynamicMetadata } = metatype;
return { type, dynamicMetadata };
return { type: metatype.module, dynamicMetadata: metatype };
}

public isDynamicModule(
Expand Down
1 change: 1 addition & 0 deletions packages/core/injector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { ContextId, HostComponentInfo } from './instance-wrapper';
export * from './lazy-module-loader/lazy-module-loader';
export * from './module-ref';
export * from './modules-container';
export { UniqueDynamicModuleFactory } from './unique-dynamic-module-factory';
10 changes: 9 additions & 1 deletion packages/core/injector/module-token-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { DynamicModule, Logger } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { isFunction, isSymbol } from '@nestjs/common/utils/shared.utils';
import {
getUniqueDynamicModuleId,
isUniqueDynamicModule,
} from './unique-dynamic-module-factory';
import { createHash } from 'crypto';
import stringify from 'fast-safe-stringify';
import { performance } from 'perf_hooks';
Expand All @@ -18,8 +22,12 @@ export class ModuleTokenFactory {

public create(
metatype: Type<unknown>,
dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
dynamicModuleMetadata?: DynamicModule | undefined,
): string {
if (isUniqueDynamicModule(dynamicModuleMetadata)) {
return getUniqueDynamicModuleId(dynamicModuleMetadata);
}

const moduleId = this.getModuleId(metatype);

if (!dynamicModuleMetadata) {
Expand Down
63 changes: 63 additions & 0 deletions packages/core/injector/unique-dynamic-module-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DynamicModule, Type } from '@nestjs/common';
import { RuntimeException } from '@nestjs/core/errors/exceptions';

const kUniqueModuleId = Symbol('kUniqueModuleId');

export class UniqueDynamicModuleFactory {
protected static readonly uniqueMap = new Map<string, string>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit concerned on this being static

what if we want to create multiple apps (let's say, one NestFactory.createApplicationContext and other with NestFactory.create)

I can't think of any other approach tho

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we want to create multiple apps (let's say, one NestFactory.createApplicationContext and other with NestFactory.create)

Just chiming in here - I don't think anyone should be doing that 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, me too :p So we can treat that as a limitation of UniqueDynamicModuleFactory if someone try to do so


/**
* You can use this method to avoid the hash algorithm during the DI compilation.
*
* Imagine you have a module that exports a provider, and that provider is
* imported using `MyModule.forRoot('myToken')`, and you can get the reference of this provider using
* injected using @Inject('myToken'). If you import this module twice, with different arguments on `.forRoot`,
* in order to register both modules, we need to serialize and hash the dynamic module created by `.forRoot`.
* Otherwise, it overrides the providers defined in the first import with the providers declared on the second import.
*
* This function tells the algorithm to skip the hash and directly use the `staticUniqueId`, and
*you are responsible for making it unique.
*
* @param staticUniqueId The unique ID across all modules.
* @param dynamicModule The dynamic module.
*
* @throws RuntimeException If the ID is already registered, an error will occur.
*/
static wrap(staticUniqueId: string, dynamicModule: DynamicModule) {
if (!this.uniqueMap.has(staticUniqueId)) {
dynamicModule[
kUniqueModuleId
] = `${dynamicModule.module.name}_${staticUniqueId}`;
this.uniqueMap.set(staticUniqueId, dynamicModule.module.name);
return dynamicModule;
}

throw new RuntimeException(
`A module with this ID was already added before for "${
this.uniqueMap.get(staticUniqueId).split('_')[0]
Copy link
Member

@micalevisk micalevisk Dec 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use | as the separator because _ is valid in class names so one could have this:

image

and so .split('|', 1)[0]

}".`,
);
}
}

/**
* Check if the given dynamic module was marked as unique.
*
* @param dynamicModule The dynamic module
*/
export function isUniqueDynamicModule(
dynamicModule: DynamicModule | Type<any> | undefined,
): boolean {
return dynamicModule && dynamicModule[kUniqueModuleId] !== undefined;
}

/**
* Get the stored unique dynamic module id.
*
* @param dynamicModule The dynamic module
*/
export function getUniqueDynamicModuleId(
dynamicModule: DynamicModule | Type<any>,
): string | undefined {
return dynamicModule[kUniqueModuleId];
}
Loading