refactor(core): Allow modules to define paths to load nodes from (#18193)

This commit is contained in:
Iván Ovejero
2025-08-14 11:12:08 +02:00
committed by GitHub
parent a94acbd828
commit c6ae71817e
5 changed files with 42 additions and 8 deletions

View File

@@ -218,3 +218,22 @@ describe('initModules', () => {
expect(moduleRegistry.getActiveModules()).toEqual([moduleName]);
});
});
describe('loadDir', () => {
it('should load dirs defined by modules', async () => {
const TEST_LOAD_DIR = '/path/to/module/load/dir';
const ModuleClass = {
entities: jest.fn().mockReturnValue([]),
loadDir: jest.fn().mockReturnValue(TEST_LOAD_DIR),
};
const moduleMetadata = mock<ModuleMetadata>({
getClasses: jest.fn().mockReturnValue([ModuleClass]),
});
Container.get = jest.fn().mockReturnValue(ModuleClass);
const moduleRegistry = new ModuleRegistry(moduleMetadata, mock(), mock(), mock());
await moduleRegistry.loadModules([]); // empty to skip dynamic imports
expect(moduleRegistry.loadDirs).toEqual([TEST_LOAD_DIR]);
});
});

View File

@@ -15,6 +15,8 @@ import { Logger } from '../logging/logger';
export class ModuleRegistry {
readonly entities: EntityClass[] = [];
readonly loadDirs: string[] = [];
readonly settings: Map<string, ModuleSettings> = new Map();
constructor(
@@ -79,9 +81,11 @@ export class ModuleRegistry {
for (const ModuleClass of this.moduleMetadata.getClasses()) {
const entities = await Container.get(ModuleClass).entities?.();
if (!entities || entities.length === 0) continue;
if (entities?.length) this.entities.push(...entities);
this.entities.push(...entities);
const loadDir = Container.get(ModuleClass).loadDir?.();
if (loadDir) this.loadDirs.push(loadDir);
}
}

View File

@@ -31,6 +31,12 @@ export interface ModuleInterface {
shutdown?(): Promise<void>;
entities?(): Promise<EntityClass[]>;
settings?(): Promise<ModuleSettings>;
/**
* @returns Path to a dir to load nodes and credentials from.
* @example '/Users/nathan/.n8n/nodes/node_modules'
*/
loadDir?(): string;
}
export type ModuleClass = Constructable<ModuleInterface>;

View File

@@ -30,7 +30,7 @@ describe('LoadNodesAndCredentials', () => {
let instance: LoadNodesAndCredentials;
beforeEach(() => {
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
instance.loaders.package1 = mock<DirectoryLoader>({
directory: '/icons/package1',
});
@@ -58,7 +58,7 @@ describe('LoadNodesAndCredentials', () => {
});
describe('convertNodeToAiTool', () => {
const instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
const instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
let fullNodeWrapper: { description: INodeTypeDescription };
@@ -290,7 +290,7 @@ describe('LoadNodesAndCredentials', () => {
let instance: LoadNodesAndCredentials;
beforeEach(() => {
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
instance.knownNodes['n8n-nodes-base.test'] = {
className: 'Test',
sourcePath: '/nodes-base/dist/nodes/Test/Test.node.js',
@@ -330,7 +330,7 @@ describe('LoadNodesAndCredentials', () => {
let instance: LoadNodesAndCredentials;
beforeEach(() => {
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
instance.types.nodes = [
{
name: 'testNode',
@@ -455,7 +455,7 @@ describe('LoadNodesAndCredentials', () => {
});
beforeEach(() => {
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock());
instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock(), mock());
instance.loaders = { CUSTOM: mockLoader };
// Allow access to directory

View File

@@ -1,4 +1,4 @@
import { inTest, isContainedWithin, Logger } from '@n8n/backend-common';
import { inTest, isContainedWithin, Logger, ModuleRegistry } from '@n8n/backend-common';
import { GlobalConfig } from '@n8n/config';
import { Container, Service } from '@n8n/di';
import type ParcelWatcher from '@parcel/watcher';
@@ -58,6 +58,7 @@ export class LoadNodesAndCredentials {
private readonly errorReporter: ErrorReporter,
private readonly instanceSettings: InstanceSettings,
private readonly globalConfig: GlobalConfig,
private readonly moduleRegistry: ModuleRegistry,
) {}
async init() {
@@ -98,6 +99,10 @@ export class LoadNodesAndCredentials {
);
}
for (const dir of this.moduleRegistry.loadDirs) {
await this.loadNodesFromNodeModules(dir);
}
await this.loadNodesFromCustomDirectories();
await this.postProcessLoaders();
}