Skip to content

Commit ddf68b9

Browse files
committed
feat: new npm copy command
1 parent 4ca858c commit ddf68b9

File tree

8 files changed

+407
-9
lines changed

8 files changed

+407
-9
lines changed

lib/commands/copy.js

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

lib/utils/cmd-list.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const aliases = {
4040
x: 'exec',
4141
why: 'explain',
4242
la: 'll',
43+
cp: 'copy',
4344
verison: 'version',
4445
ic: 'ci',
4546

@@ -137,6 +138,7 @@ const cmdList = [
137138
'version',
138139
'view',
139140
'whoami',
141+
'copy',
140142
]
141143

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

smoke-tests/tap-snapshots/test/index.js.test.cjs

Lines changed: 5 additions & 5 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

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
version
111111
view
112112
whoami
113+
copy
113114
login
114115
author
115116
home
@@ -144,6 +145,7 @@ Array [
144145
x
145146
why
146147
la
148+
cp
147149
verison
148150
ic
149151
innit

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,22 @@ alias: c
153153
Run "npm help config" for more info
154154
`
155155

156+
exports[`test/lib/load-all-commands.js TAP load each command copy > must match snapshot 1`] = `
157+
Copy package to new location
158+
159+
Usage:
160+
npm copy <destination>
161+
162+
Options:
163+
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
164+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
165+
[-ws|--workspaces] [--include-workspace-root]
166+
167+
alias: cp
168+
169+
Run "npm help copy" for more info
170+
`
171+
156172
exports[`test/lib/load-all-commands.js TAP load each command dedupe > must match snapshot 1`] = `
157173
Reduce duplication in the package tree
158174

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Object {
6060
"conf": "config",
6161
"confi": "config",
6262
"config": "config",
63+
"cop": "copy",
64+
"copy": "copy",
65+
"cp": "cp",
6366
"cr": "create",
6467
"cre": "create",
6568
"crea": "create",
@@ -358,6 +361,7 @@ Object {
358361
"cit": "install-ci-test",
359362
"clean-install": "ci",
360363
"clean-install-test": "cit",
364+
"cp": "copy",
361365
"create": "init",
362366
"ddp": "dedupe",
363367
"dist-tags": "dist-tag",
@@ -476,6 +480,7 @@ Object {
476480
"version",
477481
"view",
478482
"whoami",
483+
"copy",
479484
],
480485
"plumbing": Array [
481486
"birthday",

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ All commands:
2929
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,
32-
uninstall, unpublish, unstar, update, version, view, whoami
32+
uninstall, unpublish, unstar, update, version, view, whoami,
33+
copy
3334
3435
Specify configs in the ini-formatted file:
3536
/some/config/file/.npmrc
@@ -65,7 +66,8 @@ All commands:
6566
pkg, prefix, profile, prune, publish, rebuild, repo,
6667
restart, root, run-script, search, set, set-script,
6768
shrinkwrap, star, stars, start, stop, team, test, token,
68-
uninstall, unpublish, unstar, update, version, view, whoami
69+
uninstall, unpublish, unstar, update, version, view, whoami,
70+
copy
6971
7072
Specify configs in the ini-formatted file:
7173
/some/config/file/.npmrc
@@ -101,7 +103,8 @@ All commands:
101103
pkg, prefix, profile, prune, publish, rebuild, repo,
102104
restart, root, run-script, search, set, set-script,
103105
shrinkwrap, star, stars, start, stop, team, test, token,
104-
uninstall, unpublish, unstar, update, version, view, whoami
106+
uninstall, unpublish, unstar, update, version, view, whoami,
107+
copy
105108
106109
Specify configs in the ini-formatted file:
107110
/some/config/file/.npmrc
@@ -137,7 +140,8 @@ All commands:
137140
pkg, prefix, profile, prune, publish, rebuild, repo,
138141
restart, root, run-script, search, set, set-script,
139142
shrinkwrap, star, stars, start, stop, team, test, token,
140-
uninstall, unpublish, unstar, update, version, view, whoami
143+
uninstall, unpublish, unstar, update, version, view, whoami,
144+
copy
141145
142146
Specify configs in the ini-formatted file:
143147
/some/config/file/.npmrc
@@ -286,6 +290,20 @@ All commands:
286290
287291
Run "npm help config" for more info
288292
293+
copy Copy package to new location
294+
295+
Usage:
296+
npm copy <destination>
297+
298+
Options:
299+
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
300+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
301+
[-ws|--workspaces] [--include-workspace-root]
302+
303+
alias: cp
304+
305+
Run "npm help copy" for more info
306+
289307
dedupe Reduce duplication in the package tree
290308
291309
Usage:

0 commit comments

Comments
 (0)