mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
refactor(core): Load packaged modules from a static manifest (no-changelog) (#40037)
This commit is contained in:
parent
628ada76cd
commit
ea38f3a3ee
|
|
@ -5,6 +5,7 @@ export { inDevelopment, inProduction, inTest, isEnvFeatureEnabled } from './envi
|
|||
export { isObjectLiteral } from './utils/is-object-literal';
|
||||
export { Logger } from './logging/logger';
|
||||
export { ModuleRegistry } from './modules/module-registry';
|
||||
export type { PackagedModules } from './modules/module-registry';
|
||||
export type { ModuleName } from './modules/modules.config';
|
||||
export { ModulesConfig } from './modules/modules.config';
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -177,6 +177,72 @@ describe('loadModules', () => {
|
|||
expect(moduleRegistry.entities).toEqual([]);
|
||||
});
|
||||
|
||||
describe('packaged modules', () => {
|
||||
const newRegistry = () => {
|
||||
const ModuleClass = { entities: vi.fn().mockReturnValue([]) };
|
||||
const moduleMetadata = mock<ModuleMetadata>({
|
||||
getClasses: vi.fn().mockReturnValue([ModuleClass]),
|
||||
});
|
||||
Container.get = vi.fn().mockReturnValue(ModuleClass);
|
||||
|
||||
return new ModuleRegistry(moduleMetadata, mock(), mock(), mock(), mock());
|
||||
};
|
||||
|
||||
it('should load a module from the manifest instead of the filesystem', async () => {
|
||||
const importPackagedModule = vi.fn().mockResolvedValue({});
|
||||
const moduleRegistry = newRegistry();
|
||||
moduleRegistry.registerPackagedModules({ insights: importPackagedModule });
|
||||
|
||||
await moduleRegistry.loadModules(['insights']);
|
||||
|
||||
expect(importPackagedModule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should wrap a packaged module import failure', async () => {
|
||||
const importError = new Error('Package import failed');
|
||||
const moduleRegistry = newRegistry();
|
||||
moduleRegistry.registerPackagedModules({
|
||||
insights: vi.fn().mockRejectedValue(importError),
|
||||
});
|
||||
|
||||
const loading = moduleRegistry.loadModules(['insights']);
|
||||
|
||||
await expect(loading).rejects.toThrow(ModuleLoadError);
|
||||
await expect(loading).rejects.toThrow(importError.message);
|
||||
});
|
||||
|
||||
it('should use the filesystem route for a module that is not in the manifest', async () => {
|
||||
const importPackagedModule = vi.fn().mockResolvedValue({});
|
||||
const moduleRegistry = newRegistry();
|
||||
moduleRegistry.registerPackagedModules({ insights: importPackagedModule });
|
||||
|
||||
await expect(moduleRegistry.loadModules(['otel'])).rejects.toThrow(MissingModuleError);
|
||||
expect(importPackagedModule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should load an eligible packaged module', async () => {
|
||||
const importPackagedModule = vi.fn().mockResolvedValue({});
|
||||
const moduleRegistry = newRegistry();
|
||||
vi.spyOn(moduleRegistry, 'eligibleModules', 'get').mockReturnValue(['insights']);
|
||||
moduleRegistry.registerPackagedModules({ insights: importPackagedModule });
|
||||
|
||||
await moduleRegistry.loadModules();
|
||||
|
||||
expect(importPackagedModule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not load an ineligible packaged module', async () => {
|
||||
const importPackagedModule = vi.fn().mockResolvedValue({});
|
||||
const moduleRegistry = newRegistry();
|
||||
vi.spyOn(moduleRegistry, 'eligibleModules', 'get').mockReturnValue([]);
|
||||
moduleRegistry.registerPackagedModules({ insights: importPackagedModule });
|
||||
|
||||
await moduleRegistry.loadModules();
|
||||
|
||||
expect(importPackagedModule).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('entrypoint resolution', () => {
|
||||
const MISSING_DEPENDENCY = 'n8n-fixture-absent-dependency';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import { ModuleLoadError } from './errors/module-load.error';
|
|||
import { ModulesConfig } from './modules.config';
|
||||
import type { ModuleName } from './modules.config';
|
||||
|
||||
/** Modules that load from workspace packages instead of the n8n modules directory. */
|
||||
export type PackagedModules = Partial<Record<ModuleName, () => Promise<unknown>>>;
|
||||
|
||||
const getModuleEntryPath = (modulesDir: string, moduleName: string, isEnterprise = false) =>
|
||||
path.join(modulesDir, isEnterprise ? `${moduleName}.ee` : moduleName, `${moduleName}.module.js`);
|
||||
|
||||
|
|
@ -81,6 +84,12 @@ export class ModuleRegistry {
|
|||
|
||||
private readonly activeModules: string[] = [];
|
||||
|
||||
private readonly packagedModules: PackagedModules = {};
|
||||
|
||||
registerPackagedModules(packagedModules: PackagedModules) {
|
||||
Object.assign(this.packagedModules, packagedModules);
|
||||
}
|
||||
|
||||
get eligibleModules(): ModuleName[] {
|
||||
const { enabledModules, disabledModules } = this.modulesConfig;
|
||||
|
||||
|
|
@ -118,6 +127,18 @@ export class ModuleRegistry {
|
|||
}
|
||||
|
||||
for (const moduleName of modules ?? this.eligibleModules) {
|
||||
const importPackagedModule = this.packagedModules[moduleName];
|
||||
|
||||
if (importPackagedModule) {
|
||||
try {
|
||||
await importPackagedModule();
|
||||
} catch (error) {
|
||||
throw new ModuleLoadError(moduleName, error);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = getModuleEntryPath(modulesDir, moduleName);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { mock } from 'vitest-mock-extended';
|
|||
import { z } from 'zod';
|
||||
|
||||
import { CommandRegistry } from '../command-registry';
|
||||
import { packagedModules } from '../modules/modules.manifest';
|
||||
|
||||
vi.mock('fast-glob');
|
||||
vi.mock('node:fs/promises', () => ({ access: vi.fn() }));
|
||||
|
|
@ -69,6 +70,12 @@ describe('CommandRegistry', () => {
|
|||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should register packaged modules', () => {
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser);
|
||||
|
||||
expect(moduleRegistry.registerPackagedModules).toHaveBeenCalledWith(packagedModules);
|
||||
});
|
||||
|
||||
it('should execute the specified command', async () => {
|
||||
process.argv = ['node', 'n8n', 'test-command'];
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import picocolors from 'picocolors';
|
|||
import { z, ZodError } from 'zod';
|
||||
|
||||
import './zod-alias-support';
|
||||
import { packagedModules } from './modules/modules.manifest';
|
||||
|
||||
/**
|
||||
* Registry that manages CLI commands, their execution, and metadata.
|
||||
|
|
@ -24,6 +25,7 @@ export class CommandRegistry {
|
|||
private readonly cliParser: CliParser,
|
||||
) {
|
||||
this.commandName = process.argv[2] ?? 'start';
|
||||
this.moduleRegistry.registerPackagedModules(packagedModules);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
|
|
|
|||
7
packages/cli/src/modules/modules.manifest.ts
Normal file
7
packages/cli/src/modules/modules.manifest.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { PackagedModules } from '@n8n/backend-common';
|
||||
|
||||
/**
|
||||
* Backend modules that load from workspace packages instead of this directory.
|
||||
* Keep each import in a thunk so an ineligible module is not imported.
|
||||
*/
|
||||
export const packagedModules: PackagedModules = {};
|
||||
|
|
@ -6,7 +6,8 @@ through Vite aliases.
|
|||
|
||||
`<name>/backend` is a reserved path rather than a workspace package — the backend runtime discovers
|
||||
modules under `packages/cli/src/modules/<name>`. The extra nesting level is what lets both halves of
|
||||
a module sit together later.
|
||||
a module sit together later. Add packaged backend modules to
|
||||
`packages/cli/src/modules/modules.manifest.ts`.
|
||||
|
||||
`packages/modules` itself stays tracked even when it holds nothing: turbo rejects a `--filter`
|
||||
whose directory does not exist.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user