n8n/packages/cli/src/LoadNodesAndCredentials.ts
Iván Ovejero 56c4c6991f
🎨 Set up linting and formatting (#2120)
* ⬆️ Upgrade TS to 4.3.5

* 👕 Add ESLint configs

* 🎨 Add Prettier config

* 📦 Add deps and commands

*  Adjust global .editorconfig to new ruleset

* 🔥 Remove unneeded local .editorconfig

* 📦 Update deps in editor-ui

* 🔨 Limit Prettier to only TS files

*  Add recommended VSCode extensions

* 👕 Fix build

* 🔥 Remove Vue setting from global config

*  Disable prefer-default-export per feedback

* ✏️ Add forgotten divider

* 👕 Disable no-plusplus

* 👕 Disable class-methods-use-this

* ✏️ Alphabetize overrides

* 👕 Add one-var consecutive override

*  Revert one-var consecutive override

This reverts commit b9252cf935.

* 🎨 👕 Lint and format workflow package (#2121)

* 🎨 Format /workflow package

* 👕 Lint /workflow package

* 🎨 Re-format /workflow package

* 👕 Re-lint /workflow package

* ✏️ Fix typo

*  Consolidate if-checks

* 🔥 Remove prefer-default-export exceptions

* 🔥 Remove no-plusplus exceptions

* 🔥 Remove class-methods-use-this exceptions

* 🎨 👕 Lint and format node-dev package (#2122)

* 🎨 Format /node-dev package

*  Exclude templates from ESLint config

This keeps the templates consistent with the codebase while preventing lint exceptions from being made part of the templates.

* 👕 Lint /node-dev package

* 🔥 Remove prefer-default-export exceptions

* 🔥 Remove no-plusplus exceptions

* 🎨 👕 Lint and format core package (#2123)

* 🎨 Format /core package

* 👕 Lint /core package

* 🎨 Re-format /core package

* 👕 Re-lint /core package

* 🔥 Remove prefer-default-export exceptions

* 🔥 Remove no-plusplus exceptions

* 🔥 Remove class-methods-use-this exceptions

* 🎨 👕 Lint and format cli package (#2124)

* 🎨 Format /cli package

* 👕 Exclude migrations from linting

* 👕 Lint /cli package

* 🎨 Re-format /cli package

* 👕 Re-lint /cli package

* 👕 Fix build

* 🔥 Remove prefer-default-export exceptions

*  Update exceptions in ActiveExecutions

* 🔥 Remove no-plusplus exceptions

* 🔥 Remove class-methods-use-this exceptions

* 👕 fix lint issues

* 🔧 use package specific linter, remove tslint command

* 🔨 resolve build issue, sync dependencies

* 🔧 change lint command

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
2021-08-29 20:58:11 +02:00

358 lines
11 KiB
TypeScript

/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable no-prototype-builtins */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-continue */
/* eslint-disable no-restricted-syntax */
import { CUSTOM_EXTENSION_ENV, UserSettings } from 'n8n-core';
import {
CodexData,
ICredentialType,
ILogger,
INodeType,
INodeTypeData,
LoggerProxy,
} from 'n8n-workflow';
import {
access as fsAccess,
readdir as fsReaddir,
readFile as fsReadFile,
stat as fsStat,
} from 'fs/promises';
import * as glob from 'fast-glob';
import * as path from 'path';
import { getLogger } from './Logger';
import * as config from '../config';
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
class LoadNodesAndCredentialsClass {
nodeTypes: INodeTypeData = {};
credentialTypes: {
[key: string]: ICredentialType;
} = {};
excludeNodes: string[] | undefined = undefined;
includeNodes: string[] | undefined = undefined;
nodeModulesPath = '';
logger: ILogger;
async init() {
this.logger = getLogger();
LoggerProxy.init(this.logger);
// Get the path to the node-modules folder to be later able
// to load the credentials and nodes
const checkPaths = [
// In case "n8n" package is in same node_modules folder.
path.join(__dirname, '..', '..', '..', 'n8n-workflow'),
// In case "n8n" package is the root and the packages are
// in the "node_modules" folder underneath it.
path.join(__dirname, '..', '..', 'node_modules', 'n8n-workflow'),
];
for (const checkPath of checkPaths) {
try {
await fsAccess(checkPath);
// Folder exists, so use it.
this.nodeModulesPath = path.dirname(checkPath);
break;
} catch (error) {
// Folder does not exist so get next one
// eslint-disable-next-line no-continue
continue;
}
}
if (this.nodeModulesPath === '') {
throw new Error('Could not find "node_modules" folder!');
}
this.excludeNodes = config.get('nodes.exclude');
this.includeNodes = config.get('nodes.include');
// Get all the installed packages which contain n8n nodes
const packages = await this.getN8nNodePackages();
for (const packageName of packages) {
await this.loadDataFromPackage(packageName);
}
// Read nodes and credentials from custom directories
const customDirectories = [];
// Add "custom" folder in user-n8n folder
customDirectories.push(UserSettings.getUserN8nFolderCustomExtensionPath());
// Add folders from special environment variable
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
// eslint-disable-next-line prefer-spread
customDirectories.push.apply(customDirectories, customExtensionFolders);
}
for (const directory of customDirectories) {
await this.loadDataFromDirectory('CUSTOM', directory);
}
}
/**
* Returns all the names of the packages which could
* contain n8n nodes
*
* @returns {Promise<string[]>}
* @memberof LoadNodesAndCredentialsClass
*/
async getN8nNodePackages(): Promise<string[]> {
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
const results: string[] = [];
const nodeModulesPath = `${this.nodeModulesPath}/${relativePath}`;
for (const file of await fsReaddir(nodeModulesPath)) {
const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0;
const isNpmScopedPackage = file.indexOf('@') === 0;
if (!isN8nNodesPackage && !isNpmScopedPackage) {
continue;
}
if (!(await fsStat(nodeModulesPath)).isDirectory()) {
continue;
}
if (isN8nNodesPackage) {
results.push(`${relativePath}${file}`);
}
if (isNpmScopedPackage) {
results.push(...(await getN8nNodePackagesRecursive(`${relativePath}${file}/`)));
}
}
return results;
};
return getN8nNodePackagesRecursive('');
}
/**
* Loads credentials from a file
*
* @param {string} credentialName The name of the credentials
* @param {string} filePath The file to read credentials from
* @returns {Promise<void>}
*/
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath);
let tempCredential: ICredentialType;
try {
tempCredential = new tempModule[credentialName]() as ICredentialType;
} catch (e) {
if (e instanceof TypeError) {
throw new Error(
`Class with name "${credentialName}" could not be found. Please check if the class is named correctly!`,
);
} else {
throw e;
}
}
this.credentialTypes[tempCredential.name] = tempCredential;
}
/**
* Loads a node from a file
*
* @param {string} packageName The package name to set for the found nodes
* @param {string} nodeName Tha name of the node
* @param {string} filePath The file to read node from
* @returns {Promise<void>}
*/
async loadNodeFromFile(packageName: string, nodeName: string, filePath: string): Promise<void> {
let tempNode: INodeType;
let fullNodeName: string;
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
const tempModule = require(filePath);
try {
tempNode = new tempModule[nodeName]() as INodeType;
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error loading node "${nodeName}" from: "${filePath}"`);
throw error;
}
// eslint-disable-next-line prefer-const
fullNodeName = `${packageName}.${tempNode.description.name}`;
tempNode.description.name = fullNodeName;
if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) {
// If a file icon gets used add the full path
tempNode.description.icon = `file:${path.join(
path.dirname(filePath),
tempNode.description.icon.substr(5),
)}`;
}
if (tempNode.executeSingle) {
this.logger.warn(
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
{ filePath },
);
}
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
return;
}
// Check if the node should be skiped
if (this.excludeNodes !== undefined && this.excludeNodes.includes(fullNodeName)) {
return;
}
this.nodeTypes[fullNodeName] = {
type: tempNode,
sourcePath: filePath,
};
}
/**
* Retrieves `categories`, `subcategories` and alias (if defined)
* from the codex data for the node at the given file path.
*
* @param {string} filePath The file path to a `*.node.js` file
* @returns {CodexData}
*/
getCodex(filePath: string): CodexData {
// eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires
const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
...(categories && { categories }),
...(subcategories && { subcategories }),
...(alias && { alias }),
};
}
/**
* Adds a node codex `categories` and `subcategories` (if defined)
* to a node description `codex` property.
*
* @param {object} obj
* @param obj.node Node to add categories to
* @param obj.filePath Path to the built node
* @param obj.isCustom Whether the node is custom
* @returns {void}
*/
addCodex({ node, filePath, isCustom }: { node: INodeType; filePath: string; isCustom: boolean }) {
try {
const codex = this.getCodex(filePath);
if (isCustom) {
codex.categories = codex.categories
? codex.categories.concat(CUSTOM_NODES_CATEGORY)
: [CUSTOM_NODES_CATEGORY];
}
node.description.codex = codex;
} catch (_) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
if (isCustom) {
node.description.codex = {
categories: [CUSTOM_NODES_CATEGORY],
};
}
}
}
/**
* Loads nodes and credentials from the given directory
*
* @param {string} setPackageName The package name to set for the found nodes
* @param {string} directory The directory to look in
* @returns {Promise<void>}
*/
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
const files = await glob(path.join(directory, '**/*.@(node|credentials).js'));
let fileName: string;
let type: string;
const loadPromises = [];
for (const filePath of files) {
[fileName, type] = path.parse(filePath).name.split('.');
if (type === 'node') {
loadPromises.push(this.loadNodeFromFile(setPackageName, fileName, filePath));
} else if (type === 'credentials') {
loadPromises.push(this.loadCredentialsFromFile(fileName, filePath));
}
}
await Promise.all(loadPromises);
}
/**
* Loads nodes and credentials from the package with the given name
*
* @param {string} packageName The name to read data from
* @returns {Promise<void>}
*/
async loadDataFromPackage(packageName: string): Promise<void> {
// Get the absolute path of the package
const packagePath = path.join(this.nodeModulesPath, packageName);
// Read the data from the package.json file to see if any n8n data is defiend
const packageFileString = await fsReadFile(path.join(packagePath, 'package.json'), 'utf8');
const packageFile = JSON.parse(packageFileString);
if (!packageFile.hasOwnProperty('n8n')) {
return;
}
let tempPath: string;
let filePath: string;
// Read all node types
let fileName: string;
let type: string;
if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) {
for (filePath of packageFile.n8n.nodes) {
tempPath = path.join(packagePath, filePath);
[fileName, type] = path.parse(filePath).name.split('.');
await this.loadNodeFromFile(packageName, fileName, tempPath);
}
}
// Read all credential types
if (
packageFile.n8n.hasOwnProperty('credentials') &&
Array.isArray(packageFile.n8n.credentials)
) {
for (filePath of packageFile.n8n.credentials) {
tempPath = path.join(packagePath, filePath);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[fileName, type] = path.parse(filePath).name.split('.');
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.loadCredentialsFromFile(fileName, tempPath);
}
}
}
}
let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined;
export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass {
if (packagesInformationInstance === undefined) {
packagesInformationInstance = new LoadNodesAndCredentialsClass();
}
return packagesInformationInstance;
}