-
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathpackage-utils.ts
More file actions
325 lines (298 loc) · 10 KB
/
package-utils.ts
File metadata and controls
325 lines (298 loc) · 10 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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
import pathUtils from 'path';
import { promisify } from 'util';
import _glob from 'glob';
import { isTruthyString } from './misc-utils';
import { readJsonObjectFile } from './file-utils';
import { isValidSemver } from './semver-utils';
const glob = promisify(_glob);
const PACKAGE_JSON = 'package.json';
export enum ManifestDependencyFieldNames {
Production = 'dependencies',
Development = 'devDependencies',
Peer = 'peerDependencies',
Bundled = 'bundledDependencies',
Optional = 'optionalDependencies',
}
export enum ManifestFieldNames {
Name = 'name',
Private = 'private',
Version = 'version',
Workspaces = 'workspaces',
}
export interface PackageManifest
extends Partial<
Record<ManifestDependencyFieldNames, Record<string, string>>
> {
readonly [ManifestFieldNames.Name]: string;
readonly [ManifestFieldNames.Private]?: boolean;
readonly [ManifestFieldNames.Version]: string;
readonly [ManifestFieldNames.Workspaces]?: string[];
}
export interface PolyrepoPackageManifest
extends Partial<
Record<ManifestDependencyFieldNames, Record<string, string>>
> {
readonly [ManifestFieldNames.Name]: string;
readonly [ManifestFieldNames.Version]: string;
}
export interface MonorepoPackageManifest extends Partial<PackageManifest> {
readonly [ManifestFieldNames.Version]: string;
readonly [ManifestFieldNames.Private]: boolean;
readonly [ManifestFieldNames.Workspaces]: string[];
}
/**
* Read, parse, validate, and return the object corresponding to the
* package.json file in the given directory.
*
* An error is thrown if validation fails.
*
* @param containingDirPath - The complete path to the directory containing
* the package.json file.
* @returns The object corresponding to the parsed package.json file.
*/
export async function getPackageManifest(
containingDirPath: string,
): Promise<Record<string, unknown>> {
return await readJsonObjectFile(
pathUtils.join(containingDirPath, PACKAGE_JSON),
);
}
/**
* Type guard to ensure that the given manifest has a valid "name" field.
*
* @param manifest - The manifest object to validate.
* @returns Whether the manifest has a valid "name" field.
*/
function hasValidNameField(
manifest: Partial<PackageManifest>,
): manifest is typeof manifest &
Pick<PackageManifest, ManifestFieldNames.Name> {
return isTruthyString(manifest[ManifestFieldNames.Name]);
}
/**
* Type guard to ensure that the given manifest has a valid "private" field.
*
* @param manifest - The manifest object to validate.
* @returns Whether the manifest has a valid "private" field.
*/
function hasValidPrivateField(
manifest: Partial<PackageManifest>,
): manifest is typeof manifest &
Pick<MonorepoPackageManifest, ManifestFieldNames.Private> {
return manifest[ManifestFieldNames.Private] === true;
}
/**
* Type guard to ensure that the given manifest has a valid "version" field.
*
* @param manifest - The manifest object to validate.
* @returns Whether the manifest has a valid "version" field.
*/
function hasValidVersionField(
manifest: Partial<PackageManifest>,
): manifest is typeof manifest &
Pick<PackageManifest, ManifestFieldNames.Version> {
return isValidSemver(manifest[ManifestFieldNames.Version]);
}
/**
* Type guard to ensure that the given manifest has a valid "worksapces" field.
*
* @param manifest - The manifest object to validate.
* @returns Whether the manifest has a valid "worksapces" field.
*/
function hasValidWorkspacesField(
manifest: Partial<PackageManifest>,
): manifest is typeof manifest &
Pick<MonorepoPackageManifest, ManifestFieldNames.Workspaces> {
return (
Array.isArray(manifest[ManifestFieldNames.Workspaces]) &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
manifest[ManifestFieldNames.Workspaces]!.length > 0
);
}
/**
* Validates the "version" field of a package manifest object, i.e. a parsed
* "package.json" file.
*
* @param manifest - The manifest to validate.
* @param manifestDirPath - The path to the directory containing the
* manifest file relative to the root directory.
* @returns The unmodified manifest, with the "version" field typed correctly.
*/
export function validatePackageManifestVersion<
ManifestType extends Partial<PackageManifest>
>(
manifest: ManifestType,
manifestDirPath: string,
): ManifestType & Pick<PackageManifest, ManifestFieldNames.Version> {
if (!hasValidVersionField(manifest)) {
throw new Error(
`${getManifestErrorMessagePrefix(
ManifestFieldNames.Version,
manifest,
manifestDirPath,
)} is not a valid SemVer version: ${
manifest[ManifestFieldNames.Version]
}`,
);
}
return manifest;
}
/**
* Validates the "name" field of a package manifest object, i.e. a parsed
* "package.json" file.
*
* @param manifest - The manifest to validate.
* @param manifestDirPath - The path to the directory containing the
* manifest file relative to the root directory.
* @returns The unmodified manifest, with the "name" field typed correctly.
*/
export function validatePackageManifestName<
ManifestType extends Partial<PackageManifest>
>(
manifest: ManifestType,
manifestDirPath: string,
): ManifestType & Pick<PackageManifest, ManifestFieldNames.Name> {
if (!hasValidNameField(manifest)) {
throw new Error(
`Manifest in "${manifestDirPath}" does not have a valid "${ManifestFieldNames.Name}" field.`,
);
}
return manifest;
}
/**
* Validates the "version" and "name" fields of a package manifest object,
* i.e. a parsed "package.json" file.
*
* @param manifest - The manifest to validate.
* @param manifestDirPath - The path to the directory containing the
* manifest file relative to the root directory.
* @returns The unmodified manifest, with the "version" and "name" fields typed
* correctly.
*/
export function validatePolyrepoPackageManifest(
manifest: Partial<PackageManifest>,
manifestDirPath: string,
): PolyrepoPackageManifest {
return validatePackageManifestName(
validatePackageManifestVersion(manifest, manifestDirPath),
manifestDirPath,
);
}
/**
* Validates the "workspaces" and "private" fields of a package manifest object,
* i.e. a parsed "package.json" file.
*
* Assumes that the manifest's "version" field is already validated.
*
* @param manifest - The manifest to validate.
* @param manifestDirPath - The path to the directory containing the
* manifest file relative to the root directory.
* @returns The unmodified manifest, with the "workspaces" and "private" fields
* typed correctly.
*/
export function validateMonorepoPackageManifest<
ManifestType extends Pick<PackageManifest, ManifestFieldNames.Version> &
Partial<PackageManifest>
>(manifest: ManifestType, manifestDirPath: string): MonorepoPackageManifest {
if (!hasValidWorkspacesField(manifest)) {
throw new Error(
`${getManifestErrorMessagePrefix(
ManifestFieldNames.Workspaces,
manifest,
manifestDirPath,
)} must be a non-empty array if present. Received: ${
manifest[ManifestFieldNames.Workspaces]
}`,
);
}
if (!hasValidPrivateField(manifest)) {
throw new Error(
`${getManifestErrorMessagePrefix(
ManifestFieldNames.Private,
manifest,
manifestDirPath,
)} must be "true" if "${
ManifestFieldNames.Workspaces
}" is present. Received: ${manifest[ManifestFieldNames.Private]}`,
);
}
return manifest;
}
/**
* Gets the prefix of an error message for a manifest file validation error.
*
* @param invalidField - The name of the invalid field.
* @param manifest - The manifest object that's invalid.
* @param manifestDirPath - The path to the directory of the manifest file
* relative to the root directory.
* @returns The prefix of a manifest validation error message.
*/
function getManifestErrorMessagePrefix(
invalidField: ManifestFieldNames,
manifest: Partial<MonorepoPackageManifest>,
manifestDirPath: string,
) {
return `${
manifest[ManifestFieldNames.Name]
? `"${manifest[ManifestFieldNames.Name]}" manifest "${invalidField}"`
: `"${invalidField}" of manifest in "${manifestDirPath}"`
}`;
}
/**
* Get workspace directory locations, given the set of workspace patterns
* specified in the `workspaces` field of the root `package.json` file.
*
* @param workspaces - The list of workspace patterns given in the root manifest.
* @param rootDir - The monorepo root directory.
* @param recursive - Whether to search recursively.
* @returns The location of each workspace directory relative to the root directory
*/
export async function getWorkspaceLocations(
workspaces: string[],
rootDir: string,
recursive = false,
prefix = '',
): Promise<string[]> {
const resolvedWorkspaces = await workspaces.reduce<Promise<string[]>>(
async (promise, pattern) => {
const array = await promise;
const matches = (await glob(pattern, { cwd: rootDir })).map((match) =>
pathUtils.join(prefix, match),
);
return [...array, ...matches];
},
Promise.resolve([]),
);
if (recursive) {
// This reads all the package JSON files in each workspace, checks if they are a monorepo, and
// recursively calls `getWorkspaceLocations` if they are.
const resolvedSubWorkspaces = await resolvedWorkspaces.reduce<
Promise<string[]>
>(async (promise, workspacePath) => {
const array = await promise;
const rawManifest = await getPackageManifest(workspacePath);
if (ManifestFieldNames.Workspaces in rawManifest) {
const manifest = validatePackageManifestVersion(
rawManifest,
workspacePath,
);
const monorepoManifest = validateMonorepoPackageManifest(
manifest,
workspacePath,
);
return [
...array,
...(await getWorkspaceLocations(
monorepoManifest[ManifestFieldNames.Workspaces],
workspacePath,
recursive,
workspacePath,
)),
];
}
return array;
}, Promise.resolve(resolvedWorkspaces));
return resolvedSubWorkspaces;
}
return resolvedWorkspaces;
}