diff --git a/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts b/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts index 10c3cad60e8..4a7c849095b 100644 --- a/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts +++ b/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts @@ -130,6 +130,14 @@ describe('LoadNodesAndCredentials', () => { }); describe('N8N_CUSTOM_EXTENSIONS', () => { + it('should return file path if url contains a relative custom file path', () => { + const result = instanceCustom.resolveIcon( + packageNameCustom, + `${pathPrefixCustom}/node_modules/custom-node/icon.png`, + ); + expect(result).toBe(`${dirCustom}/node_modules/custom-node/icon.png`); + }); + it('should return file path if url contains "//" with absolute custom file path', () => { const result = instanceCustom.resolveIcon( packageNameCustom, diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index 8b6bb55beb9..dcfe614de77 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -245,8 +245,14 @@ export class LoadNodesAndCredentials { const pathPrefix = `/icons/${packageName}/`; const urlFilePath = url.substring(pathPrefix.length); - const filePath = isCustom ? resolvePathCustom(urlFilePath) : resolvePath(urlFilePath); + if (isCustom && !isWindowsFilePath(urlFilePath)) { + const relativeFilePath = resolvePath(urlFilePath); + if (isContainedWithin(loader.directory, relativeFilePath)) { + return relativeFilePath; + } + } + const filePath = isCustom ? resolvePathCustom(urlFilePath) : resolvePath(urlFilePath); return isContainedWithin(loader.directory, filePath) ? filePath : undefined; } 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 e5f449cb0f6..f725f5a69ff 100644 --- a/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts +++ b/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts @@ -109,6 +109,14 @@ describe('DirectoryLoader', () => { expect(mockFs.readFileSync).not.toHaveBeenCalled(); }); + it('should build custom icon URLs relative to the custom directory for absolute source paths', () => { + const loader = new CustomDirectoryLoader(directory); + + loader.loadNodeFromFile(`${directory}/dist/Node1/Node1.node.js`); + + expect(mockNode1.description.iconUrl).toBe('icons/CUSTOM/dist/Node1/node1.svg'); + }); + it('should load custom nodes when specified with CUSTOM prefix in includeNodes', async () => { const loader = new CustomDirectoryLoader(directory, [], ['CUSTOM.node1', 'CUSTOM.node2']); diff --git a/packages/core/src/nodes-loader/directory-loader.ts b/packages/core/src/nodes-loader/directory-loader.ts index 9d80718bbef..b25367e8bfe 100644 --- a/packages/core/src/nodes-loader/directory-loader.ts +++ b/packages/core/src/nodes-loader/directory-loader.ts @@ -424,14 +424,21 @@ export abstract class DirectoryLoader implements NodeLoader { private getIconPath(icon: string, filePath: string) { const iconPath = path.join(path.dirname(filePath), icon.replace('file:', '')); + const absoluteIconPath = path.isAbsolute(iconPath) + ? iconPath + : path.join(this.directory, iconPath); - if (!isContainedWithin(this.directory, path.join(this.directory, iconPath))) { + if (!isContainedWithin(this.directory, absoluteIconPath)) { throw new UnexpectedError( `Icon path "${iconPath}" is not contained within the package directory "${this.directory}"`, ); } - return `icons/${this.packageName}/${iconPath}`; + const relativeIconPath = path.isAbsolute(iconPath) + ? path.relative(this.directory, absoluteIconPath).replaceAll(path.sep, '/') + : iconPath; + + return `icons/${this.packageName}/${relativeIconPath}`; } private fixIconPaths(