refactor(core): Overhaul community packages installation code (#15104)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-05-05 14:55:10 +02:00
committed by GitHub
parent b3205c25c7
commit 7d05cb2a37
2 changed files with 20 additions and 34 deletions

View File

@@ -3,7 +3,7 @@ import { InstalledNodes } from '@n8n/db';
import { InstalledPackages } from '@n8n/db'; import { InstalledPackages } from '@n8n/db';
import axios from 'axios'; import axios from 'axios';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; import { mkdir as fsMkdir } from 'fs/promises';
import { mocked } from 'jest-mock'; import { mocked } from 'jest-mock';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { PackageDirectoryLoader } from 'n8n-core'; import type { PackageDirectoryLoader } from 'n8n-core';
@@ -128,7 +128,6 @@ describe('CommunityPackagesService', () => {
describe('executeCommand()', () => { describe('executeCommand()', () => {
beforeEach(() => { beforeEach(() => {
mocked(fsAccess).mockReset();
mocked(fsMkdir).mockReset(); mocked(fsMkdir).mockReset();
mocked(exec).mockReset(); mocked(exec).mockReset();
}); });
@@ -147,31 +146,17 @@ describe('CommunityPackagesService', () => {
await communityPackagesService.executeNpmCommand('ls'); await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled(); expect(fsMkdir).toHaveBeenCalled();
expect(exec).toHaveBeenCalled(); expect(exec).toHaveBeenCalled();
expect(fsMkdir).toBeCalledTimes(0);
}); });
test('should make sure folder exists', async () => { test('should make sure folder exists', async () => {
mocked(exec).mockImplementation(execMock); mocked(exec).mockImplementation(execMock);
await communityPackagesService.executeNpmCommand('ls'); await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
expect(fsMkdir).toBeCalledTimes(0);
});
test('should try to create folder if it does not exist', async () => {
mocked(exec).mockImplementation(execMock);
mocked(fsAccess).mockImplementation(() => {
throw new Error('Folder does not exist.');
});
await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
expect(fsMkdir).toHaveBeenCalled(); expect(fsMkdir).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
}); });
test('should throw especial error when package is not found', async () => { test('should throw especial error when package is not found', async () => {
@@ -187,9 +172,8 @@ describe('CommunityPackagesService', () => {
await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND); await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
expect(fsAccess).toHaveBeenCalled(); expect(fsMkdir).toHaveBeenCalled();
expect(exec).toHaveBeenCalled(); expect(exec).toHaveBeenCalled();
expect(fsMkdir).toHaveBeenCalledTimes(0);
}); });
}); });
@@ -406,7 +390,7 @@ describe('CommunityPackagesService', () => {
expect(exec).toHaveBeenCalledTimes(1); expect(exec).toHaveBeenCalledTimes(1);
expect(exec).toHaveBeenNthCalledWith( expect(exec).toHaveBeenNthCalledWith(
1, 1,
`npm install ${installedPackage.packageName}@latest --registry=some.random.host`, `npm install ${installedPackage.packageName}@latest --audit=false --fund=false --bin-links=false --install-strategy=shallow --omit=dev --omit=optional --omit=peer --registry=some.random.host`,
expect.any(Object), expect.any(Object),
expect.any(Function), expect.any(Function),
); );

View File

@@ -4,7 +4,7 @@ import type { InstalledPackages } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import axios from 'axios'; import axios from 'axios';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; import { mkdir as fsMkdir } from 'fs/promises';
import type { PackageDirectoryLoader } from 'n8n-core'; import type { PackageDirectoryLoader } from 'n8n-core';
import { InstanceSettings, Logger } from 'n8n-core'; import { InstanceSettings, Logger } from 'n8n-core';
import { UnexpectedError, UserError, type PublicInstalledPackage } from 'n8n-workflow'; import { UnexpectedError, UserError, type PublicInstalledPackage } from 'n8n-workflow';
@@ -26,6 +26,14 @@ import { Publisher } from '@/scaling/pubsub/publisher.service';
import { toError } from '@/utils'; import { toError } from '@/utils';
const DEFAULT_REGISTRY = 'https://registry.npmjs.org'; const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
const NPM_COMMON_ARGS = ['--audit=false', '--fund=false'];
const NPM_INSTALL_ARGS = [
'--bin-links=false',
'--install-strategy=shallow',
'--omit=dev',
'--omit=optional',
'--omit=peer',
];
const { const {
PACKAGE_NAME_NOT_PROVIDED, PACKAGE_NAME_NOT_PROVIDED,
@@ -134,17 +142,11 @@ export class CommunityPackagesService {
NODE_PATH: process.env.NODE_PATH, NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH, PATH: process.env.PATH,
APPDATA: process.env.APPDATA, APPDATA: process.env.APPDATA,
NODE_ENV: 'production',
}, },
}; };
try { await fsMkdir(downloadFolder, { recursive: true });
await fsAccess(downloadFolder);
} catch {
await fsMkdir(downloadFolder);
// Also init the folder since some versions
// of npm complain if the folder is empty
await asyncExec('npm init -y', execOptions);
}
try { try {
const commandResult = await asyncExec(command, execOptions); const commandResult = await asyncExec(command, execOptions);
@@ -326,12 +328,12 @@ export class CommunityPackagesService {
}); });
} }
private getNpmRegistry() { private getNpmInstallArgs() {
const { registry } = this.globalConfig.nodes.communityPackages; const { registry } = this.globalConfig.nodes.communityPackages;
if (registry !== DEFAULT_REGISTRY && !this.license.isCustomNpmRegistryEnabled()) { if (registry !== DEFAULT_REGISTRY && !this.license.isCustomNpmRegistryEnabled()) {
throw new FeatureNotLicensedError(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY); throw new FeatureNotLicensedError(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY);
} }
return registry; return [...NPM_COMMON_ARGS, ...NPM_INSTALL_ARGS, `--registry=${registry}`].join(' ');
} }
private async installOrUpdatePackage( private async installOrUpdatePackage(
@@ -340,7 +342,7 @@ export class CommunityPackagesService {
) { ) {
const isUpdate = 'installedPackage' in options; const isUpdate = 'installedPackage' in options;
const packageVersion = isUpdate || !options.version ? 'latest' : options.version; const packageVersion = isUpdate || !options.version ? 'latest' : options.version;
const command = `npm install ${packageName}@${packageVersion} --registry=${this.getNpmRegistry()}`; const command = `npm install ${packageName}@${packageVersion} ${this.getNpmInstallArgs()}`;
try { try {
await this.executeNpmCommand(command); await this.executeNpmCommand(command);
@@ -394,7 +396,7 @@ export class CommunityPackagesService {
async installOrUpdateNpmPackage(packageName: string, packageVersion: string) { async installOrUpdateNpmPackage(packageName: string, packageVersion: string) {
await this.executeNpmCommand( await this.executeNpmCommand(
`npm install ${packageName}@${packageVersion} --registry=${this.getNpmRegistry()}`, `npm install ${packageName}@${packageVersion} ${this.getNpmInstallArgs()}`,
); );
await this.loadNodesAndCredentials.loadPackage(packageName); await this.loadNodesAndCredentials.loadPackage(packageName);
await this.loadNodesAndCredentials.postProcessLoaders(); await this.loadNodesAndCredentials.postProcessLoaders();