mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(core): Lint to enforce lazyloading in modules (#16843)
Co-authored-by: Juuso Tapaninen <juuso@n8n.io>
This commit is contained in:
@@ -74,7 +74,7 @@ export class ModuleRegistry {
|
||||
}
|
||||
|
||||
for (const ModuleClass of this.moduleMetadata.getClasses()) {
|
||||
const entities = Container.get(ModuleClass).entities?.();
|
||||
const entities = await Container.get(ModuleClass).entities?.();
|
||||
|
||||
if (!entities || entities.length === 0) continue;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export type ModuleSettings = Record<string, unknown>;
|
||||
|
||||
export interface ModuleInterface {
|
||||
init?(): Promise<void>;
|
||||
entities?(): EntityClass[];
|
||||
entities?(): Promise<EntityClass[]>;
|
||||
settings?(): Promise<ModuleSettings>;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import { NoDynamicImportTemplateRule } from './no-dynamic-import-template.js';
|
||||
import { MisplacedN8nTypeormImportRule } from './misplaced-n8n-typeorm-import.js';
|
||||
import { NoTypeUnsafeEventEmitterRule } from './no-type-unsafe-event-emitter.js';
|
||||
import { NoUntypedConfigClassFieldRule } from './no-untyped-config-class-field.js';
|
||||
import { NoTopLevelRelativeImportsInBackendModuleRule } from './no-top-level-relative-imports-in-backend-module.js';
|
||||
import { NoConstructorInBackendModuleRule } from './no-constructor-in-backend-module.js';
|
||||
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
export const rules = {
|
||||
@@ -25,4 +27,6 @@ export const rules = {
|
||||
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
||||
'no-type-unsafe-event-emitter': NoTypeUnsafeEventEmitterRule,
|
||||
'no-untyped-config-class-field': NoUntypedConfigClassFieldRule,
|
||||
'no-top-level-relative-imports-in-backend-module': NoTopLevelRelativeImportsInBackendModuleRule,
|
||||
'no-constructor-in-backend-module': NoConstructorInBackendModuleRule,
|
||||
} satisfies Record<string, AnyRuleModule>;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoConstructorInBackendModuleRule } from './no-constructor-in-backend-module.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-constructor-in-backend-module', NoConstructorInBackendModuleRule, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
// initialization code
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
export class RegularClass {
|
||||
constructor() {
|
||||
// this is fine in regular classes
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
@SomeOtherDecorator()
|
||||
export class OtherModule {
|
||||
constructor() {
|
||||
// this is fine with other decorators
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
constructor() {
|
||||
// this should be removed
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'noConstructorInBackendModule' }],
|
||||
output: `
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
|
||||
}`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
@BackendModule({ name: 'insights' })
|
||||
export class InsightsModule {
|
||||
constructor(private service: SomeService) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// other code
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'noConstructorInBackendModule' }],
|
||||
output: `
|
||||
@BackendModule({ name: 'insights' })
|
||||
export class InsightsModule {
|
||||
|
||||
|
||||
async init() {
|
||||
// other code
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoConstructorInBackendModuleRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'A class decorated with `@BackendModule` must not have a constructor. This ensures that module dependencies are loaded only when the module is used.',
|
||||
},
|
||||
messages: {
|
||||
noConstructorInBackendModule:
|
||||
'Remove the constructor from the class decorated with `@BackendModule`.',
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
'ClassDeclaration MethodDefinition[kind="constructor"]'(node: TSESTree.MethodDefinition) {
|
||||
const classDeclaration = node.parent?.parent as TSESTree.ClassDeclaration;
|
||||
|
||||
const isBackendModule =
|
||||
classDeclaration.decorators?.some(
|
||||
(d) =>
|
||||
d.expression.type === 'CallExpression' &&
|
||||
d.expression.callee.type === 'Identifier' &&
|
||||
d.expression.callee.name === 'BackendModule',
|
||||
) ?? false;
|
||||
|
||||
if (isBackendModule) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noConstructorInBackendModule',
|
||||
fix: (fixer) => fixer.remove(node),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { NoTopLevelRelativeImportsInBackendModuleRule } from './no-top-level-relative-imports-in-backend-module.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run(
|
||||
'no-top-level-relative-imports-in-backend-module',
|
||||
NoTopLevelRelativeImportsInBackendModuleRule,
|
||||
{
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
import { Container } from '@n8n/di';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
const { LocalService } = await import('./local.service');
|
||||
}
|
||||
}`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
import { Container } from '@n8n/di';
|
||||
import { LocalService } from './local.service';
|
||||
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
// code
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'placeInsideInit' }],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
import { BackendModule } from '@n8n/decorators';
|
||||
import { helper } from './helper';
|
||||
import { config } from './config';
|
||||
|
||||
@BackendModule({ name: 'test' })
|
||||
export class TestModule {
|
||||
async init() {
|
||||
// code
|
||||
}
|
||||
}`,
|
||||
errors: [{ messageId: 'placeInsideInit' }, { messageId: 'placeInsideInit' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export const NoTopLevelRelativeImportsInBackendModuleRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Relative imports in `.module.ts` files must be placed inside the `init` method. This ensures that module imports are loaded only when the module is used.',
|
||||
},
|
||||
messages: {
|
||||
placeInsideInit:
|
||||
"Place this relative import inside the `init` method, using `await import('./path')` syntax.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
return {
|
||||
'Program > ImportDeclaration'(node: TSESTree.ImportDeclaration) {
|
||||
if (node.source.value.startsWith('.')) {
|
||||
context.report({ node, messageId: 'placeInsideInit' });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -99,4 +99,12 @@ export default defineConfig(
|
||||
'n8n-local-rules/no-uncaught-json-parse': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.module.ts'],
|
||||
|
||||
rules: {
|
||||
'n8n-local-rules/no-top-level-relative-imports-in-backend-module': 'error',
|
||||
'n8n-local-rules/no-constructor-in-backend-module': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import type { ModuleInterface } from '@n8n/decorators';
|
||||
import { BackendModule } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import './insights.controller';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
import { InsightsByPeriod } from './database/entities/insights-by-period';
|
||||
import { InsightsMetadata } from './database/entities/insights-metadata';
|
||||
import { InsightsRaw } from './database/entities/insights-raw';
|
||||
|
||||
@BackendModule({ name: 'insights' })
|
||||
export class InsightsModule implements ModuleInterface {
|
||||
async init() {
|
||||
const { instanceType } = Container.get(InstanceSettings);
|
||||
|
||||
/**
|
||||
* Only main- and webhook-type instances collect insights because
|
||||
* only they are informed of finished workflow executions.
|
||||
*/
|
||||
if (instanceType === 'worker') return;
|
||||
if (Container.get(InstanceSettings).instanceType === 'worker') return;
|
||||
|
||||
await import('./insights.controller');
|
||||
|
||||
@@ -25,12 +18,17 @@ export class InsightsModule implements ModuleInterface {
|
||||
Container.get(InsightsService).startTimers();
|
||||
}
|
||||
|
||||
entities() {
|
||||
async entities() {
|
||||
const { InsightsByPeriod } = await import('./database/entities/insights-by-period');
|
||||
const { InsightsMetadata } = await import('./database/entities/insights-metadata');
|
||||
const { InsightsRaw } = await import('./database/entities/insights-raw');
|
||||
|
||||
return [InsightsByPeriod, InsightsMetadata, InsightsRaw];
|
||||
}
|
||||
|
||||
async settings() {
|
||||
const { InsightsService } = await import('./insights.service');
|
||||
|
||||
return Container.get(InsightsService).settings();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user