diff --git a/src/release-specification.test.ts b/src/release-specification.test.ts index 9c4b2c6..fcd594f 100644 --- a/src/release-specification.test.ts +++ b/src/release-specification.test.ts @@ -602,7 +602,7 @@ Your release spec could not be processed due to the following issues: }); }); - it('throws if there are any packages in the release with a major version bump using the word "major", but any of their dependents defined as "peerDependencies" have changes since their latest release and are not listed in the release', async () => { + it('throws if there are any packages in the release with a major version bump using the word "major", but any of their peer dependents have changes since their latest release and are not listed in the release', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -642,7 +642,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -657,7 +657,7 @@ ${releaseSpecificationPath} }); }); - it('throws if there are any packages in the release with a major version bump using the word "major", but any of their dependents defined as "peerDependencies" are not listed in the release, even if they have no changes', async () => { + it('throws if there are any packages in the release with a major version bump using the word "major", but any of their peer dependents are not listed in the release, even if they have no changes', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -697,7 +697,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -712,7 +712,7 @@ ${releaseSpecificationPath} }); }); - it('throws if there are any packages in the release with a major version bump using a literal version, but any of their dependents defined as "peerDependencies" have changes since their latest release and are not listed in the release', async () => { + it('throws if there are any packages in the release with a major version bump using a literal version, but any of their peer dependents have changes since their latest release and are not listed in the release', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -752,7 +752,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -767,7 +767,7 @@ ${releaseSpecificationPath} }); }); - it('throws if there are any packages in the release with a major version bump using a literal version, but any of their dependents defined as "peerDependencies" are not listed in the release, even if they have no changes', async () => { + it('throws if there are any packages in the release with a major version bump using a literal version, but any of their peer dependents are not listed in the release, even if they have no changes', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -775,7 +775,7 @@ ${releaseSpecificationPath} hasChangesSinceLatestRelease: true, }), b: buildMockPackage('b', { - hasChangesSinceLatestRelease: true, + hasChangesSinceLatestRelease: false, validatedManifest: { peerDependencies: { a: '2.1.4', @@ -807,7 +807,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -822,7 +822,7 @@ ${releaseSpecificationPath} }); }); - it('throws if there are any packages in the release with a major version bump using the word "major", but their dependents via "peerDependencies" have changes since their latest release and have their version specified as null in the release spec', async () => { + it('throws if there are any packages in the release with a major version bump using the word "major", but their peer dependents have changes since their latest release and have their version specified as null in the release spec', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -863,7 +863,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -878,7 +878,7 @@ ${releaseSpecificationPath} }); }); - it('throws if there are any packages in the release with a major version bump using the word "major", but their dependents via "peerDependencies" have their version specified as null in the release spec, even if they have no changes', async () => { + it('throws if there are any packages in the release with a major version bump using the word "major", but their peer dependents have their version specified as null in the release spec, even if they have no changes', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -919,7 +919,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -934,7 +934,7 @@ ${releaseSpecificationPath} }); }); - it('throws if there are any packages in the release with a major version bump using a literal version, but their dependents via "peerDependencies" have changes since their latest release and have their version specified as null in the release spec', async () => { + it('throws if there are any packages in the release with a major version bump using a literal version, but their peer dependents have changes since their latest release and have their version specified as null in the release spec', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -975,7 +975,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -990,7 +990,7 @@ ${releaseSpecificationPath} }); }); - it('throws if there are any packages in the release with a major version bump using a literal version, but their dependents via "peerDependencies" have their version specified as null in the release spec, even if they have no changes', async () => { + it('throws if there are any packages in the release with a major version bump using a literal version, but their peer dependents have their version specified as null in the release spec, even if they have no changes', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -1031,7 +1031,7 @@ Your release spec could not be processed due to the following issues: - b - Consider including them in the release spec so that they are compatible with the new 'a' version. + Consider including them in the release spec so that they are compatible with the new 'a' version. If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example: @@ -1040,6 +1040,450 @@ Your release spec could not be processed due to the following issues: The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using the word "major", but any of their direct dependents have changes since their latest release and are not listed in the release', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: true, + validatedManifest: { + dependencies: { + a: '1.0.0', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: 'major', + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using the word "major", but any of their direct dependents are not listed in the release, even if they have no changes', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: false, + validatedManifest: { + dependencies: { + a: '1.0.0', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: 'major', + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using a literal version, but any of their direct dependents have changes since their latest release and are not listed in the release', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', '2.1.4', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: true, + validatedManifest: { + dependencies: { + a: '2.1.4', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: '3.0.0', + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using a literal version, but any of their direct dependents are not listed in the release, even if they have no changes', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', '2.1.4', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: false, + validatedManifest: { + dependencies: { + a: '2.1.4', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: '3.0.0', + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using the word "major", but their direct dependents have changes since their latest release and have their version specified as null in the release spec', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: true, + validatedManifest: { + dependencies: { + a: '1.0.0', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: 'major', + b: null, + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using the word "major", but their direct dependents have their version specified as null in the release spec, even if they have no changes', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: false, + validatedManifest: { + dependencies: { + a: '1.0.0', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: 'major', + b: null, + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using a literal version, but their direct dependents have changes since their latest release and have their version specified as null in the release spec', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', '2.1.4', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: true, + validatedManifest: { + dependencies: { + a: '2.1.4', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: '3.0.0', + b: null, + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + +${releaseSpecificationPath} +`.trim(), + ); + }); + }); + + it('throws if there are any packages in the release with a major version bump using a literal version, but their direct dependents have their version specified as null in the release spec, even if they have no changes', async () => { + await withSandbox(async (sandbox) => { + const project = buildMockProject({ + workspacePackages: { + a: buildMockPackage('a', '2.1.4', { + hasChangesSinceLatestRelease: true, + }), + b: buildMockPackage('b', { + hasChangesSinceLatestRelease: false, + validatedManifest: { + dependencies: { + a: '2.1.4', + }, + }, + }), + }, + }); + const releaseSpecificationPath = path.join( + sandbox.directoryPath, + 'release-spec', + ); + await fs.promises.writeFile( + releaseSpecificationPath, + YAML.stringify({ + packages: { + a: '3.0.0', + b: null, + }, + }), + ); + + await expect( + validateReleaseSpecification(project, releaseSpecificationPath), + ).rejects.toThrow( + ` +Your release spec could not be processed due to the following issues: + +* The following direct dependents of package 'a', which is being released with a major version bump, are missing from the release spec. + + - b + + Consider including them in the release spec so that the dependency tree of consuming projects can be kept small. + + If you do not want to do this, then list it with a directive of "intentionally-skip". For example: + + packages: + b: intentionally-skip + +The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool. + ${releaseSpecificationPath} `.trim(), ); @@ -1136,7 +1580,7 @@ ${releaseSpecificationPath} }); }); - it('does not throw an error if packages in the release with a major version bump using the word "major", have their dependents via "peerDependencies" with their version specified as "intentionally-skip" in the release spec', async () => { + it('does not throw an error if packages in the release with a major version bump using the word "major", have their peer dependents with their version specified as "intentionally-skip" in the release spec', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { @@ -1181,7 +1625,7 @@ ${releaseSpecificationPath} }); }); - it('does not throw an error if packages in the release with a major version bump using a literal version, have their dependents via "peerDependencies" with their version specified as "intentionally-skip" in the release spec', async () => { + it('does not throw an error if packages in the release with a major version bump using a literal version, have their peer dependents with their version specified as "intentionally-skip" in the release spec', async () => { await withSandbox(async (sandbox) => { const project = buildMockProject({ workspacePackages: { diff --git a/src/release-specification.ts b/src/release-specification.ts index e551a2e..4cc3861 100644 --- a/src/release-specification.ts +++ b/src/release-specification.ts @@ -168,44 +168,52 @@ export async function waitForUserToEditReleaseSpecification( } /** - * Finds all workspace packages that depend on the given package. + * Finds all workspace packages that have the given package under a particular + * "dependencies" section. * * @param project - The project containing workspace packages. * @param packageName - The name of the package to find dependents for. - * @returns An array of package names that depend on the given package. + * @param type - The section in which to look for the package. + * @returns An array of package names. */ -export function findAllWorkspacePackagesThatDependOnPackage( +export function findWorkspaceDependentNamesOfType( project: Project, packageName: string, + type: 'dependencies' | 'peerDependencies', ): string[] { - const dependentNames = Object.keys(project.workspacePackages).filter( + return Object.keys(project.workspacePackages).filter( (possibleDependentName) => { const possibleDependent = project.workspacePackages[possibleDependentName]; - const { peerDependencies } = possibleDependent.validatedManifest; - return hasProperty(peerDependencies, packageName); + return hasProperty( + possibleDependent.validatedManifest[type], + packageName, + ); }, ); - - return dependentNames; } /** - * Finds all workspace packages that depend on the given package. + * Finds all workspace packages that list the package in a particular + * "dependencies" section but are missing from the release spec. * * @param project - The project containing workspace packages. * @param packageName - The name of the package to find dependents for. - * @param unvalidatedReleaseSpecificationPackages - The packages in the release specification. - * @returns An array of package names that depend on the given package and are missing from the release spec. + * @param unvalidatedReleaseSpecificationPackages - The packages in the release + * specification. + * @param type - The type of dependents to find for. + * @returns An array of package names. */ -export function findMissingUnreleasedDependents( +export function findMissingWorkspaceDependentNamesOfType( project: Project, packageName: string, unvalidatedReleaseSpecificationPackages: Record, + type: 'dependencies' | 'peerDependencies', ): string[] { - const dependentNames = findAllWorkspacePackagesThatDependOnPackage( + const dependentNames = findWorkspaceDependentNamesOfType( project, packageName, + type, ); return dependentNames.filter((dependentName) => { @@ -214,12 +222,14 @@ export function findMissingUnreleasedDependents( } /** - * Finds all workspace packages that are dependencies of the given package and have changes since their latest release. + * Finds all workspace packages that are direct or peer dependencies of the + * given package and have changes since their latest release. * * @param project - The project containing workspace packages. * @param changedPackage - The package to find dependencies for. - * @param unvalidatedReleaseSpecificationPackages - The packages in the release specification. - * @returns An array of package names that are dependencies and are missing from the release spec. + * @param unvalidatedReleaseSpecificationPackages - The packages in the release + * specification. + * @returns An array of package names. */ function findMissingUnreleasedDependencies( project: Project, @@ -238,21 +248,25 @@ function findMissingUnreleasedDependencies( } /** - * Finds all dependents of a package that are missing from the release spec when - * making breaking changes. + * Finds the direct or peer dependents of a major-bumped package that are + * candidates for inclusion in the release. * * @param project - Information about the whole project (e.g., names of packages * and where they can found). * @param packageName - The name of the package to validate. - * @param versionSpecifierOrDirective - The version specifier or directive for the package. - * @param unvalidatedReleaseSpecificationPackages - The packages in the release specification. - * @returns An array of validation errors, if any. + * @param versionSpecifierOrDirective - The version specifier or directive for + * the package. + * @param unvalidatedReleaseSpecificationPackages - The packages in the release + * specification. + * @param type - The type of dependents to search for. + * @returns An array of direct dependents for the package. */ -export function findMissingUnreleasedDependentsForBreakingChanges( +export function findCandidateDependentsOfTypeForMajorBump( project: Project, packageName: string, versionSpecifierOrDirective: string | null, unvalidatedReleaseSpecificationPackages: Record, + type: 'dependencies' | 'peerDependencies', ): string[] { const changedPackage = project.workspacePackages[packageName]; @@ -264,10 +278,11 @@ export function findMissingUnreleasedDependentsForBreakingChanges( versionSpecifierOrDirective, ) === 'major') ) { - return findMissingUnreleasedDependents( + return findMissingWorkspaceDependentNamesOfType( project, packageName, unvalidatedReleaseSpecificationPackages, + type, ); } @@ -275,8 +290,9 @@ export function findMissingUnreleasedDependentsForBreakingChanges( } /** - * Finds all dependencies of a package that are missing from the release spec when - * making breaking changes. + * For a package being included in the release, finds all direct peer or + * dependencies that have changes since their last release but have not been + * added to the release yet. * * @param project - Information about the whole project (e.g., names of packages * and where they can found). @@ -285,7 +301,7 @@ export function findMissingUnreleasedDependentsForBreakingChanges( * @param unvalidatedReleaseSpecificationPackages - The packages in the release specification. * @returns An array of validation errors, if any. */ -export function findMissingUnreleasedDependenciesForRelease( +export function findCandidateDependencies( project: Project, changedPackage: Package, versionSpecifierOrDirective: string | null, @@ -375,25 +391,61 @@ export function validateAllPackageEntries( } } - const missingDependentNames = - findMissingUnreleasedDependentsForBreakingChanges( + const missingDirectDependentNames = + findCandidateDependentsOfTypeForMajorBump( + project, + changedPackageName, + versionSpecifierOrDirective, + unvalidatedReleaseSpecificationPackages, + 'dependencies', + ); + + if (missingDirectDependentNames.length > 0) { + errors.push({ + message: [ + `The following direct dependents of package '${changedPackageName}', which is being released with a major version bump, are missing from the release spec.`, + missingDirectDependentNames + .map((dependent) => ` - ${dependent}`) + .join('\n'), + ` Consider including them in the release spec so that the dependency tree of consuming projects can be kept small.`, + ` If you do not want to do this, then list it with a directive of "intentionally-skip". For example:`, + YAML.stringify({ + packages: missingDirectDependentNames.reduce( + (object, dependent) => ({ + ...object, + [dependent]: INTENTIONALLY_SKIP_PACKAGE_DIRECTIVE, + }), + {}, + ), + }) + .trim() + .split('\n') + .map((line) => ` ${line}`) + .join('\n'), + ].join('\n\n'), + }); + } + + const missingPeerDependentNames = + findCandidateDependentsOfTypeForMajorBump( project, changedPackageName, versionSpecifierOrDirective, unvalidatedReleaseSpecificationPackages, + 'peerDependencies', ); - if (missingDependentNames.length > 0) { + if (missingPeerDependentNames.length > 0) { errors.push({ message: [ `The following dependents of package '${changedPackageName}', which is being released with a major version bump, are missing from the release spec.`, - missingDependentNames + missingPeerDependentNames .map((dependent) => ` - ${dependent}`) .join('\n'), - ` Consider including them in the release spec so that they are compatible with the new '${changedPackageName}' version.`, + ` Consider including them in the release spec so that they are compatible with the new '${changedPackageName}' version.`, ` If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:`, YAML.stringify({ - packages: missingDependentNames.reduce( + packages: missingPeerDependentNames.reduce( (object, dependent) => ({ ...object, [dependent]: INTENTIONALLY_SKIP_PACKAGE_DIRECTIVE, @@ -409,7 +461,7 @@ export function validateAllPackageEntries( }); } - const missingDependencies = findMissingUnreleasedDependenciesForRelease( + const missingDependencies = findCandidateDependencies( project, changedPackage, versionSpecifierOrDirective, diff --git a/src/ui.ts b/src/ui.ts index f4661f6..1f235ec 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -10,9 +10,9 @@ import { } from './project.js'; import { Package } from './package.js'; import { - findAllWorkspacePackagesThatDependOnPackage, - findMissingUnreleasedDependenciesForRelease, - findMissingUnreleasedDependentsForBreakingChanges, + findWorkspaceDependentNamesOfType, + findCandidateDependencies, + findCandidateDependentsOfTypeForMajorBump, IncrementableVersionParts, ReleaseSpecification, validateAllPackageEntries, @@ -149,16 +149,27 @@ function createApp({ ? majorBumps.split(',').filter(Boolean) : (req.query.majorBumps as string[] | undefined) || []; - const requiredDependents = new Set( + const requiredDirectDependentNames = new Set( majorBumpsArray.flatMap((majorBump) => - findAllWorkspacePackagesThatDependOnPackage(project, majorBump), + findWorkspaceDependentNamesOfType(project, majorBump, 'dependencies'), + ), + ); + + const requiredPeerDependentNames = new Set( + majorBumpsArray.flatMap((majorBump) => + findWorkspaceDependentNamesOfType( + project, + majorBump, + 'peerDependencies', + ), ), ); const pkgs = Object.values(project.workspacePackages).filter( (pkg) => pkg.hasChangesSinceLatestRelease || - requiredDependents.has(pkg.validatedManifest.name), + requiredDirectDependentNames.has(pkg.validatedManifest.name) || + requiredPeerDependentNames.has(pkg.validatedManifest.name), ); const packages = pkgs.map((pkg) => ({ @@ -197,24 +208,34 @@ function createApp({ const changedPackage = project.workspacePackages[changedPackageName]; - const missingDependentNames = - findMissingUnreleasedDependentsForBreakingChanges( + const missingDirectDependentNames = + findCandidateDependentsOfTypeForMajorBump( project, changedPackageName, versionSpecifierOrDirective, releasedPackages, + 'dependencies', ); - const missingDependencies = - findMissingUnreleasedDependenciesForRelease( + const missingPeerDependentNames = + findCandidateDependentsOfTypeForMajorBump( project, - changedPackage, + changedPackageName, versionSpecifierOrDirective, releasedPackages, + 'peerDependencies', ); + const missingDependencies = findCandidateDependencies( + project, + changedPackage, + versionSpecifierOrDirective, + releasedPackages, + ); + if ( - missingDependentNames.length === 0 && + missingDirectDependentNames.length === 0 && + missingPeerDependentNames.length === 0 && missingDependencies.length === 0 ) { return map; @@ -223,7 +244,8 @@ function createApp({ return { ...map, [changedPackageName]: { - missingDependentNames, + missingDirectDependentNames, + missingPeerDependentNames, missingDependencies, }, }; diff --git a/src/ui/App.tsx b/src/ui/App.tsx index e15ff9e..303f220 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -22,7 +22,11 @@ type SubmitButtonProps = { selections: Record; packageDependencyErrors: Record< string, - { missingDependentNames: string[]; missingDependencies: string[] } + { + missingDirectDependentNames: string[]; + missingPeerDependentNames: string[]; + missingDependencies: string[]; + } >; onSubmit: () => Promise; }; @@ -84,7 +88,8 @@ function App() { Record< string, { - missingDependentNames: string[]; + missingDirectDependentNames: string[]; + missingPeerDependentNames: string[]; missingDependencies: string[]; } > @@ -247,7 +252,8 @@ function App() { packagesErrors?: Record< string, { - missingDependentNames: string[]; + missingDirectDependentNames: string[]; + missingPeerDependentNames: string[]; missingDependencies: string[]; } >; diff --git a/src/ui/PackageItem.tsx b/src/ui/PackageItem.tsx index 84babe2..0f89b0c 100644 --- a/src/ui/PackageItem.tsx +++ b/src/ui/PackageItem.tsx @@ -11,7 +11,8 @@ type PackageItemProps = { packageDependencyErrors: Record< string, { - missingDependentNames: string[]; + missingDirectDependentNames: string[]; + missingPeerDependentNames: string[]; missingDependencies: string[]; } >; @@ -177,11 +178,79 @@ export function PackageItem({ } /> )} - {packageDependencyErrors[pkg.name].missingDependentNames.length > + {packageDependencyErrors[pkg.name].missingDirectDependentNames + .length > 0 && ( + +

+ To resolve these errors, for each dependent listed below, + you need to look at its changelog or commit history to make + the following decision: +

+
    +
  • + + Did you need to make changes to a dependent package in + response to the breaking changes? You must{' '} + include that dependent in the release. + {' '} + For instance, say you've modified the return type of a + messenger action{' '} + + FooController:doSomething + + {', '} + which is used by a controller in other package,{' '} + BarController. In this + case, you would need to release the package that contains{' '} + BarController as well, + or else it wouldn't work at runtime. +
  • +
  • + + Even if you didn't have to make changes to a dependent + package, it's highly recommended you include the + dependent in the release anyway. + {' '} + If you don't do this, multiple versions of the same + dependent will exist in the dependency tree, bloating the + installed size of the extension or mobile app. For + instance, you could end up with something like this: +
    +                        {`
    +- @metamask/foo-controller 2.0.0     # new version released
    +- @metamask/bar-controller 5.0.0     # no new version released
    +  - @metamask/foo-controller 1.0.0   # previous version
    +                                `.trim()}
    +                      
    + Whereas ideally we want this: +
    +                        {`
    +- @metamask/foo-controller 2.0.0     # new version released
    +- @metamask/bar-controller 6.0.0     # new version released
    +  - @metamask/foo-controller 2.0.0   # will get de-duplicated with direct dependency
    +                                `.trim()}
    +                      
    +
  • +
+ + } + /> + )} + {packageDependencyErrors[pkg.name].missingPeerDependentNames.length > 0 && (