Skip to content

Commit 12cf6a3

Browse files
[rush-lib] Add ensureFolderExists to plugin autoinstaller file writes (#5697)
* [rush-lib] Add ensureFolderExists to plugin autoinstaller file writes The read+write pattern introduced in PR #5678 replaced FileSystem.copyFile() which implicitly created destination folders. The writeFile() calls need ensureFolderExists: true to maintain the same behavior. * [rush-lib] Add plugin autoinstaller update regression test * Prepare to publish a PATCH release of Rush * rush change --------- Co-authored-by: Pete Gonzalez <4673363+octogonz@users.noreply.github.com>
1 parent 47be70b commit 12cf6a3

File tree

4 files changed

+165
-3
lines changed

4 files changed

+165
-3
lines changed
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": "Fix a recent regression that sometimes produced ENOENT errors when installing autoinstallers",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

common/config/rush/version-policies.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"policyName": "rush",
104104
"definitionName": "lockStepVersion",
105105
"version": "5.170.0",
106-
"nextBump": "minor",
106+
"nextBump": "patch",
107107
"mainProject": "@microsoft/rush"
108108
}
109109
]
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
jest.mock(`@rushstack/package-deps-hash`, () => {
5+
return {
6+
getRepoRoot(dir: string): string {
7+
return dir;
8+
},
9+
getDetailedRepoStateAsync(): IDetailedRepoState {
10+
return {
11+
hasSubmodules: false,
12+
hasUncommittedChanges: false,
13+
files: new Map([['common/config/rush/npm-shrinkwrap.json', 'hash']]),
14+
symlinks: new Map()
15+
};
16+
},
17+
getRepoChangesAsync(): ReadonlyMap<string, string> {
18+
return new Map();
19+
},
20+
getGitHashForFiles(filePaths: Iterable<string>): ReadonlyMap<string, string> {
21+
return new Map(Array.from(filePaths, (filePath: string) => [filePath, filePath]));
22+
},
23+
hashFilesAsync(rootDirectory: string, filePaths: Iterable<string>): Promise<ReadonlyMap<string, string>> {
24+
return Promise.resolve(new Map(Array.from(filePaths, (filePath: string) => [filePath, filePath])));
25+
}
26+
};
27+
});
28+
29+
import './mockRushCommandLineParser';
30+
31+
import { FileSystem, JsonFile } from '@rushstack/node-core-library';
32+
import type { IDetailedRepoState } from '@rushstack/package-deps-hash';
33+
import { Autoinstaller } from '../../logic/Autoinstaller';
34+
import type { IRushPluginManifestJson } from '../../pluginFramework/PluginLoader/PluginLoaderBase';
35+
import { BaseInstallAction } from '../actions/BaseInstallAction';
36+
import {
37+
getCommandLineParserInstanceAsync,
38+
isolateEnvironmentConfigurationForTests,
39+
type IEnvironmentConfigIsolation
40+
} from './TestUtils';
41+
42+
interface IPluginTestPaths {
43+
autoinstallerStorePath: string;
44+
sourceCommandLineJsonPath: string;
45+
sourceManifestPath: string;
46+
destinationCommandLineJsonPath: string;
47+
destinationManifestPath: string;
48+
}
49+
50+
function convertToCrLf(content: string): string {
51+
return content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
52+
}
53+
54+
function mockAutoinstallerPrepareAsync(): jest.SpyInstance<Promise<void>, []> {
55+
return jest.spyOn(Autoinstaller.prototype, 'prepareAsync').mockResolvedValue(undefined);
56+
}
57+
58+
function seedPluginFilesWithCrLf(repoPath: string, packageName: string): IPluginTestPaths {
59+
const autoinstallerPath: string = `${repoPath}/common/autoinstallers/plugins`;
60+
const autoinstallerStorePath: string = `${autoinstallerPath}/rush-plugins`;
61+
const installedPluginPath: string = `${autoinstallerPath}/node_modules/${packageName}`;
62+
const sourcePluginPath: string = `${repoPath}/${packageName}`;
63+
const sourceManifestPath: string = `${installedPluginPath}/rush-plugin-manifest.json`;
64+
const sourceCommandLineJsonPath: string = `${installedPluginPath}/command-line.json`;
65+
const destinationManifestPath: string = `${autoinstallerStorePath}/${packageName}/rush-plugin-manifest.json`;
66+
const destinationCommandLineJsonPath: string = `${autoinstallerStorePath}/${packageName}/${packageName}/command-line.json`;
67+
68+
FileSystem.copyFiles({
69+
sourcePath: sourcePluginPath,
70+
destinationPath: installedPluginPath
71+
});
72+
73+
const manifestJson: IRushPluginManifestJson = JsonFile.load(
74+
`${sourcePluginPath}/rush-plugin-manifest.json`
75+
);
76+
manifestJson.plugins[0].commandLineJsonFilePath = 'command-line.json';
77+
FileSystem.writeFile(sourceManifestPath, convertToCrLf(`${JSON.stringify(manifestJson, undefined, 2)}\n`), {
78+
ensureFolderExists: true
79+
});
80+
81+
const commandLineJsonFixturePath: string = destinationCommandLineJsonPath;
82+
const commandLineJsonContent: string = FileSystem.readFile(commandLineJsonFixturePath);
83+
FileSystem.writeFile(sourceCommandLineJsonPath, convertToCrLf(commandLineJsonContent), {
84+
ensureFolderExists: true
85+
});
86+
87+
return {
88+
autoinstallerStorePath,
89+
sourceCommandLineJsonPath,
90+
sourceManifestPath,
91+
destinationCommandLineJsonPath,
92+
destinationManifestPath
93+
};
94+
}
95+
96+
function expectFileToUseLfLineEndings(filePath: string): void {
97+
const content: string = FileSystem.readFile(filePath);
98+
expect(content).toContain('\n');
99+
expect(content).not.toContain('\r\n');
100+
}
101+
102+
describe('RushPluginAutoinstallerUpdate', () => {
103+
let _envIsolation: IEnvironmentConfigIsolation;
104+
105+
beforeEach(() => {
106+
_envIsolation = isolateEnvironmentConfigurationForTests();
107+
});
108+
109+
afterEach(() => {
110+
_envIsolation.restore();
111+
jest.restoreAllMocks();
112+
});
113+
114+
it('update() creates destination folders that do not exist when writing plugin files', async () => {
115+
const repoName: string = 'pluginWithBuildCommandRepo';
116+
const packageName: string = 'rush-build-command-plugin';
117+
const { parser, repoPath } = await getCommandLineParserInstanceAsync(repoName, 'update');
118+
const {
119+
autoinstallerStorePath,
120+
sourceCommandLineJsonPath,
121+
sourceManifestPath,
122+
destinationCommandLineJsonPath,
123+
destinationManifestPath
124+
} = seedPluginFilesWithCrLf(repoPath, packageName);
125+
126+
FileSystem.ensureEmptyFolder(autoinstallerStorePath);
127+
128+
expect(FileSystem.exists(destinationManifestPath)).toBe(false);
129+
expect(FileSystem.exists(destinationCommandLineJsonPath)).toBe(false);
130+
expect(FileSystem.readFile(sourceManifestPath)).toContain('\r\n');
131+
expect(FileSystem.readFile(sourceCommandLineJsonPath)).toContain('\r\n');
132+
133+
const prepareAsyncSpy = mockAutoinstallerPrepareAsync();
134+
const baseInstallActionPrototype: { runAsync(): Promise<void> } =
135+
BaseInstallAction.prototype as unknown as { runAsync(): Promise<void> };
136+
const runAsyncSpy = jest.spyOn(baseInstallActionPrototype, 'runAsync').mockResolvedValue(undefined);
137+
138+
try {
139+
await expect(parser.executeAsync()).resolves.toEqual(true);
140+
} finally {
141+
runAsyncSpy.mockRestore();
142+
prepareAsyncSpy.mockRestore();
143+
}
144+
145+
expect(FileSystem.exists(destinationManifestPath)).toBe(true);
146+
expect(FileSystem.exists(destinationCommandLineJsonPath)).toBe(true);
147+
expectFileToUseLfLineEndings(destinationManifestPath);
148+
expectFileToUseLfLineEndings(destinationCommandLineJsonPath);
149+
});
150+
});

