Skip to content

Commit 8c63ca7

Browse files
authored
fix(core): Unloading a community package should also unload all its files from require.cache (#16072)
1 parent dfdc223 commit 8c63ca7

File tree

3 files changed

+39
-12
lines changed

3 files changed

+39
-12
lines changed

packages/cli/src/load-nodes-and-credentials.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -523,22 +523,15 @@ export class LoadNodesAndCredentials {
523523
const push = Container.get(Push);
524524

525525
Object.values(this.loaders).forEach(async (loader) => {
526+
const { directory } = loader;
526527
try {
527-
await fsPromises.access(loader.directory);
528+
await fsPromises.access(directory);
528529
} catch {
529530
// If directory doesn't exist, there is nothing to watch
530531
return;
531532
}
532533

533-
const realModulePath = path.join(await fsPromises.realpath(loader.directory), path.sep);
534534
const reloader = debounce(async () => {
535-
const modulesToUnload = Object.keys(require.cache).filter((filePath) =>
536-
filePath.startsWith(realModulePath),
537-
);
538-
modulesToUnload.forEach((filePath) => {
539-
delete require.cache[filePath];
540-
});
541-
542535
loader.reset();
543536
await loader.loadAll();
544537
await this.postProcessLoaders();
@@ -549,11 +542,11 @@ export class LoadNodesAndCredentials {
549542
? ['**/nodes.json', '**/credentials.json']
550543
: ['**/*.js', '**/*.json'];
551544
const files = await glob(toWatch, {
552-
cwd: realModulePath,
545+
cwd: directory,
553546
ignore: ['node_modules/**'],
554547
});
555548
const watcher = watch(files, {
556-
cwd: realModulePath,
549+
cwd: directory,
557550
ignoreInitial: true,
558551
});
559552
watcher.on('add', reloader).on('change', reloader).on('unlink', reloader);

packages/core/src/nodes-loader/__tests__/directory-loader.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jest.mock('node:fs');
1313
jest.mock('node:fs/promises');
1414
const mockFs = mock<typeof fs>();
1515
const mockFsPromises = mock<typeof fsPromises>();
16+
fs.realpathSync = mockFs.realpathSync;
1617
fs.readFileSync = mockFs.readFileSync;
1718
fsPromises.readFile = mockFsPromises.readFile;
1819

@@ -64,6 +65,7 @@ describe('DirectoryLoader', () => {
6465
let mockCredential1: ICredentialType, mockNode1: INodeType, mockNode2: INodeType;
6566

6667
beforeEach(() => {
68+
mockFs.realpathSync.mockImplementation((path) => String(path));
6769
mockCredential1 = createCredential('credential1');
6870
mockNode1 = createNode('node1', 'credential1');
6971
mockNode2 = createNode('node2');
@@ -330,6 +332,19 @@ describe('DirectoryLoader', () => {
330332
});
331333
});
332334

335+
describe('constructor', () => {
336+
it('should resolve symlinks to real paths when directory is a symlink', () => {
337+
const symlinkPath = '/symlink/path';
338+
const realPath = '/real/path';
339+
mockFs.realpathSync.mockReturnValueOnce(realPath);
340+
341+
const loader = new CustomDirectoryLoader(symlinkPath);
342+
343+
expect(mockFs.realpathSync).toHaveBeenCalledWith(symlinkPath);
344+
expect(loader.directory).toBe(realPath);
345+
});
346+
});
347+
333348
describe('reset()', () => {
334349
it('should reset all properties to their initial state', async () => {
335350
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);

packages/core/src/nodes-loader/directory-loader.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
KnownNodesAndCredentials,
1818
} from 'n8n-workflow';
1919
import { ApplicationError, isSubNodeType } from 'n8n-workflow';
20+
import { realpathSync } from 'node:fs';
2021
import * as path from 'path';
2122

2223
import { UnrecognizedCredentialTypeError } from '@/errors/unrecognized-credential-type.error';
@@ -83,13 +84,22 @@ export abstract class DirectoryLoader {
8384
readonly directory: string,
8485
protected excludeNodes: string[] = [],
8586
protected includeNodes: string[] = [],
86-
) {}
87+
) {
88+
// If `directory` is a symlink, we try to resolve it to its real path
89+
try {
90+
this.directory = realpathSync(directory);
91+
} catch (error) {
92+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
93+
if (error.code !== 'ENOENT') throw error;
94+
}
95+
}
8796

8897
abstract packageName: string;
8998

9099
abstract loadAll(): Promise<void>;
91100

92101
reset() {
102+
this.unloadAll();
93103
this.loadedNodes = [];
94104
this.nodeTypes = {};
95105
this.credentialTypes = {};
@@ -450,4 +460,13 @@ export abstract class DirectoryLoader {
450460

451461
return;
452462
}
463+
464+
private unloadAll() {
465+
const filesToUnload = Object.keys(require.cache).filter((filePath) =>
466+
filePath.startsWith(this.directory),
467+
);
468+
filesToUnload.forEach((filePath) => {
469+
delete require.cache[filePath];
470+
});
471+
}
453472
}

0 commit comments

Comments
 (0)