From d3b0bb0c7520aa142c0b6ddbb50faec6e6bea32f Mon Sep 17 00:00:00 2001 From: zkochan Date: Sun, 12 Mar 2017 22:49:43 +0200 Subject: [PATCH] feat: use a shrinkwrap file instead of graph.yaml Ref #658 BREAKING CHANGE: no `node_modules/.graph.yaml` used anymore --- docs/api.md | 10 ---- package.json | 2 +- src/api/checkCompatibility.ts | 7 +++ src/api/getContext.ts | 10 +--- src/api/install.ts | 61 +++----------------- src/api/prune.ts | 40 +++++-------- src/api/removeOrphanPkgs.ts | 64 +++++++++++++++++++++ src/api/uninstall.ts | 101 +++++---------------------------- src/api/verify.ts | 13 +---- src/cmd/prune.ts | 5 +- src/fs/graphController.ts | 37 ------------ src/fs/safeReadPkg.ts | 11 ++++ src/fs/shrinkwrap.ts | 37 ++++++++++-- src/install/installMultiple.ts | 37 ------------ src/install/linkBins.ts | 12 +--- test/api.ts | 1 - test/prune.ts | 36 +----------- test/uninstall.ts | 4 +- tsconfig.json | 6 +- 19 files changed, 168 insertions(+), 326 deletions(-) create mode 100644 src/api/removeOrphanPkgs.ts delete mode 100644 src/fs/graphController.ts create mode 100644 src/fs/safeReadPkg.ts diff --git a/docs/api.md b/docs/api.md index 8e00ca5e2d..e4089d5d13 100644 --- a/docs/api.md +++ b/docs/api.md @@ -86,16 +86,6 @@ Remove extraneous packages. Extraneous packages are packages that are not listed * `options.production` - *Boolean* - by default `false`. If this property is `true`, prune will remove the packages specified in `devDependencies`. * `options.cwd` - *String* - by default `process.cwd()`. -## `pnpm.prunePkgs(pkgs, [options])` - -Remove extraneous packages specified in the `pkgs` arguments. Extraneous packages are packages that are not listed on the parent package's dependencies list. - -**Arguments:** - -* `pkgs` - *String[]* - prune only the specified packages. -* `options.production` - *Boolean* - by default `false`. If this property is `true`, prune will remove the packages specified in `devDependencies`. -* `options.cwd` - *String* - by default `process.cwd()`. - ## `pnpm.verify([options])` Return the list of modified dependencies. diff --git a/package.json b/package.json index 2b373dbe40..270827fb02 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pnpm", "description": "Fast, disk space efficient npm installs", - "version": "0.61.0", + "version": "0.62.0", "author": "Rico Sta. Cruz ", "bin": { "pnpm": "lib/bin/pnpm.js" diff --git a/src/api/checkCompatibility.ts b/src/api/checkCompatibility.ts index d0fa96be6e..13c63aa989 100644 --- a/src/api/checkCompatibility.ts +++ b/src/api/checkCompatibility.ts @@ -72,6 +72,13 @@ function check (pnpmVersion: string, storePath: string, modulesPath: string) { if (semver.lt(pnpmVersion, '0.52.0')) { throw new ModulesBreakingChangeError({ modulesPath, relatedPR: 593 }) } + if (semver.lt(pnpmVersion, '0.62.0')) { + throw new ModulesBreakingChangeError({ + modulesPath, + relatedPR: 660, + additionalInformation: 'Information about the node_modules structure is stored in a node_modules/.shrinkwrap.yaml file instead of a node_modules/.graph.yaml file' + }) + } } class UnexpectedStoreError extends PnpmError { diff --git a/src/api/getContext.ts b/src/api/getContext.ts index d0778ccd10..c51d2818fc 100644 --- a/src/api/getContext.ts +++ b/src/api/getContext.ts @@ -4,12 +4,9 @@ import readPkg from '../fs/readPkg' import writePkg = require('write-pkg') import expandTilde, {isHomepath} from '../fs/expandTilde' import {StrictPnpmOptions} from '../types' -import { - read as readGraph, - Graph, -} from '../fs/graphController' import { read as readShrinkwrap, + readPrivate as readPrivateShrinkwrap, Shrinkwrap, } from '../fs/shrinkwrap' import { @@ -26,7 +23,7 @@ export type PnpmContext = { pkg?: Package, storePath: string, root: string, - graph: Graph, + privateShrinkwrap: Shrinkwrap, shrinkwrap: Shrinkwrap, isFirstInstallation: boolean, } @@ -55,14 +52,13 @@ export default async function getContext (opts: StrictPnpmOptions): Promise, fetchingLocker: MemoizedFunc, @@ -51,7 +47,7 @@ export type InstallContext = { export async function install (maybeOpts?: PnpmOptions) { const opts = extendOptions(maybeOpts) const ctx = await getContext(opts) - const installCtx = await createInstallCmd(opts, ctx.graph, ctx.shrinkwrap) + const installCtx = await createInstallCmd(opts, ctx.shrinkwrap) if (!ctx.pkg) throw new Error('No package.json found') const optionalDeps = ctx.pkg.optionalDependencies || {} @@ -86,7 +82,7 @@ export async function installPkgs (fuzzyDeps: string[] | Dependencies, maybeOpts throw new Error('At least one package has to be installed') } const ctx = await getContext(opts) - const installCtx = await createInstallCmd(opts, ctx.graph, ctx.shrinkwrap) + const installCtx = await createInstallCmd(opts, ctx.shrinkwrap) return lock( ctx.storePath, @@ -127,8 +123,6 @@ async function installInContext ( installCtx: InstallContext, opts: StrictPnpmOptions ) { - // TODO: ctx.graph should not be muted. installMultiple should return a new graph - const oldGraph: Graph = R.clone(ctx.graph) const nodeModulesPath = path.join(ctx.root, 'node_modules') const client = new RegClient(adaptConfig(opts)) @@ -198,10 +192,9 @@ async function installInContext ( }) } - const newGraph = Object.assign({}, ctx.graph) - await removeOrphanPkgs(oldGraph, newGraph, ctx.root, ctx.storePath) - await saveGraph(path.join(ctx.root, 'node_modules'), newGraph) - await saveShrinkwrap(ctx.root, ctx.shrinkwrap) + const newShr = pruneShrinkwrap(ctx.shrinkwrap) + await removeOrphanPkgs(ctx.privateShrinkwrap, newShr, ctx.root, ctx.storePath) + await saveShrinkwrap(ctx.root, newShr) if (ctx.isFirstInstallation) { await saveModules(path.join(ctx.root, 'node_modules'), { packageManager: `${pnpmPkgJson.name}@${pnpmPkgJson.version}`, @@ -243,47 +236,9 @@ async function installInContext ( } } -async function removeOrphanPkgs (oldGraphJson: Graph, newGraphJson: Graph, root: string, storePath: string) { - const oldPkgIds = new Set(Object.keys(oldGraphJson)) - const newPkgIds = new Set(Object.keys(newGraphJson)) - - const store = await readStore(storePath) || {} - const notDependents = difference(oldPkgIds, newPkgIds) - - await Promise.all(Array.from(notDependents).map(async function (notDependent) { - if (store[notDependent]) { - store[notDependent].splice(store[notDependent].indexOf(root), 1) - if (!store[notDependent].length) { - delete store[notDependent] - await rimraf(path.join(storePath, notDependent)) - } - } - })) - - const newDependents = difference(newPkgIds, oldPkgIds) - - newDependents.forEach(newDependent => { - store[newDependent] = store[newDependent] || [] - if (store[newDependent].indexOf(root) === -1) { - store[newDependent].push(root) - } - }) - - await saveStore(storePath, store) -} - -function difference (setA: Set, setB: Set) { - const difference = new Set(setA) - for (const elem of setB) { - difference.delete(elem) - } - return difference -} - -async function createInstallCmd (opts: StrictPnpmOptions, graph: Graph, shrinkwrap: Shrinkwrap): Promise { +async function createInstallCmd (opts: StrictPnpmOptions, shrinkwrap: Shrinkwrap): Promise { return { installs: {}, - graph, shrinkwrap, installed: new Set(), installationSequence: [], diff --git a/src/api/prune.ts b/src/api/prune.ts index c6a5ed25fa..0d47ee0c24 100644 --- a/src/api/prune.ts +++ b/src/api/prune.ts @@ -1,11 +1,18 @@ import path = require('path') +import R = require('ramda') import getContext from './getContext' import {PnpmOptions, Package} from '../types' import extendOptions from './extendOptions' -import {uninstallInContext} from './uninstall' import getPkgDirs from '../fs/getPkgDirs' import readPkg from '../fs/readPkg' import lock from './lock' +import removeOrphanPkgs from './removeOrphanPkgs' +import npa = require('npm-package-arg') +import {PackageSpec} from '../resolve' +import { + ResolvedDependencies, + prune as pruneShrinkwrap, +} from '../fs/shrinkwrap' export async function prune(maybeOpts?: PnpmOptions): Promise { const opts = extendOptions(maybeOpts) @@ -21,32 +28,15 @@ export async function prune(maybeOpts?: PnpmOptions): Promise { const extraneousPkgs = await getExtraneousPkgs(pkg, ctx.root, opts.production) - await uninstallInContext(extraneousPkgs, ctx.pkg, ctx, opts) - }, - {stale: opts.lockStaleDuration}) -} + const newShr = ctx.shrinkwrap + newShr.dependencies = R.pickBy((value, key) => { + const spec: PackageSpec = npa(key) + return extraneousPkgs.indexOf(spec.name) === -1 + }, newShr.dependencies) -export async function prunePkgs(pkgsToPrune: string[], maybeOpts?: PnpmOptions): Promise { - const opts = extendOptions(maybeOpts) + const prunedShr = pruneShrinkwrap(newShr) - const ctx = await getContext(opts) - - return lock(ctx.storePath, async function () { - if (!ctx.pkg) { - throw new Error('No package.json found - cannot prune') - } - const pkg = ctx.pkg - - const extraneousPkgs = await getExtraneousPkgs(pkg, ctx.root, opts.production) - - const notPrunable = pkgsToPrune.filter(pkgToPrune => extraneousPkgs.indexOf(pkgToPrune) === -1) - if (notPrunable.length) { - const err = new Error(`Unable to prune ${notPrunable.join(', ')} because it is not an extraneous package`) - err['code'] = 'PRUNE_NOT_EXTR' - throw err - } - - await uninstallInContext(pkgsToPrune, ctx.pkg, ctx, opts) + await removeOrphanPkgs(ctx.privateShrinkwrap, prunedShr, ctx.root, ctx.storePath) }, {stale: opts.lockStaleDuration}) } diff --git a/src/api/removeOrphanPkgs.ts b/src/api/removeOrphanPkgs.ts new file mode 100644 index 0000000000..3f5e4dea29 --- /dev/null +++ b/src/api/removeOrphanPkgs.ts @@ -0,0 +1,64 @@ +import rimraf = require('rimraf-then') +import path = require('path') +import {Shrinkwrap} from '../fs/shrinkwrap' +import {read as readStore, save as saveStore} from '../fs/storeController' +import binify from '../binify' +import safeReadPkg from '../fs/safeReadPkg' +import R = require('ramda') +import npa = require('npm-package-arg') +import {PackageSpec} from '../resolve' + +export default async function removeOrphanPkgs ( + oldShr: Shrinkwrap, + newShr: Shrinkwrap, + root: string, + storePath: string +) { + const oldPkgNames = Object.keys(oldShr.dependencies).map(npa).map((spec: PackageSpec) => spec.name) + const newPkgNames = Object.keys(newShr.dependencies).map(npa).map((spec: PackageSpec) => spec.name) + + const removedTopDeps = R.difference(oldPkgNames, newPkgNames) + + await Promise.all(removedTopDeps.map(depName => Promise.all([ + rimraf(path.join(root, 'node_modules', depName)), + removeBins(depName, root), + ]))) + + const oldPkgIds = Object.keys(oldShr.packages) + const newPkgIds = Object.keys(newShr.packages) + + const store = await readStore(storePath) || {} + const notDependents = R.difference(oldPkgIds, newPkgIds) + + await Promise.all(Array.from(notDependents).map(async notDependent => { + if (store[notDependent]) { + store[notDependent].splice(store[notDependent].indexOf(root), 1) + if (!store[notDependent].length) { + delete store[notDependent] + await rimraf(path.join(storePath, notDependent)) + } + } + await rimraf(path.join(root, 'node_modules', `.${notDependent}`)) + })) + + const newDependents = R.difference(newPkgIds, oldPkgIds) + + newDependents.forEach(newDependent => { + store[newDependent] = store[newDependent] || [] + if (store[newDependent].indexOf(root) === -1) { + store[newDependent].push(root) + } + }) + + await saveStore(storePath, store) +} + +async function removeBins (uninstalledPkg: string, root: string) { + const uninstalledPkgPath = path.join(root, 'node_modules', uninstalledPkg) + const uninstalledPkgJson = await safeReadPkg(uninstalledPkgPath) + if (!uninstalledPkgJson) return + const cmds = await binify(uninstalledPkgJson, uninstalledPkgPath) + return Promise.all( + cmds.map(cmd => rimraf(path.join(root, 'node_modules', '.bin', cmd.name))) + ) +} diff --git a/src/api/uninstall.ts b/src/api/uninstall.ts index cf3ed724e9..b34641544b 100644 --- a/src/api/uninstall.ts +++ b/src/api/uninstall.ts @@ -8,8 +8,14 @@ import extendOptions from './extendOptions' import readPkg from '../fs/readPkg' import {PnpmOptions, StrictPnpmOptions, Package} from '../types' import lock from './lock' -import {save as saveGraph, Graph, GRAPH_ENTRY} from '../fs/graphController' -import {save as saveShrinkwrap} from '../fs/shrinkwrap' +import { + Shrinkwrap, + save as saveShrinkwrap, + prune as pruneShrinkwrap, +} from '../fs/shrinkwrap' +import removeOrphanPkgs from './removeOrphanPkgs' +import npa = require('npm-package-arg') +import {PackageSpec} from '../resolve' export default async function uninstallCmd (pkgsToUninstall: string[], maybeOpts?: PnpmOptions) { const opts = extendOptions(maybeOpts) @@ -29,47 +35,19 @@ export default async function uninstallCmd (pkgsToUninstall: string[], maybeOpts } export async function uninstallInContext (pkgsToUninstall: string[], pkg: Package, ctx: PnpmContext, opts: StrictPnpmOptions) { - pkg.dependencies = pkg.dependencies || {} - - // for backward compatibility - const entry = ctx.graph[ctx.root] ? ctx.root : GRAPH_ENTRY - - // this is OK. The store might not have records for the package - // maybe it was cloned, `pnpm install` was not executed - // and remove is done on a package with no dependencies installed - ctx.graph[entry] = ctx.graph[entry] || {} - ctx.graph[entry].dependencies = ctx.graph[entry].dependencies || {} - - const pkgIds = pkgsToUninstall - .map(dep => ctx.graph[entry].dependencies[dep]) - .filter(pkgId => !!pkgId) - const uninstalledPkgs = tryUninstall(pkgIds.slice(), ctx.graph, entry) - await Promise.all( - uninstalledPkgs.map(uninstalledPkg => removeBins(uninstalledPkg, ctx.storePath, ctx.root)) - ) - if (ctx.graph[entry].dependencies) { - pkgsToUninstall.forEach(dep => { - delete ctx.graph[entry].dependencies[dep] - }) - if (!Object.keys(ctx.graph[entry].dependencies).length) { - delete ctx.graph[entry].dependencies - } - } - await Promise.all(uninstalledPkgs.map(pkgId => removePkgFromStore(pkgId, ctx.storePath))) - - await saveGraph(path.join(ctx.root, 'node_modules'), ctx.graph) - await Promise.all(pkgsToUninstall.map(dep => rimraf(path.join(ctx.root, 'node_modules', dep)))) - const saveType = getSaveType(opts) if (saveType) { const pkgJsonPath = path.join(ctx.root, 'package.json') const pkg = await removeDeps(pkgJsonPath, pkgsToUninstall, saveType) - for (let depName in ctx.shrinkwrap.dependencies) { - if (!isDependentOn(pkg, depName)) { - delete ctx.shrinkwrap.dependencies[depName] + for (let depSpecRaw in ctx.shrinkwrap.dependencies) { + const depSpec: PackageSpec = npa(depSpecRaw) + if (!isDependentOn(pkg, depSpec.name)) { + delete ctx.shrinkwrap.dependencies[depSpecRaw] } } - await saveShrinkwrap(ctx.root, ctx.shrinkwrap) + const newShr = await pruneShrinkwrap(ctx.shrinkwrap) + await removeOrphanPkgs(ctx.privateShrinkwrap, newShr, ctx.root, ctx.storePath) + await saveShrinkwrap(ctx.root, newShr) } } @@ -81,52 +59,3 @@ function isDependentOn (pkg: Package, depName: string): boolean { ] .some(deptype => pkg[deptype] && pkg[deptype][depName]) } - -function canBeUninstalled (pkgId: string, graph: Graph, pkgPath: string) { - return !graph[pkgId] || !graph[pkgId].dependents || !graph[pkgId].dependents.length || - graph[pkgId].dependents.length === 1 && graph[pkgId].dependents.indexOf(pkgPath) !== -1 -} - -export function tryUninstall (pkgIds: string[], graph: Graph, pkgPath: string) { - const uninstalledPkgs: string[] = [] - let numberOfUninstalls: number - do { - numberOfUninstalls = 0 - for (let i = 0; i < pkgIds.length; ) { - if (canBeUninstalled(pkgIds[i], graph, pkgPath)) { - const uninstalledPkg = pkgIds.splice(i, 1)[0] - uninstalledPkgs.push(uninstalledPkg) - const deps = graph[uninstalledPkg] && graph[uninstalledPkg].dependencies || {} - const depIds = Object.keys(deps).map(depName => deps[depName]) - delete graph[uninstalledPkg] - depIds.forEach((dep: string) => removeDependency(dep, uninstalledPkg, graph)) - Array.prototype.push.apply(uninstalledPkgs, tryUninstall(depIds, graph, uninstalledPkg)) - numberOfUninstalls++ - continue - } - i++ - } - } while (numberOfUninstalls) - return uninstalledPkgs -} - -function removeDependency (dependentPkgName: string, uninstalledPkg: string, graph: Graph) { - if (!graph[dependentPkgName].dependents) return - graph[dependentPkgName].dependents.splice(graph[dependentPkgName].dependents.indexOf(uninstalledPkg), 1) - if (!graph[dependentPkgName].dependents.length) { - delete graph[dependentPkgName].dependents - } -} - -async function removeBins (uninstalledPkg: string, store: string, root: string) { - const uninstalledPkgPath = path.join(store, uninstalledPkg) - const uninstalledPkgJson = await readPkg(uninstalledPkgPath) - const cmds = await binify(uninstalledPkgJson, uninstalledPkgPath) - return Promise.all( - cmds.map(cmd => rimraf(path.join(root, 'node_modules', '.bin', cmd.name))) - ) -} - -export function removePkgFromStore (pkgId: string, store: string) { - return rimraf(path.join(store, pkgId)) -} diff --git a/src/api/verify.ts b/src/api/verify.ts index bda247deaa..44f9cdf6b7 100644 --- a/src/api/verify.ts +++ b/src/api/verify.ts @@ -4,23 +4,14 @@ import {PnpmOptions} from '../types' import extendOptions from './extendOptions' import getContext from './getContext' import untouched from '../pkgIsUntouched' -import {GRAPH_ENTRY} from '../fs/graphController' export default async function (maybeOpts: PnpmOptions) { const opts = extendOptions(maybeOpts) const ctx = await getContext(opts) - if (!ctx.graph) return [] + if (!ctx.shrinkwrap) return [] - const pkgPaths = Object.keys(ctx.graph) - .filter(pkgPath => !isProjectPath(pkgPath)) + const pkgPaths = Object.keys(ctx.shrinkwrap.packages || {}) .map(pkgPath => path.join(ctx.storePath, pkgPath)) return await pFilter(pkgPaths, async (pkgPath: string) => !await untouched(pkgPath)) } - -function isProjectPath (pkgPath: string) { - return pkgPath === GRAPH_ENTRY || - // next are for backward compatibility - // previous versions of .graph.yaml had the package path as the entry point - pkgPath.startsWith('/') || pkgPath[1] === ':' -} diff --git a/src/cmd/prune.ts b/src/cmd/prune.ts index 9340107e74..d4834a634c 100644 --- a/src/cmd/prune.ts +++ b/src/cmd/prune.ts @@ -1,9 +1,6 @@ -import {prune, prunePkgs} from '../api/prune' +import {prune} from '../api/prune' import {PnpmOptions} from '../types' export default (input: string[], opts: PnpmOptions) => { - if (input && input.length) { - return prunePkgs(input, opts) - } return prune(opts) } diff --git a/src/fs/graphController.ts b/src/fs/graphController.ts deleted file mode 100644 index be8acf3218..0000000000 --- a/src/fs/graphController.ts +++ /dev/null @@ -1,37 +0,0 @@ -import path = require('path') -import loadYamlFile = require('load-yaml-file') -import writeYamlFile = require('write-yaml-file') - -const graphFileName = '.graph.yaml' - -export const GRAPH_ENTRY = '..' - -export type Graph = { - [name: string]: PackageGraph -} - -export type PackageGraph = { - dependents: string[], - dependencies: DependenciesResolution -} - -export type DependenciesResolution = { - [name: string]: string -} - -export async function read (modulesPath: string): Promise { - const graphYamlPath = path.join(modulesPath, graphFileName) - try { - return await loadYamlFile(graphYamlPath) - } catch (err) { - if ((err).code !== 'ENOENT') { - throw err - } - return null - } -} - -export function save (modulesPath: string, graph: Graph) { - const graphYamlPath = path.join(modulesPath, graphFileName) - return writeYamlFile(graphYamlPath, graph, {sortKeys: true}) -} diff --git a/src/fs/safeReadPkg.ts b/src/fs/safeReadPkg.ts new file mode 100644 index 0000000000..8e97e13e4d --- /dev/null +++ b/src/fs/safeReadPkg.ts @@ -0,0 +1,11 @@ +import {Package} from '../types' +import readPkg from './readPkg' + +export default async function safeReadPkg (pkgPath: string): Promise { + try { + return await readPkg(pkgPath) + } catch (err) { + if ((err).code !== 'ENOENT') throw err + return null + } +} diff --git a/src/fs/shrinkwrap.ts b/src/fs/shrinkwrap.ts index 29a716d4cf..c52174994a 100644 --- a/src/fs/shrinkwrap.ts +++ b/src/fs/shrinkwrap.ts @@ -11,6 +11,7 @@ import isCI = require('is-ci') const shrinkwrapLogger = logger('shrinkwrap') const SHRINKWRAP_FILENAME = 'shrinkwrap.yaml' +const PRIVATE_SHRINKWRAP_FILENAME = path.join('node_modules', '.shrinkwrap.yaml') const SHRINKWRAP_VERSION = 1 function getDefaultShrinkwrap () { @@ -45,6 +46,23 @@ export type ResolvedDependencies = { [pkgName: string]: string, } +export async function readPrivate (pkgPath: string): Promise { + const shrinkwrapPath = path.join(pkgPath, PRIVATE_SHRINKWRAP_FILENAME) + let shrinkwrap + try { + shrinkwrap = await loadYamlFile(shrinkwrapPath) + } catch (err) { + if ((err).code !== 'ENOENT') { + throw err + } + return getDefaultShrinkwrap() + } + if (shrinkwrap && shrinkwrap.version === SHRINKWRAP_VERSION) { + return shrinkwrap + } + throw new ShrinkwrapBreakingChangeError(shrinkwrapPath) +} + export async function read (pkgPath: string, opts: {force: boolean}): Promise { const shrinkwrapPath = path.join(pkgPath, SHRINKWRAP_FILENAME) let shrinkwrap @@ -68,22 +86,29 @@ export async function read (pkgPath: string, opts: {force: boolean}): Promise { - ctx.graph = ctx.graph || {} - const nonLinkedPkgs = modules === options.baseNodeModules // only check modules on the first level ? await pFilter(specs, (spec: PackageSpec) => !spec.name || isInnerLink(modules, spec.name)) @@ -227,8 +224,6 @@ async function install ( addInstalledPkg(ctx.installs, dependency) - addToGraph(ctx.graph, dependentId, dependency) - const linking = ctx.linkingLocker(dependency.hardlinkedLocation, async function () { const newlyFetched = await fetchedPkg.fetchingFiles const pkgJsonPath = path.join(dependency.hardlinkedLocation, 'package.json') @@ -293,38 +288,6 @@ async function isSameFile (file1: string, file2: string) { return stats[0].ino === stats[1].ino } -function addToGraph (graph: Graph, dependent: string, dependency: InstalledPackage) { - dependent = dependent || GRAPH_ENTRY - - graph[dependent] = graph[dependent] || {} - graph[dependent].dependencies = graph[dependent].dependencies || {} - - updateDependencyResolution(graph, dependent, dependency.pkg.name, dependency.id) - - graph[dependency.id] = graph[dependency.id] || {} - graph[dependency.id].dependents = graph[dependency.id].dependents || [] - - if (graph[dependency.id].dependents.indexOf(dependent) === -1) { - graph[dependency.id].dependents.push(dependent) - } -} - -function updateDependencyResolution (graph: Graph, dependent: string, depName: string, newDepId: string) { - if (graph[dependent].dependencies[depName] && - graph[dependent].dependencies[depName] !== newDepId) { - removeIfNoDependents(graph, graph[dependent].dependencies[depName], dependent) - } - graph[dependent].dependencies[depName] = newDepId -} - -function removeIfNoDependents(graph: Graph, id: string, removedDependent: string) { - if (graph[id] && graph[id].dependents && graph[id].dependents.length === 1 && - graph[id].dependents[0] === removedDependent) { - Object.keys(graph[id].dependencies || {}).forEach(depName => removeIfNoDependents(graph, graph[id].dependencies[depName], id)) - delete graph[id] - } -} - async function isInstallable ( pkg: Package, fetchedPkg: FetchedPackage, diff --git a/src/install/linkBins.ts b/src/install/linkBins.ts index f97a5a6b7e..f3c3bd77ef 100644 --- a/src/install/linkBins.ts +++ b/src/install/linkBins.ts @@ -2,12 +2,11 @@ import path = require('path') import normalizePath = require('normalize-path') import fs = require('mz/fs') import mkdirp = require('mkdirp-promise') -import readPkg from '../fs/readPkg' +import safeReadPkg from '../fs/safeReadPkg' import getPkgDirs from '../fs/getPkgDirs' import binify from '../binify' import isWindows = require('is-windows') import cmdShim = require('@zkochan/cmd-shim') -import {Package} from '../types' import logger from 'pnpm-logger' import Module = require('module') import R = require('ramda') @@ -54,12 +53,3 @@ async function getBinNodePaths (target: string) { Module._nodeModulePaths(target) ) } - -async function safeReadPkg (pkgPath: string): Promise { - try { - return await readPkg(pkgPath) - } catch (err) { - if ((err).code !== 'ENOENT') throw err - return null - } -} diff --git a/test/api.ts b/test/api.ts index 50956a83ff..bb7bea2b96 100644 --- a/test/api.ts +++ b/test/api.ts @@ -9,7 +9,6 @@ test('API', t => { t.equal(typeof pnpm.link, 'function', 'exports link()') t.equal(typeof pnpm.linkToGlobal, 'function', 'exports linkToGlobal()') t.equal(typeof pnpm.prune, 'function', 'exports prune()') - t.equal(typeof pnpm.prunePkgs, 'function', 'exports prunePkgs()') t.end() }) diff --git a/test/prune.ts b/test/prune.ts index 3fc84d6b99..dee9faeb48 100644 --- a/test/prune.ts +++ b/test/prune.ts @@ -2,7 +2,7 @@ import tape = require('tape') import promisifyTape from 'tape-promise' const test = promisifyTape(tape) import path = require('path') -import {installPkgs, prune, prunePkgs} from '../src' +import {installPkgs, prune} from '../src' import {prepare, testDefaults} from './utils' import exists = require('path-exists') import existsSymlink = require('exists-link') @@ -42,40 +42,6 @@ test('prune removes extraneous packages', async function (t) { await project.has('fnumber') }) -test('prune removes only the specified extraneous packages', async function (t) { - const project = prepare(t) - - await installPkgs(['is-positive@2.0.0', 'is-negative@2.1.0'], testDefaults()) - - const pkg = await readPkg() - - delete pkg.dependencies['is-positive'] - delete pkg.dependencies['is-negative'] - - await writePkg(pkg) - - await prunePkgs(['is-positive'], testDefaults()) - - await project.storeHasNot('is-positive', '2.0.0') - await project.hasNot('is-positive') - - await project.storeHas('is-negative', '2.1.0') - await project.has('is-negative') -}) - -test('prune throws error when trying to removes not an extraneous package', async function (t) { - prepare(t) - - await installPkgs(['is-positive@2.0.0'], testDefaults({save: true})) - - try { - await prunePkgs(['is-positive'], testDefaults()) - t.fail('prune had to fail') - } catch (err) { - t.equal(err['code'], 'PRUNE_NOT_EXTR', 'cannot prune non-extraneous package error thrown') - } -}) - test('prune removes dev dependencies in production', async function (t) { const project = prepare(t) diff --git a/test/uninstall.ts b/test/uninstall.ts index 7e8e40faf9..55a3c58cb1 100644 --- a/test/uninstall.ts +++ b/test/uninstall.ts @@ -69,7 +69,9 @@ test('uninstall package with dependencies and do not touch other deps', async fu t.deepEqual(pkgJson.dependencies, {'is-negative': '^2.1.0'}, 'camelcase-keys has been removed from dependencies') const shr = await project.loadShrinkwrap() - t.ok(!shr, 'camelcase-keys removed from shrinkwrap dependencies') + t.deepEqual(shr.dependencies, { + 'is-negative@^2.1.0': 'localhost+4873/is-negative/2.1.0', + }, 'camelcase-keys removed from shrinkwrap dependencies') }) test('uninstall package with its bin files', async function (t) { diff --git a/tsconfig.json b/tsconfig.json index e073f18c1b..c56c27446a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "src/api/link.ts", "src/api/lock.ts", "src/api/prune.ts", + "src/api/removeOrphanPkgs.ts", "src/api/uninstall.ts", "src/api/verify.ts", "src/bin/pnpm.ts", @@ -44,15 +45,16 @@ "src/cmd/uninstall.ts", "src/cmd/update.ts", "src/cmd/verify.ts", + "src/depsToSpecs.ts", "src/err.ts", "src/errorTypes.ts", "src/fs/dirsum.ts", "src/fs/expandTilde.ts", "src/fs/getPkgDirs.ts", "src/fs/gracefulify.ts", - "src/fs/graphController.ts", "src/fs/modulesController.ts", "src/fs/readPkg.ts", + "src/fs/safeReadPkg.ts", "src/fs/shrinkwrap.ts", "src/fs/storeController.ts", "src/getSaveType.ts", @@ -76,6 +78,8 @@ "src/resolve/index.ts", "src/resolve/local.ts", "src/resolve/npm/createNpmPkgId.ts", + "src/resolve/npm/getHost.ts", + "src/resolve/npm/getRegistryFolderName.ts", "src/resolve/npm/index.ts", "src/resolve/npm/loadPackageMeta.ts", "src/resolve/tarball.ts",