forked from angular/dev-infra
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.ts
More file actions
230 lines (209 loc) · 7.99 KB
/
config.ts
File metadata and controls
230 lines (209 loc) · 7.99 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
/**
* @license
* Copyright Google LLC
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {join} from 'path';
import glob from 'fast-glob';
import {register} from 'tsx/esm/api';
import {Assertions, MultipleAssertions} from './config-assertions.js';
import {Log} from './logging.js';
import {getCachedConfig, setCachedConfig} from './config-cache.js';
import {determineRepoBaseDirFromCwd} from './repo-directory.js';
import {pathToFileURL} from 'url';
/**
* Type describing a ng-dev configuration.
*
* This is a branded type to ensure that we can safely assert an object
* being a config object instead of it being e.g. a `Promise` object.
*/
export type NgDevConfig<T = {}> = T & {
__isNgDevConfigObject: boolean;
};
/**
* Describes the Github configuration for dev-infra. This configuration is
* used for API requests, determining the upstream remote, etc.
*/
export interface GithubConfig {
/** Owner name of the repository. */
owner: string;
/** Name of the repository. */
name: string;
/** Main branch name for the repository. */
mainBranchName: string;
/** If SSH protocol should be used for git interactions. */
useSsh?: boolean;
/** Whether the specified repository is private. */
private?: boolean;
/** Whether to default to use NgDevService for authentication. */
useNgDevAuthService?: boolean;
}
/** Configuration describing how files are synced into Google. */
export interface GoogleSyncConfig {
/**
* Patterns matching files which are synced into Google. Patterns
* should be relative to the project directory.
*/
syncedFilePatterns: string[];
/**
* Patterns matching files which are never synced into Google. Patterns
* should be relative to the project directory.
*/
alwaysExternalFilePatterns: string[];
/**
* Patterns matching files which need to be synced separately.
* Patterns should be relative to the project directory.
*/
separateFilePatterns: string[];
}
export interface CaretakerConfig {
/** Github queries showing a snapshot of pulls/issues caretakers need to monitor. */
githubQueries?: {name: string; query: string}[];
/**
* The Github group used to track current caretakers. A second group is assumed to exist with the
* name "<group-name>-roster" containing a list of all users eligible for the caretaker group.
* */
caretakerGroup?: string;
/**
* Project-relative path to a config file describing how the project is synced into Google.
* The configuration file is expected to be valid JSONC and match {@see GoogleSyncConfig}.
*/
g3SyncConfigPath?: string;
}
/**
* The filename expected for creating the ng-dev config, without the file
* extension to allow either a typescript or javascript file to be used.
*/
const CONFIG_FILE_PATH_MATCHER = '.ng-dev/config.{mjs,mts}';
/**
* The filename expected for local user config, without the file extension to allow a typescript,
* javascript or json file to be used.
*/
const USER_CONFIG_FILE_PATH = '.ng-dev.user';
/** The local user configuration for ng-dev. */
let userConfig: {[key: string]: any} | null = null;
/**
* Set the cached configuration object to be loaded later. Only to be used on
* CI and test situations in which loading from the `.ng-dev/` directory is not possible.
*/
export const setConfig = setCachedConfig;
/**
* Get the configuration from the file system, returning the already loaded
* copy if it is defined.
*/
export async function getConfig(): Promise<NgDevConfig>;
export async function getConfig(baseDir: string): Promise<NgDevConfig>;
export async function getConfig<A extends MultipleAssertions>(
assertions: A,
): Promise<NgDevConfig<Assertions<A>>>;
export async function getConfig(baseDirOrAssertions?: unknown) {
let cachedConfig = getCachedConfig();
if (cachedConfig === null) {
let baseDir: string;
if (typeof baseDirOrAssertions === 'string') {
baseDir = baseDirOrAssertions;
} else {
baseDir = determineRepoBaseDirFromCwd();
}
/** The configuration file discovered based on a glob match. */
const [matchedFile] = await glob(CONFIG_FILE_PATH_MATCHER, {cwd: baseDir});
// If the global config is not defined, load it from the file system.
// The full path to the configuration file.
const configPath = join(baseDir, matchedFile);
// Read the configuration and validate it before caching it for the future.
cachedConfig = await readConfigFile(configPath);
// Store the newly-read configuration in the cache.
setCachedConfig(cachedConfig);
}
if (Array.isArray(baseDirOrAssertions)) {
for (const assertion of baseDirOrAssertions) {
assertion(cachedConfig);
}
}
// Return a clone of the cached global config to ensure that a new instance of the config
// is returned each time, preventing unexpected effects of modifications to the config object.
return {...cachedConfig, __isNgDevConfigObject: true};
}
/**
* Get the local user configuration from the file system, returning the already loaded copy if it is
* defined.
*
* @returns The user configuration object, or an empty object if no user configuration file is
* present. The object is an untyped object as there are no required user configurations.
*/
export async function getUserConfig() {
// If the global config is not defined, load it from the file system.
if (userConfig === null) {
// The full path to the configuration file.
const configPath = join(determineRepoBaseDirFromCwd(), USER_CONFIG_FILE_PATH);
// Set the global config object.
userConfig = await readConfigFile(configPath, true);
}
// Return a clone of the user config to ensure that a new instance of the config is returned
// each time, preventing unexpected effects of modifications to the config object.
return {...userConfig};
}
/** A standard error class to thrown during assertions while validating configuration. */
export class ConfigValidationError extends Error {
constructor(
message?: string,
public readonly errors: string[] = [],
) {
super(message);
}
}
/** Validate th configuration has been met for the ng-dev command. */
export function assertValidGithubConfig<T extends NgDevConfig>(
config: T & Partial<{github: GithubConfig}>,
): asserts config is T & {github: GithubConfig} {
const errors: string[] = [];
// Validate the github configuration.
if (config.github === undefined) {
errors.push(`Github repository not configured. Set the "github" option.`);
} else {
if (config.github.name === undefined) {
errors.push(`"github.name" is not defined`);
}
if (config.github.owner === undefined) {
errors.push(`"github.owner" is not defined`);
}
}
if (errors.length) {
throw new ConfigValidationError('Invalid `github` configuration', errors);
}
}
/** Retrieve and validate the config as `CaretakerConfig`. */
export function assertValidCaretakerConfig<T extends NgDevConfig>(
config: T & Partial<{caretaker: CaretakerConfig}>,
): asserts config is T & {caretaker: CaretakerConfig} {
if (config.caretaker === undefined) {
throw new ConfigValidationError(`No configuration defined for "caretaker"`);
}
}
/**
* Resolves and reads the specified configuration file, optionally returning an empty object
* if the configuration file cannot be read.
*/
async function readConfigFile(configPath: string, returnEmptyObjectOnError = false): Promise<{}> {
const unregister = register({tsconfig: false});
try {
// ESM imports expect a valid URL. On Windows, the disk name causes errors like:
// `ERR_UNSUPPORTED_ESM_URL_SCHEME: <..> Received protocol 'c:'`
return await import(pathToFileURL(configPath).toString());
} catch (e) {
if (returnEmptyObjectOnError) {
Log.debug(
`Could not read configuration file at ${configPath}, returning empty object instead.`,
);
Log.debug(e);
return {};
}
Log.error(`Could not read configuration file at ${configPath}.`);
Log.error(e);
process.exit(1);
} finally {
unregister();
}
}