libraries/rush-lib/src/pluginFramework/PluginLoader/AutoinstallerPluginLoader.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export class AutoinstallerPluginLoader extends PluginLoaderBase<IRushPluginConfi
7373
// Use read+write instead of copy to ensure line endings are normalized
7474
const manifestContent: string = FileSystem.readFile(manifestPath);
7575
FileSystem.writeFile(destinationManifestPath, manifestContent, {
76-
convertLineEndings: NewlineKind.Lf
76+
convertLineEndings: NewlineKind.Lf,
77+
ensureFolderExists: true
7778
});
7879
// Make permission consistent since it will be committed to Git
7980
FileSystem.changePosixModeBits(
@@ -105,7 +106,8 @@ export class AutoinstallerPluginLoader extends PluginLoaderBase<IRushPluginConfi
105106
// Use read+write instead of copy to ensure line endings are normalized
106107
const commandLineContent: string = FileSystem.readFile(commandLineJsonFullFilePath);
107108
FileSystem.writeFile(destinationCommandLineJsonFilePath, commandLineContent, {
108-
convertLineEndings: NewlineKind.Lf
109+
convertLineEndings: NewlineKind.Lf,
110+
ensureFolderExists: true
109111
});
110112
// Make permission consistent since it will be committed to Git
111113
FileSystem.changePosixModeBits(

0 commit comments

Comments
 (0)