Skip to content

Commit 9869596

Browse files
committed
feat: new npm copy command
1 parent a993599 commit 9869596

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

lib/commands/copy.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
const Arborist = require('@npmcli/arborist')
2+
const getWorkspaces = require('../workspaces/get-workspaces.js')
3+
const path = require('path')
4+
const packlist = require('npm-packlist')
5+
const fs = require('fs-extra')
6+
7+
const BaseCommand = require('../base-command.js')
8+
9+
class Copy extends BaseCommand {
10+
/* istanbul ignore next - see test/lib/load-all-commands.js */
11+
static get description () {
12+
return 'Copy package to new location'
13+
}
14+
15+
/* istanbul ignore next - see test/lib/load-all-commands.js */
16+
static get name () {
17+
return 'copy'
18+
}
19+
20+
/* istanbul ignore next - see test/lib/load-all-commands.js */
21+
static get params () {
22+
return [
23+
'omit',
24+
'workspace',
25+
'workspaces',
26+
'include-workspace-root',
27+
]
28+
}
29+
30+
/* istanbul ignore next - see test/lib/load-all-commands.js */
31+
static get usage () {
32+
return ['<destination>']
33+
}
34+
35+
async exec (args) {
36+
await this.copyTo(args, true, new Set([]))
37+
}
38+
39+
// called when --workspace or --workspaces is passed.
40+
async execWorkspaces (args, filters) {
41+
const workspaces = await getWorkspaces(filters, {
42+
path: this.npm.localPrefix,
43+
})
44+
45+
await this.copyTo(
46+
args,
47+
this.includeWorkspaceRoot,
48+
new Set(workspaces.values()))
49+
}
50+
51+
async copyTo (args, includeWorkspaceRoot, workspaces) {
52+
if (args.length !== 1)
53+
this.usageError()
54+
const opts = {
55+
...this.npm.flatOptions,
56+
path: this.npm.localPrefix,
57+
log: this.npm.log,
58+
}
59+
const destination = args[0]
60+
const omit = new Set(this.npm.flatOptions.omit)
61+
62+
const tree = await new Arborist(opts).loadActual()
63+
64+
// map of node to location in destination.
65+
const destinations = new Map()
66+
67+
// calculate the root set of packages.
68+
if (includeWorkspaceRoot) {
69+
const to = path.join(destination, tree.location)
70+
destinations.set(tree, to)
71+
}
72+
for (const edge of tree.edgesOut.values()) {
73+
if (edge.workspace && workspaces.has(edge.to.realpath)) {
74+
const to = path.join(destination, edge.to.location)
75+
destinations.set(edge.to, to)
76+
}
77+
}
78+
79+
// copy the root set of packages and their dependencies.
80+
const tasks = []
81+
for (const [node, dest] of destinations) {
82+
if (node.isLink && node.target) {
83+
const targetPath = destinations.get(node.target)
84+
if (targetPath == null) {
85+
// This is the first time the link target was seen, it will be the
86+
// only copy in dest, other links to the same target will link to
87+
// this copy.
88+
destinations.set(node.target, dest)
89+
} else {
90+
// The link target is already in the destination
91+
tasks.push(relativeSymlink(dest, targetPath))
92+
}
93+
} else {
94+
if (node.isWorkspace || node.isRoot) {
95+
// workspace and root packages have not been published so they may
96+
// have files that should be excluded.
97+
tasks.push(copyPacklist(node.target.realpath, dest))
98+
} else {
99+
// copy the modules files but not dependencies.
100+
const nm = path.join(node.realpath, 'node_modules')
101+
tasks.push(fs.copy(node.realpath, dest, {
102+
recursive: true,
103+
errorOnExist: false,
104+
filter: src => src !== nm,
105+
}))
106+
}
107+
108+
// add dependency edges to the queue.
109+
for (const edge of node.edgesOut.values()) {
110+
if (!omit.has(edge.type) && edge.to != null) {
111+
destinations.set(
112+
edge.to,
113+
path.join(
114+
destinations.get(edge.to.parent) || destination,
115+
path.relative(edge.to.parent.location, edge.to.location)))
116+
}
117+
}
118+
}
119+
}
120+
await Promise.all(tasks)
121+
}
122+
}
123+
module.exports = Copy
124+
125+
async function copyPacklist (from, to) {
126+
for (const file of await packlist({path: from})) {
127+
// packlist will include bundled node_modules. ignore it because we're
128+
// already handling copying dependencies.
129+
if (file.startsWith('node_modules/'))
130+
continue
131+
132+
// using recursive copy because packlist doesn't list directories.
133+
// TODO what is npm's preferred recursive copy?
134+
await fs.copy(
135+
path.join(from, file),
136+
path.join(to, file),
137+
{ recursive: true, errorOnExist: false })
138+
}
139+
}
140+
141+
async function relativeSymlink (from, to) {
142+
await fs.ensureSymlink(path.relative(to, from), to)
143+
}

lib/utils/cmd-list.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const shorthands = {
2121
'clean-install-test': 'cit',
2222
x: 'exec',
2323
why: 'explain',
24+
cp: 'copy',
2425
}
2526

2627
const affordances = {
@@ -134,6 +135,7 @@ const cmdList = [
134135
'doctor',
135136
'exec',
136137
'explain',
138+
'copy',
137139
]
138140

139141
const plumbing = ['birthday', 'help-search']

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"cli-table3": "^0.6.0",
7373
"columnify": "~1.5.4",
7474
"fastest-levenshtein": "^1.0.12",
75+
"fs-extra": "^10.0.0",
7576
"glob": "^7.2.0",
7677
"graceful-fs": "^4.2.8",
7778
"hosted-git-info": "^4.0.2",

0 commit comments

Comments
 (0)