codekingpro/portable-devtools
114k
1const { readdir } = require('node:fs/promises')
2const { resolve } = require('node:path')
3const npa = require('npm-package-arg')
4const pkgJson = require('@npmcli/package-json')
5const semver = require('semver')
6const reifyFinish = require('../utils/reify-finish.js')
7const ArboristWorkspaceCmd = require('../arborist-cmd.js')
8
9class Link extends ArboristWorkspaceCmd {
10 static description = 'Symlink a package folder'
11 static name = 'link'
12 static usage = [
13 '[<package-spec>]',
14 ]
15
16 static params = [
17 'save',
18 'save-exact',
19 'global',
20 'install-strategy',
21 'legacy-bundling',
22 'global-style',
23 'strict-peer-deps',
24 'package-lock',
25 'omit',
26 'include',
27 'ignore-scripts',
28 'allow-git',
29 'audit',
30 'bin-links',
31 'fund',
32 'dry-run',
33 ...super.params,
34 ]
35
36 static async completion (opts, npm) {
37 const dir = npm.globalDir
38 const files = await readdir(dir)
39 return files.filter(f => !/^[._-]/.test(f))
40 }
41
42 async exec (args) {
43 if (this.npm.global) {
44 throw Object.assign(
45 new Error(
46 'link should never be --global.\n' +
47 'Please re-run this command with --local'
48 ),
49 { code: 'ELINKGLOBAL' }
50 )
51 }
52 // install-links is implicitly false when running `npm link`
53 this.npm.config.set('install-links', false)
54
55 // link with no args: symlink the folder to the global location
56 // link with package arg: symlink the global to the local
57 args = args.filter(a => resolve(a) !== this.npm.prefix)
58 return args.length
59 ? this.linkInstall(args)
60 : this.linkPkg()
61 }
62
63 async linkInstall (args) {
64 // load current packages from the global space, and then add symlinks installs locally
65 const globalTop = resolve(this.npm.globalDir, '..')
66 const Arborist = require('@npmcli/arborist')
67 const globalOpts = {
68 ...this.npm.flatOptions,
69 Arborist,
70 path: globalTop,
71 global: true,
72 prune: false,
73 }
74 const globalArb = new Arborist(globalOpts)
75
76 // get only current top-level packages from the global space
77 const globals = await globalArb.loadActual({
78 filter: (node, kid) =>
79 !node.isRoot || args.some(a => npa(a).name === kid),
80 })
81
82 // any extra arg that is missing from the current global space should be reified there first
83 const missing = this.missingArgsFromTree(globals, args)
84 if (missing.length) {
85 await globalArb.reify({
86 ...globalOpts,
87 add: missing,
88 })
89 }
90
91 // get a list of module names that should be linked in the local prefix
92 const names = []
93 for (const a of args) {
94 const arg = npa(a)
95 if (arg.type === 'directory') {
96 const { content } = await pkgJson.normalize(arg.fetchSpec)
97 names.push(content.name)
98 } else {
99 names.push(arg.name)
100 }
101 }
102
103 // npm link should not save=true by default unless you're using any of --save-dev or other types
104 const save =
105 Boolean(
106 (this.npm.config.find('save') !== 'default' &&
107 this.npm.config.get('save')) ||
108 this.npm.config.get('save-optional') ||
109 this.npm.config.get('save-peer') ||
110 this.npm.config.get('save-dev') ||
111 this.npm.config.get('save-prod')
112 )
113 // create a new arborist instance for the local prefix and
114 // reify all the pending names as symlinks there
115 const localArb = new Arborist({
116 ...this.npm.flatOptions,
117 prune: false,
118 path: this.npm.prefix,
119 save,
120 })
121 await localArb.reify({
122 ...this.npm.flatOptions,
123 prune: false,
124 path: this.npm.prefix,
125 add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`),
126 save,
127 workspaces: this.workspaceNames,
128 })
129
130 await reifyFinish(this.npm, localArb)
131 }
132
133 async linkPkg () {
134 const wsp = this.workspacePaths
135 const paths = wsp && wsp.length ? wsp : [this.npm.prefix]
136 const add = paths.map(path => `file:${path}`)
137 const globalTop = resolve(this.npm.globalDir, '..')
138 const Arborist = require('@npmcli/arborist')
139 const arb = new Arborist({
140 ...this.npm.flatOptions,
141 Arborist,
142 path: globalTop,
143 global: true,
144 })
145 await arb.reify({
146 add,
147 })
148 await reifyFinish(this.npm, arb)
149 }
150
151 // Returns a list of items that can't be fulfilled by things found in the current arborist inventory
152 missingArgsFromTree (tree, args) {
153 if (tree.isLink) {
154 return this.missingArgsFromTree(tree.target, args)
155 }
156
157 const foundNodes = []
158 const missing = args.filter(a => {
159 const arg = npa(a)
160 const nodes = tree.children.values()
161 const argFound = [...nodes].every(node => {
162 // TODO: write tests for unmatching version specs
163 // this is hard to test atm but should be simple once we have a mocked registry again
164 if (arg.name !== node.name /* istanbul ignore next */ || (
165 arg.version &&
166 /* istanbul ignore next */
167 !semver.satisfies(node.version, arg.version)
168 )) {
169 foundNodes.push(node)
170 return true
171 }
172 })
173 return argFound
174 })
175
176 // remote nodes from the loaded tree in order to avoid dropping them later when reifying
177 for (const node of foundNodes) {
178 node.parent = null
179 }
180
181 return missing
182 }
183}
184
185module.exports = Link
186 