-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Isaacs/install finish #1245
Closed
+26,366
−8,189
Closed
Isaacs/install finish #1245
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
5e940d9
update @npmcli/arborist, dedupe/organize metadeps
isaacs 59cd29d
remove libcipm, correct bundleDependencies
isaacs 4f9f144
dedupe npm-pick-manifest up to 6.1.0
isaacs e1943ac
npm-audit-report@2.1.0
isaacs 873665e
update arborist, dedupe pacote/make-fetch-happen
isaacs 59d9373
Consistent output for most reify() commands
isaacs df53085
shrinkwrap: port to using Arborist
isaacs 87a1559
@npmcli/arborist@0.0.0-pre.19
isaacs 8749628
show stdout/stderr output for failed scripts
isaacs 56a688a
add the --legacy-peer-deps config
isaacs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,162 +1,19 @@ | ||
// XXX replace this with @npmcli/arborist | ||
// dedupe duplicated packages, or find them in the tree | ||
const util = require('util') | ||
const Arborist = require('@npmcli/arborist') | ||
const rimraf = util.promisify(require('rimraf')) | ||
const reifyOutput = require('./utils/reify-output.js') | ||
const usageUtil = require('./utils/usage.js') | ||
|
||
var util = require('util') | ||
var path = require('path') | ||
var validate = require('aproba') | ||
var without = require('lodash.without') | ||
var asyncMap = require('slide').asyncMap | ||
var chain = require('slide').chain | ||
var npa = require('npm-package-arg') | ||
var log = require('npmlog') | ||
var npm = require('./npm.js') | ||
var Installer = require('./install.js').Installer | ||
var findRequirement = require('./install/deps.js').findRequirement | ||
var earliestInstallable = require('./install/deps.js').earliestInstallable | ||
var checkPermissions = require('./install/check-permissions.js') | ||
var decomposeActions = require('./install/decompose-actions.js') | ||
var loadExtraneous = require('./install/deps.js').loadExtraneous | ||
var computeMetadata = require('./install/deps.js').computeMetadata | ||
var sortActions = require('./install/diff-trees.js').sortActions | ||
var moduleName = require('./utils/module-name.js') | ||
var packageId = require('./utils/package-id.js') | ||
var childPath = require('./utils/child-path.js') | ||
var usage = require('./utils/usage') | ||
var getRequested = require('./install/get-requested.js') | ||
const usage = usageUtil('dedupe', 'npm dedupe') | ||
|
||
module.exports = dedupe | ||
module.exports.Deduper = Deduper | ||
const completion = (cb) => cb(null, []) | ||
|
||
dedupe.usage = usage( | ||
'dedupe', | ||
'npm dedupe' | ||
) | ||
const cmd = (args, cb) => dedupe(args).then(() => cb()).catch(cb) | ||
|
||
function dedupe (args, cb) { | ||
validate('AF', arguments) | ||
// the /path/to/node_modules/.. | ||
var where = path.resolve(npm.dir, '..') | ||
var dryrun = false | ||
if (npm.command.match(/^find/)) dryrun = true | ||
if (npm.config.get('dry-run')) dryrun = true | ||
if (dryrun && !npm.config.get('json')) npm.config.set('parseable', true) | ||
|
||
new Deduper(where, dryrun).run(cb) | ||
} | ||
|
||
function Deduper (where, dryrun) { | ||
validate('SB', arguments) | ||
Installer.call(this, where, dryrun, []) | ||
this.noPackageJsonOk = true | ||
this.topLevelLifecycles = false | ||
} | ||
util.inherits(Deduper, class {}) // Installer) | ||
|
||
Deduper.prototype.loadIdealTree = function (cb) { | ||
validate('F', arguments) | ||
log.silly('install', 'loadIdealTree') | ||
|
||
var self = this | ||
chain([ | ||
[this.newTracker(this.progress.loadIdealTree, 'cloneCurrentTree')], | ||
[this, this.cloneCurrentTreeToIdealTree], | ||
[this, this.finishTracker, 'cloneCurrentTree'], | ||
|
||
[this.newTracker(this.progress.loadIdealTree, 'loadAllDepsIntoIdealTree', 10)], | ||
[ function (next) { | ||
loadExtraneous(self.idealTree, self.progress.loadAllDepsIntoIdealTree, next) | ||
} ], | ||
[this, this.finishTracker, 'loadAllDepsIntoIdealTree'], | ||
|
||
[this, andComputeMetadata(this.idealTree)] | ||
], cb) | ||
} | ||
|
||
function andComputeMetadata (tree) { | ||
return function (next) { | ||
next(null, computeMetadata(tree)) | ||
} | ||
} | ||
|
||
Deduper.prototype.generateActionsToTake = function (cb) { | ||
validate('F', arguments) | ||
log.silly('dedupe', 'generateActionsToTake') | ||
chain([ | ||
[this.newTracker(log, 'hoist', 1)], | ||
[hoistChildren, this.idealTree, this.differences], | ||
[this, this.finishTracker, 'hoist'], | ||
[this.newTracker(log, 'sort-actions', 1)], | ||
[this, function (next) { | ||
this.differences = sortActions(this.differences) | ||
next() | ||
}], | ||
[this, this.finishTracker, 'sort-actions'], | ||
[checkPermissions, this.differences], | ||
[decomposeActions, this.differences, this.todo] | ||
], cb) | ||
const dedupe = async args => { | ||
require('npmlog').warn('coming soon!') | ||
throw new Error('not yet implemented') | ||
} | ||
|
||
function move (node, hoistTo, diff) { | ||
node.parent.children = without(node.parent.children, node) | ||
hoistTo.children.push(node) | ||
node.fromPath = node.path | ||
node.path = childPath(hoistTo.path, node) | ||
node.parent = hoistTo | ||
if (!diff.filter(function (action) { return action[0] === 'move' && action[1] === node }).length) { | ||
diff.push(['move', node]) | ||
} | ||
} | ||
|
||
function moveRemainingChildren (node, diff) { | ||
node.children.forEach(function (child) { | ||
move(child, node, diff) | ||
moveRemainingChildren(child, diff) | ||
}) | ||
} | ||
|
||
function remove (child, diff, done) { | ||
remove_(child, diff, new Set(), done) | ||
} | ||
|
||
function remove_ (child, diff, seen, done) { | ||
if (seen.has(child)) return done() | ||
seen.add(child) | ||
diff.push(['remove', child]) | ||
child.parent.children = without(child.parent.children, child) | ||
asyncMap(child.children, function (child, next) { | ||
remove_(child, diff, seen, next) | ||
}, done) | ||
} | ||
|
||
function hoistChildren (tree, diff, next) { | ||
hoistChildren_(tree, diff, new Set(), next) | ||
} | ||
|
||
function hoistChildren_ (tree, diff, seen, next) { | ||
validate('OAOF', arguments) | ||
if (seen.has(tree)) return next() | ||
seen.add(tree) | ||
asyncMap(tree.children, function (child, done) { | ||
if (!tree.parent || child.fromBundle || child.package._inBundle) return hoistChildren_(child, diff, seen, done) | ||
var better = findRequirement(tree.parent, moduleName(child), getRequested(child) || npa(packageId(child))) | ||
if (better) { | ||
return chain([ | ||
[remove, child, diff], | ||
[andComputeMetadata(tree)] | ||
], done) | ||
} | ||
var hoistTo = earliestInstallable(tree, tree.parent, child.package, log) | ||
if (hoistTo) { | ||
move(child, hoistTo, diff) | ||
chain([ | ||
[andComputeMetadata(hoistTo)], | ||
[hoistChildren_, child, diff, seen], | ||
[ function (next) { | ||
moveRemainingChildren(child, diff) | ||
next() | ||
} ] | ||
], done) | ||
} else { | ||
done() | ||
} | ||
}, next) | ||
} | ||
module.exports = Object.assign(cmd, { usage, completion }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,197 +1,68 @@ | ||
// link with no args: symlink the folder to the global location | ||
// link with package arg: symlink the global to the local | ||
|
||
var npm = require('./npm.js') | ||
var symlink = require('./utils/link.js') | ||
var fs = require('graceful-fs') | ||
var log = require('npmlog') | ||
var asyncMap = require('slide').asyncMap | ||
var chain = require('slide').chain | ||
var path = require('path') | ||
var build = require('./build.js') | ||
var npa = require('npm-package-arg') | ||
var usage = require('./utils/usage') | ||
var output = require('./utils/output.js') | ||
|
||
module.exports = link | ||
const npm = require('./npm.js') | ||
const usageUtil = require('./utils/usage.js') | ||
const reifyOutput = require('./utils/reify-output.js') | ||
const log = require('npmlog') | ||
const { resolve } = require('path') | ||
const Arborist = require('@npmcli/arborist') | ||
|
||
const completion = (opts, cb) => { | ||
const { readdir } = require('fs') | ||
const dir = npm.globalDir | ||
readdir(dir, (er, files) => cb(er, files.filter(f => !/^[._-]/.test(f)))) | ||
} | ||
|
||
link.usage = usage( | ||
const usage = usageUtil( | ||
'link', | ||
'npm link (in package dir)' + | ||
'\nnpm link [<@scope>/]<pkg>[@<version>]' | ||
) | ||
|
||
link.completion = function (opts, cb) { | ||
var dir = npm.globalDir | ||
fs.readdir(dir, function (er, files) { | ||
cb(er, files.filter(function (f) { | ||
return !f.match(/^[._-]/) | ||
})) | ||
}) | ||
} | ||
|
||
function link (args, cb) { | ||
if (process.platform === 'win32') { | ||
var semver = require('semver') | ||
if (!semver.gte(process.version, '0.7.9')) { | ||
var msg = 'npm link not supported on windows prior to node 0.7.9' | ||
var e = new Error(msg) | ||
e.code = 'ENOTSUP' | ||
e.errno = require('constants').ENOTSUP // eslint-disable-line node/no-deprecated-api | ||
return cb(e) | ||
} | ||
} | ||
const cmd = (args, cb) => link(args).then(() => cb()).catch(cb) | ||
|
||
const link = async args => { | ||
if (npm.config.get('global')) { | ||
return cb(new Error( | ||
'link should never be --global.\n' + | ||
'Please re-run this command with --local' | ||
)) | ||
} | ||
|
||
if (args.length === 1 && args[0] === '.') args = [] | ||
if (args.length) return linkInstall(args, cb) | ||
linkPkg(npm.prefix, cb) | ||
args = args.filter(a => resolve(a) !== npm.prefix) | ||
return args.length ? linkInstall(args) : linkPkg() | ||
} | ||
|
||
function parentFolder (id, folder) { | ||
if (id[0] === '@') { | ||
return path.resolve(folder, '..', '..') | ||
} else { | ||
return path.resolve(folder, '..') | ||
} | ||
} | ||
|
||
function linkInstall (pkgs, cb) { | ||
asyncMap(pkgs, function (pkg, cb) { | ||
var t = path.resolve(npm.globalDir, '..') | ||
var pp = path.resolve(npm.globalDir, pkg) | ||
var rp = null | ||
var target = path.resolve(npm.dir, pkg) | ||
|
||
function n (er, data) { | ||
if (er) return cb(er, data) | ||
// we want the ONE thing that was installed into the global dir | ||
var installed = data.filter(function (info) { | ||
var id = info[0] | ||
var folder = info[1] | ||
return parentFolder(id, folder) === npm.globalDir | ||
}) | ||
var id = installed[0][0] | ||
pp = installed[0][1] | ||
var what = npa(id) | ||
pkg = what.name | ||
target = path.resolve(npm.dir, pkg) | ||
next() | ||
} | ||
|
||
// if it's a folder, a random not-installed thing, or not a scoped package, | ||
// then link or install it first | ||
if (pkg[0] !== '@' && (pkg.indexOf('/') !== -1 || pkg.indexOf('\\') !== -1)) { | ||
return fs.lstat(path.resolve(pkg), function (er, st) { | ||
if (er || !st.isDirectory()) { | ||
npm.commands.install(t, pkg, n) | ||
} else { | ||
rp = path.resolve(pkg) | ||
linkPkg(rp, n) | ||
} | ||
}) | ||
} | ||
|
||
fs.lstat(pp, function (er, st) { | ||
if (er) { | ||
rp = pp | ||
return npm.commands.install(t, [pkg], n) | ||
} else if (!st.isSymbolicLink()) { | ||
rp = pp | ||
next() | ||
} else { | ||
return fs.realpath(pp, function (er, real) { | ||
if (er) log.warn('invalid symbolic link', pkg) | ||
else rp = real | ||
next() | ||
}) | ||
} | ||
}) | ||
|
||
function next () { | ||
if (npm.config.get('dry-run')) return resultPrinter(pkg, pp, target, rp, cb) | ||
chain( | ||
[ | ||
[ function (cb) { | ||
log.verbose('link', 'symlinking %s to %s', pp, target) | ||
cb() | ||
} ], | ||
[symlink, pp, target, false, false], | ||
// do not run any scripts | ||
rp && [build, [target], npm.config.get('global'), build._noLC, true], | ||
[resultPrinter, pkg, pp, target, rp] | ||
], | ||
cb | ||
) | ||
} | ||
}, cb) | ||
} | ||
|
||
function linkPkg (folder, cb_) { | ||
var me = folder || npm.prefix | ||
var readJson = require('read-package-json') | ||
const linkInstall = async args => { | ||
// add all the args as global installs, and then add symlink installs locally | ||
// to the packages in the global space. | ||
const globalArb = new Arborist({ | ||
...npm.flatOptions, | ||
path: resolve(npm.globalDir, '..'), | ||
global: true | ||
}) | ||
|
||
log.verbose('linkPkg', folder) | ||
const globals = await globalArb.reify({ add: args }) | ||
|
||
readJson(path.resolve(me, 'package.json'), function (er, d) { | ||
function cb (er) { | ||
return cb_(er, [[d && d._id, target, null, null]]) | ||
} | ||
if (er) return cb(er) | ||
if (!d.name) { | ||
er = new Error('Package must have a name field to be linked') | ||
return cb(er) | ||
} | ||
var target = path.resolve(npm.globalDir, d.name) | ||
if (npm.config.get('dry-run')) return resultPrinter(path.basename(me), me, target, cb) | ||
symlink(me, target, false, true, function (er) { | ||
if (er) return cb(er) | ||
log.verbose('link', 'build target', target) | ||
// also install missing dependencies. | ||
npm.commands.install(me, [], function (er) { | ||
if (er) return cb(er) | ||
// build the global stuff. Don't run *any* scripts, because | ||
// install command already will have done that. | ||
build([target], true, build._noLC, true, function (er) { | ||
if (er) return cb(er) | ||
resultPrinter(path.basename(me), me, target, cb) | ||
}) | ||
}) | ||
}) | ||
const links = globals.edgesOut.keys() | ||
const localArb = new Arborist({ | ||
...npm.flatOptions, | ||
path: npm.prefix | ||
}) | ||
await localArb.reify({ | ||
add: links.map(l => `file:${resolve(globalTop, 'node_modules', l)}`) | ||
}) | ||
} | ||
|
||
function resultPrinter (pkg, src, dest, rp, cb) { | ||
if (typeof cb !== 'function') { | ||
cb = rp | ||
rp = null | ||
} | ||
var where = dest | ||
rp = (rp || '').trim() | ||
src = (src || '').trim() | ||
// XXX If --json is set, then look up the data from the package.json | ||
if (npm.config.get('parseable')) { | ||
return parseableOutput(dest, rp || src, cb) | ||
} | ||
if (rp === src) rp = null | ||
output(where + ' -> ' + src + (rp ? ' -> ' + rp : '')) | ||
cb() | ||
reifyOutput(localArb) | ||
} | ||
|
||
function parseableOutput (dest, rp, cb) { | ||
// XXX this should match ls --parseable and install --parseable | ||
// look up the data from package.json, format it the same way. | ||
// | ||
// link is always effectively 'long', since it doesn't help much to | ||
// *just* print the target folder. | ||
// However, we don't actually ever read the version number, so | ||
// the second field is always blank. | ||
output(dest + '::' + rp) | ||
cb() | ||
const linkPkg = async () => { | ||
const arb = new Arborist({ | ||
...npm.flatOptions, | ||
path: resolve(npm.globalDir, '..'), | ||
global: true | ||
}) | ||
await arb.reify({ add: [`file:${npm.prefix}`] }) | ||
reifyOutput(arb) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,20 @@ | ||
// XXX replace this with @npmcli/arborist | ||
// prune extraneous packages. | ||
const util = require('util') | ||
const Arborist = require('@npmcli/arborist') | ||
const rimraf = util.promisify(require('rimraf')) | ||
const reifyOutput = require('./utils/reify-output.js') | ||
const usageUtil = require('./utils/usage.js') | ||
|
||
module.exports = prune | ||
module.exports.Pruner = Pruner | ||
const usage = usageUtil('prune', | ||
'npm prune [[<@scope>/]<pkg>...] [--production]') | ||
|
||
prune.usage = 'npm prune [[<@scope>/]<pkg>...] [--production]' | ||
const completion = require('./utils/completion/installed-deep.js') | ||
|
||
var npm = require('./npm.js') | ||
var log = require('npmlog') | ||
var util = require('util') | ||
var moduleName = require('./utils/module-name.js') | ||
var Installer = require('./install.js').Installer | ||
var isExtraneous = require('./install/is-extraneous.js') | ||
var isOnlyDev = require('./install/is-only-dev.js') | ||
var removeDeps = require('./install/deps.js').removeDeps | ||
var loadExtraneous = require('./install/deps.js').loadExtraneous | ||
var chain = require('slide').chain | ||
var computeMetadata = require('./install/deps.js').computeMetadata | ||
const cmd = (args, cb) => prune(args).then(() => cb()).catch(cb) | ||
|
||
prune.completion = require('./utils/completion/installed-deep.js') | ||
|
||
function prune (args, cb) { | ||
var dryrun = !!npm.config.get('dry-run') | ||
new Pruner('.', dryrun, args).run(cb) | ||
} | ||
|
||
function Pruner (where, dryrun, args) { | ||
Installer.call(this, where, dryrun, args) | ||
this.autoPrune = true | ||
} | ||
util.inherits(Pruner, class {}) // Installer) | ||
|
||
Pruner.prototype.loadAllDepsIntoIdealTree = function (cb) { | ||
log.silly('uninstall', 'loadAllDepsIntoIdealTree') | ||
|
||
var cg = this.progress['loadIdealTree:loadAllDepsIntoIdealTree'] | ||
var steps = [] | ||
|
||
computeMetadata(this.idealTree) | ||
var self = this | ||
var excludeDev = npm.config.get('production') || /^prod(uction)?$/.test(npm.config.get('only')) | ||
function shouldPrune (child) { | ||
if (isExtraneous(child)) return true | ||
if (!excludeDev) return false | ||
return isOnlyDev(child) | ||
} | ||
function getModuleName (child) { | ||
// wrapping because moduleName doesn't like extra args and we're called | ||
// from map. | ||
return moduleName(child) | ||
} | ||
function matchesArg (name) { | ||
return self.args.length === 0 || self.args.indexOf(name) !== -1 | ||
} | ||
function nameObj (name) { | ||
return {name: name} | ||
} | ||
var toPrune = this.idealTree.children.filter(shouldPrune).map(getModuleName).filter(matchesArg).map(nameObj) | ||
|
||
steps.push( | ||
[removeDeps, toPrune, this.idealTree, null], | ||
[loadExtraneous, this.idealTree, cg.newGroup('loadExtraneous')]) | ||
chain(steps, cb) | ||
const prune = async args => { | ||
require('npmlog').warn('coming soon!') | ||
throw new Error('not yet implemented') | ||
} | ||
|
||
Pruner.prototype.runPreinstallTopLevelLifecycles = function (cb) { cb() } | ||
Pruner.prototype.runPostinstallTopLevelLifecycles = function (cb) { cb() } | ||
Pruner.prototype.saveToDependencies = function (cb) { cb() } | ||
module.exports = Object.assign(cmd, { usage, completion }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,296 +1,52 @@ | ||
'use strict' | ||
|
||
const BB = require('bluebird') | ||
|
||
const chain = require('slide').chain | ||
const detectIndent = require('detect-indent') | ||
const detectNewline = require('detect-newline') | ||
const readFile = BB.promisify(require('graceful-fs').readFile) | ||
const getRequested = require('./install/get-requested.js') | ||
const id = require('./install/deps.js') | ||
const iferr = require('iferr') | ||
const isOnlyOptional = require('./install/is-only-optional.js') | ||
const isOnlyDev = require('./install/is-only-dev.js') | ||
const lifecycle = require('./utils/lifecycle.js') | ||
const log = require('npmlog') | ||
const moduleName = require('./utils/module-name.js') | ||
const move = require('move-concurrently') | ||
const Arborist = require('@npmcli/arborist') | ||
const npm = require('./npm.js') | ||
const path = require('path') | ||
const readPackageTree = BB.promisify(require('read-package-tree')) | ||
const ssri = require('ssri') | ||
const stringifyPackage = require('stringify-package') | ||
const validate = require('aproba') | ||
const writeFileAtomic = require('write-file-atomic') | ||
const unixFormatPath = require('./utils/unix-format-path.js') | ||
const isRegistry = require('./utils/is-registry.js') | ||
|
||
const { chown } = require('fs') | ||
const inferOwner = require('infer-owner') | ||
const selfOwner = { | ||
uid: process.getuid && process.getuid(), | ||
gid: process.getgid && process.getgid() | ||
} | ||
|
||
const PKGLOCK = 'package-lock.json' | ||
const SHRINKWRAP = 'npm-shrinkwrap.json' | ||
const PKGLOCK_VERSION = npm.lockfileVersion | ||
|
||
// emit JSON describing versions of all packages currently installed (for later | ||
// use with shrinkwrap install) | ||
shrinkwrap.usage = 'npm shrinkwrap' | ||
|
||
module.exports = exports = shrinkwrap | ||
exports.treeToShrinkwrap = treeToShrinkwrap | ||
|
||
function shrinkwrap (args, silent, cb) { | ||
if (typeof cb !== 'function') { | ||
cb = silent | ||
silent = false | ||
} | ||
|
||
if (args.length) { | ||
log.warn('shrinkwrap', "doesn't take positional args") | ||
} | ||
|
||
move( | ||
path.resolve(npm.prefix, PKGLOCK), | ||
path.resolve(npm.prefix, SHRINKWRAP), | ||
{ Promise: BB } | ||
).then(() => { | ||
log.notice('', `${PKGLOCK} has been renamed to ${SHRINKWRAP}. ${SHRINKWRAP} will be used for future installations.`) | ||
return readFile(path.resolve(npm.prefix, SHRINKWRAP)).then((d) => { | ||
return JSON.parse(d) | ||
}) | ||
}, (err) => { | ||
if (err.code !== 'ENOENT') { | ||
throw err | ||
} else { | ||
return readPackageTree(npm.localPrefix).then( | ||
id.computeMetadata | ||
).then((tree) => { | ||
return BB.fromNode((cb) => { | ||
createShrinkwrap(tree, { | ||
silent, | ||
defaultFile: SHRINKWRAP | ||
}, cb) | ||
}) | ||
}) | ||
} | ||
}).then((data) => cb(null, data), cb) | ||
} | ||
|
||
module.exports.createShrinkwrap = createShrinkwrap | ||
|
||
function createShrinkwrap (tree, opts, cb) { | ||
opts = opts || {} | ||
lifecycle(tree.package, 'preshrinkwrap', tree.path, function () { | ||
const pkginfo = treeToShrinkwrap(tree) | ||
chain([ | ||
[lifecycle, tree.package, 'shrinkwrap', tree.path], | ||
[shrinkwrap_, tree.path, pkginfo, opts], | ||
[lifecycle, tree.package, 'postshrinkwrap', tree.path] | ||
], iferr(cb, function (data) { | ||
cb(null, pkginfo) | ||
})) | ||
}) | ||
} | ||
|
||
function treeToShrinkwrap (tree) { | ||
validate('O', arguments) | ||
var pkginfo = {} | ||
if (tree.package.name) pkginfo.name = tree.package.name | ||
if (tree.package.version) pkginfo.version = tree.package.version | ||
if (tree.children.length) { | ||
pkginfo.requires = true | ||
shrinkwrapDeps(pkginfo.dependencies = {}, tree, tree) | ||
} | ||
return pkginfo | ||
} | ||
|
||
function shrinkwrapDeps (deps, top, tree, seen) { | ||
validate('OOO', [deps, top, tree]) | ||
if (!seen) seen = new Set() | ||
if (seen.has(tree)) return | ||
seen.add(tree) | ||
sortModules(tree.children).forEach(function (child) { | ||
var childIsOnlyDev = isOnlyDev(child) | ||
var pkginfo = deps[moduleName(child)] = {} | ||
var requested = getRequested(child) || child.package._requested || {} | ||
var linked = child.isLink || child.isInLink | ||
pkginfo.version = childVersion(top, child, requested) | ||
if (requested.type === 'git' && child.package._from) { | ||
pkginfo.from = child.package._from | ||
} | ||
if (child.fromBundle && !linked) { | ||
pkginfo.bundled = true | ||
} else { | ||
if (isRegistry(requested)) { | ||
pkginfo.resolved = child.package._resolved | ||
} | ||
// no integrity for git deps as integrity hashes are based on the | ||
// tarball and we can't (yet) create consistent tarballs from a stable | ||
// source. | ||
if (requested.type !== 'git') { | ||
pkginfo.integrity = child.package._integrity || undefined | ||
if (!pkginfo.integrity && child.package._shasum) { | ||
pkginfo.integrity = ssri.fromHex(child.package._shasum, 'sha1') | ||
} | ||
} | ||
} | ||
if (childIsOnlyDev) pkginfo.dev = true | ||
if (isOnlyOptional(child)) pkginfo.optional = true | ||
if (child.requires.length) { | ||
pkginfo.requires = {} | ||
sortModules(child.requires).forEach((required) => { | ||
var requested = getRequested(required, child) || required.package._requested || {} | ||
pkginfo.requires[moduleName(required)] = childRequested(top, required, requested) | ||
}) | ||
} | ||
// iterate into children on non-links and links contained within the top level package | ||
if (child.children.length) { | ||
pkginfo.dependencies = {} | ||
shrinkwrapDeps(pkginfo.dependencies, top, child, seen) | ||
} | ||
}) | ||
} | ||
|
||
function sortModules (modules) { | ||
// sort modules with the locale-agnostic Unicode sort | ||
var sortedModuleNames = modules.map(moduleName).sort() | ||
return modules.sort((a, b) => ( | ||
sortedModuleNames.indexOf(moduleName(a)) - sortedModuleNames.indexOf(moduleName(b)) | ||
)) | ||
} | ||
const usageUtil = require('./utils/usage.js') | ||
const usage = usageUtil('shrinkwrap', 'npm shrinkwrap') | ||
const { resolve, basename } = require('path') | ||
const log = require('npmlog') | ||
|
||
function childVersion (top, child, req) { | ||
if (req.type === 'directory' || req.type === 'file') { | ||
return 'file:' + unixFormatPath(path.relative(top.path, child.package._resolved || req.fetchSpec)) | ||
} else if (!isRegistry(req) && !child.fromBundle) { | ||
return child.package._resolved || req.saveSpec || req.rawSpec | ||
} else if (req.type === 'alias') { | ||
return `npm:${child.package.name}@${child.package.version}` | ||
} else { | ||
return child.package.version | ||
const cmd = (args, cb) => shrinkwrap().then(() => cb()).catch(cb) | ||
|
||
const completion = (cb) => cb(null, []) | ||
|
||
const shrinkwrap = async () => { | ||
// if has a npm-shrinkwrap.json, nothing to do | ||
// if has a package-lock.json, rename to npm-shrinkwrap.json | ||
// if has neither, load the actual tree and save that as npm-shrinkwrap.json | ||
// in all cases, re-cast to current lockfile version | ||
// | ||
// loadVirtual, fall back to loadActual | ||
// rename shrinkwrap file type, and tree.meta.save() | ||
if (npm.flatOptions.global) { | ||
const er = new Error('`npm shrinkwrap` does not work for global packages') | ||
er.code = 'ESHRINKWRAPGLOBAL' | ||
throw er | ||
} | ||
} | ||
|
||
function childRequested (top, child, requested) { | ||
if (requested.type === 'directory' || requested.type === 'file') { | ||
return 'file:' + unixFormatPath(path.relative(top.path, child.package._resolved || requested.fetchSpec)) | ||
} else if (requested.type === 'git' && child.package._from) { | ||
return child.package._from | ||
} else if (!isRegistry(requested) && !child.fromBundle) { | ||
return child.package._resolved || requested.saveSpec || requested.rawSpec | ||
} else if (requested.type === 'tag') { | ||
// tags are not ranges we can match against, so we invent a "reasonable" | ||
// one based on what we actually installed. | ||
return npm.config.get('save-prefix') + child.package.version | ||
} else if (requested.saveSpec || requested.rawSpec) { | ||
return requested.saveSpec || requested.rawSpec | ||
} else if (child.package._from || (child.package._requested && child.package._requested.rawSpec)) { | ||
return child.package._from.replace(/^@?[^@]+@/, '') || child.package._requested.rawSpec | ||
const path = npm.prefix | ||
const sw = resolve(path, 'npm-shrinkwrap.json') | ||
const arb = new Arborist({ ...npm.flatOptions, path }) | ||
const tree = await arb.loadVirtual().catch(() => arb.loadActual()) | ||
const { meta } = tree | ||
const newFile = meta.hiddenLockfile || !meta.loadedFromDisk | ||
const oldFilename = meta.filename | ||
const notSW = !newFile && basename(oldFilename) !== 'npm-shrinkwrap.json' | ||
const { promises: { unlink } } = require('fs') | ||
|
||
meta.hiddenLockfile = false | ||
meta.filename = sw | ||
await meta.save() | ||
|
||
if (newFile) { | ||
log.notice('', 'created a lockfile as npm-shrinkwrap.json') | ||
} else if (notSW) { | ||
await unlink(oldFilename) | ||
log.notice('', 'package-lock.json has been renamed to npm-shrinkwrap.json') | ||
} else if (meta.originalLockfileVersion !== npm.lockfileVersion) { | ||
log.notice('', `npm-shrinkwrap.json updated to version ${npm.lockfileVersion}`) | ||
} else { | ||
return child.package.version | ||
} | ||
} | ||
|
||
function shrinkwrap_ (dir, pkginfo, opts, cb) { | ||
save(dir, pkginfo, opts, cb) | ||
} | ||
|
||
function save (dir, pkginfo, opts, cb) { | ||
// copy the keys over in a well defined order | ||
// because javascript objects serialize arbitrarily | ||
BB.join( | ||
checkPackageFile(dir, SHRINKWRAP), | ||
checkPackageFile(dir, PKGLOCK), | ||
checkPackageFile(dir, 'package.json'), | ||
(shrinkwrap, lockfile, pkg) => { | ||
const info = ( | ||
shrinkwrap || | ||
lockfile || | ||
{ | ||
path: path.resolve(dir, opts.defaultFile || PKGLOCK), | ||
data: '{}', | ||
indent: pkg && pkg.indent, | ||
newline: pkg && pkg.newline | ||
} | ||
) | ||
const updated = updateLockfileMetadata(pkginfo, pkg && JSON.parse(pkg.raw)) | ||
const swdata = stringifyPackage(updated, info.indent, info.newline) | ||
if (swdata === info.raw) { | ||
// skip writing if file is identical | ||
log.verbose('shrinkwrap', `skipping write for ${path.basename(info.path)} because there were no changes.`) | ||
cb(null, pkginfo) | ||
} else { | ||
inferOwner(info.path).then(owner => { | ||
writeFileAtomic(info.path, swdata, (err) => { | ||
if (err) return cb(err) | ||
if (opts.silent) return cb(null, pkginfo) | ||
if (!shrinkwrap && !lockfile) { | ||
log.notice('', `created a lockfile as ${path.basename(info.path)}. You should commit this file.`) | ||
} | ||
if (selfOwner.uid === 0 && (selfOwner.uid !== owner.uid || selfOwner.gid !== owner.gid)) { | ||
chown(info.path, owner.uid, owner.gid, er => cb(er, pkginfo)) | ||
} else { | ||
cb(null, pkginfo) | ||
} | ||
}) | ||
}) | ||
} | ||
} | ||
).then((file) => { | ||
}, cb) | ||
} | ||
|
||
function updateLockfileMetadata (pkginfo, pkgJson) { | ||
// This is a lot of work just to make sure the extra metadata fields are | ||
// between version and dependencies fields, without affecting any other stuff | ||
const newPkg = {} | ||
let metainfoWritten = false | ||
const metainfo = new Set([ | ||
'lockfileVersion', | ||
'preserveSymlinks' | ||
]) | ||
Object.keys(pkginfo).forEach((k) => { | ||
if (k === 'dependencies') { | ||
writeMetainfo(newPkg) | ||
} | ||
if (!metainfo.has(k)) { | ||
newPkg[k] = pkginfo[k] | ||
} | ||
if (k === 'version') { | ||
writeMetainfo(newPkg) | ||
} | ||
}) | ||
if (!metainfoWritten) { | ||
writeMetainfo(newPkg) | ||
} | ||
function writeMetainfo (pkginfo) { | ||
pkginfo.lockfileVersion = PKGLOCK_VERSION | ||
if (process.env.NODE_PRESERVE_SYMLINKS) { | ||
pkginfo.preserveSymlinks = process.env.NODE_PRESERVE_SYMLINKS | ||
} | ||
metainfoWritten = true | ||
log.notice('', 'npm-shrinkwrap.json up to date') | ||
} | ||
return newPkg | ||
} | ||
|
||
function checkPackageFile (dir, name) { | ||
const file = path.resolve(dir, name) | ||
return readFile( | ||
file, 'utf8' | ||
).then((data) => { | ||
const format = npm.config.get('format-package-lock') !== false | ||
const indent = format ? detectIndent(data).indent : 0 | ||
const newline = format ? detectNewline(data) : 0 | ||
|
||
return { | ||
path: file, | ||
raw: data, | ||
indent, | ||
newline | ||
} | ||
}).catch({code: 'ENOENT'}, () => {}) | ||
} | ||
module.exports = Object.assign(cmd, { usage, completion }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,80 +1,45 @@ | ||
'use strict' | ||
// XXX replace this with @npmcli/arborist | ||
// remove a package. | ||
|
||
module.exports = uninstall | ||
|
||
const path = require('path') | ||
const validate = require('aproba') | ||
const readJson = require('read-package-json') | ||
const iferr = require('iferr') | ||
const Arborist = require('@npmcli/arborist') | ||
const npm = require('./npm.js') | ||
const Installer = require('./install.js').Installer | ||
const getSaveType = require('./install/save.js').getSaveType | ||
const removeDeps = require('./install/deps.js').removeDeps | ||
const log = require('npmlog') | ||
const usage = require('./utils/usage') | ||
|
||
uninstall.usage = usage( | ||
'uninstall', | ||
'npm uninstall [<@scope>/]<pkg>[@<version>]... [--save-prod|--save-dev|--save-optional] [--no-save]' | ||
) | ||
const rpj = require('read-package-json-fast') | ||
const { resolve } = require('path') | ||
const usageUtil = require('./utils/usage.js') | ||
const reifyOutput = require('./utils/reify-output.js') | ||
|
||
uninstall.completion = require('./utils/completion/installed-shallow.js') | ||
const cmd = (args, cb) => rm(args).then(() => cb()).catch(cb) | ||
|
||
function uninstall (args, cb) { | ||
validate('AF', arguments) | ||
const rm = async args => { | ||
// the /path/to/node_modules/.. | ||
const dryrun = !!npm.config.get('dry-run') | ||
|
||
if (args.length === 1 && args[0] === '.') args = [] | ||
const { dryRun, global, prefix } = npm.flatOptions | ||
const path = global ? resolve(npm.globalDir, '..') : prefix | ||
|
||
if (!args.length) { | ||
if (!global) { | ||
throw new Error('must provide a package name to remove') | ||
} else { | ||
const pkg = await rpj(resolve(npm.localPrefix, 'package.json')) | ||
.catch(er => { | ||
throw er.code !== 'ENOENT' && er.code !== 'ENOTDIR' ? er : usage() | ||
}) | ||
args.push(pkg.name) | ||
} | ||
} | ||
|
||
const where = npm.config.get('global') || !args.length | ||
? path.resolve(npm.globalDir, '..') | ||
: npm.prefix | ||
const arb = new Arborist({ ...npm.flatOptions, path }) | ||
|
||
args = args.filter(function (a) { | ||
return path.resolve(a) !== where | ||
const tree = await arb.reify({ | ||
...npm.flatOptions, | ||
rm: args, | ||
}) | ||
|
||
if (args.length) { | ||
new Uninstaller(where, dryrun, args).run(cb) | ||
} else { | ||
// remove this package from the global space, if it's installed there | ||
readJson(path.resolve(npm.localPrefix, 'package.json'), function (er, pkg) { | ||
if (er && er.code !== 'ENOENT' && er.code !== 'ENOTDIR') return cb(er) | ||
if (er) return cb(uninstall.usage) | ||
new Uninstaller(where, dryrun, [pkg.name]).run(cb) | ||
}) | ||
} | ||
reifyOutput(arb) | ||
} | ||
|
||
class Uninstaller extends (class {}) { | ||
constructor (where, dryrun, args) { | ||
super(where, dryrun, args) | ||
this.remove = [] | ||
} | ||
|
||
loadArgMetadata (next) { | ||
this.args = this.args.map(function (arg) { return {name: arg} }) | ||
next() | ||
} | ||
|
||
loadAllDepsIntoIdealTree (cb) { | ||
validate('F', arguments) | ||
this.remove = this.args | ||
this.args = [] | ||
log.silly('uninstall', 'loadAllDepsIntoIdealTree') | ||
const saveDeps = getSaveType() | ||
|
||
super.loadAllDepsIntoIdealTree(iferr(cb, () => { | ||
removeDeps(this.remove, this.idealTree, saveDeps, cb) | ||
})) | ||
} | ||
const usage = usageUtil( | ||
'uninstall', | ||
'npm uninstall [<@scope>/]<pkg>[@<version>]... [--save-prod|--save-dev|--save-optional] [--no-save]' | ||
) | ||
|
||
// no top level lifecycles on rm | ||
runPreinstallTopLevelLifecycles (cb) { cb() } | ||
runPostinstallTopLevelLifecycles (cb) { cb() } | ||
} | ||
const completion = require('./utils/completion/installed-shallow.js') | ||
|
||
module.exports.Uninstaller = Uninstaller | ||
module.exports = Object.assign(cmd, { usage, completion }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// pass in an arborist object, and it'll output the data about what | ||
// was done, what was audited, etc. | ||
// | ||
// added 351 packages, removed 132 packages, and audited 13388 packages in 19.157s | ||
// | ||
// 1 package is looking for funding | ||
// run `npm fund` for details | ||
// | ||
// found 37 vulnerabilities (5 low, 7 moderate, 25 high) | ||
// run `npm audit fix` to fix them, or `npm audit` for details | ||
|
||
const npm = require('../npm.js') | ||
const log = require('npmlog') | ||
const output = log.level === 'silent' ? () => {} : require('./output.js') | ||
const { depth } = require('treeverse') | ||
const ms = require('ms') | ||
const auditReport = require('npm-audit-report') | ||
|
||
// TODO: output JSON if flatOptions.json is true | ||
const reifyOutput = arb => { | ||
const {diff, auditReport, actualTree} = arb | ||
|
||
const summary = { | ||
added: 0, | ||
removed: 0, | ||
changed: 0, | ||
audited: auditReport ? actualTree.inventory.size : 0, | ||
fund: 0 | ||
} | ||
|
||
depth({ | ||
tree: diff, | ||
visit: d => { | ||
switch (d.action) { | ||
case 'REMOVE': | ||
summary.removed ++ | ||
break | ||
case 'ADD': | ||
summary.added ++ | ||
break | ||
case 'CHANGE': | ||
summary.changed ++ | ||
break | ||
default: | ||
return | ||
} | ||
const node = d.actual || d.ideal | ||
log.silly(d.action, node.location) | ||
}, | ||
getChildren: d => d.children | ||
}) | ||
|
||
for (const node of actualTree.inventory.filter(n => n.package.funding)) { | ||
summary.fund ++ | ||
} | ||
|
||
if (npm.flatOptions.json) { | ||
if (arb.auditReport) { | ||
summary.audit = npm.command === 'audit' ? arb.auditReport | ||
: arb.auditReport.toJSON().metadata | ||
} | ||
output(JSON.stringify(summary, 0, 2)) | ||
} else { | ||
packagesChangedMessage(summary) | ||
packagesFundingMessage(summary) | ||
printAuditReport(arb) | ||
} | ||
} | ||
|
||
// if we're running `npm audit fix`, then we print the full audit report | ||
// at the end if there's still stuff, because it's silly for `npm audit` | ||
// to tell you to run `npm audit` for details. otherwise, use the summary | ||
// report. if we get here, we know it's not quiet or json. | ||
const printAuditReport = arb => { | ||
const reporter = npm.command !== 'audit' ? 'install' : 'detail' | ||
const res = auditReport(arb.auditReport, { | ||
reporter, | ||
...npm.flatOptions | ||
}) | ||
process.exitCode = process.exitCode || res.exitCode | ||
output('\n' + res.report) | ||
} | ||
|
||
const packagesChangedMessage = ({ added, removed, changed, audited }) => { | ||
const msg = ['\n'] | ||
if (added === 0 && removed === 0 && changed === 0) { | ||
msg.push('up to date') | ||
if (audited) { | ||
msg.push(', ') | ||
} | ||
} else { | ||
if (added) { | ||
msg.push(`added ${added} package${ added === 1 ? '' : 's' }`) | ||
} | ||
if (removed) { | ||
if (added) { | ||
msg.push(', ') | ||
} | ||
if (!audited && !changed) { | ||
msg.push('and ') | ||
} | ||
msg.push(`removed ${removed} package${ removed === 1 ? '' : 's' }`) | ||
} | ||
if (changed) { | ||
if (added || removed) { | ||
msg.push(', ') | ||
} | ||
if (!audited) { | ||
msg.push('and ') | ||
} | ||
msg.push(`changed ${changed} package${ changed === 1 ? '' : 's' }`) | ||
} | ||
if (audited) { | ||
msg.push(', and ') | ||
} | ||
} | ||
if (audited) { | ||
msg.push(`audited ${audited} package${ audited === 1 ? '' : 's' }`) | ||
} | ||
msg.push(` in ${ms(Date.now() - npm.started)}`) | ||
output(msg.join('')) | ||
} | ||
|
||
const packagesFundingMessage = ({ fund }) => { | ||
if (!fund) { | ||
return | ||
} | ||
|
||
output('') | ||
const pkg = fund === 1 ? 'package' : 'packages' | ||
const is = fund === 1 ? 'is' : 'are' | ||
output(`${fund} ${pkg} ${is} looking for funding`) | ||
output(' run `npm fund` for details') | ||
} | ||
|
||
module.exports = reifyOutput |
Oops, something went wrong.
182 changes: 168 additions & 14 deletions
182
node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js
Large diffs are not rendered by default.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
45 changes: 39 additions & 6 deletions
45
node_modules/@npmcli/arborist/lib/arborist/load-virtual.js
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
4 changes: 2 additions & 2 deletions
4
node_modules/@npmcli/arborist/node_modules/mkdirp/lib/opts-arg.js
Oops, something went wrong.
14 changes: 7 additions & 7 deletions
14
node_modules/@npmcli/arborist/node_modules/mkdirp/package.json
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
2 changes: 1 addition & 1 deletion
2
...cote/node_modules/npm-package-arg/LICENSE → node_modules/@npmcli/map-workspaces/LICENSE
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to be even more cautious and link to: https://github.com/npm/rfcs/blob/ea2d3024e6e149cd8c6366ed18373c9a566b1124/accepted/0019-remove-update-depth-option.md instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm! I considered that, but I figured if we ever update it or something, we'd want to have the user see the latest and greatest version, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would still prefer linking to the commit as I can see many possible problematic things happening:
latest
tomaster
, etcaccepted
folder?Although it seems unlikely in the near-future, I had been bitten by these sorts of changes in the past and I grew to really appreciate linking to the commit blob links instead, just by seeing these things actually happening 😊
but then again I also see that the chance of this message changing before any of the aforementioned happening is also really high 😄