Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
diff.js281 linesDownload Raw Back to commands
1const { resolve } = require('node:path')
2const semver = require('semver')
3const libnpmdiff = require('libnpmdiff')
4const npa = require('npm-package-arg')
5const pacote = require('pacote')
6const pickManifest = require('npm-pick-manifest')
7const { log, output } = require('proc-log')
8const pkgJson = require('@npmcli/package-json')
9const BaseCommand = require('../base-cmd.js')
10
11class Diff extends BaseCommand {
12  static description = 'The registry diff command'
13  static name = 'diff'
14  static usage = [
15    '[...<paths>]',
16  ]
17
18  static params = [
19    'diff',
20    'diff-name-only',
21    'diff-unified',
22    'diff-ignore-all-space',
23    'diff-no-prefix',
24    'diff-src-prefix',
25    'diff-dst-prefix',
26    'diff-text',
27    'global',
28    'tag',
29    'workspace',
30    'workspaces',
31    'include-workspace-root',
32  ]
33
34  static workspaces = true
35  static ignoreImplicitWorkspace = false
36
37  async exec (args) {
38    const specs = this.npm.config.get('diff').filter(d => d)
39    if (specs.length > 2) {
40      throw this.usageError(`Can't use more than two --diff arguments.`)
41    }
42
43    // execWorkspaces may have set this already
44    if (!this.prefix) {
45      this.prefix = this.npm.prefix
46    }
47
48    // 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)
49    if (this.npm.global) {
50      this.top = resolve(this.npm.globalDir, '..')
51    } else {
52      this.top = this.prefix
53    }
54
55    const [a, b] = await this.retrieveSpecs(specs)
56    log.info('diff', { src: a, dst: b })
57
58    const res = await libnpmdiff([a, b], {
59      ...this.npm.flatOptions,
60      diffFiles: args,
61      where: this.top,
62    })
63    return output.standard(res)
64  }
65
66  async execWorkspaces (args) {
67    await this.setWorkspaces()
68    for (const workspacePath of this.workspacePaths) {
69      this.top = workspacePath
70      this.prefix = workspacePath
71      await this.exec(args)
72    }
73  }
74
75  // get the package name from the packument at `path`
76  // throws if no packument is present OR if it does not have `name` attribute
77  async packageName () {
78    let name
79    try {
80      const { content: pkg } = await pkgJson.normalize(this.prefix)
81      name = pkg.name
82    } catch {
83      log.verbose('diff', 'could not read project dir package.json')
84    }
85
86    if (!name) {
87      throw this.usageError('Needs multiple arguments to compare or run from a project dir.')
88    }
89
90    return name
91  }
92
93  async retrieveSpecs ([a, b]) {
94    if (a && b) {
95      const specs = await this.convertVersionsToSpecs([a, b])
96      return this.findVersionsByPackageName(specs)
97    }
98
99    // no arguments, defaults to comparing cwd to its latest published registry version
100    if (!a) {
101      const pkgName = await this.packageName()
102      return [
103        `${pkgName}@${this.npm.config.get('tag')}`,
104        `file:${this.prefix}`,
105      ]
106    }
107
108    // single argument, used to compare wanted versions of an installed dependency or to compare the cwd to a published version
109    let noPackageJson
110    let pkgName
111    try {
112      const { content: pkg } = await pkgJson.normalize(this.prefix)
113      pkgName = pkg.name
114    } catch {
115      log.verbose('diff', 'could not read project dir package.json')
116      noPackageJson = true
117    }
118
119    const missingPackageJson =
120      this.usageError('Needs multiple arguments to compare or run from a project dir.')
121
122    // 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
123    if (semver.validRange(a)) {
124      if (!pkgName) {
125        throw missingPackageJson
126      }
127      return [
128        `${pkgName}@${a}`,
129        `file:${this.prefix}`,
130      ]
131    }
132
133    // 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
134    const spec = npa(a)
135    if (spec.registry) {
136      let actualTree
137      let node
138      const Arborist = require('@npmcli/arborist')
139      try {
140        const opts = {
141          ...this.npm.flatOptions,
142          path: this.top,
143        }
144        const arb = new Arborist(opts)
145        actualTree = await arb.loadActual(opts)
146        node = actualTree &&
147          actualTree.inventory.query('name', spec.name)
148            .values().next().value
149      } catch {
150        log.verbose('diff', 'failed to load actual install tree')
151      }
152
153      if (!node || !node.name || !node.package || !node.package.version) {
154        if (noPackageJson) {
155          throw missingPackageJson
156        }
157        return [
158          `${spec.name}@${spec.fetchSpec}`,
159          `file:${this.prefix}`,
160        ]
161      }
162
163      const tryRootNodeSpec = () =>
164        (actualTree && actualTree.edgesOut.get(spec.name) || {}).spec
165
166      const tryAnySpec = () => {
167        for (const edge of node.edgesIn) {
168          return edge.spec
169        }
170      }
171
172      const aSpec = `file:${node.realpath}`
173
174      // finds what version of the package to compare against
175      // if an exact version or tag was passed than it should use that
176      // otherwise, work from the top of the arborist tree to find the original semver range declared in the package that depends on the package.
177      let bSpec
178      if (spec.rawSpec !== '*') {
179        bSpec = spec.rawSpec
180      } else {
181        const bTargetVersion =
182          tryRootNodeSpec()
183          || tryAnySpec()
184
185        // figure out what to compare against
186        // follows same logic to npm outdated "Wanted" results
187        const packument = await pacote.packument(spec, {
188          ...this.npm.flatOptions,
189          preferOnline: true,
190          _isRoot: true,
191        })
192        bSpec = pickManifest(
193          packument,
194          bTargetVersion,
195          { ...this.npm.flatOptions }
196        ).version
197      }
198
199      return [
200        `${spec.name}@${aSpec}`,
201        `${spec.name}@${bSpec}`,
202      ]
203    } else if (spec.type === 'directory') {
204      return [
205        `file:${spec.fetchSpec}`,
206        `file:${this.prefix}`,
207      ]
208    } else {
209      throw this.usageError(`Spec type ${spec.type} not supported.`)
210    }
211  }
212
213  async convertVersionsToSpecs ([a, b]) {
214    const semverA = semver.validRange(a)
215    const semverB = semver.validRange(b)
216
217    // both specs are semver versions, assume current project dir name
218    if (semverA && semverB) {
219      let pkgName
220      try {
221        const { content: pkg } = await pkgJson.normalize(this.prefix)
222        pkgName = pkg.name
223      } catch {
224        log.verbose('diff', 'could not read project dir package.json')
225      }
226
227      if (!pkgName) {
228        throw this.usageError('Needs to be run from a project dir in order to diff two versions.')
229      }
230
231      return [`${pkgName}@${a}`, `${pkgName}@${b}`]
232    }
233
234    // otherwise uses the name from the other arg to figure out the spec.name of what to compare
235    if (!semverA && semverB) {
236      return [a, `${npa(a).name}@${b}`]
237    }
238
239    if (semverA && !semverB) {
240      return [`${npa(b).name}@${a}`, b]
241    }
242
243    // no valid semver ranges used
244    return [a, b]
245  }
246
247  async findVersionsByPackageName (specs) {
248    let actualTree
249    const Arborist = require('@npmcli/arborist')
250    try {
251      const opts = {
252        ...this.npm.flatOptions,
253        path: this.top,
254      }
255      const arb = new Arborist(opts)
256      actualTree = await arb.loadActual(opts)
257    } catch {
258      log.verbose('diff', 'failed to load actual install tree')
259    }
260
261    return specs.map(i => {
262      const spec = npa(i)
263      if (spec.rawSpec !== '*') {
264        return i
265      }
266
267      const node = actualTree
268        && actualTree.inventory.query('name', spec.name)
269          .values().next().value
270
271      const res = !node || !node.package || !node.package.version
272        ? spec.fetchSpec
273        : `file:${node.realpath}`
274
275      return `${spec.name}@${res}`
276    })
277  }
278}
279
280module.exports = Diff
281 
codekingpro/portable-devtools · Team Ai