Repository navigation
Expand file tree
/
Copy pathCoreConfigFiles.ts
More file actions
249 lines (219 loc) · 9.82 KB
/
Copy pathCoreConfigFiles.ts
File metadata and controls
249 lines (219 loc) · 9.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import {
ProjectConfigurationFile,
InheritanceType,
PathResolutionMethod,
type IJsonPathMetadataResolverOptions
} from '@rushstack/heft-config-file';
import { Import, PackageJsonLookup, InternalError } from '@rushstack/node-core-library';
import type { ITerminal } from '@rushstack/terminal';
import type { IRigConfig } from '@rushstack/rig-package';
import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin';
import type { INodeServicePluginConfiguration } from '../plugins/NodeServicePlugin';
import { Constants } from './Constants';
export interface IHeftConfigurationJsonActionReference {
actionName: string;
defaultParameters?: string[];
}
export interface IHeftConfigurationJsonAliases {
[aliasName: string]: IHeftConfigurationJsonActionReference;
}
export interface IHeftConfigurationJsonPluginSpecifier {
pluginPackage: string;
pluginPackageRoot: string;
pluginName?: string;
options?: object;
}
export interface IHeftConfigurationJsonTaskSpecifier {
taskDependencies?: string[];
taskPlugin: IHeftConfigurationJsonPluginSpecifier;
}
export interface IHeftConfigurationJsonTasks {
[taskName: string]: IHeftConfigurationJsonTaskSpecifier;
}
export interface IHeftConfigurationJsonPhaseSpecifier {
phaseDescription?: string;
phaseDependencies?: string[];
cleanFiles?: IDeleteOperation[];
tasksByName?: IHeftConfigurationJsonTasks;
}
export interface IHeftConfigurationJsonPhases {
[phaseName: string]: IHeftConfigurationJsonPhaseSpecifier;
}
export interface IHeftConfigurationJson {
heftPlugins?: IHeftConfigurationJsonPluginSpecifier[];
aliasesByName?: IHeftConfigurationJsonAliases;
phasesByName?: IHeftConfigurationJsonPhases;
}
let _heftConfigFileLoader: ProjectConfigurationFile<IHeftConfigurationJson> | undefined;
let _nodeServiceConfigurationLoader: ProjectConfigurationFile<INodeServicePluginConfiguration> | undefined;
export class CoreConfigFiles {
public static heftConfigurationProjectRelativeFilePath: string = `${Constants.projectConfigFolderName}/${Constants.heftConfigurationFilename}`;
public static nodeServiceConfigurationProjectRelativeFilePath: string = `${Constants.projectConfigFolderName}/${Constants.nodeServiceConfigurationFilename}`;
/**
* Returns the loader for the `config/heft.json` config file.
*/
public static async loadHeftConfigurationFileForProjectAsync(
terminal: ITerminal,
projectPath: string,
rigConfig?: IRigConfig | undefined
): Promise<IHeftConfigurationJson> {
if (!_heftConfigFileLoader) {
let heftPluginPackageFolder: string | undefined;
const pluginPackageResolver: (
options: IJsonPathMetadataResolverOptions<IHeftConfigurationJson>
) => string = (options: IJsonPathMetadataResolverOptions<IHeftConfigurationJson>) => {
const { propertyValue, configurationFilePath } = options;
if (propertyValue === Constants.heftPackageName) {
// If the value is "@rushstack/heft", then resolve to the Heft package that is
// installed in the project folder. This avoids issues with mismatched versions
// between the project and the globally installed Heft. Use the PackageJsonLookup
// class to find the package folder to avoid hardcoding the path for compatibility
// with bundling.
if (!heftPluginPackageFolder) {
heftPluginPackageFolder = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname);
}
if (!heftPluginPackageFolder) {
// This should never happen
throw new InternalError('Unable to find the @rushstack/heft package folder');
}
return heftPluginPackageFolder;
} else {
const configurationFileDirectory: string = path.dirname(configurationFilePath);
return Import.resolvePackage({
packageName: propertyValue,
baseFolderPath: configurationFileDirectory,
allowSelfReference: true
});
}
};
const schemaObject: object = await import('../schemas/heft.schema.json');
// eslint-disable-next-line require-atomic-updates
_heftConfigFileLoader = new ProjectConfigurationFile<IHeftConfigurationJson>({
projectRelativeFilePath: CoreConfigFiles.heftConfigurationProjectRelativeFilePath,
jsonSchemaObject: schemaObject,
propertyInheritanceDefaults: {
array: { inheritanceType: InheritanceType.append },
object: { inheritanceType: InheritanceType.merge }
},
jsonPathMetadata: {
// Use a custom resolver for the plugin packages, since the NodeResolve algorithm will resolve to the
// package.json exports/module property, which may or may not exist.
'$.heftPlugins.*.pluginPackage': {
pathResolutionMethod: PathResolutionMethod.custom,
customResolver: pluginPackageResolver
},
// Use a custom resolver for the plugin packages, since the NodeResolve algorithm will resolve to the
// package.json exports/module property, which may or may not exist.
'$.phasesByName.*.tasksByName.*.taskPlugin.pluginPackage': {
pathResolutionMethod: PathResolutionMethod.custom,
customResolver: pluginPackageResolver
}
}
});
}
const heftConfigFileLoader: ProjectConfigurationFile<IHeftConfigurationJson> = _heftConfigFileLoader;
let configurationFile: IHeftConfigurationJson;
try {
configurationFile = await heftConfigFileLoader.loadConfigurationFileForProjectAsync(
terminal,
projectPath,
rigConfig
);
} catch (e: unknown) {
if (
!(e instanceof Error) ||
!e.message.startsWith('Resolved configuration object does not match schema')
) {
throw e;
}
try {
// If the config file doesn't match the schema, then we should check to see if it does
// match the legacy schema. We don't need to worry about the resulting object, we just
// want to see if it parses. We will use the ConfigurationFile class to load it to ensure
// that we follow the "extends" chain for the entire config file.
const legacySchemaObject: object = await import('../schemas/heft-legacy.schema.json');
const legacyConfigFileLoader: ProjectConfigurationFile<unknown> =
new ProjectConfigurationFile<unknown>({
projectRelativeFilePath: CoreConfigFiles.heftConfigurationProjectRelativeFilePath,
jsonSchemaObject: legacySchemaObject
});
await legacyConfigFileLoader.loadConfigurationFileForProjectAsync(terminal, projectPath, rigConfig);
} catch (e2) {
// It doesn't match the legacy schema either. Throw the original error.
throw e;
}
// Matches the legacy schema, so throw a more helpful error.
throw new Error(
"This project's Heft configuration appears to be using an outdated schema.\n\n" +
'Heft 0.51.0 introduced a major breaking change for Heft configuration files. ' +
'Your project appears to be using the older file format. You will need to ' +
'migrate your project to the new format. Follow these instructions: ' +
'https://rushstack.io/link/heft-0.51'
);
}
// The pluginPackage field was resolved to the root of the package, but we also want to have
// the original plugin package name in the config file.
function getUpdatedPluginSpecifier(
rawSpecifier: IHeftConfigurationJsonPluginSpecifier
): IHeftConfigurationJsonPluginSpecifier {
const pluginPackageName: string = heftConfigFileLoader.getPropertyOriginalValue({
parentObject: rawSpecifier,
propertyName: 'pluginPackage'
})!;
const newSpecifier: IHeftConfigurationJsonPluginSpecifier = {
...rawSpecifier,
pluginPackageRoot: rawSpecifier.pluginPackage,
pluginPackage: pluginPackageName
};
return newSpecifier;
}
const phasesByName: IHeftConfigurationJsonPhases = {};
const normalizedConfigurationFile: IHeftConfigurationJson = {
...configurationFile,
heftPlugins: configurationFile.heftPlugins?.map(getUpdatedPluginSpecifier) ?? [],
phasesByName
};
for (const [phaseName, phase] of Object.entries(configurationFile.phasesByName || {})) {
const tasksByName: IHeftConfigurationJsonTasks = {};
phasesByName[phaseName] = {
...phase,
tasksByName
};
for (const [taskName, task] of Object.entries(phase.tasksByName || {})) {
if (task.taskPlugin) {
tasksByName[taskName] = {
...task,
taskPlugin: getUpdatedPluginSpecifier(task.taskPlugin)
};
} else {
tasksByName[taskName] = task;
}
}
}
return normalizedConfigurationFile;
}
public static async tryLoadNodeServiceConfigurationFileAsync(
terminal: ITerminal,
projectPath: string,
rigConfig?: IRigConfig | undefined
): Promise<INodeServicePluginConfiguration | undefined> {
if (!_nodeServiceConfigurationLoader) {
const schemaObject: object = await import('../schemas/node-service.schema.json');
// eslint-disable-next-line require-atomic-updates
_nodeServiceConfigurationLoader = new ProjectConfigurationFile<INodeServicePluginConfiguration>({
projectRelativeFilePath: CoreConfigFiles.nodeServiceConfigurationProjectRelativeFilePath,
jsonSchemaObject: schemaObject
});
}
const configurationFile: INodeServicePluginConfiguration | undefined =
await _nodeServiceConfigurationLoader.tryLoadConfigurationFileForProjectAsync(
terminal,
projectPath,
rigConfig
);
return configurationFile;
}
}