11import uniq from 'lodash.uniq' ;
2+ import glob from 'fast-glob' ;
23import type { DirectoryLoader , Types } from 'n8n-core' ;
34import {
45 CUSTOM_EXTENSION_ENV ,
@@ -18,18 +19,18 @@ import type {
1819import { LoggerProxy , ErrorReporterProxy as ErrorReporter } from 'n8n-workflow' ;
1920
2021import { createWriteStream } from 'fs' ;
21- import { access as fsAccess , mkdir , readdir as fsReaddir , stat as fsStat } from 'fs/promises' ;
22+ import { mkdir } from 'fs/promises' ;
2223import path from 'path' ;
2324import config from '@/config' ;
2425import type { InstalledPackages } from '@db/entities/InstalledPackages' ;
2526import { executeCommand } from '@/CommunityNodes/helpers' ;
2627import {
27- CLI_DIR ,
2828 GENERATED_STATIC_DIR ,
2929 RESPONSE_ERROR_MESSAGES ,
3030 CUSTOM_API_CALL_KEY ,
3131 CUSTOM_API_CALL_NAME ,
3232 inTest ,
33+ CLI_DIR ,
3334} from '@/constants' ;
3435import { CredentialsOverwrites } from '@/CredentialsOverwrites' ;
3536import { Service } from 'typedi' ;
@@ -52,6 +53,8 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
5253
5354 logger : ILogger ;
5455
56+ private downloadFolder : string ;
57+
5558 async init ( ) {
5659 // Make sure the imported modules can resolve dependencies fine.
5760 const delimiter = process . platform === 'win32' ? ';' : ':' ;
@@ -61,8 +64,13 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
6164 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
6265 if ( ! inTest ) module . constructor . _initPaths ( ) ;
6366
64- await this . loadNodesFromBasePackages ( ) ;
65- await this . loadNodesFromDownloadedPackages ( ) ;
67+ this . downloadFolder = UserSettings . getUserN8nFolderDownloadedNodesPath ( ) ;
68+
69+ // Load nodes from `n8n-nodes-base` and any other `n8n-nodes-*` package in the main `node_modules`
70+ await this . loadNodesFromNodeModules ( CLI_DIR ) ;
71+ // Load nodes from installed community packages
72+ await this . loadNodesFromNodeModules ( this . downloadFolder ) ;
73+
6674 await this . loadNodesFromCustomDirectories ( ) ;
6775 await this . postProcessLoaders ( ) ;
6876 this . injectCustomApiCallOptions ( ) ;
@@ -109,32 +117,20 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
109117 await writeStaticJSON ( 'credentials' , this . types . credentials ) ;
110118 }
111119
112- private async loadNodesFromBasePackages ( ) {
113- const nodeModulesPath = await this . getNodeModulesPath ( ) ;
114- const nodePackagePaths = await this . getN8nNodePackages ( nodeModulesPath ) ;
115-
116- for ( const packagePath of nodePackagePaths ) {
117- await this . runDirectoryLoader ( LazyPackageDirectoryLoader , packagePath ) ;
118- }
119- }
120-
121- private async loadNodesFromDownloadedPackages ( ) : Promise < void > {
122- const nodePackages = [ ] ;
123- try {
124- // Read downloaded nodes and credentials
125- const downloadedNodesDir = UserSettings . getUserN8nFolderDownloadedNodesPath ( ) ;
126- const downloadedNodesDirModules = path . join ( downloadedNodesDir , 'node_modules' ) ;
127- await fsAccess ( downloadedNodesDirModules ) ;
128- const downloadedPackages = await this . getN8nNodePackages ( downloadedNodesDirModules ) ;
129- nodePackages . push ( ...downloadedPackages ) ;
130- } catch ( error ) {
131- // Folder does not exist so ignore and return
132- return ;
133- }
120+ private async loadNodesFromNodeModules ( scanDir : string ) : Promise < void > {
121+ const nodeModulesDir = path . join ( scanDir , 'node_modules' ) ;
122+ const globOptions = { cwd : nodeModulesDir , onlyDirectories : true } ;
123+ const installedPackagePaths = [
124+ ...( await glob ( 'n8n-nodes-*' , { ...globOptions , deep : 1 } ) ) ,
125+ ...( await glob ( '@*/n8n-nodes-*' , { ...globOptions , deep : 2 } ) ) ,
126+ ] ;
134127
135- for ( const packagePath of nodePackages ) {
128+ for ( const packagePath of installedPackagePaths ) {
136129 try {
137- await this . runDirectoryLoader ( PackageDirectoryLoader , packagePath ) ;
130+ await this . runDirectoryLoader (
131+ LazyPackageDirectoryLoader ,
132+ path . join ( nodeModulesDir , packagePath ) ,
133+ ) ;
138134 } catch ( error ) {
139135 ErrorReporter . error ( error ) ;
140136 }
@@ -158,49 +154,45 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
158154 }
159155 }
160156
161- /**
162- * Returns all the names of the packages which could contain n8n nodes
163- */
164- private async getN8nNodePackages ( baseModulesPath : string ) : Promise < string [ ] > {
165- const getN8nNodePackagesRecursive = async ( relativePath : string ) : Promise < string [ ] > => {
166- const results : string [ ] = [ ] ;
167- const nodeModulesPath = `${ baseModulesPath } /${ relativePath } ` ;
168- const nodeModules = await fsReaddir ( nodeModulesPath ) ;
169- for ( const nodeModule of nodeModules ) {
170- const isN8nNodesPackage = nodeModule . indexOf ( 'n8n-nodes-' ) === 0 ;
171- const isNpmScopedPackage = nodeModule . indexOf ( '@' ) === 0 ;
172- if ( ! isN8nNodesPackage && ! isNpmScopedPackage ) {
173- continue ;
174- }
175- if ( ! ( await fsStat ( nodeModulesPath ) ) . isDirectory ( ) ) {
176- continue ;
177- }
178- if ( isN8nNodesPackage ) {
179- results . push ( `${ baseModulesPath } /${ relativePath } ${ nodeModule } ` ) ;
180- }
181- if ( isNpmScopedPackage ) {
182- results . push ( ...( await getN8nNodePackagesRecursive ( `${ relativePath } ${ nodeModule } /` ) ) ) ;
183- }
184- }
185- return results ;
186- } ;
187- return getN8nNodePackagesRecursive ( '' ) ;
188- }
189-
190- async loadNpmModule ( packageName : string , version ?: string ) : Promise < InstalledPackages > {
191- const downloadFolder = UserSettings . getUserN8nFolderDownloadedNodesPath ( ) ;
192- const command = `npm install ${ packageName } ${ version ? `@${ version } ` : '' } ` ;
157+ private async installOrUpdateNpmModule (
158+ packageName : string ,
159+ options : { version ?: string } | { installedPackage : InstalledPackages } ,
160+ ) {
161+ const isUpdate = 'installedPackage' in options ;
162+ const command = isUpdate
163+ ? `npm update ${ packageName } `
164+ : `npm install ${ packageName } ${ options . version ? `@${ options . version } ` : '' } ` ;
193165
194- await executeCommand ( command ) ;
166+ try {
167+ await executeCommand ( command ) ;
168+ } catch ( error ) {
169+ if ( error instanceof Error && error . message === RESPONSE_ERROR_MESSAGES . PACKAGE_NOT_FOUND ) {
170+ throw new Error ( `The npm package "${ packageName } " could not be found.` ) ;
171+ }
172+ throw error ;
173+ }
195174
196- const finalNodeUnpackedPath = path . join ( downloadFolder , 'node_modules' , packageName ) ;
175+ const finalNodeUnpackedPath = path . join ( this . downloadFolder , 'node_modules' , packageName ) ;
197176
198- const loader = await this . runDirectoryLoader ( PackageDirectoryLoader , finalNodeUnpackedPath ) ;
177+ let loader : PackageDirectoryLoader ;
178+ try {
179+ loader = await this . runDirectoryLoader ( PackageDirectoryLoader , finalNodeUnpackedPath ) ;
180+ } catch ( error ) {
181+ // Remove this package since loading it failed
182+ const removeCommand = `npm remove ${ packageName } ` ;
183+ try {
184+ await executeCommand ( removeCommand ) ;
185+ } catch { }
186+ throw new Error ( RESPONSE_ERROR_MESSAGES . PACKAGE_LOADING_FAILED , { cause : error } ) ;
187+ }
199188
200189 if ( loader . loadedNodes . length > 0 ) {
201190 // Save info to DB
202191 try {
203- const { persistInstalledPackageData } = await import ( '@/CommunityNodes/packageModel' ) ;
192+ const { persistInstalledPackageData, removePackageFromDatabase } = await import (
193+ '@/CommunityNodes/packageModel'
194+ ) ;
195+ if ( isUpdate ) await removePackageFromDatabase ( options . installedPackage ) ;
204196 const installedPackage = await persistInstalledPackageData ( loader ) ;
205197 await this . postProcessLoaders ( ) ;
206198 await this . generateTypesForFrontend ( ) ;
@@ -223,6 +215,10 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
223215 }
224216 }
225217
218+ async installNpmModule ( packageName : string , version ?: string ) : Promise < InstalledPackages > {
219+ return this . installOrUpdateNpmModule ( packageName , { version } ) ;
220+ }
221+
226222 async removeNpmModule ( packageName : string , installedPackage : InstalledPackages ) : Promise < void > {
227223 const command = `npm remove ${ packageName } ` ;
228224
@@ -244,49 +240,7 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
244240 packageName : string ,
245241 installedPackage : InstalledPackages ,
246242 ) : Promise < InstalledPackages > {
247- const downloadFolder = UserSettings . getUserN8nFolderDownloadedNodesPath ( ) ;
248-
249- const command = `npm i ${ packageName } @latest` ;
250-
251- try {
252- await executeCommand ( command ) ;
253- } catch ( error ) {
254- if ( error instanceof Error && error . message === RESPONSE_ERROR_MESSAGES . PACKAGE_NOT_FOUND ) {
255- throw new Error ( `The npm package "${ packageName } " could not be found.` ) ;
256- }
257- throw error ;
258- }
259-
260- const finalNodeUnpackedPath = path . join ( downloadFolder , 'node_modules' , packageName ) ;
261-
262- const loader = await this . runDirectoryLoader ( PackageDirectoryLoader , finalNodeUnpackedPath ) ;
263-
264- if ( loader . loadedNodes . length > 0 ) {
265- // Save info to DB
266- try {
267- const { persistInstalledPackageData, removePackageFromDatabase } = await import (
268- '@/CommunityNodes/packageModel'
269- ) ;
270- await removePackageFromDatabase ( installedPackage ) ;
271- const newlyInstalledPackage = await persistInstalledPackageData ( loader ) ;
272- await this . postProcessLoaders ( ) ;
273- await this . generateTypesForFrontend ( ) ;
274- return newlyInstalledPackage ;
275- } catch ( error ) {
276- LoggerProxy . error ( 'Failed to save installed packages and nodes' , {
277- error : error as Error ,
278- packageName,
279- } ) ;
280- throw error ;
281- }
282- } else {
283- // Remove this package since it contains no loadable nodes
284- const removeCommand = `npm remove ${ packageName } ` ;
285- try {
286- await executeCommand ( removeCommand ) ;
287- } catch { }
288- throw new Error ( RESPONSE_ERROR_MESSAGES . PACKAGE_DOES_NOT_CONTAIN_NODES ) ;
289- }
243+ return this . installOrUpdateNpmModule ( packageName , { installedPackage } ) ;
290244 }
291245
292246 /**
@@ -399,27 +353,4 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
399353 }
400354 }
401355 }
402-
403- private async getNodeModulesPath ( ) : Promise < string > {
404- // Get the path to the node-modules folder to be later able
405- // to load the credentials and nodes
406- const checkPaths = [
407- // In case "n8n" package is in same node_modules folder.
408- path . join ( CLI_DIR , '..' , 'n8n-workflow' ) ,
409- // In case "n8n" package is the root and the packages are
410- // in the "node_modules" folder underneath it.
411- path . join ( CLI_DIR , 'node_modules' , 'n8n-workflow' ) ,
412- // In case "n8n" package is installed using npm/yarn workspaces
413- // the node_modules folder is in the root of the workspace.
414- path . join ( CLI_DIR , '..' , '..' , 'node_modules' , 'n8n-workflow' ) ,
415- ] ;
416- for ( const checkPath of checkPaths ) {
417- try {
418- await fsAccess ( checkPath ) ;
419- // Folder exists, so use it.
420- return path . dirname ( checkPath ) ;
421- } catch { } // Folder does not exist so get next one
422- }
423- throw new Error ( 'Could not find "node_modules" folder!' ) ;
424- }
425356}
0 commit comments