mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-13 16:14: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 { GlobalConfig } from '@n8n/config';
|
||||
import { Command, Errors } from '@oclif/core';
|
||||
import glob from 'fast-glob';
|
||||
import { access as fsAccess, realpath as fsRealPath } from 'fs/promises';
|
||||
import {
|
||||
BinaryDataService,
|
||||
InstanceSettings,
|
||||
|
@ -13,6 +15,8 @@ import {
|
|||
ErrorReporterProxy as ErrorReporter,
|
||||
sleep,
|
||||
} from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
import picocolors from 'picocolors';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import type { AbstractServer } from '@/abstract-server';
|
||||
|
@ -42,6 +46,8 @@ export abstract class BaseCommand extends Command {
|
|||
|
||||
protected nodeTypes: NodeTypes;
|
||||
|
||||
protected loadNodesAndCredentials: LoadNodesAndCredentials;
|
||||
|
||||
protected instanceSettings: InstanceSettings = Container.get(InstanceSettings);
|
||||
|
||||
protected server?: AbstractServer;
|
||||
|
@ -69,7 +75,8 @@ export abstract class BaseCommand extends Command {
|
|||
process.once('SIGINT', this.onTerminationSignal('SIGINT'));
|
||||
|
||||
this.nodeTypes = Container.get(NodeTypes);
|
||||
await Container.get(LoadNodesAndCredentials).init();
|
||||
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
||||
await this.loadNodesAndCredentials.init();
|
||||
|
||||
await Db.init().catch(
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -94,6 +94,8 @@ export class Webhook extends BaseCommand {
|
|||
await this.server.start();
|
||||
this.logger.info('Webhook listener waiting for requests.');
|
||||
|
||||
void this.setupHotReload();
|
||||
|
||||
// Make sure that the process does not close
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
|
|
@ -186,6 +186,8 @@ export class Worker extends BaseCommand {
|
|||
});
|
||||
}
|
||||
|
||||
void this.setupHotReload();
|
||||
|
||||
// Make sure that the process does not close
|
||||
if (!inTest) await new Promise(() => {});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { GlobalConfig } from '@n8n/config';
|
||||
import glob from 'fast-glob';
|
||||
import fsPromises from 'fs/promises';
|
||||
import type { Class, DirectoryLoader, Types } from 'n8n-core';
|
||||
import {
|
||||
CUSTOM_EXTENSION_ENV,
|
||||
|
@ -19,7 +18,7 @@ import type {
|
|||
import { NodeHelpers, ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
import picocolors from 'picocolors';
|
||||
import { Container, Service } from 'typedi';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
import {
|
||||
CUSTOM_API_CALL_KEY,
|
||||
|
@ -355,50 +354,4 @@ export class LoadNodesAndCredentials {
|
|||
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();
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue