123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- const { resolve } = require('path')
- const semver = require('semver')
- const libnpmdiff = require('libnpmdiff')
- const npa = require('npm-package-arg')
- const Arborist = require('@npmcli/arborist')
- const npmlog = require('npmlog')
- const pacote = require('pacote')
- const pickManifest = require('npm-pick-manifest')
- const readPackageName = require('./utils/read-package-name.js')
- const BaseCommand = require('./base-command.js')
- class Diff extends BaseCommand {
- static get description () {
- return 'The registry diff command'
- }
- /* istanbul ignore next - see test/lib/load-all-commands.js */
- static get name () {
- return 'diff'
- }
- /* istanbul ignore next - see test/lib/load-all-commands.js */
- static get usage () {
- return [
- '[...<paths>]',
- ]
- }
- /* istanbul ignore next - see test/lib/load-all-commands.js */
- static get params () {
- return [
- 'diff',
- 'diff-name-only',
- 'diff-unified',
- 'diff-ignore-all-space',
- 'diff-no-prefix',
- 'diff-src-prefix',
- 'diff-dst-prefix',
- 'diff-text',
- 'global',
- 'tag',
- 'workspace',
- 'workspaces',
- 'include-workspace-root',
- ]
- }
- exec (args, cb) {
- this.diff(args).then(() => cb()).catch(cb)
- }
- execWorkspaces (args, filters, cb) {
- this.diffWorkspaces(args, filters).then(() => cb()).catch(cb)
- }
- async diff (args) {
- const specs = this.npm.config.get('diff').filter(d => d)
- if (specs.length > 2) {
- throw new TypeError(
- 'Can\'t use more than two --diff arguments.\n\n' +
- `Usage:\n${this.usage}`
- )
- }
- // diffWorkspaces may have set this already
- if (!this.prefix)
- this.prefix = this.npm.prefix
- // this is the "top" directory, one up from node_modules
- // in global mode we have to walk one up from globalDir because our
- // node_modules is sometimes under ./lib, and in global mode we're only ever
- // walking through node_modules (because we will have been given a package
- // name already)
- if (this.npm.config.get('global'))
- this.top = resolve(this.npm.globalDir, '..')
- else
- this.top = this.prefix
- const [a, b] = await this.retrieveSpecs(specs)
- npmlog.info('diff', { src: a, dst: b })
- const res = await libnpmdiff([a, b], {
- ...this.npm.flatOptions,
- diffFiles: args,
- where: this.top,
- })
- return this.npm.output(res)
- }
- async diffWorkspaces (args, filters) {
- await this.setWorkspaces(filters)
- for (const workspacePath of this.workspacePaths) {
- this.top = workspacePath
- this.prefix = workspacePath
- await this.diff(args)
- }
- }
- // get the package name from the packument at `path`
- // throws if no packument is present OR if it does not have `name` attribute
- async packageName (path) {
- let name
- try {
- name = await readPackageName(this.prefix)
- } catch (e) {
- npmlog.verbose('diff', 'could not read project dir package.json')
- }
- if (!name)
- throw this.usageError('Needs multiple arguments to compare or run from a project dir.\n')
- return name
- }
- async retrieveSpecs ([a, b]) {
- if (a && b) {
- const specs = await this.convertVersionsToSpecs([a, b])
- return this.findVersionsByPackageName(specs)
- }
- // no arguments, defaults to comparing cwd
- // to its latest published registry version
- if (!a) {
- const pkgName = await this.packageName(this.prefix)
- return [
- `${pkgName}@${this.npm.config.get('tag')}`,
- `file:${this.prefix}`,
- ]
- }
- // single argument, used to compare wanted versions of an
- // installed dependency or to compare the cwd to a published version
- let noPackageJson
- let pkgName
- try {
- pkgName = await readPackageName(this.prefix)
- } catch (e) {
- npmlog.verbose('diff', 'could not read project dir package.json')
- noPackageJson = true
- }
- const missingPackageJson = this.usageError('Needs multiple arguments to compare or run from a project dir.\n')
- // using a valid semver range, that means it should just diff
- // the cwd against a published version to the registry using the
- // same project name and the provided semver range
- if (semver.validRange(a)) {
- if (!pkgName)
- throw missingPackageJson
- return [
- `${pkgName}@${a}`,
- `file:${this.prefix}`,
- ]
- }
- // when using a single package name as arg and it's part of the current
- // install tree, then retrieve the current installed version and compare
- // it against the same value `npm outdated` would suggest you to update to
- const spec = npa(a)
- if (spec.registry) {
- let actualTree
- let node
- try {
- const opts = {
- ...this.npm.flatOptions,
- path: this.top,
- }
- const arb = new Arborist(opts)
- actualTree = await arb.loadActual(opts)
- node = actualTree &&
- actualTree.inventory.query('name', spec.name)
- .values().next().value
- } catch (e) {
- npmlog.verbose('diff', 'failed to load actual install tree')
- }
- if (!node || !node.name || !node.package || !node.package.version) {
- if (noPackageJson)
- throw missingPackageJson
- return [
- `${spec.name}@${spec.fetchSpec}`,
- `file:${this.prefix}`,
- ]
- }
- const tryRootNodeSpec = () =>
- (actualTree && actualTree.edgesOut.get(spec.name) || {}).spec
- const tryAnySpec = () => {
- for (const edge of node.edgesIn)
- return edge.spec
- }
- const aSpec = `file:${node.realpath}`
- // finds what version of the package to compare against, if a exact
- // version or tag was passed than it should use that, otherwise
- // work from the top of the arborist tree to find the original semver
- // range declared in the package that depends on the package.
- let bSpec
- if (spec.rawSpec)
- bSpec = spec.rawSpec
- else {
- const bTargetVersion =
- tryRootNodeSpec()
- || tryAnySpec()
- // figure out what to compare against,
- // follows same logic to npm outdated "Wanted" results
- const packument = await pacote.packument(spec, {
- ...this.npm.flatOptions,
- preferOnline: true,
- })
- bSpec = pickManifest(
- packument,
- bTargetVersion,
- { ...this.npm.flatOptions }
- ).version
- }
- return [
- `${spec.name}@${aSpec}`,
- `${spec.name}@${bSpec}`,
- ]
- } else if (spec.type === 'directory') {
- return [
- `file:${spec.fetchSpec}`,
- `file:${this.prefix}`,
- ]
- } else
- throw this.usageError(`Spec type ${spec.type} not supported.\n`)
- }
- async convertVersionsToSpecs ([a, b]) {
- const semverA = semver.validRange(a)
- const semverB = semver.validRange(b)
- // both specs are semver versions, assume current project dir name
- if (semverA && semverB) {
- let pkgName
- try {
- pkgName = await readPackageName(this.prefix)
- } catch (e) {
- npmlog.verbose('diff', 'could not read project dir package.json')
- }
- if (!pkgName)
- throw this.usageError('Needs to be run from a project dir in order to diff two versions.\n')
- return [`${pkgName}@${a}`, `${pkgName}@${b}`]
- }
- // otherwise uses the name from the other arg to
- // figure out the spec.name of what to compare
- if (!semverA && semverB)
- return [a, `${npa(a).name}@${b}`]
- if (semverA && !semverB)
- return [`${npa(b).name}@${a}`, b]
- // no valid semver ranges used
- return [a, b]
- }
- async findVersionsByPackageName (specs) {
- let actualTree
- try {
- const opts = {
- ...this.npm.flatOptions,
- path: this.top,
- }
- const arb = new Arborist(opts)
- actualTree = await arb.loadActual(opts)
- } catch (e) {
- npmlog.verbose('diff', 'failed to load actual install tree')
- }
- return specs.map(i => {
- const spec = npa(i)
- if (spec.rawSpec)
- return i
- const node = actualTree
- && actualTree.inventory.query('name', spec.name)
- .values().next().value
- const res = !node || !node.package || !node.package.version
- ? spec.fetchSpec
- : `file:${node.realpath}`
- return `${spec.name}@${res}`
- })
- }
- }
- module.exports = Diff
|