diff --git a/lib/commands/diff.js b/lib/commands/diff.js index a97eed92c83cb..0ab7f6fccc9c6 100644 --- a/lib/commands/diff.js +++ b/lib/commands/diff.js @@ -106,7 +106,7 @@ class Diff extends BaseCommand { const pkgName = await this.packageName() return [ `${pkgName}@${this.npm.config.get('tag')}`, - `file:${this.prefix.replace(/#/g, '%23')}`, + `file:${this.prefix}`, ] } @@ -134,7 +134,7 @@ class Diff extends BaseCommand { } return [ `${pkgName}@${a}`, - `file:${this.prefix.replace(/#/g, '%23')}`, + `file:${this.prefix}`, ] } @@ -166,7 +166,7 @@ class Diff extends BaseCommand { } return [ `${spec.name}@${spec.fetchSpec}`, - `file:${this.prefix.replace(/#/g, '%23')}`, + `file:${this.prefix}`, ] } @@ -179,7 +179,7 @@ class Diff extends BaseCommand { } } - const aSpec = `file:${node.realpath.replace(/#/g, '%23')}` + const aSpec = `file:${node.realpath}` // finds what version of the package to compare against, if a exact // version or tag was passed than it should use that, otherwise @@ -212,8 +212,8 @@ class Diff extends BaseCommand { ] } else if (spec.type === 'directory') { return [ - `file:${spec.fetchSpec.replace(/#/g, '%23')}`, - `file:${this.prefix.replace(/#/g, '%23')}`, + `file:${spec.fetchSpec}`, + `file:${this.prefix}`, ] } else { throw this.usageError(`Spec type ${spec.type} not supported.`) @@ -281,7 +281,7 @@ class Diff extends BaseCommand { const res = !node || !node.package || !node.package.version ? spec.fetchSpec - : `file:${node.realpath.replace(/#/g, '%23')}` + : `file:${node.realpath}` return `${spec.name}@${res}` }) diff --git a/lib/commands/link.js b/lib/commands/link.js index 8a41548d7f108..4955a5b77d338 100644 --- a/lib/commands/link.js +++ b/lib/commands/link.js @@ -124,7 +124,7 @@ class Link extends ArboristWorkspaceCmd { ...this.npm.flatOptions, prune: false, path: this.npm.prefix, - add: names.map(l => `file:${resolve(globalTop, 'node_modules', l).replace(/#/g, '%23')}`), + add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`), save, workspaces: this.workspaceNames, }) @@ -135,7 +135,7 @@ class Link extends ArboristWorkspaceCmd { async linkPkg () { const wsp = this.workspacePaths const paths = wsp && wsp.length ? wsp : [this.npm.prefix] - const add = paths.map(path => `file:${path.replace(/#/g, '%23')}`) + const add = paths.map(path => `file:${path}`) const globalTop = resolve(this.npm.globalDir, '..') const Arborist = require('@npmcli/arborist') const arb = new Arborist({ diff --git a/node_modules/npm-package-arg/lib/npa.js b/node_modules/npm-package-arg/lib/npa.js index e92605811ae3e..d409b7f1becfc 100644 --- a/node_modules/npm-package-arg/lib/npa.js +++ b/node_modules/npm-package-arg/lib/npa.js @@ -1,23 +1,24 @@ 'use strict' -module.exports = npa -module.exports.resolve = resolve -module.exports.toPurl = toPurl -module.exports.Result = Result -const { URL } = require('url') +const isWindows = process.platform === 'win32' + +const { URL } = require('node:url') +// We need to use path/win32 so that we get consistent results in tests, but this also means we need to manually convert backslashes to forward slashes when generating file: urls with paths. +const path = isWindows ? require('node:path/win32') : require('node:path') +const { homedir } = require('node:os') const HostedGit = require('hosted-git-info') const semver = require('semver') -const path = global.FAKE_WINDOWS ? require('path').win32 : require('path') const validatePackageName = require('validate-npm-package-name') -const { homedir } = require('os') const { log } = require('proc-log') -const isWindows = process.platform === 'win32' || global.FAKE_WINDOWS const hasSlashes = isWindows ? /\\|[/]/ : /[/]/ const isURL = /^(?:git[+])?[a-z]+:/i const isGit = /^[^@]+@[^:.]+\.[^:]+:.+$/i -const isFilename = /[.](?:tgz|tar.gz|tar)$/i +const isFileType = /[.](?:tgz|tar.gz|tar)$/i const isPortNumber = /:[0-9]+(\/|$)/i +const isWindowsFile = /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/ +const isPosixFile = /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/ +const defaultRegistry = 'https://registry.npmjs.org' function npa (arg, where) { let name @@ -31,13 +32,14 @@ function npa (arg, where) { return npa(arg.raw, where || arg.where) } } - const nameEndsAt = arg[0] === '@' ? arg.slice(1).indexOf('@') + 1 : arg.indexOf('@') + const nameEndsAt = arg.indexOf('@', 1) // Skip possible leading @ const namePart = nameEndsAt > 0 ? arg.slice(0, nameEndsAt) : arg if (isURL.test(arg)) { spec = arg } else if (isGit.test(arg)) { spec = `git+ssh://${arg}` - } else if (namePart[0] !== '@' && (hasSlashes.test(namePart) || isFilename.test(namePart))) { + // eslint-disable-next-line max-len + } else if (!namePart.startsWith('@') && (hasSlashes.test(namePart) || isFileType.test(namePart))) { spec = arg } else if (nameEndsAt > 0) { name = namePart @@ -54,7 +56,27 @@ function npa (arg, where) { return resolve(name, spec, where, arg) } -const isFilespec = isWindows ? /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/ : /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/ +function isFileSpec (spec) { + if (!spec) { + return false + } + if (spec.toLowerCase().startsWith('file:')) { + return true + } + if (isWindows) { + return isWindowsFile.test(spec) + } + // We never hit this in windows tests, obviously + /* istanbul ignore next */ + return isPosixFile.test(spec) +} + +function isAliasSpec (spec) { + if (!spec) { + return false + } + return spec.toLowerCase().startsWith('npm:') +} function resolve (name, spec, where, arg) { const res = new Result({ @@ -65,12 +87,16 @@ function resolve (name, spec, where, arg) { }) if (name) { - res.setName(name) + res.name = name } - if (spec && (isFilespec.test(spec) || /^file:/i.test(spec))) { + if (!where) { + where = process.cwd() + } + + if (isFileSpec(spec)) { return fromFile(res, where) - } else if (spec && /^npm:/i.test(spec)) { + } else if (isAliasSpec(spec)) { return fromAlias(res, where) } @@ -82,15 +108,13 @@ function resolve (name, spec, where, arg) { return fromHostedGit(res, hosted) } else if (spec && isURL.test(spec)) { return fromURL(res) - } else if (spec && (hasSlashes.test(spec) || isFilename.test(spec))) { + } else if (spec && (hasSlashes.test(spec) || isFileType.test(spec))) { return fromFile(res, where) } else { return fromRegistry(res) } } -const defaultRegistry = 'https://registry.npmjs.org' - function toPurl (arg, reg = defaultRegistry) { const res = npa(arg) @@ -128,60 +152,62 @@ function invalidPurlType (type, raw) { return err } -function Result (opts) { - this.type = opts.type - this.registry = opts.registry - this.where = opts.where - if (opts.raw == null) { - this.raw = opts.name ? opts.name + '@' + opts.rawSpec : opts.rawSpec - } else { - this.raw = opts.raw +class Result { + constructor (opts) { + this.type = opts.type + this.registry = opts.registry + this.where = opts.where + if (opts.raw == null) { + this.raw = opts.name ? `${opts.name}@${opts.rawSpec}` : opts.rawSpec + } else { + this.raw = opts.raw + } + this.name = undefined + this.escapedName = undefined + this.scope = undefined + this.rawSpec = opts.rawSpec || '' + this.saveSpec = opts.saveSpec + this.fetchSpec = opts.fetchSpec + if (opts.name) { + this.setName(opts.name) + } + this.gitRange = opts.gitRange + this.gitCommittish = opts.gitCommittish + this.gitSubdir = opts.gitSubdir + this.hosted = opts.hosted } - this.name = undefined - this.escapedName = undefined - this.scope = undefined - this.rawSpec = opts.rawSpec || '' - this.saveSpec = opts.saveSpec - this.fetchSpec = opts.fetchSpec - if (opts.name) { - this.setName(opts.name) - } - this.gitRange = opts.gitRange - this.gitCommittish = opts.gitCommittish - this.gitSubdir = opts.gitSubdir - this.hosted = opts.hosted -} + // TODO move this to a getter/setter in a semver major + setName (name) { + const valid = validatePackageName(name) + if (!valid.validForOldPackages) { + throw invalidPackageName(name, valid, this.raw) + } -Result.prototype.setName = function (name) { - const valid = validatePackageName(name) - if (!valid.validForOldPackages) { - throw invalidPackageName(name, valid, this.raw) + this.name = name + this.scope = name[0] === '@' ? name.slice(0, name.indexOf('/')) : undefined + // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar + this.escapedName = name.replace('/', '%2f') + return this } - this.name = name - this.scope = name[0] === '@' ? name.slice(0, name.indexOf('/')) : undefined - // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar - this.escapedName = name.replace('/', '%2f') - return this -} - -Result.prototype.toString = function () { - const full = [] - if (this.name != null && this.name !== '') { - full.push(this.name) - } - const spec = this.saveSpec || this.fetchSpec || this.rawSpec - if (spec != null && spec !== '') { - full.push(spec) + toString () { + const full = [] + if (this.name != null && this.name !== '') { + full.push(this.name) + } + const spec = this.saveSpec || this.fetchSpec || this.rawSpec + if (spec != null && spec !== '') { + full.push(spec) + } + return full.length ? full.join('@') : this.raw } - return full.length ? full.join('@') : this.raw -} -Result.prototype.toJSON = function () { - const result = Object.assign({}, this) - delete result.hosted - return result + toJSON () { + const result = Object.assign({}, this) + delete result.hosted + return result + } } // sets res.gitCommittish, res.gitRange, and res.gitSubdir @@ -228,25 +254,67 @@ function setGitAttrs (res, committish) { } } -function fromFile (res, where) { - if (!where) { - where = process.cwd() +// Taken from: EncodePathChars and lookup_table in src/node_url.cc +// url.pathToFileURL only returns absolute references. We can't use it to encode paths. +// encodeURI mangles windows paths. We can't use it to encode paths. +// Under the hood, url.pathToFileURL does a limited set of encoding, with an extra windows step, and then calls path.resolve. +// The encoding node does without path.resolve is not available outside of the source, so we are recreating it here. +const encodedPathChars = new Map([ + ['\0', '%00'], + ['\t', '%09'], + ['\n', '%0A'], + ['\r', '%0D'], + [' ', '%20'], + ['"', '%22'], + ['#', '%23'], + ['%', '%25'], + ['?', '%3F'], + ['[', '%5B'], + ['\\', isWindows ? '/' : '%5C'], + [']', '%5D'], + ['^', '%5E'], + ['|', '%7C'], + ['~', '%7E'], +]) + +function pathToFileURL (str) { + let result = '' + for (let i = 0; i < str.length; i++) { + result = `${result}${encodedPathChars.get(str[i]) ?? str[i]}` + } + if (result.startsWith('file:')) { + return result } - res.type = isFilename.test(res.rawSpec) ? 'file' : 'directory' + return `file:${result}` +} + +function fromFile (res, where) { + res.type = isFileType.test(res.rawSpec) ? 'file' : 'directory' res.where = where - // always put the '/' on where when resolving urls, or else - // file:foo from /path/to/bar goes to /path/to/foo, when we want - // it to be /path/to/bar/foo + let rawSpec = pathToFileURL(res.rawSpec) + + if (rawSpec.startsWith('file:/')) { + // XXX backwards compatibility lack of compliance with RFC 8089 + + // turn file://path into file:/path + if (/^file:\/\/[^/]/.test(rawSpec)) { + rawSpec = `file:/${rawSpec.slice(5)}` + } + + // turn file:/../path into file:../path + // for 1 or 3 leading slashes (2 is already ruled out from handling file:// explicitly above) + if (/^\/{1,3}\.\.?(\/|$)/.test(rawSpec.slice(5))) { + rawSpec = rawSpec.replace(/^file:\/{1,3}/, 'file:') + } + } - let specUrl let resolvedUrl - const prefix = (!/^file:/.test(res.rawSpec) ? 'file:' : '') - const rawWithPrefix = prefix + res.rawSpec - let rawNoPrefix = rawWithPrefix.replace(/^file:/, '') + let specUrl try { - resolvedUrl = new URL(rawWithPrefix, `file://${path.resolve(where)}/`) - specUrl = new URL(rawWithPrefix) + // always put the '/' on "where", or else file:foo from /path/to/bar goes to /path/to/foo, when we want it to be /path/to/bar/foo + resolvedUrl = new URL(rawSpec, `${pathToFileURL(path.resolve(where))}/`) + specUrl = new URL(rawSpec) } catch (originalError) { const er = new Error('Invalid file: URL, must comply with RFC 8089') throw Object.assign(er, { @@ -257,24 +325,6 @@ function fromFile (res, where) { }) } - // XXX backwards compatibility lack of compliance with RFC 8089 - if (resolvedUrl.host && resolvedUrl.host !== 'localhost') { - const rawSpec = res.rawSpec.replace(/^file:\/\//, 'file:///') - resolvedUrl = new URL(rawSpec, `file://${path.resolve(where)}/`) - specUrl = new URL(rawSpec) - rawNoPrefix = rawSpec.replace(/^file:/, '') - } - // turn file:/../foo into file:../foo - // for 1, 2 or 3 leading slashes since we attempted - // in the previous step to make it a file protocol url with a leading slash - if (/^\/{1,3}\.\.?(\/|$)/.test(rawNoPrefix)) { - const rawSpec = res.rawSpec.replace(/^file:\/{1,3}/, 'file:') - resolvedUrl = new URL(rawSpec, `file://${path.resolve(where)}/`) - specUrl = new URL(rawSpec) - rawNoPrefix = rawSpec.replace(/^file:/, '') - } - // XXX end RFC 8089 violation backwards compatibility section - // turn /C:/blah into just C:/blah on windows let specPath = decodeURIComponent(specUrl.pathname) let resolvedPath = decodeURIComponent(resolvedUrl.pathname) @@ -288,13 +338,21 @@ function fromFile (res, where) { if (/^\/~(\/|$)/.test(specPath)) { res.saveSpec = `file:${specPath.substr(1)}` resolvedPath = path.resolve(homedir(), specPath.substr(3)) - } else if (!path.isAbsolute(rawNoPrefix)) { + } else if (!path.isAbsolute(rawSpec.slice(5))) { res.saveSpec = `file:${path.relative(where, resolvedPath)}` } else { res.saveSpec = `file:${path.resolve(resolvedPath)}` } res.fetchSpec = path.resolve(where, resolvedPath) + // re-normalize the slashes in saveSpec due to node:path/win32 behavior in windows + res.saveSpec = res.saveSpec.split('\\').join('/') + // Ignoring because this only happens in windows + /* istanbul ignore next */ + if (res.saveSpec.startsWith('file://')) { + // normalization of \\win32\root paths can cause a double / which we don't want + res.saveSpec = `file:/${res.saveSpec.slice(7)}` + } return res } @@ -416,3 +474,8 @@ function fromRegistry (res) { } return res } + +module.exports = npa +module.exports.resolve = resolve +module.exports.toPurl = toPurl +module.exports.Result = Result diff --git a/node_modules/npm-package-arg/package.json b/node_modules/npm-package-arg/package.json index ab285eb6c610c..58920fe240e5f 100644 --- a/node_modules/npm-package-arg/package.json +++ b/node_modules/npm-package-arg/package.json @@ -1,6 +1,6 @@ { "name": "npm-package-arg", - "version": "12.0.1", + "version": "12.0.2", "description": "Parse the things that can be arguments to `npm install`", "main": "./lib/npa.js", "directories": { @@ -18,7 +18,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^5.0.0", - "@npmcli/template-oss": "4.23.4", + "@npmcli/template-oss": "4.23.5", "tap": "^16.0.1" }, "scripts": { @@ -55,7 +55,7 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.23.4", + "version": "4.23.5", "publish": true } } diff --git a/package-lock.json b/package-lock.json index 2652fc67f9e06..da6a77c099f8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -129,7 +129,7 @@ "normalize-package-data": "^7.0.0", "npm-audit-report": "^6.0.0", "npm-install-checks": "^7.1.1", - "npm-package-arg": "^12.0.1", + "npm-package-arg": "^12.0.2", "npm-pick-manifest": "^10.0.0", "npm-profile": "^11.0.1", "npm-registry-fetch": "^18.0.2", @@ -12397,9 +12397,9 @@ } }, "node_modules/npm-package-arg": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.1.tgz", - "integrity": "sha512-aDxjFfPV3Liw0WOBWlyZLMBqtbgbg03rmGvHDJa2Ttv7tIz+1oB5qWec4psCDFZcZi9b5XdGkPdQiJxOPzvQRQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", "inBundle": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 4b0eacb637447..1bb7d1fa8d0c9 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "normalize-package-data": "^7.0.0", "npm-audit-report": "^6.0.0", "npm-install-checks": "^7.1.1", - "npm-package-arg": "^12.0.1", + "npm-package-arg": "^12.0.2", "npm-pick-manifest": "^10.0.0", "npm-profile": "^11.0.1", "npm-registry-fetch": "^18.0.2", diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 6bd4e9407e72d..54f86dea0f65c 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -447,7 +447,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { .catch(/* istanbul ignore next */ () => null) if (st && st.isSymbolicLink()) { const target = await readlink(dir) - const real = resolve(dirname(dir), target).replace(/#/g, '%23') + const real = resolve(dirname(dir), target) tree.package.dependencies[name] = `file:${real}` } else { tree.package.dependencies[name] = '*' @@ -522,12 +522,12 @@ module.exports = cls => class IdealTreeBuilder extends cls { const { name } = spec if (spec.type === 'file') { - spec = npa(`file:${relpath(path, spec.fetchSpec).replace(/#/g, '%23')}`, path) + spec = npa(`file:${relpath(path, spec.fetchSpec)}`, path) spec.name = name } else if (spec.type === 'directory') { try { const real = await realpath(spec.fetchSpec, this[_rpcache], this[_stcache]) - spec = npa(`file:${relpath(path, real).replace(/#/g, '%23')}`, path) + spec = npa(`file:${relpath(path, real)}`, path) spec.name = name } catch { // TODO: create synthetic test case to simulate realpath failure diff --git a/workspaces/arborist/lib/arborist/load-actual.js b/workspaces/arborist/lib/arborist/load-actual.js index 22c1c2875f1b1..2add9553688a4 100644 --- a/workspaces/arborist/lib/arborist/load-actual.js +++ b/workspaces/arborist/lib/arborist/load-actual.js @@ -216,7 +216,7 @@ module.exports = cls => class ActualLoader extends cls { const actualRoot = tree.isLink ? tree.target : tree const { dependencies = {} } = actualRoot.package for (const [name, kid] of actualRoot.children.entries()) { - const def = kid.isLink ? `file:${kid.realpath.replace(/#/g, '%23')}` : '*' + const def = kid.isLink ? `file:${kid.realpath}` : '*' dependencies[name] = dependencies[name] || def } actualRoot.package = { ...actualRoot.package, dependencies } diff --git a/workspaces/arborist/lib/arborist/load-virtual.js b/workspaces/arborist/lib/arborist/load-virtual.js index 7c51f8b9bef79..07c986853913e 100644 --- a/workspaces/arborist/lib/arborist/load-virtual.js +++ b/workspaces/arborist/lib/arborist/load-virtual.js @@ -149,7 +149,7 @@ module.exports = cls => class VirtualLoader extends cls { }) for (const [name, path] of workspaces.entries()) { - lockWS[name] = `file:${path.replace(/#/g, '%23')}` + lockWS[name] = `file:${path}` } // Should rootNames exclude optional? diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index be920272d48f0..4083d79f4fa25 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -1364,7 +1364,7 @@ module.exports = cls => class Reifier extends cls { // path initially, in which case we can end up with the wrong // thing, so just get the ultimate fetchSpec and relativize it. const p = req.fetchSpec.replace(/^file:/, '') - const rel = relpath(addTree.realpath, p).replace(/#/g, '%23') + const rel = relpath(addTree.realpath, p) newSpec = `file:${rel}` } } else { diff --git a/workspaces/arborist/lib/consistent-resolve.js b/workspaces/arborist/lib/consistent-resolve.js index 7c988048057c7..cbd3392a9300c 100644 --- a/workspaces/arborist/lib/consistent-resolve.js +++ b/workspaces/arborist/lib/consistent-resolve.js @@ -20,7 +20,7 @@ const consistentResolve = (resolved, fromPath, toPath, relPaths = false) => { raw, } = npa(resolved, fromPath) if (type === 'file' || type === 'directory') { - const cleanFetchSpec = fetchSpec.replace(/#/g, '%23') + const cleanFetchSpec = fetchSpec if (relPaths && toPath) { return `file:${relpath(toPath, cleanFetchSpec)}` } diff --git a/workspaces/arborist/lib/link.js b/workspaces/arborist/lib/link.js index 266ec45168839..42bc1faf48860 100644 --- a/workspaces/arborist/lib/link.js +++ b/workspaces/arborist/lib/link.js @@ -99,7 +99,7 @@ class Link extends Node { // the path/realpath guard is there for the benefit of setting // these things in the "wrong" order return this.path && this.realpath - ? `file:${relpath(dirname(this.path), this.realpath).replace(/#/g, '%23')}` + ? `file:${relpath(dirname(this.path), this.realpath)}` : null } diff --git a/workspaces/arborist/lib/node.js b/workspaces/arborist/lib/node.js index c519a7b543d4d..dccbf99bf6080 100644 --- a/workspaces/arborist/lib/node.js +++ b/workspaces/arborist/lib/node.js @@ -842,7 +842,7 @@ class Node { } for (const [name, path] of this.#workspaces.entries()) { - new Edge({ from: this, name, spec: `file:${path.replace(/#/g, '%23')}`, type: 'workspace' }) + new Edge({ from: this, name, spec: `file:${path}`, type: 'workspace' }) } } diff --git a/workspaces/arborist/lib/shrinkwrap.js b/workspaces/arborist/lib/shrinkwrap.js index 5f720ed9bd440..11703fad4b925 100644 --- a/workspaces/arborist/lib/shrinkwrap.js +++ b/workspaces/arborist/lib/shrinkwrap.js @@ -817,7 +817,7 @@ class Shrinkwrap { if (!/^file:/.test(resolved)) { pathFixed = resolved } else { - pathFixed = `file:${resolve(this.path, resolved.slice(5)).replace(/#/g, '%23')}` + pathFixed = `file:${resolve(this.path, resolved.slice(5))}` } } @@ -1011,7 +1011,7 @@ class Shrinkwrap { } if (node.isLink) { - lock.version = `file:${relpath(this.path, node.realpath).replace(/#/g, '%23')}` + lock.version = `file:${relpath(this.path, node.realpath)}` } else if (spec && (spec.type === 'file' || spec.type === 'remote')) { lock.version = spec.saveSpec } else if (spec && spec.type === 'git' || rSpec.type === 'git') { @@ -1089,7 +1089,7 @@ class Shrinkwrap { // this especially shows up with workspace edges when the root // node is also a workspace in the set. const p = resolve(node.realpath, spec.slice('file:'.length)) - set[k] = `file:${relpath(node.realpath, p).replace(/#/g, '%23')}` + set[k] = `file:${relpath(node.realpath, p)}` } else { set[k] = spec }