import { CUSTOM_EXTENSION_ENV , UserSettings } from 'n8n-core' ; import { CodexData , ICredentialType , ILogger , INodeType , INodeTypeData , INodeVersionedType , 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 ] : ICredentialType ; } = { } ; excludeNodes : string [ ] | undefined = undefined ; includeNodes : string [ ] | undefined = undefined ; nodeModulesPath = '' ; logger : ILogger ; async init ( ) { this . logger = getLogger ( ) ; LoggerProxy . init ( this . logger ) ; const checkPaths = [ path . join ( __dirname , '..' , '..' , '..' , 'n8n-workflow' ) , path . join ( __dirname , '..' , '..' , 'node_modules' , 'n8n-workflow' ) , ] ; for ( const checkPath of checkPaths ) { try { await fsAccess ( checkPath ) ; this . nodeModulesPath = path . dirname ( checkPath ) ; break ; } catch ( error ) { 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' ) ; const packages = await this . getN8nNodePackages ( ) ; for ( const packageName of packages ) { await this . loadDataFromPackage ( packageName ) ; } const customDirectories = [ ] ; customDirectories . push ( UserSettings . getUserN8nFolderCustomExtensionPath ( ) ) ; if ( process . env [ CUSTOM_EXTENSION_ENV ] !== undefined ) { const customExtensionFolders = process . env [ CUSTOM_EXTENSION_ENV ] ! . split ( ';' ) ; customDirectories . push . apply ( customDirectories , customExtensionFolders ) ; } for ( const directory of customDirectories ) { await this . loadDataFromDirectory ( 'CUSTOM' , directory ) ; } } returns { Promise < string [ ] > } memberof async getN8nNodePackages ( ) { const getN8nNodePackagesRecursive = async ( relativePath ) : Promise < string [ ] > => { const results = [ ] ; 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 ( '' ) ; } param { string } credentialName param { string } filePath returns { Promise < void > } async loadCredentialsFromFile ( credentialName , filePath ) { const tempModule = require ( filePath ) ; let tempCredential ; try { tempCredential = new tempModule [ credentialName ] ( ) as ICredentialType ; if ( tempCredential . icon && tempCredential . icon . startsWith ( 'file:' ) ) { 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 ] = tempCredential ; } param { string } packageName param { string } nodeName param { string } filePath returns { Promise < void > } async loadNodeFromFile ( packageName , nodeName , filePath ) { let tempNode ; let fullNodeName ; const tempModule = require ( filePath ) ; try { tempNode = new tempModule [ nodeName ] ( ) ; this . addCodex ( { node : tempNode , filePath , isCustom : packageName === 'CUSTOM' } ) ; } catch ( error ) { console . error ( `Error loading node "${ nodeName }" from: "${ filePath }"` ) ; throw error ; } fullNodeName = `${ packageName }.${ tempNode . description . name }` ; tempNode . description . name = fullNodeName ; if ( tempNode . description . icon !== undefined && tempNode . description . icon . startsWith ( 'file:' ) ) { tempNode . description . icon = `file:${ path . join ( path . dirname ( filePath ) , tempNode . description . icon . substr ( 5 ) ) }` ; } if ( tempNode . hasOwnProperty ( 'executeSingle' ) ) { this . logger . warn ( `"executeSingle" will get deprecated soon. Please update the code of node "${ packageName }.${ nodeName }" to use "execute" instead!` , { filePath } ) ; } if ( tempNode . hasOwnProperty ( 'nodeVersions' ) ) { const versionedNodeType = ( tempNode as INodeVersionedType ) . getNodeType ( ) ; this . addCodex ( { node : versionedNodeType , filePath , isCustom : packageName === 'CUSTOM' } ) ; if ( versionedNodeType . description . icon !== undefined && versionedNodeType . description . icon . startsWith ( 'file:' ) ) { 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 } ) ; } } if ( this . includeNodes !== undefined && ! this . includeNodes . includes ( fullNodeName ) ) { return ; } if ( this . excludeNodes !== undefined && this . excludeNodes . includes ( fullNodeName ) ) { return ; } this . nodeTypes [ fullNodeName ] = { type : tempNode , sourcePath : filePath , } ; } param { string } filePath returns { CodexData } getCodex ( filePath ) { const { categories , subcategories , alias } = require ( `${ filePath }on` ) ; return { ... ( categories && { categories } ) , ... ( subcategories && { subcategories } ) , ... ( alias && { alias } ) , } ; } param obj param obj . node param obj . filePath param obj . isCustom returns { void } addCodex ( { node , filePath , isCustom , } ) { 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 ( _ ) { this . logger . debug ( `No codex available for: ${ filePath . split ( '/' ) . pop ( ) }` ) ; if ( isCustom ) { node . description . codex = { categories : [ CUSTOM_NODES_CATEGORY ] , } ; } } } param { string } setPackageName param { string } directory returns { Promise < void > } async loadDataFromDirectory ( setPackageName , directory ) { const files = await glob ( path . join ( directory , '**/*.@(node|credentials).js' ) ) ; let fileName ; let type ; 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 ) ; } param { string } packageName returns { Promise < void > } async loadDataFromPackage ( packageName ) { const packagePath = path . join ( this . nodeModulesPath , packageName ) ; const packageFileString = await fsReadFile ( path . join ( packagePath , 'package.json' ) , 'utf8' ) ; const packageFile = JSON . parse ( packageFileString ) ; if ( ! packageFile . hasOwnProperty ( 'n8n' ) ) { return ; } let tempPath ; let filePath ; let fileName ; let type ; 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 ) ; } } if ( packageFile . n8n . hasOwnProperty ( 'credentials' ) && Array . isArray ( packageFile . n8n . credentials ) ) { for ( filePath of packageFile . n8n . credentials ) { tempPath = path . join ( packagePath , filePath ) ; [ fileName , type ] = path . parse ( filePath ) . name . split ( '.' ) ; this . loadCredentialsFromFile ( fileName , tempPath ) ; } } } } let packagesInformationInstance ; export function LoadNodesAndCredentials ( ) { if ( packagesInformationInstance === undefined ) { packagesInformationInstance = new LoadNodesAndCredentialsClass ( ) ; } return packagesInformationInstance ; }