Files
n8n-nodes-gwezz-changdunovel/node_modules/@n8n/eslint-plugin-community-nodes/dist/utils/file-utils.js
2025-10-26 23:10:15 +08:00

221 lines
8.3 KiB
JavaScript

import { parse, simpleTraverse, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import * as path from 'node:path';
import { dirname, parse as parsePath } from 'node:path';
import { isCredentialTypeClass, isNodeTypeClass, findClassProperty, getStringLiteralValue, findArrayLiteralProperty, extractCredentialInfoFromArray, findSimilarStrings, } from './ast-utils.js';
/**
* Checks if the given childPath is contained within the parentPath. Resolves
* the paths before comparing them, so that relative paths are also supported.
*/
export function isContainedWithin(parentPath, childPath) {
parentPath = path.resolve(parentPath);
childPath = path.resolve(childPath);
if (parentPath === childPath) {
return true;
}
return childPath.startsWith(parentPath + path.sep);
}
/**
* Joins the given paths to the parentPath, ensuring that the resulting path
* is still contained within the parentPath. If not, it throws an error to
* prevent path traversal vulnerabilities.
*
* @throws {UnexpectedError} If the resulting path is not contained within the parentPath.
*/
export function safeJoinPath(parentPath, ...paths) {
const candidate = path.join(parentPath, ...paths);
if (!isContainedWithin(parentPath, candidate)) {
throw new Error(`Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}`);
}
return candidate;
}
export function findPackageJson(startPath) {
let currentDir = path.dirname(startPath);
while (parsePath(currentDir).dir !== parsePath(currentDir).root) {
const testPath = safeJoinPath(currentDir, 'package.json');
if (fileExistsWithCaseSync(testPath)) {
return testPath;
}
currentDir = dirname(currentDir);
}
return null;
}
function isValidPackageJson(obj) {
return typeof obj === 'object' && obj !== null;
}
function readPackageJsonN8n(packageJsonPath) {
try {
const content = readFileSync(packageJsonPath, 'utf8');
const parsed = JSON.parse(content);
if (isValidPackageJson(parsed)) {
return parsed.n8n ?? {};
}
return {};
}
catch {
return {};
}
}
function resolveN8nFilePaths(packageJsonPath, filePaths) {
const packageDir = dirname(packageJsonPath);
const resolvedFiles = [];
for (const filePath of filePaths) {
const sourcePath = filePath.replace(/^dist\//, '').replace(/\.js$/, '.ts');
const fullSourcePath = safeJoinPath(packageDir, sourcePath);
if (existsSync(fullSourcePath)) {
resolvedFiles.push(fullSourcePath);
}
}
return resolvedFiles;
}
export function readPackageJsonCredentials(packageJsonPath) {
const n8nConfig = readPackageJsonN8n(packageJsonPath);
const credentialPaths = n8nConfig.credentials ?? [];
const credentialFiles = resolveN8nFilePaths(packageJsonPath, credentialPaths);
const credentialNames = [];
for (const credentialFile of credentialFiles) {
try {
const credentialName = extractCredentialNameFromFile(credentialFile);
if (credentialName) {
credentialNames.push(credentialName);
}
}
catch {
// Silently continue if file can't be parsed
}
}
return new Set(credentialNames);
}
export function extractCredentialNameFromFile(credentialFilePath) {
try {
const sourceCode = readFileSync(credentialFilePath, 'utf8');
const ast = parse(sourceCode, {
jsx: false,
range: true,
});
let credentialName = null;
simpleTraverse(ast, {
enter(node) {
if (node.type === AST_NODE_TYPES.ClassDeclaration && isCredentialTypeClass(node)) {
const nameProperty = findClassProperty(node, 'name');
if (nameProperty) {
const nameValue = getStringLiteralValue(nameProperty.value);
if (nameValue) {
credentialName = nameValue;
}
}
}
},
});
return credentialName;
}
catch {
return null;
}
}
export function validateIconPath(iconPath, baseDir) {
const isFile = iconPath.startsWith('file:');
const relativePath = iconPath.replace(/^file:/, '');
const isSvg = relativePath.endsWith('.svg');
// Should not use safeJoinPath here because iconPath can be outside of the node class folder
const fullPath = path.join(baseDir, relativePath);
const exists = fileExistsWithCaseSync(fullPath);
return {
isValid: isFile && isSvg && exists,
isFile,
isSvg,
exists,
};
}
export function readPackageJsonNodes(packageJsonPath) {
const n8nConfig = readPackageJsonN8n(packageJsonPath);
const nodePaths = n8nConfig.nodes ?? [];
return resolveN8nFilePaths(packageJsonPath, nodePaths);
}
export function areAllCredentialUsagesTestedByNodes(credentialName, packageDir) {
const packageJsonPath = safeJoinPath(packageDir, 'package.json');
if (!existsSync(packageJsonPath)) {
return false;
}
const nodeFiles = readPackageJsonNodes(packageJsonPath);
let hasAnyCredentialUsage = false;
for (const nodeFile of nodeFiles) {
const result = checkCredentialUsageInFile(nodeFile, credentialName);
if (result.hasUsage) {
hasAnyCredentialUsage = true;
if (!result.allTestedBy) {
return false; // Found usage without testedBy
}
}
}
return hasAnyCredentialUsage;
}
function checkCredentialUsageInFile(nodeFile, credentialName) {
try {
const sourceCode = readFileSync(nodeFile, 'utf8');
const ast = parse(sourceCode, { jsx: false, range: true });
let hasUsage = false;
let allTestedBy = true;
simpleTraverse(ast, {
enter(node) {
if (node.type === AST_NODE_TYPES.ClassDeclaration && isNodeTypeClass(node)) {
const descriptionProperty = findClassProperty(node, 'description');
if (!descriptionProperty?.value ||
descriptionProperty.value.type !== AST_NODE_TYPES.ObjectExpression) {
return;
}
const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
if (!credentialsArray) {
return;
}
for (const element of credentialsArray.elements) {
const credentialInfo = extractCredentialInfoFromArray(element);
if (credentialInfo?.name === credentialName) {
hasUsage = true;
if (!credentialInfo.testedBy) {
allTestedBy = false;
}
}
}
}
},
});
return { hasUsage, allTestedBy };
}
catch {
return { hasUsage: false, allTestedBy: true };
}
}
function fileExistsWithCaseSync(filePath) {
try {
const dir = path.dirname(filePath);
const file = path.basename(filePath);
const files = new Set(readdirSync(dir));
return files.has(file);
}
catch {
return false;
}
}
export function findSimilarSvgFiles(targetPath, baseDir) {
try {
const targetFileName = path.basename(targetPath, path.extname(targetPath));
const targetDir = path.dirname(targetPath);
// Should not use safeJoinPath here because iconPath can be outside of the node class folder
const searchDir = path.join(baseDir, targetDir);
if (!existsSync(searchDir)) {
return [];
}
const files = readdirSync(searchDir);
const svgFileNames = files
.filter((file) => file.endsWith('.svg'))
.map((file) => path.basename(file, '.svg'));
const candidateNames = new Set(svgFileNames);
const similarNames = findSimilarStrings(targetFileName, candidateNames);
return similarNames.map((name) => path.join(targetDir, `${name}.svg`));
}
catch {
return [];
}
}
//# sourceMappingURL=file-utils.js.map