fix(core): Import security audit risk reporters with a file extension (backport to release-candidate/2.32.x) (#35267)

Co-authored-by: Lorent Lempereur <looorent@users.noreply.github.com>
This commit is contained in:
n8n-assistant[bot] 2026-07-30 13:06:46 +00:00 committed by GitHub
parent c580585cf4
commit 7d61ea0122
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 4 deletions

View File

@ -0,0 +1,43 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { NoDynamicImportTemplateRule } from './no-dynamic-import-template.js';
const ruleTester = new RuleTester();
ruleTester.run('no-dynamic-import-template', NoDynamicImportTemplateRule, {
valid: [
{
code: 'await import(`./reporters/${name}.js`)',
},
{
code: 'await import(`../reporters/${name}.mjs`)',
},
{
code: 'await import(`./data/${name}.json`)',
},
{
code: "await import('./reporters/credentials.js')",
},
{
code: 'await import(`n8n-nodes-base/${name}`)',
},
],
invalid: [
{
code: 'await import(`@/security-audit/${name}.js`)',
errors: [{ messageId: 'noDynamicImportTemplate' }],
},
{
code: 'await import(`./reporters/${name}`)',
errors: [{ messageId: 'missingExtension' }],
},
{
code: 'await import(`../reporters/${name}`)',
errors: [{ messageId: 'missingExtension' }],
},
{
code: 'await import(`./reporters/${name}.ts`)',
errors: [{ messageId: 'missingExtension' }],
},
],
});

View File

@ -1,16 +1,20 @@
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
const MODULE_EXTENSIONS = ['.js', '.mjs', '.cjs', '.json', '.node'];
export const NoDynamicImportTemplateRule = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
docs: {
description:
'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.',
'Disallow non-relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario. Also require an explicit file extension, because `module: NodeNext` emits a native `import()` whose specifier Node resolves without extension inference.',
},
schema: [],
messages: {
noDynamicImportTemplate:
'Use relative imports in template string argument to `await import()`, because `tsc-alias` as of 1.8.7 is unable to resolve aliased paths in this scenario.',
missingExtension:
'End the template string argument to `await import()` with an explicit file extension (e.g. `.js`). Under `module: NodeNext` this compiles to a native `import()`, and Node throws ERR_MODULE_NOT_FOUND for an extensionless relative specifier.',
},
},
defaultOptions: [],
@ -26,6 +30,23 @@ export const NoDynamicImportTemplateRule = ESLintUtils.RuleCreator.withoutDocs({
messageId: 'noDynamicImportTemplate',
});
},
'AwaitExpression > ImportExpression > TemplateLiteral'(node: TSESTree.TemplateLiteral) {
const head = node.quasis[0].value.cooked;
if (!head?.startsWith('./') && !head?.startsWith('../')) {
return;
}
const tail = node.quasis[node.quasis.length - 1].value.cooked ?? '';
if (!MODULE_EXTENSIONS.some((extension) => tail.endsWith(extension))) {
context.report({
node,
messageId: 'missingExtension',
});
}
},
};
},
});

View File

@ -63,11 +63,10 @@ export class SecurityAuditService {
};
// `@vite-ignore` keeps vite's dynamic-import-vars plugin from analyzing this
// runtime-only import: without a static file extension it otherwise errors,
// which surfaces fatally during v8 coverage's uncovered-file walk.
// runtime-only import, which surfaces fatally during v8 coverage's uncovered-file walk.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const RiskReporterModule = await import(
/* @vite-ignore */ `./risk-reporters/${toFilename[className]}`
/* @vite-ignore */ `./risk-reporters/${toFilename[className]}.js`
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access