diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index 244d9af04ba..e9550fcdd6f 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -523,22 +523,15 @@ export class LoadNodesAndCredentials { const push = Container.get(Push); Object.values(this.loaders).forEach(async (loader) => { + const { directory } = loader; try { - await fsPromises.access(loader.directory); + await fsPromises.access(directory); } catch { // If directory doesn't exist, there is nothing to watch return; } - const realModulePath = path.join(await fsPromises.realpath(loader.directory), path.sep); const reloader = debounce(async () => { - const modulesToUnload = Object.keys(require.cache).filter((filePath) => - filePath.startsWith(realModulePath), - ); - modulesToUnload.forEach((filePath) => { - delete require.cache[filePath]; - }); - loader.reset(); await loader.loadAll(); await this.postProcessLoaders(); @@ -549,11 +542,11 @@ export class LoadNodesAndCredentials { ? ['**/nodes.json', '**/credentials.json'] : ['**/*.js', '**/*.json']; const files = await glob(toWatch, { - cwd: realModulePath, + cwd: directory, ignore: ['node_modules/**'], }); const watcher = watch(files, { - cwd: realModulePath, + cwd: directory, ignoreInitial: true, }); watcher.on('add', reloader).on('change', reloader).on('unlink', reloader); diff --git a/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts b/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts index c577e9d398e..5d80b28ab57 100644 --- a/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts +++ b/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts @@ -13,6 +13,7 @@ jest.mock('node:fs'); jest.mock('node:fs/promises'); const mockFs = mock(); const mockFsPromises = mock(); +fs.realpathSync = mockFs.realpathSync; fs.readFileSync = mockFs.readFileSync; fsPromises.readFile = mockFsPromises.readFile; @@ -64,6 +65,7 @@ describe('DirectoryLoader', () => { let mockCredential1: ICredentialType, mockNode1: INodeType, mockNode2: INodeType; beforeEach(() => { + mockFs.realpathSync.mockImplementation((path) => String(path)); mockCredential1 = createCredential('credential1'); mockNode1 = createNode('node1', 'credential1'); mockNode2 = createNode('node2'); @@ -330,6 +332,19 @@ describe('DirectoryLoader', () => { }); }); + describe('constructor', () => { + it('should resolve symlinks to real paths when directory is a symlink', () => { + const symlinkPath = '/symlink/path'; + const realPath = '/real/path'; + mockFs.realpathSync.mockReturnValueOnce(realPath); + + const loader = new CustomDirectoryLoader(symlinkPath); + + expect(mockFs.realpathSync).toHaveBeenCalledWith(symlinkPath); + expect(loader.directory).toBe(realPath); + }); + }); + describe('reset()', () => { it('should reset all properties to their initial state', async () => { mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson); diff --git a/packages/core/src/nodes-loader/directory-loader.ts b/packages/core/src/nodes-loader/directory-loader.ts index 3e5353d4434..677844936f1 100644 --- a/packages/core/src/nodes-loader/directory-loader.ts +++ b/packages/core/src/nodes-loader/directory-loader.ts @@ -17,6 +17,7 @@ import type { KnownNodesAndCredentials, } from 'n8n-workflow'; import { ApplicationError, isSubNodeType } from 'n8n-workflow'; +import { realpathSync } from 'node:fs'; import * as path from 'path'; import { UnrecognizedCredentialTypeError } from '@/errors/unrecognized-credential-type.error'; @@ -83,13 +84,22 @@ export abstract class DirectoryLoader { readonly directory: string, protected excludeNodes: string[] = [], protected includeNodes: string[] = [], - ) {} + ) { + // If `directory` is a symlink, we try to resolve it to its real path + try { + this.directory = realpathSync(directory); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (error.code !== 'ENOENT') throw error; + } + } abstract packageName: string; abstract loadAll(): Promise; reset() { + this.unloadAll(); this.loadedNodes = []; this.nodeTypes = {}; this.credentialTypes = {}; @@ -450,4 +460,13 @@ export abstract class DirectoryLoader { return; } + + private unloadAll() { + const filesToUnload = Object.keys(require.cache).filter((filePath) => + filePath.startsWith(this.directory), + ); + filesToUnload.forEach((filePath) => { + delete require.cache[filePath]; + }); + } }