Skip to content

Commit 162a383

Browse files
committed
Use AsyncRecycler for autoinstaller cleanup
1 parent f7eebd6 commit 162a383

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Move stale autoinstaller node_modules folders into Rush's recycler before deleting them, instead of clearing them in place.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import * as path from 'node:path';
5+
6+
import './mockRushCommandLineParser';
7+
8+
import { FileSystem } from '@rushstack/node-core-library';
9+
10+
import { Autoinstaller } from '../../logic/Autoinstaller';
11+
import { InstallHelpers } from '../../logic/installManager/InstallHelpers';
12+
import { RushConstants } from '../../logic/RushConstants';
13+
import { Utilities } from '../../utilities/Utilities';
14+
import {
15+
getCommandLineParserInstanceAsync,
16+
isolateEnvironmentConfigurationForTests,
17+
type IEnvironmentConfigIsolation
18+
} from './TestUtils';
19+
20+
describe('Autoinstaller', () => {
21+
let _envIsolation: IEnvironmentConfigIsolation;
22+
23+
beforeEach(() => {
24+
_envIsolation = isolateEnvironmentConfigurationForTests();
25+
});
26+
27+
afterEach(() => {
28+
_envIsolation.restore();
29+
jest.restoreAllMocks();
30+
});
31+
32+
it('moves an existing node_modules folder into the Rush recycler before reinstalling', async () => {
33+
const { parser, repoPath, spawnMock } = await getCommandLineParserInstanceAsync(
34+
'pluginWithBuildCommandRepo',
35+
'update'
36+
);
37+
const autoinstallerPath: string = path.join(repoPath, 'common/autoinstallers/plugins');
38+
const nodeModulesFolder: string = path.join(autoinstallerPath, RushConstants.nodeModulesFolderName);
39+
const staleFilePath: string = path.join(nodeModulesFolder, 'stale-package/index.js');
40+
const recyclerFolder: string = path.join(
41+
parser.rushConfiguration.commonTempFolder,
42+
RushConstants.rushRecyclerFolderName
43+
);
44+
45+
FileSystem.writeFile(staleFilePath, 'stale', {
46+
ensureFolderExists: true
47+
});
48+
49+
const recyclerEntriesBefore: Set<string> = FileSystem.exists(recyclerFolder)
50+
? new Set(FileSystem.readFolderItemNames(recyclerFolder))
51+
: new Set();
52+
53+
jest.spyOn(InstallHelpers, 'ensureLocalPackageManagerAsync').mockResolvedValue(undefined);
54+
jest.spyOn(Utilities, 'syncNpmrc').mockImplementation(() => undefined);
55+
jest
56+
.spyOn(Utilities, 'executeCommandAsync')
57+
.mockImplementation(async (options: Parameters<typeof Utilities.executeCommandAsync>[0]) => {
58+
FileSystem.ensureFolder(path.join(options.workingDirectory, RushConstants.nodeModulesFolderName));
59+
});
60+
61+
const autoinstaller: Autoinstaller = new Autoinstaller({
62+
autoinstallerName: 'plugins',
63+
rushConfiguration: parser.rushConfiguration,
64+
rushGlobalFolder: parser.rushGlobalFolder
65+
});
66+
67+
await autoinstaller.prepareAsync();
68+
69+
const recyclerEntriesAfter: string[] = FileSystem.readFolderItemNames(recyclerFolder).filter(
70+
(entry: string) => !recyclerEntriesBefore.has(entry)
71+
);
72+
73+
expect(recyclerEntriesAfter).toHaveLength(1);
74+
expect(
75+
FileSystem.exists(path.join(recyclerFolder, recyclerEntriesAfter[0], 'stale-package/index.js'))
76+
).toBe(true);
77+
expect(FileSystem.exists(staleFilePath)).toBe(false);
78+
expect(FileSystem.exists(path.join(nodeModulesFolder, 'rush-autoinstaller.flag'))).toBe(true);
79+
80+
if (process.platform === 'win32') {
81+
expect(spawnMock).toHaveBeenCalledWith(
82+
'cmd.exe',
83+
expect.arrayContaining(['/c']),
84+
expect.objectContaining({
85+
detached: true,
86+
stdio: 'ignore',
87+
windowsVerbatimArguments: true
88+
})
89+
);
90+
} else {
91+
expect(spawnMock).toHaveBeenCalledWith(
92+
'rm',
93+
expect.arrayContaining(['-rf']),
94+
expect.objectContaining({
95+
detached: true,
96+
stdio: 'ignore'
97+
})
98+
);
99+
}
100+
});
101+
});

libraries/rush-lib/src/logic/Autoinstaller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@rushstack/node-core-library';
1515
import { Colorize } from '@rushstack/terminal';
1616

17+
import { AsyncRecycler } from '../utilities/AsyncRecycler';
1718
import { Utilities } from '../utilities/Utilities';
1819
import type { RushConfiguration } from '../api/RushConfiguration';
1920
import { PackageJsonEditor } from '../api/PackageJsonEditor';
@@ -131,7 +132,11 @@ export class Autoinstaller {
131132
if (isLastInstallFlagDirty || lock.dirtyWhenAcquired) {
132133
if (FileSystem.exists(nodeModulesFolder)) {
133134
this._logIfConsoleOutputIsNotRestricted('Deleting old files from ' + nodeModulesFolder);
134-
FileSystem.ensureEmptyFolder(nodeModulesFolder);
135+
const recycler = new AsyncRecycler(
136+
path.join(this._rushConfiguration.commonTempFolder, RushConstants.rushRecyclerFolderName)
137+
);
138+
recycler.moveFolder(nodeModulesFolder);
139+
await recycler.startDeleteAllAsync();
135140
}
136141

137142
// Copy: .../common/autoinstallers/my-task/.npmrc

0 commit comments

Comments
 (0)