Skip to content

Commit

Permalink
feat: new npm copy command
Browse files Browse the repository at this point in the history
  • Loading branch information
Caleb ツ Everett committed Nov 15, 2021
1 parent a993599 commit 9869596
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 0 deletions.
143 changes: 143 additions & 0 deletions lib/commands/copy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
const Arborist = require('@npmcli/arborist')
const getWorkspaces = require('../workspaces/get-workspaces.js')
const path = require('path')
const packlist = require('npm-packlist')
const fs = require('fs-extra')

const BaseCommand = require('../base-command.js')

class Copy extends BaseCommand {
/* istanbul ignore next - see test/lib/load-all-commands.js */
static get description () {
return 'Copy package to new location'
}

/* istanbul ignore next - see test/lib/load-all-commands.js */
static get name () {
return 'copy'
}

/* istanbul ignore next - see test/lib/load-all-commands.js */
static get params () {
return [
'omit',
'workspace',
'workspaces',
'include-workspace-root',
]
}

/* istanbul ignore next - see test/lib/load-all-commands.js */
static get usage () {
return ['<destination>']
}

async exec (args) {
await this.copyTo(args, true, new Set([]))
}

// called when --workspace or --workspaces is passed.
async execWorkspaces (args, filters) {
const workspaces = await getWorkspaces(filters, {
path: this.npm.localPrefix,
})

await this.copyTo(
args,
this.includeWorkspaceRoot,
new Set(workspaces.values()))
}

async copyTo (args, includeWorkspaceRoot, workspaces) {
if (args.length !== 1)
this.usageError()
const opts = {
...this.npm.flatOptions,
path: this.npm.localPrefix,
log: this.npm.log,
}
const destination = args[0]
const omit = new Set(this.npm.flatOptions.omit)

const tree = await new Arborist(opts).loadActual()

// map of node to location in destination.
const destinations = new Map()

// calculate the root set of packages.
if (includeWorkspaceRoot) {
const to = path.join(destination, tree.location)
destinations.set(tree, to)
}
for (const edge of tree.edgesOut.values()) {
if (edge.workspace && workspaces.has(edge.to.realpath)) {
const to = path.join(destination, edge.to.location)
destinations.set(edge.to, to)
}
}

// copy the root set of packages and their dependencies.
const tasks = []
for (const [node, dest] of destinations) {
if (node.isLink && node.target) {
const targetPath = destinations.get(node.target)
if (targetPath == null) {
// This is the first time the link target was seen, it will be the
// only copy in dest, other links to the same target will link to
// this copy.
destinations.set(node.target, dest)
} else {
// The link target is already in the destination
tasks.push(relativeSymlink(dest, targetPath))
}
} else {
if (node.isWorkspace || node.isRoot) {
// workspace and root packages have not been published so they may
// have files that should be excluded.
tasks.push(copyPacklist(node.target.realpath, dest))
} else {
// copy the modules files but not dependencies.
const nm = path.join(node.realpath, 'node_modules')
tasks.push(fs.copy(node.realpath, dest, {
recursive: true,
errorOnExist: false,
filter: src => src !== nm,
}))
}

// add dependency edges to the queue.
for (const edge of node.edgesOut.values()) {
if (!omit.has(edge.type) && edge.to != null) {
destinations.set(
edge.to,
path.join(
destinations.get(edge.to.parent) || destination,
path.relative(edge.to.parent.location, edge.to.location)))
}
}
}
}
await Promise.all(tasks)
}
}
module.exports = Copy

async function copyPacklist (from, to) {
for (const file of await packlist({path: from})) {
// packlist will include bundled node_modules. ignore it because we're
// already handling copying dependencies.
if (file.startsWith('node_modules/'))
continue

// using recursive copy because packlist doesn't list directories.
// TODO what is npm's preferred recursive copy?
await fs.copy(
path.join(from, file),
path.join(to, file),
{ recursive: true, errorOnExist: false })
}
}

async function relativeSymlink (from, to) {
await fs.ensureSymlink(path.relative(to, from), to)
}
2 changes: 2 additions & 0 deletions lib/utils/cmd-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const shorthands = {
'clean-install-test': 'cit',
x: 'exec',
why: 'explain',
cp: 'copy',
}

const affordances = {
Expand Down Expand Up @@ -134,6 +135,7 @@ const cmdList = [
'doctor',
'exec',
'explain',
'copy',
]

const plumbing = ['birthday', 'help-search']
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"cli-table3": "^0.6.0",
"columnify": "~1.5.4",
"fastest-levenshtein": "^1.0.12",
"fs-extra": "^10.0.0",
"glob": "^7.2.0",
"graceful-fs": "^4.2.8",
"hosted-git-info": "^4.0.2",
Expand Down

0 comments on commit 9869596

Please sign in to comment.