diff --git a/specification.md b/specification.md index 78913f5..f794a2b 100644 --- a/specification.md +++ b/specification.md @@ -549,8 +549,8 @@ For all project commands the \<`path`\> option is optional: - If not valid json, return error. - Validate local package json file structure, fields and values. - If not validate structure, fields or values, return error. -- Check whether the dependency has already been added. - - If already added, then return error. +- Check whether the dependency has already been added at the requested version. + - If already added, the requested end state already holds - return the local package file as-is without error (idempotent success), rather than re-installing or re-adding it. - Install dependency using same logic as package install - Add dependency to the local package file and save. @@ -586,7 +586,7 @@ Install existing dependencies listed inside a package json file. - Validate local package json file structure, fields and values. - If not validate structure, fields or values, return error. - Check whether the dependency exists in the local package json file. - - If not a dependency, then return error. + - If not a dependency, the requested end state already holds - return the local package file as-is without error (idempotent success, mirroring [Install and add dependency logic](#install-and-add-dependency-logic)), rather than treating it as a failure. - Uninstall dependency using same logic as package uninstall - Remove dependency from the local package file and save. diff --git a/src/classes/ManagerLocal.ts b/src/classes/ManagerLocal.ts index 95d14d4..aa10a73 100644 --- a/src/classes/ManagerLocal.ts +++ b/src/classes/ManagerLocal.ts @@ -717,8 +717,14 @@ export class ManagerLocal extends Manager { async uninstallDependency(slug: string, version?: string, filePath?: string, type = RegistryType.Plugins) { // Get local package file. const pkgFile = packageLoadFile(filePath) as any; - if (!pkgFile[type]) throw new Error(`Package ${type} is missing`); - if (!pkgFile[type][slug]) throw new Error(`Package ${type} ${slug} is not a dependency`); + if (!pkgFile[type] || !pkgFile[type][slug]) { + // Mirrors installDependency()'s "already a dependency" no-op: the requested end state + // (this dependency is gone) is already true, so this is idempotent success rather than an + // error - matches how most package managers treat "already removed"/"already installed". + this.log(`Package ${type} ${slug} is not a dependency`); + pkgFile.installed = true; + return pkgFile; + } // Uninstall dependency. const manager = new ManagerLocal(type, this.config.config); diff --git a/tests/classes/ManagerLocal.test.ts b/tests/classes/ManagerLocal.test.ts index 3764809..38f90f6 100644 --- a/tests/classes/ManagerLocal.test.ts +++ b/tests/classes/ManagerLocal.test.ts @@ -262,8 +262,18 @@ test('Project sync, install project, add new dependency, remove new dependency', const pkgDeps = await manager.installDependency(PLUGIN_PACKAGE.slug, '1.3.4', PROJECT_PATH); expect(omitDownloads(pkgDeps)).toEqual(omitDownloads(PROJECT_DEPS)); + // Adding the same dependency again is idempotent success, not an error - the requested end + // state (dependency present at this version) already holds. + const pkgDepsAgain = await manager.installDependency(PLUGIN_PACKAGE.slug, '1.3.4', PROJECT_PATH); + expect(omitDownloads(pkgDepsAgain)).toEqual(omitDownloads(PROJECT_DEPS)); + const pkgNoDeps = await manager.uninstallDependency(PLUGIN_PACKAGE.slug, '1.3.4', PROJECT_PATH); expect(omitDownloads(pkgNoDeps)).toEqual(omitDownloads(PROJECT_NO_DEPS)); + + // Removing a dependency that's already gone is likewise idempotent success, mirroring + // installDependency() above, rather than throwing "not a dependency". + const pkgNoDepsAgain = await manager.uninstallDependency(PLUGIN_PACKAGE.slug, '1.3.4', PROJECT_PATH); + expect(omitDownloads(pkgNoDepsAgain)).toEqual(omitDownloads(PROJECT_NO_DEPS)); }); test('Create save persists an incomplete package without throwing', () => {