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 axios from 'axios';
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 { mock } from 'jest-mock-extended';
import type { PackageDirectoryLoader } from 'n8n-core';
@@ -128,7 +128,6 @@ describe('CommunityPackagesService', () => {
describe('executeCommand()', () => {
beforeEach(() => {
mocked(fsAccess).mockReset();
mocked(fsMkdir).mockReset();
mocked(exec).mockReset();
});
@@ -147,31 +146,17 @@ describe('CommunityPackagesService', () => {
await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled();
expect(fsMkdir).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
expect(fsMkdir).toBeCalledTimes(0);
});
test('should make sure folder exists', async () => {
mocked(exec).mockImplementation(execMock);
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(exec).toHaveBeenCalled();
});
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);
expect(fsAccess).toHaveBeenCalled();
expect(fsMkdir).toHaveBeenCalled();
expect(exec).toHaveBeenCalled();
expect(fsMkdir).toHaveBeenCalledTimes(0);
});
});
@@ -406,7 +390,7 @@ describe('CommunityPackagesService', () => {
expect(exec).toHaveBeenCalledTimes(1);
expect(exec).toHaveBeenNthCalledWith(
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(Function),
);

View File

@@ -4,7 +4,7 @@ import type { InstalledPackages } from '@n8n/db';
import { Service } from '@n8n/di';
import axios from 'axios';
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 { InstanceSettings, Logger } from 'n8n-core';
import { UnexpectedError, UserError, type PublicInstalledPackage } from 'n8n-workflow';
@@ -26,6 +26,14 @@ import { Publisher } from '@/scaling/pubsub/publisher.service';
import { toError } from '@/utils';
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 {
PACKAGE_NAME_NOT_PROVIDED,
@@ -134,17 +142,11 @@ export class CommunityPackagesService {
NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH,
APPDATA: process.env.APPDATA,
NODE_ENV: 'production',
},
};
try {
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);
}
await fsMkdir(downloadFolder, { recursive: true });
try {
const commandResult = await asyncExec(command, execOptions);
@@ -326,12 +328,12 @@ export class CommunityPackagesService {
});
}
private getNpmRegistry() {
private getNpmInstallArgs() {
const { registry } = this.globalConfig.nodes.communityPackages;
if (registry !== DEFAULT_REGISTRY && !this.license.isCustomNpmRegistryEnabled()) {
throw new FeatureNotLicensedError(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY);
}
return registry;
return [...NPM_COMMON_ARGS, ...NPM_INSTALL_ARGS, `--registry=${registry}`].join(' ');
}
private async installOrUpdatePackage(
@@ -340,7 +342,7 @@ export class CommunityPackagesService {
) {
const isUpdate = 'installedPackage' in options;
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 {
await this.executeNpmCommand(command);
@@ -394,7 +396,7 @@ export class CommunityPackagesService {
async installOrUpdateNpmPackage(packageName: string, packageVersion: string) {
await this.executeNpmCommand(
`npm install ${packageName}@${packageVersion} --registry=${this.getNpmRegistry()}`,
`npm install ${packageName}@${packageVersion} ${this.getNpmInstallArgs()}`,
);
await this.loadNodesAndCredentials.loadPackage(packageName);
await this.loadNodesAndCredentials.postProcessLoaders();