mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Lazy-load nodes and credentials to reduce baseline memory usage (#4577)
This commit is contained in:
committed by
GitHub
parent
f63cd3b89e
commit
b6c57e19fc
@@ -1,132 +1,108 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* 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,
|
||||
ICredentialTypeData,
|
||||
CUSTOM_EXTENSION_ENV,
|
||||
UserSettings,
|
||||
CustomDirectoryLoader,
|
||||
DirectoryLoader,
|
||||
PackageDirectoryLoader,
|
||||
LazyPackageDirectoryLoader,
|
||||
Types,
|
||||
} from 'n8n-core';
|
||||
import type {
|
||||
ILogger,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypeNameVersion,
|
||||
IVersionedNodeType,
|
||||
LoggerProxy,
|
||||
jsonParse,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
INodesAndCredentials,
|
||||
KnownNodesAndCredentials,
|
||||
LoadedNodesAndCredentials,
|
||||
} from 'n8n-workflow';
|
||||
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
access as fsAccess,
|
||||
copyFile,
|
||||
mkdir,
|
||||
readdir as fsReaddir,
|
||||
readFile as fsReadFile,
|
||||
stat as fsStat,
|
||||
writeFile,
|
||||
} from 'fs/promises';
|
||||
import glob from 'fast-glob';
|
||||
import path from 'path';
|
||||
import pick from 'lodash.pick';
|
||||
import { IN8nNodePackageJson } from '@/Interfaces';
|
||||
import { getLogger } from '@/Logger';
|
||||
import config from '@/config';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||
import { executeCommand, loadClassInIsolation } from '@/CommunityNodes/helpers';
|
||||
import { CLI_DIR, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { executeCommand } from '@/CommunityNodes/helpers';
|
||||
import { CLI_DIR, GENERATED_STATIC_DIR, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import {
|
||||
persistInstalledPackageData,
|
||||
removePackageFromDatabase,
|
||||
} from '@/CommunityNodes/packageModel';
|
||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||
|
||||
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||
export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
|
||||
known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
|
||||
|
||||
function toJSON() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...this,
|
||||
authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate,
|
||||
};
|
||||
}
|
||||
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
|
||||
|
||||
class LoadNodesAndCredentialsClass {
|
||||
nodeTypes: INodeTypeData = {};
|
||||
types: Types = { nodes: [], credentials: [] };
|
||||
|
||||
credentialTypes: ICredentialTypeData = {};
|
||||
excludeNodes = config.getEnv('nodes.exclude');
|
||||
|
||||
excludeNodes: string | undefined = undefined;
|
||||
|
||||
includeNodes: string | undefined = undefined;
|
||||
includeNodes = config.getEnv('nodes.include');
|
||||
|
||||
logger: ILogger;
|
||||
|
||||
async init() {
|
||||
this.logger = getLogger();
|
||||
LoggerProxy.init(this.logger);
|
||||
|
||||
// Make sure the imported modules can resolve dependencies fine.
|
||||
const delimiter = process.platform === 'win32' ? ';' : ':';
|
||||
process.env.NODE_PATH = module.paths.join(delimiter);
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
module.constructor._initPaths();
|
||||
|
||||
const nodeModulesPath = await this.getNodeModulesFolderLocation();
|
||||
|
||||
this.excludeNodes = config.getEnv('nodes.exclude');
|
||||
this.includeNodes = config.getEnv('nodes.include');
|
||||
|
||||
// Get all the installed packages which contain n8n nodes
|
||||
const nodePackages = await this.getN8nNodePackages(nodeModulesPath);
|
||||
|
||||
for (const packagePath of nodePackages) {
|
||||
await this.loadDataFromPackage(packagePath);
|
||||
}
|
||||
await mkdir(path.join(GENERATED_STATIC_DIR, 'icons/nodes'), { recursive: true });
|
||||
await mkdir(path.join(GENERATED_STATIC_DIR, 'icons/credentials'), { recursive: true });
|
||||
|
||||
await this.loadNodesFromBasePackages();
|
||||
await this.loadNodesFromDownloadedPackages();
|
||||
|
||||
await this.loadNodesFromCustomFolders();
|
||||
await this.loadNodesFromCustomDirectories();
|
||||
}
|
||||
|
||||
async getNodeModulesFolderLocation(): Promise<string> {
|
||||
// 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(CLI_DIR, '..', 'n8n-workflow'),
|
||||
// In case "n8n" package is the root and the packages are
|
||||
// in the "node_modules" folder underneath it.
|
||||
path.join(CLI_DIR, 'node_modules', 'n8n-workflow'),
|
||||
// In case "n8n" package is installed using npm/yarn workspaces
|
||||
// the node_modules folder is in the root of the workspace.
|
||||
path.join(CLI_DIR, '..', '..', 'node_modules', 'n8n-workflow'),
|
||||
];
|
||||
for (const checkPath of checkPaths) {
|
||||
try {
|
||||
await fsAccess(checkPath);
|
||||
// Folder exists, so use it.
|
||||
return path.dirname(checkPath);
|
||||
} catch (_) {
|
||||
// Folder does not exist so get next one
|
||||
async generateTypesForFrontend() {
|
||||
const credentialsOverwrites = CredentialsOverwrites().getAll();
|
||||
for (const credential of this.types.credentials) {
|
||||
if (credential.name in credentialsOverwrites) {
|
||||
credential.__overwrittenProperties = Object.keys(credentialsOverwrites[credential.name]);
|
||||
}
|
||||
}
|
||||
throw new Error('Could not find "node_modules" folder!');
|
||||
|
||||
// pre-render all the node and credential types as static json files
|
||||
await mkdir(path.join(GENERATED_STATIC_DIR, 'types'), { recursive: true });
|
||||
|
||||
const writeStaticJSON = async (name: string, data: any[]) => {
|
||||
const filePath = path.join(GENERATED_STATIC_DIR, `types/${name}.json`);
|
||||
const payload = `[\n${data.map((entry) => JSON.stringify(entry)).join(',\n')}\n]`;
|
||||
await writeFile(filePath, payload, { encoding: 'utf-8' });
|
||||
};
|
||||
|
||||
await writeStaticJSON('nodes', this.types.nodes);
|
||||
await writeStaticJSON('credentials', this.types.credentials);
|
||||
}
|
||||
|
||||
async loadNodesFromBasePackages() {
|
||||
const nodeModulesPath = await this.getNodeModulesPath();
|
||||
const nodePackagePaths = await this.getN8nNodePackages(nodeModulesPath);
|
||||
|
||||
for (const packagePath of nodePackagePaths) {
|
||||
await this.runDirectoryLoader(LazyPackageDirectoryLoader, packagePath);
|
||||
}
|
||||
}
|
||||
|
||||
async loadNodesFromDownloadedPackages(): Promise<void> {
|
||||
const nodePackages = [];
|
||||
try {
|
||||
// Read downloaded nodes and credentials
|
||||
const downloadedNodesFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||
const downloadedNodesFolderModules = path.join(downloadedNodesFolder, 'node_modules');
|
||||
await fsAccess(downloadedNodesFolderModules);
|
||||
const downloadedPackages = await this.getN8nNodePackages(downloadedNodesFolderModules);
|
||||
const downloadedNodesDir = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||
const downloadedNodesDirModules = path.join(downloadedNodesDir, 'node_modules');
|
||||
await fsAccess(downloadedNodesDirModules);
|
||||
const downloadedPackages = await this.getN8nNodePackages(downloadedNodesDirModules);
|
||||
nodePackages.push(...downloadedPackages);
|
||||
} catch (error) {
|
||||
// Folder does not exist so ignore and return
|
||||
@@ -135,15 +111,14 @@ class LoadNodesAndCredentialsClass {
|
||||
|
||||
for (const packagePath of nodePackages) {
|
||||
try {
|
||||
await this.loadDataFromPackage(packagePath);
|
||||
// eslint-disable-next-line no-empty
|
||||
await this.runDirectoryLoader(PackageDirectoryLoader, packagePath);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadNodesFromCustomFolders(): Promise<void> {
|
||||
async loadNodesFromCustomDirectories(): Promise<void> {
|
||||
// Read nodes and credentials from custom directories
|
||||
const customDirectories = [];
|
||||
|
||||
@@ -158,7 +133,7 @@ class LoadNodesAndCredentialsClass {
|
||||
}
|
||||
|
||||
for (const directory of customDirectories) {
|
||||
await this.loadDataFromDirectory('CUSTOM', directory);
|
||||
await this.runDirectoryLoader(CustomDirectoryLoader, directory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,46 +167,6 @@ class LoadNodesAndCredentialsClass {
|
||||
return getN8nNodePackagesRecursive('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads credentials from a file
|
||||
*
|
||||
* @param {string} credentialName The name of the credentials
|
||||
* @param {string} filePath The file to read credentials from
|
||||
*/
|
||||
loadCredentialsFromFile(credentialName: string, filePath: string): void {
|
||||
let tempCredential: ICredentialType;
|
||||
try {
|
||||
tempCredential = loadClassInIsolation(filePath, credentialName);
|
||||
|
||||
// Add serializer method "toJSON" to the class so that authenticate method (if defined)
|
||||
// gets mapped to the authenticate attribute before it is sent to the client.
|
||||
// The authenticate property is used by the client to decide whether or not to
|
||||
// include the credential type in the predefined credentials (HTTP node)
|
||||
Object.assign(tempCredential, { toJSON });
|
||||
|
||||
if (tempCredential.icon && tempCredential.icon.startsWith('file:')) {
|
||||
// If a file icon gets used add the full path
|
||||
tempCredential.icon = `file:${path.join(
|
||||
path.dirname(filePath),
|
||||
tempCredential.icon.substr(5),
|
||||
)}`;
|
||||
}
|
||||
} 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] = {
|
||||
type: tempCredential,
|
||||
sourcePath: filePath,
|
||||
};
|
||||
}
|
||||
|
||||
async loadNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||
const command = `npm install ${packageName}${version ? `@${version}` : ''}`;
|
||||
@@ -240,24 +175,30 @@ class LoadNodesAndCredentialsClass {
|
||||
|
||||
const finalNodeUnpackedPath = path.join(downloadFolder, 'node_modules', packageName);
|
||||
|
||||
const loadedNodes = await this.loadDataFromPackage(finalNodeUnpackedPath);
|
||||
const { loadedNodes, packageJson } = await this.runDirectoryLoader(
|
||||
PackageDirectoryLoader,
|
||||
finalNodeUnpackedPath,
|
||||
);
|
||||
|
||||
if (loadedNodes.length > 0) {
|
||||
const packageFile = await this.readPackageJson(finalNodeUnpackedPath);
|
||||
// Save info to DB
|
||||
try {
|
||||
const installedPackage = await persistInstalledPackageData(
|
||||
packageFile.name,
|
||||
packageFile.version,
|
||||
packageJson.name,
|
||||
packageJson.version,
|
||||
loadedNodes,
|
||||
this.nodeTypes,
|
||||
packageFile.author?.name,
|
||||
packageFile.author?.email,
|
||||
this.loaded.nodes,
|
||||
packageJson.author?.name,
|
||||
packageJson.author?.email,
|
||||
);
|
||||
this.attachNodesToNodeTypes(installedPackage.installedNodes);
|
||||
await this.generateTypesForFrontend();
|
||||
return installedPackage;
|
||||
} catch (error) {
|
||||
LoggerProxy.error('Failed to save installed packages and nodes', { error, packageName });
|
||||
LoggerProxy.error('Failed to save installed packages and nodes', {
|
||||
error: error as Error,
|
||||
packageName,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
@@ -265,9 +206,7 @@ class LoadNodesAndCredentialsClass {
|
||||
const removeCommand = `npm remove ${packageName}`;
|
||||
try {
|
||||
await executeCommand(removeCommand);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
||||
}
|
||||
@@ -278,7 +217,9 @@ class LoadNodesAndCredentialsClass {
|
||||
|
||||
await executeCommand(command);
|
||||
|
||||
void (await removePackageFromDatabase(installedPackage));
|
||||
await removePackageFromDatabase(installedPackage);
|
||||
|
||||
await this.generateTypesForFrontend();
|
||||
|
||||
this.unloadNodes(installedPackage.installedNodes);
|
||||
}
|
||||
@@ -294,7 +235,7 @@ class LoadNodesAndCredentialsClass {
|
||||
try {
|
||||
await executeCommand(command);
|
||||
} catch (error) {
|
||||
if (error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
|
||||
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
|
||||
throw new Error(`The npm package "${packageName}" could not be found.`);
|
||||
}
|
||||
throw error;
|
||||
@@ -304,29 +245,35 @@ class LoadNodesAndCredentialsClass {
|
||||
|
||||
const finalNodeUnpackedPath = path.join(downloadFolder, 'node_modules', packageName);
|
||||
|
||||
const loadedNodes = await this.loadDataFromPackage(finalNodeUnpackedPath);
|
||||
const { loadedNodes, packageJson } = await this.runDirectoryLoader(
|
||||
PackageDirectoryLoader,
|
||||
finalNodeUnpackedPath,
|
||||
);
|
||||
|
||||
if (loadedNodes.length > 0) {
|
||||
const packageFile = await this.readPackageJson(finalNodeUnpackedPath);
|
||||
|
||||
// Save info to DB
|
||||
try {
|
||||
await removePackageFromDatabase(installedPackage);
|
||||
|
||||
const newlyInstalledPackage = await persistInstalledPackageData(
|
||||
packageFile.name,
|
||||
packageFile.version,
|
||||
packageJson.name,
|
||||
packageJson.version,
|
||||
loadedNodes,
|
||||
this.nodeTypes,
|
||||
packageFile.author?.name,
|
||||
packageFile.author?.email,
|
||||
this.loaded.nodes,
|
||||
packageJson.author?.name,
|
||||
packageJson.author?.email,
|
||||
);
|
||||
|
||||
this.attachNodesToNodeTypes(newlyInstalledPackage.installedNodes);
|
||||
|
||||
await this.generateTypesForFrontend();
|
||||
|
||||
return newlyInstalledPackage;
|
||||
} catch (error) {
|
||||
LoggerProxy.error('Failed to save installed packages and nodes', { error, packageName });
|
||||
LoggerProxy.error('Failed to save installed packages and nodes', {
|
||||
error: error as Error,
|
||||
packageName,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
@@ -334,249 +281,119 @@ class LoadNodesAndCredentialsClass {
|
||||
const removeCommand = `npm remove ${packageName}`;
|
||||
try {
|
||||
await executeCommand(removeCommand);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
} catch (_) {}
|
||||
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
loadNodeFromFile(
|
||||
packageName: string,
|
||||
nodeName: string,
|
||||
filePath: string,
|
||||
): INodeTypeNameVersion | undefined {
|
||||
let tempNode: INodeType | IVersionedNodeType;
|
||||
let nodeVersion = 1;
|
||||
private unloadNodes(installedNodes: InstalledNodes[]): void {
|
||||
installedNodes.forEach((installedNode) => {
|
||||
delete this.loaded.nodes[installedNode.type];
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
tempNode = loadClassInIsolation(filePath, nodeName);
|
||||
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
|
||||
console.error(`Error loading node "${nodeName}" from: "${filePath}" - ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
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.hasOwnProperty('nodeVersions')) {
|
||||
const versionedNodeType = (tempNode as IVersionedNodeType).getNodeType();
|
||||
this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' });
|
||||
nodeVersion = (tempNode as IVersionedNodeType).currentVersion;
|
||||
|
||||
if (
|
||||
versionedNodeType.description.icon !== undefined &&
|
||||
versionedNodeType.description.icon.startsWith('file:')
|
||||
) {
|
||||
// If a file icon gets used add the full path
|
||||
versionedNodeType.description.icon = `file:${path.join(
|
||||
path.dirname(filePath),
|
||||
versionedNodeType.description.icon.substr(5),
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (versionedNodeType.hasOwnProperty('executeSingle')) {
|
||||
this.logger.warn(
|
||||
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
|
||||
{ filePath },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Short renaming to avoid type issues
|
||||
const tmpNode = tempNode as INodeType;
|
||||
nodeVersion = Array.isArray(tmpNode.description.version)
|
||||
? tmpNode.description.version.slice(-1)[0]
|
||||
: tmpNode.description.version;
|
||||
}
|
||||
|
||||
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the node should be skipped
|
||||
if (this.excludeNodes !== undefined && this.excludeNodes.includes(fullNodeName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodeTypes[fullNodeName] = {
|
||||
type: tempNode,
|
||||
sourcePath: filePath,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return {
|
||||
name: fullNodeName,
|
||||
version: nodeVersion,
|
||||
} as INodeTypeNameVersion;
|
||||
private attachNodesToNodeTypes(installedNodes: InstalledNodes[]): void {
|
||||
const loadedNodes = this.loaded.nodes;
|
||||
installedNodes.forEach((installedNode) => {
|
||||
const { type, sourcePath } = loadedNodes[installedNode.type];
|
||||
loadedNodes[installedNode.type] = { type, sourcePath };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves `categories`, `subcategories`, partial `resources` 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
|
||||
* Run a loader of source files of nodes and credentials in a directory.
|
||||
*/
|
||||
getCodex(filePath: string): CodexData {
|
||||
// eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires
|
||||
const { categories, subcategories, resources: allResources, alias } = require(`${filePath}on`); // .js to .json
|
||||
private async runDirectoryLoader<T extends DirectoryLoader>(
|
||||
constructor: new (...args: ConstructorParameters<typeof DirectoryLoader>) => T,
|
||||
dir: string,
|
||||
) {
|
||||
const loader = new constructor(dir, this.excludeNodes, this.includeNodes);
|
||||
await loader.loadAll();
|
||||
|
||||
const resources = pick(allResources, ['primaryDocumentation', 'credentialDocumentation']);
|
||||
// list of node & credential types that will be sent to the frontend
|
||||
const { types } = loader;
|
||||
this.types.nodes = this.types.nodes.concat(types.nodes);
|
||||
this.types.credentials = this.types.credentials.concat(types.credentials);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...(categories && { categories }),
|
||||
...(subcategories && { subcategories }),
|
||||
...(resources && { resources }),
|
||||
...(alias && { alias }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node codex `categories` and `subcategories` (if defined)
|
||||
* to a node description `codex` property.
|
||||
*
|
||||
* @param obj.node Node to add categories to
|
||||
* @param obj.filePath Path to the built node
|
||||
* @param obj.isCustom Whether the node is custom
|
||||
*/
|
||||
addCodex({
|
||||
node,
|
||||
filePath,
|
||||
isCustom,
|
||||
}: {
|
||||
node: INodeType | IVersionedNodeType;
|
||||
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];
|
||||
// Copy over all icons and set `iconUrl` for the frontend
|
||||
const iconPromises: Array<Promise<void>> = [];
|
||||
for (const node of types.nodes) {
|
||||
if (node.icon?.startsWith('file:')) {
|
||||
const icon = node.icon.substring(5);
|
||||
const iconUrl = `icons/nodes/${node.name}${path.extname(icon)}`;
|
||||
delete node.icon;
|
||||
node.iconUrl = iconUrl;
|
||||
iconPromises.push(copyFile(path.join(dir, icon), path.join(GENERATED_STATIC_DIR, iconUrl)));
|
||||
}
|
||||
}
|
||||
for (const credential of types.credentials) {
|
||||
if (credential.icon?.startsWith('file:')) {
|
||||
const icon = credential.icon.substring(5);
|
||||
const iconUrl = `icons/credentials/${credential.name}${path.extname(icon)}`;
|
||||
delete credential.icon;
|
||||
credential.iconUrl = iconUrl;
|
||||
iconPromises.push(copyFile(path.join(dir, icon), path.join(GENERATED_STATIC_DIR, iconUrl)));
|
||||
}
|
||||
}
|
||||
await Promise.all(iconPromises);
|
||||
|
||||
node.description.codex = codex;
|
||||
} catch (_) {
|
||||
this.logger.debug(`No codex available for: ${filePath.split('/').pop() ?? ''}`);
|
||||
// Nodes and credentials that have been loaded immediately
|
||||
for (const nodeTypeName in loader.nodeTypes) {
|
||||
this.loaded.nodes[nodeTypeName] = loader.nodeTypes[nodeTypeName];
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
node.description.codex = {
|
||||
categories: [CUSTOM_NODES_CATEGORY],
|
||||
for (const credentialTypeName in loader.credentialTypes) {
|
||||
this.loaded.credentials[credentialTypeName] = loader.credentialTypes[credentialTypeName];
|
||||
}
|
||||
|
||||
// Nodes and credentials that will be lazy loaded
|
||||
if (loader instanceof LazyPackageDirectoryLoader) {
|
||||
const { packageName, known } = loader;
|
||||
|
||||
for (const type in known.nodes) {
|
||||
const { className, sourcePath } = known.nodes[type];
|
||||
this.known.nodes[`${packageName}.${type}`] = {
|
||||
className,
|
||||
sourcePath: path.join(dir, sourcePath),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
||||
const files = await glob('**/*.@(node|credentials).js', {
|
||||
cwd: directory,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
for (const filePath of files) {
|
||||
const [fileName, type] = path.parse(filePath).name.split('.');
|
||||
|
||||
if (type === 'node') {
|
||||
this.loadNodeFromFile(setPackageName, fileName, filePath);
|
||||
} else if (type === 'credentials') {
|
||||
this.loadCredentialsFromFile(fileName, filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readPackageJson(packagePath: string): Promise<IN8nNodePackageJson> {
|
||||
// Get the absolute path of the package
|
||||
const packageFileString = await fsReadFile(path.join(packagePath, 'package.json'), 'utf8');
|
||||
return jsonParse(packageFileString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads nodes and credentials from the package with the given name
|
||||
*
|
||||
* @param {string} packagePath The path to read data from
|
||||
*/
|
||||
async loadDataFromPackage(packagePath: string): Promise<INodeTypeNameVersion[]> {
|
||||
// Get the absolute path of the package
|
||||
const packageFile = await this.readPackageJson(packagePath);
|
||||
if (!packageFile.n8n) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const packageName = packageFile.name;
|
||||
const { nodes, credentials } = packageFile.n8n;
|
||||
const returnData: INodeTypeNameVersion[] = [];
|
||||
|
||||
// Read all node types
|
||||
if (Array.isArray(nodes)) {
|
||||
for (const filePath of nodes) {
|
||||
const tempPath = path.join(packagePath, filePath);
|
||||
const [fileName] = path.parse(filePath).name.split('.');
|
||||
const loadData = this.loadNodeFromFile(packageName, fileName, tempPath);
|
||||
if (loadData) {
|
||||
returnData.push(loadData);
|
||||
}
|
||||
for (const type in known.credentials) {
|
||||
const { className, sourcePath } = known.credentials[type];
|
||||
this.known.credentials[type] = { className, sourcePath: path.join(dir, sourcePath) };
|
||||
}
|
||||
}
|
||||
|
||||
// Read all credential types
|
||||
if (Array.isArray(credentials)) {
|
||||
for (const filePath of credentials) {
|
||||
const tempPath = path.join(packagePath, filePath);
|
||||
const [fileName] = path.parse(filePath).name.split('.');
|
||||
this.loadCredentialsFromFile(fileName, tempPath);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
private async getNodeModulesPath(): Promise<string> {
|
||||
// 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(CLI_DIR, '..', 'n8n-workflow'),
|
||||
// In case "n8n" package is the root and the packages are
|
||||
// in the "node_modules" folder underneath it.
|
||||
path.join(CLI_DIR, 'node_modules', 'n8n-workflow'),
|
||||
// In case "n8n" package is installed using npm/yarn workspaces
|
||||
// the node_modules folder is in the root of the workspace.
|
||||
path.join(CLI_DIR, '..', '..', 'node_modules', 'n8n-workflow'),
|
||||
];
|
||||
for (const checkPath of checkPaths) {
|
||||
try {
|
||||
await fsAccess(checkPath);
|
||||
// Folder exists, so use it.
|
||||
return path.dirname(checkPath);
|
||||
} catch (_) {} // Folder does not exist so get next one
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
unloadNodes(installedNodes: InstalledNodes[]): void {
|
||||
const nodeTypes = NodeTypes();
|
||||
installedNodes.forEach((installedNode) => {
|
||||
nodeTypes.removeNodeType(installedNode.type);
|
||||
delete this.nodeTypes[installedNode.type];
|
||||
});
|
||||
}
|
||||
|
||||
attachNodesToNodeTypes(installedNodes: InstalledNodes[]): void {
|
||||
const nodeTypes = NodeTypes();
|
||||
installedNodes.forEach((installedNode) => {
|
||||
nodeTypes.attachNodeType(
|
||||
installedNode.type,
|
||||
this.nodeTypes[installedNode.type].type,
|
||||
this.nodeTypes[installedNode.type].sourcePath,
|
||||
);
|
||||
});
|
||||
throw new Error('Could not find "node_modules" folder!');
|
||||
}
|
||||
}
|
||||
|
||||
let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass {
|
||||
if (packagesInformationInstance === undefined) {
|
||||
packagesInformationInstance = new LoadNodesAndCredentialsClass();
|
||||
|
||||
Reference in New Issue
Block a user