mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 00:24:07 -08:00
feat(core): Hot reload nodes on all servers
This commit is contained in:
parent
fb06b55211
commit
d7d620bf29
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,6 +356,8 @@ export class Start extends BaseCommand {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void this.setupHotReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
async catch(error: Error) {
|
async catch(error: Error) {
|
||||||
|
|
|
@ -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(() => {});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(() => {});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue