Skip to content

Commit d0ab412

Browse files
authored
Resolve workspaces recursively for better Yarn 3 support (#85)
* Resolve workspaces recursively * Update dist * Use fullWorkspacePath as root * Use absolute paths * Attempt to use relative paths * Update dist * Fix dirPath * Don't throw when package doesn't have a changelog * Fix tests * Update dist * Add tests * Update dist
1 parent ca36dca commit d0ab412

File tree

3 files changed

+234
-46
lines changed

3 files changed

+234
-46
lines changed

dist/index.js

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12489,30 +12489,54 @@ var auto_changelog_dist = __nccwpck_require__(9272);
1248912489
const MANIFEST_FILE_NAME = 'package.json';
1249012490
const CHANGELOG_FILE_NAME = 'CHANGELOG.md';
1249112491
/**
12492-
* Finds the package manifest for each workspace, and collects
12492+
* Recursively finds the package manifest for each workspace, and collects
1249312493
* metadata for each package.
1249412494
*
1249512495
* @param workspaces - The list of workspace patterns given in the root manifest.
1249612496
* @param rootDir - The monorepo root directory.
12497+
* @param parentDir - The parent directory of the current package.
1249712498
* @returns The metadata for all packages in the monorepo.
1249812499
*/
12499-
async function getMetadataForAllPackages(workspaces, rootDir = WORKSPACE_ROOT) {
12500+
async function getMetadataForAllPackages(workspaces, rootDir = WORKSPACE_ROOT, parentDir = '') {
1250012501
const workspaceLocations = await (0,dist.getWorkspaceLocations)(workspaces, rootDir);
12501-
const result = {};
12502-
await Promise.all(workspaceLocations.map(async (workspaceDirectory) => {
12502+
return workspaceLocations.reduce(async (promise, workspaceDirectory) => {
12503+
const result = await promise;
1250312504
const fullWorkspacePath = external_path_default().join(rootDir, workspaceDirectory);
1250412505
if ((await external_fs_.promises.lstat(fullWorkspacePath)).isDirectory()) {
1250512506
const rawManifest = await (0,dist.getPackageManifest)(fullWorkspacePath);
12507+
// If the package is a sub-workspace, resolve all packages in the sub-workspace and add them
12508+
// to the result.
12509+
if (dist.ManifestFieldNames.Workspaces in rawManifest) {
12510+
const rootManifest = (0,dist.validatePackageManifestVersion)(rawManifest, workspaceDirectory);
12511+
const manifest = (0,dist.validateMonorepoPackageManifest)(rootManifest, workspaceDirectory);
12512+
const name = manifest[dist.ManifestFieldNames.Name];
12513+
if (!name) {
12514+
throw new Error(`Expected sub-workspace in "${workspaceDirectory}" to have a name.`);
12515+
}
12516+
return {
12517+
...result,
12518+
...(await getMetadataForAllPackages(manifest.workspaces, workspaceDirectory, workspaceDirectory)),
12519+
[name]: {
12520+
dirName: external_path_default().basename(workspaceDirectory),
12521+
manifest,
12522+
name,
12523+
dirPath: external_path_default().join(parentDir, workspaceDirectory),
12524+
},
12525+
};
12526+
}
1250612527
const manifest = (0,dist.validatePolyrepoPackageManifest)(rawManifest, workspaceDirectory);
12507-
result[manifest.name] = {
12508-
dirName: external_path_default().basename(workspaceDirectory),
12509-
manifest,
12510-
name: manifest.name,
12511-
dirPath: workspaceDirectory,
12528+
return {
12529+
...result,
12530+
[manifest.name]: {
12531+
dirName: external_path_default().basename(workspaceDirectory),
12532+
manifest,
12533+
name: manifest.name,
12534+
dirPath: external_path_default().join(parentDir, workspaceDirectory),
12535+
},
1251212536
};
1251312537
}
12514-
}));
12515-
return result;
12538+
return result;
12539+
}, Promise.resolve({}));
1251612540
}
1251712541
/**
1251812542
* @param allPackages - The metadata of all packages in the monorepo.
@@ -12596,8 +12620,12 @@ async function updatePackageChangelog(packageMetadata, updateSpecification, root
1259612620
changelogContent = await external_fs_.promises.readFile(changelogPath, 'utf-8');
1259712621
}
1259812622
catch (error) {
12599-
console.error(`Failed to read changelog in "${projectRootDirectory}".`);
12600-
throw error;
12623+
// If the error is not a file not found error, throw it
12624+
if (error.code !== 'ENOENT') {
12625+
console.error(`Failed to read changelog in "${projectRootDirectory}".`);
12626+
throw error;
12627+
}
12628+
return console.warn(`Failed to read changelog in "${projectRootDirectory}".`);
1260112629
}
1260212630
const newChangelogContent = await (0,auto_changelog_dist.updateChangelog)({
1260312631
changelogContent,
@@ -12610,7 +12638,7 @@ async function updatePackageChangelog(packageMetadata, updateSpecification, root
1261012638
const packageName = packageMetadata.manifest.name;
1261112639
throw new Error(`"updateChangelog" returned an empty value for package ${packageName ? `"${packageName}"` : `at "${packagePath}"`}.`);
1261212640
}
12613-
await external_fs_.promises.writeFile(changelogPath, newChangelogContent);
12641+
return await external_fs_.promises.writeFile(changelogPath, newChangelogContent);
1261412642
}
1261512643
/**
1261612644
* Updates the given manifest per the update specification as follows:

src/package-operations.test.ts

Lines changed: 120 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import fs from 'fs';
22
import cloneDeep from 'lodash.clonedeep';
3-
import * as actionUtils from '@metamask/action-utils/dist/file-utils';
3+
import * as actionUtils from '@metamask/action-utils';
44
import {
55
ManifestDependencyFieldNames,
66
ManifestFieldNames,
77
} from '@metamask/action-utils';
88
import * as autoChangelog from '@metamask/auto-changelog';
9+
import glob from 'glob';
910
import * as gitOps from './git-operations';
1011
import {
1112
getMetadataForAllPackages,
@@ -23,20 +24,7 @@ jest.mock('fs', () => ({
2324
},
2425
}));
2526

26-
jest.mock('glob', () => {
27-
return (
28-
_pattern: string,
29-
_options: Record<string, unknown>,
30-
callback: (error: Error | null, results: string[]) => unknown,
31-
) => {
32-
callback(null, [
33-
'packages/dir1',
34-
'packages/dir2',
35-
'packages/dir3',
36-
'packages/someFile',
37-
]);
38-
};
39-
});
27+
jest.mock('glob');
4028

4129
jest.mock('@metamask/action-utils/dist/file-utils', () => {
4230
const actualModule = jest.requireActual(
@@ -119,19 +107,98 @@ describe('package-operations', () => {
119107
? { isDirectory: () => false }
120108
: { isDirectory: () => true };
121109
}) as any);
110+
});
111+
112+
it('does not throw', async () => {
113+
(glob as jest.MockedFunction<any>).mockImplementation(
114+
(
115+
_pattern: string,
116+
_options: unknown,
117+
callback: (error: null, data: string[]) => void,
118+
) =>
119+
callback(null, [
120+
'packages/dir1',
121+
'packages/dir2',
122+
'packages/dir3',
123+
'packages/someFile',
124+
]),
125+
);
122126

123127
jest
124128
.spyOn(actionUtils, 'readJsonObjectFile')
125129
.mockImplementation(getMockReadJsonFile());
126-
});
127130

128-
it('does not throw', async () => {
129131
expect(await getMetadataForAllPackages(['packages/*'])).toStrictEqual({
130132
[names[0]]: getMockPackageMetadata(0),
131133
[names[1]]: getMockPackageMetadata(1),
132134
[names[2]]: getMockPackageMetadata(2),
133135
});
134136
});
137+
138+
it('resolves recursive workspaces', async () => {
139+
(glob as jest.MockedFunction<any>)
140+
.mockImplementationOnce(
141+
(
142+
_pattern: string,
143+
_options: unknown,
144+
callback: (error: null, data: string[]) => void,
145+
) => callback(null, ['packages/dir1']),
146+
)
147+
.mockImplementationOnce(
148+
(
149+
_pattern: string,
150+
_options: unknown,
151+
callback: (error: null, data: string[]) => void,
152+
) => callback(null, ['packages/dir2']),
153+
);
154+
155+
jest
156+
.spyOn(actionUtils, 'readJsonObjectFile')
157+
.mockImplementationOnce(async () => ({
158+
...getMockManifest(names[0], version),
159+
private: true,
160+
workspaces: ['packages/*'],
161+
}))
162+
.mockImplementationOnce(async () => getMockManifest(names[1], version));
163+
164+
expect(await getMetadataForAllPackages(['packages/*'])).toStrictEqual({
165+
[names[0]]: {
166+
...getMockPackageMetadata(0),
167+
manifest: {
168+
...getMockManifest(names[0], version),
169+
private: true,
170+
workspaces: ['packages/*'],
171+
},
172+
},
173+
[names[1]]: {
174+
...getMockPackageMetadata(1),
175+
dirPath: 'packages/dir1/packages/dir2',
176+
},
177+
});
178+
});
179+
180+
it('throws if a sub-workspace does not have a name', async () => {
181+
(glob as jest.MockedFunction<any>).mockImplementationOnce(
182+
(
183+
_pattern: string,
184+
_options: unknown,
185+
callback: (error: null, data: string[]) => void,
186+
) => callback(null, ['packages/dir1']),
187+
);
188+
189+
jest
190+
.spyOn(actionUtils, 'readJsonObjectFile')
191+
.mockImplementationOnce(async () => ({
192+
...getMockManifest(names[0], version),
193+
private: true,
194+
workspaces: ['packages/*'],
195+
name: undefined,
196+
}));
197+
198+
await expect(getMetadataForAllPackages(['packages/*'])).rejects.toThrow(
199+
'Expected sub-workspace in "packages/dir1" to have a name.',
200+
);
201+
});
135202
});
136203

137204
describe('getPackagesToUpdate', () => {
@@ -313,6 +380,42 @@ describe('package-operations', () => {
313380
);
314381
});
315382

383+
it('does not throw if the file cannot be found', async () => {
384+
const originalVersion = '1.0.0';
385+
const newVersion = '1.0.1';
386+
const dir = mockDirs[0];
387+
const name = packageNames[0];
388+
const manifest = getMockManifest(name, originalVersion);
389+
390+
readFileMock.mockImplementationOnce(async () => {
391+
const error = new Error('readError');
392+
(error as any).code = 'ENOENT';
393+
394+
throw error;
395+
});
396+
397+
const packageMetadata = getMockPackageMetadata(dir, manifest);
398+
const updateSpecification = {
399+
newVersion,
400+
packagesToUpdate: new Set(packageNames),
401+
repositoryUrl: 'https://fake',
402+
shouldUpdateChangelog: true,
403+
synchronizeVersions: false,
404+
};
405+
406+
const consoleWarnSpy = jest
407+
.spyOn(console, 'warn')
408+
.mockImplementationOnce(() => undefined);
409+
410+
await updatePackage(packageMetadata, updateSpecification);
411+
412+
expect(updateChangelogMock).not.toHaveBeenCalled();
413+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
414+
expect(consoleWarnSpy).toHaveBeenCalledWith(
415+
expect.stringMatching(/^Failed to read changelog/u),
416+
);
417+
});
418+
316419
it('throws if updated changelog is empty', async () => {
317420
const originalVersion = '1.0.0';
318421
const newVersion = '1.0.1';

0 commit comments

Comments
 (0)