mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +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()) {
|
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;
|
if (!entities || entities.length === 0) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export type ModuleSettings = Record<string, unknown>;
|
|||||||
|
|
||||||
export interface ModuleInterface {
|
export interface ModuleInterface {
|
||||||
init?(): Promise<void>;
|
init?(): Promise<void>;
|
||||||
entities?(): EntityClass[];
|
entities?(): Promise<EntityClass[]>;
|
||||||
settings?(): Promise<ModuleSettings>;
|
settings?(): Promise<ModuleSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { NoDynamicImportTemplateRule } from './no-dynamic-import-template.js';
|
|||||||
import { MisplacedN8nTypeormImportRule } from './misplaced-n8n-typeorm-import.js';
|
import { MisplacedN8nTypeormImportRule } from './misplaced-n8n-typeorm-import.js';
|
||||||
import { NoTypeUnsafeEventEmitterRule } from './no-type-unsafe-event-emitter.js';
|
import { NoTypeUnsafeEventEmitterRule } from './no-type-unsafe-event-emitter.js';
|
||||||
import { NoUntypedConfigClassFieldRule } from './no-untyped-config-class-field.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';
|
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
||||||
|
|
||||||
export const rules = {
|
export const rules = {
|
||||||
@@ -25,4 +27,6 @@ export const rules = {
|
|||||||
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
'misplaced-n8n-typeorm-import': MisplacedN8nTypeormImportRule,
|
||||||
'no-type-unsafe-event-emitter': NoTypeUnsafeEventEmitterRule,
|
'no-type-unsafe-event-emitter': NoTypeUnsafeEventEmitterRule,
|
||||||
'no-untyped-config-class-field': NoUntypedConfigClassFieldRule,
|
'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>;
|
} 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',
|
'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 type { ModuleInterface } from '@n8n/decorators';
|
||||||
import { BackendModule } from '@n8n/decorators';
|
import { BackendModule } from '@n8n/decorators';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import './insights.controller';
|
|
||||||
import { InstanceSettings } from 'n8n-core';
|
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' })
|
@BackendModule({ name: 'insights' })
|
||||||
export class InsightsModule implements ModuleInterface {
|
export class InsightsModule implements ModuleInterface {
|
||||||
async init() {
|
async init() {
|
||||||
const { instanceType } = Container.get(InstanceSettings);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only main- and webhook-type instances collect insights because
|
* Only main- and webhook-type instances collect insights because
|
||||||
* only they are informed of finished workflow executions.
|
* only they are informed of finished workflow executions.
|
||||||
*/
|
*/
|
||||||
if (instanceType === 'worker') return;
|
if (Container.get(InstanceSettings).instanceType === 'worker') return;
|
||||||
|
|
||||||
await import('./insights.controller');
|
await import('./insights.controller');
|
||||||
|
|
||||||
@@ -25,12 +18,17 @@ export class InsightsModule implements ModuleInterface {
|
|||||||
Container.get(InsightsService).startTimers();
|
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];
|
return [InsightsByPeriod, InsightsMetadata, InsightsRaw];
|
||||||
}
|
}
|
||||||
|
|
||||||
async settings() {
|
async settings() {
|
||||||
const { InsightsService } = await import('./insights.service');
|
const { InsightsService } = await import('./insights.service');
|
||||||
|
|
||||||
return Container.get(InsightsService).settings();
|
return Container.get(InsightsService).settings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user