feat(core): Hot reload nodes on all servers

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-11-08 16:55:12 +01:00
parent fb06b55211
commit d7d620bf29
No known key found for this signature in database
6 changed files with 69 additions and 53 deletions

View file

@ -1,6 +1,8 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { Command, Errors } from '@oclif/core'; import { Command, Errors } from '@oclif/core';
import glob from 'fast-glob';
import { access as fsAccess, realpath as fsRealPath } from 'fs/promises';
import { import {
BinaryDataService, BinaryDataService,
InstanceSettings, InstanceSettings,
@ -13,6 +15,8 @@ import {
ErrorReporterProxy as ErrorReporter, ErrorReporterProxy as ErrorReporter,
sleep, sleep,
} from 'n8n-workflow'; } from 'n8n-workflow';
import path from 'path';
import picocolors from 'picocolors';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { AbstractServer } from '@/abstract-server'; import type { AbstractServer } from '@/abstract-server';
@ -42,6 +46,8 @@ export abstract class BaseCommand extends Command {
protected nodeTypes: NodeTypes; protected nodeTypes: NodeTypes;
protected loadNodesAndCredentials: LoadNodesAndCredentials;
protected instanceSettings: InstanceSettings = Container.get(InstanceSettings); protected instanceSettings: InstanceSettings = Container.get(InstanceSettings);
protected server?: AbstractServer; protected server?: AbstractServer;
@ -69,7 +75,8 @@ export abstract class BaseCommand extends Command {
process.once('SIGINT', this.onTerminationSignal('SIGINT')); process.once('SIGINT', this.onTerminationSignal('SIGINT'));
this.nodeTypes = Container.get(NodeTypes); this.nodeTypes = Container.get(NodeTypes);
await Container.get(LoadNodesAndCredentials).init(); this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
await this.loadNodesAndCredentials.init();
await Db.init().catch( await Db.init().catch(
async (error: Error) => await this.exitWithCrash('There was an error initializing DB', error), async (error: Error) => await this.exitWithCrash('There was an error initializing DB', error),
@ -338,4 +345,58 @@ export abstract class BaseCommand extends Command {
clearTimeout(forceShutdownTimer); clearTimeout(forceShutdownTimer);
}; };
} }
protected async setupHotReload() {
if (!inDevelopment || process.env.N8N_DEV_RELOAD !== 'true') return;
const { default: debounce } = await import('lodash/debounce');
// eslint-disable-next-line import/no-extraneous-dependencies
const { watch } = await import('chokidar');
const { Push } = await import('@/push');
const push = Container.get(Push);
Object.values(this.loadNodesAndCredentials.loaders).forEach(async (loader) => {
try {
await fsAccess(loader.directory);
} catch {
// If directory doesn't exist, there is nothing to watch
return;
}
const realModulePath = path.join(await fsRealPath(loader.directory), path.sep);
const reloader = debounce(async (fileName: string) => {
console.info(
picocolors.green('⭮ Reloading'),
picocolors.bold(fileName),
'in',
loader.packageName,
);
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.loadNodesAndCredentials.postProcessLoaders();
push.broadcast('nodeDescriptionUpdated', {});
}, 100);
const toWatch = loader.isLazyLoaded
? ['**/nodes.json', '**/credentials.json']
: ['**/*.js', '**/*.json'];
const files = await glob(toWatch, {
cwd: realModulePath,
ignore: ['node_modules/**'],
});
const watcher = watch(files, {
cwd: realModulePath,
ignoreInitial: true,
});
watcher.on('add', reloader).on('change', reloader).on('unlink', reloader);
});
}
} }

View file

@ -356,6 +356,8 @@ export class Start extends BaseCommand {
} }
}); });
} }
void this.setupHotReload();
} }
async catch(error: Error) { async catch(error: Error) {

View file

@ -94,6 +94,8 @@ export class Webhook extends BaseCommand {
await this.server.start(); await this.server.start();
this.logger.info('Webhook listener waiting for requests.'); this.logger.info('Webhook listener waiting for requests.');
void this.setupHotReload();
// Make sure that the process does not close // Make sure that the process does not close
await new Promise(() => {}); await new Promise(() => {});
} }

View file

@ -186,6 +186,8 @@ export class Worker extends BaseCommand {
}); });
} }
void this.setupHotReload();
// Make sure that the process does not close // Make sure that the process does not close
if (!inTest) await new Promise(() => {}); if (!inTest) await new Promise(() => {});
} }

View file

@ -1,6 +1,5 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import glob from 'fast-glob'; import glob from 'fast-glob';
import fsPromises from 'fs/promises';
import type { Class, DirectoryLoader, Types } from 'n8n-core'; import type { Class, DirectoryLoader, Types } from 'n8n-core';
import { import {
CUSTOM_EXTENSION_ENV, CUSTOM_EXTENSION_ENV,
@ -19,7 +18,7 @@ import type {
import { NodeHelpers, ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { NodeHelpers, ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import path from 'path'; import path from 'path';
import picocolors from 'picocolors'; import picocolors from 'picocolors';
import { Container, Service } from 'typedi'; import { Service } from 'typedi';
import { import {
CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_KEY,
@ -355,50 +354,4 @@ export class LoadNodesAndCredentials {
await postProcessor(); await postProcessor();
} }
} }
async setupHotReload() {
const { default: debounce } = await import('lodash/debounce');
// eslint-disable-next-line import/no-extraneous-dependencies
const { watch } = await import('chokidar');
const { Push } = await import('@/push');
const push = Container.get(Push);
Object.values(this.loaders).forEach(async (loader) => {
try {
await fsPromises.access(loader.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();
push.broadcast('nodeDescriptionUpdated', {});
}, 100);
const toWatch = loader.isLazyLoaded
? ['**/nodes.json', '**/credentials.json']
: ['**/*.js', '**/*.json'];
const files = await glob(toWatch, {
cwd: realModulePath,
ignore: ['node_modules/**'],
});
const watcher = watch(files, {
cwd: realModulePath,
ignoreInitial: true,
});
watcher.on('add', reloader).on('change', reloader).on('unlink', reloader);
});
}
} }

View file

@ -100,10 +100,6 @@ export class Server extends AbstractServer {
await super.start(); await super.start();
this.logger.debug(`Server ID: ${this.instanceSettings.hostId}`); this.logger.debug(`Server ID: ${this.instanceSettings.hostId}`);
if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') {
void this.loadNodesAndCredentials.setupHotReload();
}
this.eventService.emit('server-started'); this.eventService.emit('server-started');
} }