Skip to content

Commit 9d7cca6

Browse files
committed
feat: new npm copy command
1 parent cd0f3c8 commit 9d7cca6

File tree

8 files changed

+413
-20
lines changed

8 files changed

+413
-20
lines changed

lib/commands/copy.js

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

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",

tap-snapshots/test/lib/commands/completion.js.test.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Array [
110110
doctor
111111
exec
112112
explain
113+
copy
113114
un
114115
rb
115116
list
@@ -131,6 +132,7 @@ Array [
131132
clean-install-test
132133
x
133134
why
135+
cp
134136
la
135137
verison
136138
ic

tap-snapshots/test/lib/load-all-commands.js.test.cjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,24 @@ alias: c
160160
Run "npm help config" for more info
161161
`
162162

163+
exports[`test/lib/load-all-commands.js TAP load each command copy > must match snapshot 1`] = `
164+
npm copy
165+
166+
Copy package to new location
167+
168+
Usage:
169+
npm copy <destination>
170+
171+
Options:
172+
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
173+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
174+
[-ws|--workspaces] [--include-workspace-root]
175+
176+
alias: cp
177+
178+
Run "npm help copy" for more info
179+
`
180+
163181
exports[`test/lib/load-all-commands.js TAP load each command dedupe > must match snapshot 1`] = `
164182
npm dedupe
165183

tap-snapshots/test/lib/utils/cmd-list.js.test.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Object {
5252
"cit": "install-ci-test",
5353
"clean-install": "ci",
5454
"clean-install-test": "cit",
55+
"cp": "copy",
5556
"create": "init",
5657
"ddp": "dedupe",
5758
"dist-tags": "dist-tag",
@@ -169,6 +170,7 @@ Object {
169170
"doctor",
170171
"exec",
171172
"explain",
173+
"copy",
172174
],
173175
"plumbing": Array [
174176
"birthday",
@@ -188,6 +190,7 @@ Object {
188190
"cit": "install-ci-test",
189191
"clean-install": "ci",
190192
"clean-install-test": "cit",
193+
"cp": "copy",
191194
"create": "init",
192195
"ddp": "dedupe",
193196
"i": "install",

tap-snapshots/test/lib/utils/npm-usage.js.test.cjs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ npm help npm more involved overview
2222
All commands:
2323
2424
access, adduser, audit, bin, bugs, cache, ci, completion,
25-
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
26-
edit, exec, explain, explore, find-dupes, fund, get, help,
27-
hook, init, install, install-ci-test, install-test, link,
28-
ll, login, logout, ls, org, outdated, owner, pack, ping,
29-
pkg, prefix, profile, prune, publish, rebuild, repo,
25+
config, copy, dedupe, deprecate, diff, dist-tag, docs,
26+
doctor, edit, exec, explain, explore, find-dupes, fund, get,
27+
help, hook, init, install, install-ci-test, install-test,
28+
link, ll, login, logout, ls, org, outdated, owner, pack,
29+
ping, pkg, prefix, profile, prune, publish, rebuild, repo,
3030
restart, root, run-script, search, set, set-script,
3131
shrinkwrap, star, stars, start, stop, team, test, token,
3232
uninstall, unpublish, unstar, update, version, view, whoami
@@ -58,11 +58,11 @@ npm help npm more involved overview
5858
All commands:
5959
6060
access, adduser, audit, bin, bugs, cache, ci, completion,
61-
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
62-
edit, exec, explain, explore, find-dupes, fund, get, help,
63-
hook, init, install, install-ci-test, install-test, link,
64-
ll, login, logout, ls, org, outdated, owner, pack, ping,
65-
pkg, prefix, profile, prune, publish, rebuild, repo,
61+
config, copy, dedupe, deprecate, diff, dist-tag, docs,
62+
doctor, edit, exec, explain, explore, find-dupes, fund, get,
63+
help, hook, init, install, install-ci-test, install-test,
64+
link, ll, login, logout, ls, org, outdated, owner, pack,
65+
ping, pkg, prefix, profile, prune, publish, rebuild, repo,
6666
restart, root, run-script, search, set, set-script,
6767
shrinkwrap, star, stars, start, stop, team, test, token,
6868
uninstall, unpublish, unstar, update, version, view, whoami
@@ -94,11 +94,11 @@ npm help npm more involved overview
9494
All commands:
9595
9696
access, adduser, audit, bin, bugs, cache, ci, completion,
97-
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
98-
edit, exec, explain, explore, find-dupes, fund, get, help,
99-
hook, init, install, install-ci-test, install-test, link,
100-
ll, login, logout, ls, org, outdated, owner, pack, ping,
101-
pkg, prefix, profile, prune, publish, rebuild, repo,
97+
config, copy, dedupe, deprecate, diff, dist-tag, docs,
98+
doctor, edit, exec, explain, explore, find-dupes, fund, get,
99+
help, hook, init, install, install-ci-test, install-test,
100+
link, ll, login, logout, ls, org, outdated, owner, pack,
101+
ping, pkg, prefix, profile, prune, publish, rebuild, repo,
102102
restart, root, run-script, search, set, set-script,
103103
shrinkwrap, star, stars, start, stop, team, test, token,
104104
uninstall, unpublish, unstar, update, version, view, whoami
@@ -130,11 +130,11 @@ npm help npm more involved overview (in a browser)
130130
All commands:
131131
132132
access, adduser, audit, bin, bugs, cache, ci, completion,
133-
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
134-
edit, exec, explain, explore, find-dupes, fund, get, help,
135-
hook, init, install, install-ci-test, install-test, link,
136-
ll, login, logout, ls, org, outdated, owner, pack, ping,
137-
pkg, prefix, profile, prune, publish, rebuild, repo,
133+
config, copy, dedupe, deprecate, diff, dist-tag, docs,
134+
doctor, edit, exec, explain, explore, find-dupes, fund, get,
135+
help, hook, init, install, install-ci-test, install-test,
136+
link, ll, login, logout, ls, org, outdated, owner, pack,
137+
ping, pkg, prefix, profile, prune, publish, rebuild, repo,
138138
restart, root, run-script, search, set, set-script,
139139
shrinkwrap, star, stars, start, stop, team, test, token,
140140
uninstall, unpublish, unstar, update, version, view, whoami
@@ -302,6 +302,22 @@ All commands:
302302
303303
Run "npm help config" for more info
304304
305+
copy npm copy
306+
307+
Copy package to new location
308+
309+
Usage:
310+
npm copy <destination>
311+
312+
Options:
313+
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
314+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
315+
[-ws|--workspaces] [--include-workspace-root]
316+
317+
alias: cp
318+
319+
Run "npm help copy" for more info
320+
305321
dedupe npm dedupe
306322
307323
Reduce duplication in the package tree

0 commit comments

Comments
 (0)