2022-08-02 01:40:57 -07:00
|
|
|
/* eslint-disable no-restricted-syntax */
|
2022-07-20 07:24:03 -07:00
|
|
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
|
|
import { promisify } from 'util';
|
|
|
|
import { exec } from 'child_process';
|
|
|
|
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
2022-08-02 01:40:57 -07:00
|
|
|
import axios from 'axios';
|
2022-07-20 07:24:03 -07:00
|
|
|
import { UserSettings } from 'n8n-core';
|
2023-01-27 05:56:56 -08:00
|
|
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
|
|
|
import { LoggerProxy } from 'n8n-workflow';
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
import {
|
|
|
|
NODE_PACKAGE_PREFIX,
|
|
|
|
NPM_COMMAND_TOKENS,
|
|
|
|
NPM_PACKAGE_STATUS_GOOD,
|
|
|
|
RESPONSE_ERROR_MESSAGES,
|
2022-08-02 01:40:57 -07:00
|
|
|
UNKNOWN_FAILURE_REASON,
|
2022-11-09 06:25:00 -08:00
|
|
|
} from '@/constants';
|
2023-01-27 05:56:56 -08:00
|
|
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
2022-11-09 06:25:00 -08:00
|
|
|
import config from '@/config';
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-11-09 06:25:00 -08:00
|
|
|
import type { CommunityPackages } from '@/Interfaces';
|
2022-08-02 01:40:57 -07:00
|
|
|
|
|
|
|
const {
|
|
|
|
PACKAGE_NAME_NOT_PROVIDED,
|
|
|
|
DISK_IS_FULL,
|
|
|
|
PACKAGE_FAILED_TO_INSTALL,
|
|
|
|
PACKAGE_VERSION_NOT_FOUND,
|
|
|
|
PACKAGE_DOES_NOT_CONTAIN_NODES,
|
|
|
|
PACKAGE_NOT_FOUND,
|
|
|
|
} = RESPONSE_ERROR_MESSAGES;
|
|
|
|
|
|
|
|
const {
|
|
|
|
NPM_PACKAGE_NOT_FOUND_ERROR,
|
|
|
|
NPM_NO_VERSION_AVAILABLE,
|
|
|
|
NPM_DISK_NO_SPACE,
|
|
|
|
NPM_DISK_INSUFFICIENT_SPACE,
|
|
|
|
NPM_PACKAGE_VERSION_NOT_FOUND_ERROR,
|
|
|
|
} = NPM_COMMAND_TOKENS;
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
const execAsync = promisify(exec);
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
export const parseNpmPackageName = (rawString?: string): CommunityPackages.ParsedPackageName => {
|
|
|
|
if (!rawString) throw new Error(PACKAGE_NAME_NOT_PROVIDED);
|
|
|
|
|
|
|
|
if (INVALID_OR_SUSPICIOUS_PACKAGE_NAME.test(rawString))
|
2022-07-20 07:24:03 -07:00
|
|
|
throw new Error('Package name must be a single word');
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const scope = rawString.includes('/') ? rawString.split('/')[0] : undefined;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const packageNameWithoutScope = scope ? rawString.replace(`${scope}/`, '') : rawString;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
if (!packageNameWithoutScope.startsWith(NODE_PACKAGE_PREFIX)) {
|
2022-08-02 01:40:57 -07:00
|
|
|
throw new Error(`Package name must start with ${NODE_PACKAGE_PREFIX}`);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const version = packageNameWithoutScope.includes('@')
|
|
|
|
? packageNameWithoutScope.split('@')[1]
|
|
|
|
: undefined;
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const packageName = version ? rawString.replace(`@${version}`, '') : rawString;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
return {
|
|
|
|
packageName,
|
|
|
|
scope,
|
|
|
|
version,
|
2022-08-02 01:40:57 -07:00
|
|
|
rawString,
|
2022-07-20 07:24:03 -07:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
export const sanitizeNpmPackageName = parseNpmPackageName;
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
export const executeCommand = async (
|
|
|
|
command: string,
|
2022-08-02 01:40:57 -07:00
|
|
|
options?: { doNotHandleError?: boolean },
|
2022-07-20 07:24:03 -07:00
|
|
|
): Promise<string> => {
|
2022-11-04 09:34:47 -07:00
|
|
|
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
const execOptions = {
|
|
|
|
cwd: downloadFolder,
|
|
|
|
env: {
|
|
|
|
NODE_PATH: process.env.NODE_PATH,
|
|
|
|
PATH: process.env.PATH,
|
2022-08-03 09:10:59 -07:00
|
|
|
APPDATA: process.env.APPDATA,
|
2022-07-20 07:24:03 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2022-08-24 02:58:47 -07:00
|
|
|
try {
|
|
|
|
await fsAccess(downloadFolder);
|
|
|
|
} catch (_) {
|
|
|
|
await fsMkdir(downloadFolder);
|
|
|
|
// Also init the folder since some versions
|
|
|
|
// of npm complain if the folder is empty
|
|
|
|
await execAsync('npm init -y', execOptions);
|
|
|
|
}
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
try {
|
|
|
|
const commandResult = await execAsync(command, execOptions);
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
return commandResult.stdout;
|
|
|
|
} catch (error) {
|
2022-08-02 01:40:57 -07:00
|
|
|
if (options?.doNotHandleError) throw error;
|
|
|
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
|
|
|
|
|
|
|
const map = {
|
|
|
|
[NPM_PACKAGE_NOT_FOUND_ERROR]: PACKAGE_NOT_FOUND,
|
|
|
|
[NPM_NO_VERSION_AVAILABLE]: PACKAGE_NOT_FOUND,
|
|
|
|
[NPM_PACKAGE_VERSION_NOT_FOUND_ERROR]: PACKAGE_VERSION_NOT_FOUND,
|
|
|
|
[NPM_DISK_NO_SPACE]: DISK_IS_FULL,
|
|
|
|
[NPM_DISK_INSUFFICIENT_SPACE]: DISK_IS_FULL,
|
|
|
|
};
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
Object.entries(map).forEach(([npmMessage, n8nMessage]) => {
|
|
|
|
if (errorMessage.includes(npmMessage)) throw new Error(n8nMessage);
|
|
|
|
});
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
LoggerProxy.warn('npm command failed', { errorMessage });
|
|
|
|
|
|
|
|
throw new Error(PACKAGE_FAILED_TO_INSTALL);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export function matchPackagesWithUpdates(
|
2022-08-02 01:40:57 -07:00
|
|
|
packages: InstalledPackages[],
|
|
|
|
updates?: CommunityPackages.AvailableUpdates,
|
2022-07-20 07:24:03 -07:00
|
|
|
): PublicInstalledPackage[] {
|
2022-08-02 01:40:57 -07:00
|
|
|
if (!updates) return packages;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
return packages.reduce<PublicInstalledPackage[]>((acc, cur) => {
|
|
|
|
const publicPackage: PublicInstalledPackage = { ...cur };
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
const update = updates[cur.packageName];
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
if (update) publicPackage.updateAvailable = update.latest;
|
|
|
|
|
|
|
|
acc.push(publicPackage);
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}, []);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function matchMissingPackages(
|
|
|
|
installedPackages: PublicInstalledPackage[],
|
|
|
|
missingPackages: string,
|
|
|
|
): PublicInstalledPackage[] {
|
|
|
|
const missingPackageNames = missingPackages.split(' ');
|
|
|
|
|
|
|
|
const missingPackagesList = missingPackageNames.map((missingPackageName: string) => {
|
|
|
|
// Strip away versions but maintain scope and package name
|
|
|
|
try {
|
2022-08-02 01:40:57 -07:00
|
|
|
const parsedPackageData = parseNpmPackageName(missingPackageName);
|
2022-07-20 07:24:03 -07:00
|
|
|
return parsedPackageData.packageName;
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-empty
|
|
|
|
} catch (_) {}
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
|
|
|
|
const hydratedPackageList = [] as PublicInstalledPackage[];
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
installedPackages.forEach((installedPackage) => {
|
|
|
|
const hydratedInstalledPackage = { ...installedPackage };
|
|
|
|
if (missingPackagesList.includes(hydratedInstalledPackage.packageName)) {
|
|
|
|
hydratedInstalledPackage.failedLoading = true;
|
|
|
|
}
|
|
|
|
hydratedPackageList.push(hydratedInstalledPackage);
|
|
|
|
});
|
|
|
|
|
|
|
|
return hydratedPackageList;
|
|
|
|
}
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
export async function checkNpmPackageStatus(
|
|
|
|
packageName: string,
|
|
|
|
): Promise<CommunityPackages.PackageStatusCheck> {
|
|
|
|
const N8N_BACKEND_SERVICE_URL = 'https://api.n8n.io/api/package';
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
try {
|
2022-08-02 01:40:57 -07:00
|
|
|
const response = await axios.post<CommunityPackages.PackageStatusCheck>(
|
|
|
|
N8N_BACKEND_SERVICE_URL,
|
2022-07-20 07:24:03 -07:00
|
|
|
{ name: packageName },
|
2022-08-02 01:40:57 -07:00
|
|
|
{ method: 'POST' },
|
2022-07-20 07:24:03 -07:00
|
|
|
);
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
if (response.data.status !== NPM_PACKAGE_STATUS_GOOD) return response.data;
|
2022-07-20 07:24:03 -07:00
|
|
|
} catch (error) {
|
|
|
|
// Do nothing if service is unreachable
|
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
return { status: NPM_PACKAGE_STATUS_GOOD };
|
|
|
|
}
|
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
export function hasPackageLoaded(packageName: string): boolean {
|
|
|
|
const missingPackages = config.get('nodes.packagesMissing') as string | undefined;
|
|
|
|
|
|
|
|
if (!missingPackages) return true;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-08-02 01:40:57 -07:00
|
|
|
return !missingPackages
|
|
|
|
.split(' ')
|
|
|
|
.some(
|
2022-07-20 07:24:03 -07:00
|
|
|
(packageNameAndVersion) =>
|
|
|
|
packageNameAndVersion.startsWith(packageName) &&
|
|
|
|
packageNameAndVersion.replace(packageName, '').startsWith('@'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function removePackageFromMissingList(packageName: string): void {
|
|
|
|
try {
|
|
|
|
const failedPackages = (config.get('nodes.packagesMissing') as string).split(' ');
|
|
|
|
|
|
|
|
const packageFailedToLoad = failedPackages.filter(
|
|
|
|
(packageNameAndVersion) =>
|
|
|
|
!packageNameAndVersion.startsWith(packageName) ||
|
|
|
|
!packageNameAndVersion.replace(packageName, '').startsWith('@'),
|
|
|
|
);
|
|
|
|
|
|
|
|
config.set('nodes.packagesMissing', packageFailedToLoad.join(' '));
|
|
|
|
} catch (_error) {
|
|
|
|
// Do nothing
|
|
|
|
}
|
|
|
|
}
|
2022-08-02 01:40:57 -07:00
|
|
|
|
|
|
|
export const isClientError = (error: Error): boolean => {
|
|
|
|
const clientErrors = [
|
|
|
|
PACKAGE_VERSION_NOT_FOUND,
|
|
|
|
PACKAGE_DOES_NOT_CONTAIN_NODES,
|
|
|
|
PACKAGE_NOT_FOUND,
|
|
|
|
];
|
|
|
|
|
|
|
|
return clientErrors.some((message) => error.message.includes(message));
|
|
|
|
};
|
|
|
|
|
|
|
|
export function isNpmError(error: unknown): error is { code: number; stdout: string } {
|
|
|
|
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
|
|
|
|
}
|