diff --git a/lib/utils/completion/installed-shallow.js b/lib/utils/completion/installed-shallow.js index 686c95e63245e..cff12f1f30736 100644 --- a/lib/utils/completion/installed-shallow.js +++ b/lib/utils/completion/installed-shallow.js @@ -1,8 +1,7 @@ -const { promisify } = require('util') -const readdir = promisify(require('readdir-scoped-modules')) +const { readdirScoped } = require('@npmcli/fs') const installedShallow = async (npm, opts) => { - const names = global => readdir(global ? npm.globalDir : npm.localDir) + const names = global => readdirScoped(global ? npm.globalDir : npm.localDir) const { conf: { argv: { remain } } } = opts if (remain.length > 3) { return null diff --git a/node_modules/@npmcli/fs/lib/index.js b/node_modules/@npmcli/fs/lib/index.js index 0dc02f8a3da44..81c746304cc42 100644 --- a/node_modules/@npmcli/fs/lib/index.js +++ b/node_modules/@npmcli/fs/lib/index.js @@ -2,8 +2,12 @@ const cp = require('./cp/index.js') const withTempDir = require('./with-temp-dir.js') +const readdirScoped = require('./readdir-scoped.js') +const moveFile = require('./move-file.js') module.exports = { cp, withTempDir, + readdirScoped, + moveFile, } diff --git a/node_modules/@npmcli/fs/lib/move-file.js b/node_modules/@npmcli/fs/lib/move-file.js new file mode 100644 index 0000000000000..d56e06d384659 --- /dev/null +++ b/node_modules/@npmcli/fs/lib/move-file.js @@ -0,0 +1,78 @@ +const { dirname, join, resolve, relative, isAbsolute } = require('path') +const fs = require('fs/promises') + +const pathExists = async path => { + try { + await fs.access(path) + return true + } catch (er) { + return er.code !== 'ENOENT' + } +} + +const moveFile = async (source, destination, options = {}, root = true, symlinks = []) => { + if (!source || !destination) { + throw new TypeError('`source` and `destination` file required') + } + + options = { + overwrite: true, + ...options, + } + + if (!options.overwrite && await pathExists(destination)) { + throw new Error(`The destination file exists: ${destination}`) + } + + await fs.mkdir(dirname(destination), { recursive: true }) + + try { + await fs.rename(source, destination) + } catch (error) { + if (error.code === 'EXDEV' || error.code === 'EPERM') { + const sourceStat = await fs.lstat(source) + if (sourceStat.isDirectory()) { + const files = await fs.readdir(source) + await Promise.all(files.map((file) => + moveFile(join(source, file), join(destination, file), options, false, symlinks) + )) + } else if (sourceStat.isSymbolicLink()) { + symlinks.push({ source, destination }) + } else { + await fs.copyFile(source, destination) + } + } else { + throw error + } + } + + if (root) { + await Promise.all(symlinks.map(async ({ source: symSource, destination: symDestination }) => { + let target = await fs.readlink(symSource) + // junction symlinks in windows will be absolute paths, so we need to + // make sure they point to the symlink destination + if (isAbsolute(target)) { + target = resolve(symDestination, relative(symSource, target)) + } + // try to determine what the actual file is so we can create the correct + // type of symlink in windows + let targetStat = 'file' + try { + targetStat = await fs.stat(resolve(dirname(symSource), target)) + if (targetStat.isDirectory()) { + targetStat = 'junction' + } + } catch { + // targetStat remains 'file' + } + await fs.symlink( + target, + symDestination, + targetStat + ) + })) + await fs.rm(source, { recursive: true, force: true }) + } +} + +module.exports = moveFile diff --git a/node_modules/@npmcli/fs/lib/readdir-scoped.js b/node_modules/@npmcli/fs/lib/readdir-scoped.js new file mode 100644 index 0000000000000..cd601dfbe7486 --- /dev/null +++ b/node_modules/@npmcli/fs/lib/readdir-scoped.js @@ -0,0 +1,20 @@ +const { readdir } = require('fs/promises') +const { join } = require('path') + +const readdirScoped = async (dir) => { + const results = [] + + for (const item of await readdir(dir)) { + if (item.startsWith('@')) { + for (const scopedItem of await readdir(join(dir, item))) { + results.push(join(item, scopedItem)) + } + } else { + results.push(item) + } + } + + return results +} + +module.exports = readdirScoped diff --git a/node_modules/@npmcli/fs/package.json b/node_modules/@npmcli/fs/package.json index 2eabc35dab33b..28eb613388418 100644 --- a/node_modules/@npmcli/fs/package.json +++ b/node_modules/@npmcli/fs/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/fs", - "version": "3.0.0", + "version": "3.1.0", "description": "filesystem utilities for the npm cli", "main": "lib/index.js", "files": [ @@ -29,8 +29,8 @@ "author": "GitHub Inc.", "license": "ISC", "devDependencies": { - "@npmcli/eslint-config": "^3.0.1", - "@npmcli/template-oss": "4.5.1", + "@npmcli/eslint-config": "^4.0.0", + "@npmcli/template-oss": "4.8.0", "tap": "^16.0.1" }, "dependencies": { @@ -41,7 +41,7 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.5.1" + "version": "4.8.0" }, "tap": { "nyc-arg": [ diff --git a/package-lock.json b/package-lock.json index 6376f40669081..5ecefbd8d36ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,6 @@ "read", "read-package-json", "read-package-json-fast", - "readdir-scoped-modules", "rimraf", "semver", "ssri", @@ -143,7 +142,6 @@ "read": "~1.0.7", "read-package-json": "^6.0.0", "read-package-json-fast": "^3.0.1", - "readdir-scoped-modules": "^1.1.0", "rimraf": "^3.0.2", "semver": "^7.3.8", "ssri": "^10.0.0", @@ -162,7 +160,7 @@ "devDependencies": { "@npmcli/docs": "^1.0.0", "@npmcli/eslint-config": "^4.0.0", - "@npmcli/fs": "^3.0.0", + "@npmcli/fs": "^3.1.0", "@npmcli/git": "^4.0.1", "@npmcli/promise-spawn": "^6.0.1", "@npmcli/template-oss": "4.8.0", @@ -2106,9 +2104,9 @@ } }, "node_modules/@npmcli/fs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.0.0.tgz", - "integrity": "sha512-GdeVD+dnBxzMslTFvnctLX5yIqV4ZNZBWNbo1OejQ++bZpnFNQ1AjOn9Sboi+LzheQbCBU1ts1mhEVduHrcZOQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", "inBundle": true, "dependencies": { "semver": "^7.3.5" @@ -2874,7 +2872,7 @@ }, "node_modules/asap": { "version": "2.0.6", - "inBundle": true, + "dev": true, "license": "MIT" }, "node_modules/async-hook-domain": { @@ -3874,7 +3872,7 @@ }, "node_modules/debuglog": { "version": "1.0.1", - "inBundle": true, + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -4035,7 +4033,7 @@ }, "node_modules/dezalgo": { "version": "1.0.4", - "inBundle": true, + "dev": true, "license": "ISC", "dependencies": { "asap": "^2.0.0", @@ -10049,7 +10047,7 @@ }, "node_modules/readdir-scoped-modules": { "version": "1.1.0", - "inBundle": true, + "dev": true, "license": "ISC", "dependencies": { "debuglog": "^1.0.1", @@ -14007,10 +14005,10 @@ "license": "ISC", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.0", "@npmcli/installed-package-contents": "^2.0.0", "@npmcli/map-workspaces": "^3.0.0", "@npmcli/metavuln-calculator": "^5.0.0", - "@npmcli/move-file": "^3.0.0", "@npmcli/name-from-folder": "^1.0.1", "@npmcli/node-gyp": "^3.0.0", "@npmcli/package-json": "^3.0.0", @@ -14035,7 +14033,6 @@ "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^1.0.1", "read-package-json-fast": "^3.0.1", - "readdir-scoped-modules": "^1.1.0", "semver": "^7.3.7", "ssri": "^10.0.0", "treeverse": "^3.0.0", diff --git a/package.json b/package.json index 3c2efd351785f..51fade9a8bf64 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,6 @@ "read": "~1.0.7", "read-package-json": "^6.0.0", "read-package-json-fast": "^3.0.1", - "readdir-scoped-modules": "^1.1.0", "rimraf": "^3.0.2", "semver": "^7.3.8", "ssri": "^10.0.0", @@ -184,7 +183,6 @@ "read", "read-package-json", "read-package-json-fast", - "readdir-scoped-modules", "rimraf", "semver", "ssri", @@ -199,7 +197,7 @@ "devDependencies": { "@npmcli/docs": "^1.0.0", "@npmcli/eslint-config": "^4.0.0", - "@npmcli/fs": "^3.0.0", + "@npmcli/fs": "^3.1.0", "@npmcli/git": "^4.0.1", "@npmcli/promise-spawn": "^6.0.1", "@npmcli/template-oss": "4.8.0", diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index afcb67903da6d..09d3a37d9c3f8 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -7,9 +7,8 @@ const cacache = require('cacache') const promiseCallLimit = require('promise-call-limit') const realpath = require('../../lib/realpath.js') const { resolve, dirname } = require('path') -const { promisify } = require('util') const treeCheck = require('../tree-check.js') -const readdir = promisify(require('readdir-scoped-modules')) +const { readdirScoped } = require('@npmcli/fs') const { lstat, readlink } = require('fs/promises') const { depth } = require('treeverse') const log = require('proc-log') @@ -447,7 +446,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { const globalExplicitUpdateNames = [] if (this[_global] && (this[_updateAll] || this[_updateNames].length)) { const nm = resolve(this.path, 'node_modules') - for (const name of await readdir(nm).catch(() => [])) { + for (const name of await readdirScoped(nm).catch(() => [])) { tree.package.dependencies = tree.package.dependencies || {} const updateName = this[_updateNames].includes(name) if (this[_updateAll] || updateName) { diff --git a/workspaces/arborist/lib/arborist/load-actual.js b/workspaces/arborist/lib/arborist/load-actual.js index bb813806e5556..d2bff77214e1d 100644 --- a/workspaces/arborist/lib/arborist/load-actual.js +++ b/workspaces/arborist/lib/arborist/load-actual.js @@ -3,8 +3,7 @@ const { relative, dirname, resolve, join, normalize } = require('path') const rpj = require('read-package-json-fast') -const { promisify } = require('util') -const readdir = promisify(require('readdir-scoped-modules')) +const { readdirScoped } = require('@npmcli/fs') const walkUp = require('walk-up-path') const ancestorPath = require('common-ancestor-path') const treeCheck = require('../tree-check.js') @@ -362,7 +361,7 @@ module.exports = cls => class ActualLoader extends cls { async [_loadFSChildren] (node) { const nm = resolve(node.realpath, 'node_modules') try { - const kids = await readdir(nm) + const kids = await readdirScoped(nm) return Promise.all( // ignore . dirs and retired scoped package folders kids.filter(kid => !/^(@[^/]+\/)?\./.test(kid)) @@ -412,7 +411,7 @@ module.exports = cls => class ActualLoader extends cls { } const entries = nmContents.get(p) || - await readdir(p + '/node_modules').catch(() => []) + await readdirScoped(p + '/node_modules').catch(() => []) nmContents.set(p, entries) if (!entries.includes(name)) { continue diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index c3cbf02b31080..36aabd6f0fcdd 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -19,7 +19,7 @@ const { rm, symlink, } = require('fs/promises') -const moveFile = require('@npmcli/move-file') +const { moveFile } = require('@npmcli/fs') const PackageJson = require('@npmcli/package-json') const packageContents = require('@npmcli/installed-package-contents') const runScript = require('@npmcli/run-script') diff --git a/workspaces/arborist/package.json b/workspaces/arborist/package.json index d739aa26ab7b8..68d0d1e505c66 100644 --- a/workspaces/arborist/package.json +++ b/workspaces/arborist/package.json @@ -4,10 +4,10 @@ "description": "Manage node_modules trees", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.0", "@npmcli/installed-package-contents": "^2.0.0", "@npmcli/map-workspaces": "^3.0.0", "@npmcli/metavuln-calculator": "^5.0.0", - "@npmcli/move-file": "^3.0.0", "@npmcli/name-from-folder": "^1.0.1", "@npmcli/node-gyp": "^3.0.0", "@npmcli/package-json": "^3.0.0", @@ -32,7 +32,6 @@ "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^1.0.1", "read-package-json-fast": "^3.0.1", - "readdir-scoped-modules": "^1.1.0", "semver": "^7.3.7", "ssri": "^10.0.0", "treeverse": "^3.0.0", diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 257fcb4cbcf7b..77aee322f2788 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -52,6 +52,17 @@ const mocks = { return fs.promises.mkdir(...args) }, + rename: async (...args) => { + if (failRename) { + throw failRename + } else if (failRenameOnce) { + const er = failRenameOnce + failRenameOnce = null + throw er + } else { + return fs.promises.rename(...args) + } + }, rm: async (...args) => { if (failRm) { throw new Error('rm fail') @@ -74,8 +85,8 @@ This is a one-time fix-up, please be patient... ] // need this to be injected so that it doesn't pull from main cache -const moveFile = t.mock('@npmcli/move-file', { fs: fsMock }) -mocks['@npmcli/move-file'] = moveFile +const { moveFile } = t.mock('@npmcli/fs', { 'fs/promises': mocks['fs/promises'] }) +mocks['@npmcli/fs'] = { moveFile } // track the warnings that are emitted. returns a function that removes // the listener and provides the list of what it saw.