Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Move stale autoinstaller `node_modules` folders into Rush's recycler before asynchronously deleting them, instead of synchronously deleting them in place.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
105 changes: 105 additions & 0 deletions libraries/rush-lib/src/cli/test/Autoinstaller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import './mockRushCommandLineParser';

import { FileSystem } from '@rushstack/node-core-library';

import { Autoinstaller } from '../../logic/Autoinstaller';
import { InstallHelpers } from '../../logic/installManager/InstallHelpers';
import { RushConstants } from '../../logic/RushConstants';
import { Utilities } from '../../utilities/Utilities';
import {
getCommandLineParserInstanceAsync,
isolateEnvironmentConfigurationForTests,
type IEnvironmentConfigIsolation
} from './TestUtils';

describe(Autoinstaller.name, () => {
let _envIsolation: IEnvironmentConfigIsolation;

beforeEach(() => {
_envIsolation = isolateEnvironmentConfigurationForTests();
});

afterEach(() => {
_envIsolation.restore();
jest.restoreAllMocks();
});

it('moves an existing node_modules folder into the Rush recycler before reinstalling', async () => {
const { parser, repoPath, spawnMock } = await getCommandLineParserInstanceAsync(
'pluginWithBuildCommandRepo',
'update'
);
const autoinstallerPath: string = `${repoPath}/common/autoinstallers/plugins`;
const nodeModulesFolder: string = `${autoinstallerPath}/${RushConstants.nodeModulesFolderName}`;
const staleFilePath: string = `${nodeModulesFolder}/stale-package/index.js`;
const recyclerFolder: string = `${parser.rushConfiguration.commonTempFolder}/${RushConstants.rushRecyclerFolderName}`;

await FileSystem.writeFileAsync(staleFilePath, 'stale', {
ensureFolderExists: true
});

let recyclerEntriesBefore: Set<string>;
try {
recyclerEntriesBefore = new Set(await FileSystem.readFolderItemNamesAsync(recyclerFolder));
} catch (error) {
if (FileSystem.isNotExistError(error)) {
recyclerEntriesBefore = new Set();
} else {
throw error;
}
}

jest.spyOn(InstallHelpers, 'ensureLocalPackageManagerAsync').mockResolvedValue(undefined);
jest.spyOn(Utilities, 'syncNpmrc').mockImplementation(() => undefined);
jest
.spyOn(Utilities, 'executeCommandAsync')
.mockImplementation(async (options: Parameters<typeof Utilities.executeCommandAsync>[0]) => {
await FileSystem.ensureFolderAsync(
`${options.workingDirectory}/${RushConstants.nodeModulesFolderName}`
);
});

const autoinstaller: Autoinstaller = new Autoinstaller({
autoinstallerName: 'plugins',
rushConfiguration: parser.rushConfiguration,
rushGlobalFolder: parser.rushGlobalFolder
});

await autoinstaller.prepareAsync();

const recyclerEntriesAfter: string[] = (await FileSystem.readFolderItemNamesAsync(recyclerFolder)).filter(
(entry: string) => !recyclerEntriesBefore.has(entry)
);

expect(recyclerEntriesAfter).toHaveLength(1);
await expect(
FileSystem.existsAsync(`${recyclerFolder}/${recyclerEntriesAfter[0]}/stale-package/index.js`)
).resolves.toBe(true);
await expect(FileSystem.existsAsync(staleFilePath)).resolves.toBe(false);
await expect(FileSystem.existsAsync(`${nodeModulesFolder}/rush-autoinstaller.flag`)).resolves.toBe(true);

if (process.platform === 'win32') {
expect(spawnMock).toHaveBeenCalledWith(
'cmd.exe',
expect.arrayContaining(['/c']),
expect.objectContaining({
detached: true,
stdio: 'ignore',
windowsVerbatimArguments: true
})
);
} else {
expect(spawnMock).toHaveBeenCalledWith(
'rm',
expect.arrayContaining(['-rf']),
expect.objectContaining({
detached: true,
stdio: 'ignore'
})
);
}
});
});
7 changes: 6 additions & 1 deletion libraries/rush-lib/src/logic/Autoinstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@rushstack/node-core-library';
import { Colorize } from '@rushstack/terminal';

import { AsyncRecycler } from '../utilities/AsyncRecycler';
import { Utilities } from '../utilities/Utilities';
import type { RushConfiguration } from '../api/RushConfiguration';
import { PackageJsonEditor } from '../api/PackageJsonEditor';
Expand Down Expand Up @@ -131,7 +132,11 @@ export class Autoinstaller {
if (isLastInstallFlagDirty || lock.dirtyWhenAcquired) {
if (FileSystem.exists(nodeModulesFolder)) {
this._logIfConsoleOutputIsNotRestricted('Deleting old files from ' + nodeModulesFolder);
FileSystem.ensureEmptyFolder(nodeModulesFolder);
const recycler: AsyncRecycler = new AsyncRecycler(
`${this._rushConfiguration.commonTempFolder}/${RushConstants.rushRecyclerFolderName}`
);
recycler.moveFolder(nodeModulesFolder);
await recycler.startDeleteAllAsync();
}

// Copy: .../common/autoinstallers/my-task/.npmrc
Expand Down