dependabot-npm_and_yarn 0.91.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/helpers/build +14 -0
- data/helpers/npm/.eslintrc +14 -0
- data/helpers/npm/bin/run.js +34 -0
- data/helpers/npm/lib/helpers.js +25 -0
- data/helpers/npm/lib/peer-dependency-checker.js +102 -0
- data/helpers/npm/lib/subdependency-updater.js +48 -0
- data/helpers/npm/lib/updater.js +101 -0
- data/helpers/npm/package-lock.json +8868 -0
- data/helpers/npm/package.json +17 -0
- data/helpers/npm/test/fixtures/npm-left-pad.json +1 -0
- data/helpers/npm/test/fixtures/updater/original/package-lock.json +16 -0
- data/helpers/npm/test/fixtures/updater/original/package.json +9 -0
- data/helpers/npm/test/fixtures/updater/updated/package-lock.json +16 -0
- data/helpers/npm/test/helpers.js +7 -0
- data/helpers/npm/test/updater.test.js +50 -0
- data/helpers/npm/yarn.lock +6176 -0
- data/helpers/yarn/.eslintrc +14 -0
- data/helpers/yarn/bin/run.js +36 -0
- data/helpers/yarn/lib/fix-duplicates.js +78 -0
- data/helpers/yarn/lib/helpers.js +5 -0
- data/helpers/yarn/lib/lockfile-parser.js +21 -0
- data/helpers/yarn/lib/peer-dependency-checker.js +130 -0
- data/helpers/yarn/lib/replace-lockfile-declaration.js +57 -0
- data/helpers/yarn/lib/subdependency-updater.js +69 -0
- data/helpers/yarn/lib/updater.js +266 -0
- data/helpers/yarn/package.json +17 -0
- data/helpers/yarn/test/fixtures/updater/original/package.json +6 -0
- data/helpers/yarn/test/fixtures/updater/original/yarn.lock +11 -0
- data/helpers/yarn/test/fixtures/updater/updated/yarn.lock +12 -0
- data/helpers/yarn/test/fixtures/updater/with-version-comments/package.json +5 -0
- data/helpers/yarn/test/fixtures/updater/with-version-comments/yarn.lock +13 -0
- data/helpers/yarn/test/fixtures/yarnpkg-is-positive.json +1 -0
- data/helpers/yarn/test/fixtures/yarnpkg-left-pad.json +1 -0
- data/helpers/yarn/test/helpers.js +7 -0
- data/helpers/yarn/test/updater.test.js +93 -0
- data/helpers/yarn/yarn.lock +4760 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb +146 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher.rb +332 -0
- data/lib/dependabot/npm_and_yarn/file_parser.rb +397 -0
- data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +527 -0
- data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +190 -0
- data/lib/dependabot/npm_and_yarn/file_updater/package_json_preparer.rb +87 -0
- data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +218 -0
- data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +471 -0
- data/lib/dependabot/npm_and_yarn/file_updater.rb +189 -0
- data/lib/dependabot/npm_and_yarn/metadata_finder.rb +217 -0
- data/lib/dependabot/npm_and_yarn/native_helpers.rb +28 -0
- data/lib/dependabot/npm_and_yarn/requirement.rb +145 -0
- data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +340 -0
- data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +67 -0
- data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +224 -0
- data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +193 -0
- data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +223 -0
- data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +495 -0
- data/lib/dependabot/npm_and_yarn/update_checker.rb +282 -0
- data/lib/dependabot/npm_and_yarn/version.rb +34 -0
- data/lib/dependabot/npm_and_yarn.rb +11 -0
- metadata +226 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
const lockfileParser = require("../lib/lockfile-parser");
|
2
|
+
const updater = require("../lib/updater");
|
3
|
+
const subdependencyUpdater = require("../lib/subdependency-updater");
|
4
|
+
const peerDependencyChecker = require("../lib/peer-dependency-checker");
|
5
|
+
|
6
|
+
const functionMap = {
|
7
|
+
parseLockfile: lockfileParser.parse,
|
8
|
+
update: updater.updateDependencyFiles,
|
9
|
+
updateSubdependency: subdependencyUpdater.updateDependencyFile,
|
10
|
+
checkPeerDependencies: peerDependencyChecker.checkPeerDependencies
|
11
|
+
};
|
12
|
+
|
13
|
+
function output(obj) {
|
14
|
+
process.stdout.write(JSON.stringify(obj));
|
15
|
+
}
|
16
|
+
|
17
|
+
const input = [];
|
18
|
+
process.stdin.on("data", data => input.push(data));
|
19
|
+
process.stdin.on("end", () => {
|
20
|
+
const request = JSON.parse(input.join(""));
|
21
|
+
const func = functionMap[request.function];
|
22
|
+
if (!func) {
|
23
|
+
output({ error: `Invalid function ${request.function}` });
|
24
|
+
process.exit(1);
|
25
|
+
}
|
26
|
+
|
27
|
+
func
|
28
|
+
.apply(null, request.args)
|
29
|
+
.then(result => {
|
30
|
+
output({ result: result });
|
31
|
+
})
|
32
|
+
.catch(error => {
|
33
|
+
output({ error: error.message });
|
34
|
+
process.exit(1);
|
35
|
+
});
|
36
|
+
});
|
@@ -0,0 +1,78 @@
|
|
1
|
+
const parse = require("@dependabot/yarn-lib/lib/lockfile/parse").default;
|
2
|
+
const stringify = require("@dependabot/yarn-lib/lib/lockfile/stringify")
|
3
|
+
.default;
|
4
|
+
const semver = require("semver");
|
5
|
+
|
6
|
+
function flattenIndirectDependencies(packages) {
|
7
|
+
return (packages || []).reduce((acc, { pkg }) => {
|
8
|
+
if ("dependencies" in pkg) {
|
9
|
+
return acc.concat(Object.keys(pkg.dependencies));
|
10
|
+
}
|
11
|
+
return acc;
|
12
|
+
}, []);
|
13
|
+
}
|
14
|
+
|
15
|
+
// Inspired by yarn-deduplicate. Altered to ensure the latest version is always used
|
16
|
+
// for version ranges which allow it.
|
17
|
+
module.exports = (data, updatedDependencyName) => {
|
18
|
+
if (!updatedDependencyName) {
|
19
|
+
throw new Error("Yarn fix duplicates: must provide dependency name");
|
20
|
+
}
|
21
|
+
|
22
|
+
const json = parse(data).object;
|
23
|
+
const enableLockfileVersions = Boolean(data.match(/^# yarn v/m));
|
24
|
+
const noHeader = !Boolean(data.match(/^# THIS IS AN AU/m));
|
25
|
+
|
26
|
+
const packages = {};
|
27
|
+
const re = /^(.*)@([^@]*?)$/;
|
28
|
+
|
29
|
+
Object.entries(json).forEach(([name, pkg]) => {
|
30
|
+
if (name.match(re)) {
|
31
|
+
const [_, packageName, requestedVersion] = name.match(re);
|
32
|
+
packages[packageName] = packages[packageName] || [];
|
33
|
+
packages[packageName].push(
|
34
|
+
Object.assign({}, { name, pkg, packageName, requestedVersion })
|
35
|
+
);
|
36
|
+
}
|
37
|
+
});
|
38
|
+
|
39
|
+
const packageEntries = Object.entries(packages);
|
40
|
+
|
41
|
+
const updatedPackageEntry = packageEntries.filter(([name]) => {
|
42
|
+
return updatedDependencyName === name;
|
43
|
+
});
|
44
|
+
|
45
|
+
const updatedDependencyPackage =
|
46
|
+
updatedPackageEntry[0] && updatedPackageEntry[0][1];
|
47
|
+
|
48
|
+
const indirectDependencies = flattenIndirectDependencies(
|
49
|
+
updatedDependencyPackage
|
50
|
+
);
|
51
|
+
|
52
|
+
const packagesToDedupe = [updatedDependencyName, ...indirectDependencies];
|
53
|
+
|
54
|
+
packageEntries
|
55
|
+
.filter(([name]) => packagesToDedupe.includes(name))
|
56
|
+
.forEach(([name, packages]) => {
|
57
|
+
// Reverse sort, so we'll find the maximum satisfying version first
|
58
|
+
const versions = packages.map(p => p.pkg.version).sort(semver.rcompare);
|
59
|
+
const ranges = packages.map(p => p.requestedVersion);
|
60
|
+
|
61
|
+
// Dedup each package to its maxSatisfying version
|
62
|
+
packages.forEach(p => {
|
63
|
+
const targetVersion = semver.maxSatisfying(
|
64
|
+
versions,
|
65
|
+
p.requestedVersion
|
66
|
+
);
|
67
|
+
if (targetVersion === null) return;
|
68
|
+
if (targetVersion !== p.pkg.version) {
|
69
|
+
const dedupedPackage = packages.find(
|
70
|
+
p => p.pkg.version === targetVersion
|
71
|
+
);
|
72
|
+
json[`${name}@${p.requestedVersion}`] = dedupedPackage.pkg;
|
73
|
+
}
|
74
|
+
});
|
75
|
+
});
|
76
|
+
|
77
|
+
return stringify(json, noHeader, enableLockfileVersions);
|
78
|
+
};
|
@@ -0,0 +1,21 @@
|
|
1
|
+
/* YARN.LOCK PARSER
|
2
|
+
*
|
3
|
+
* Inputs:
|
4
|
+
* - directory containing a yarn.lock
|
5
|
+
*
|
6
|
+
* Outputs:
|
7
|
+
* - JSON formatted yarn.lock
|
8
|
+
*/
|
9
|
+
const fs = require("fs");
|
10
|
+
const path = require("path");
|
11
|
+
const parseLockfile = require("@dependabot/yarn-lib/lib/lockfile/parse")
|
12
|
+
.default;
|
13
|
+
|
14
|
+
async function parse(directory) {
|
15
|
+
const readFile = fileName =>
|
16
|
+
fs.readFileSync(path.join(directory, fileName)).toString();
|
17
|
+
const data = readFile("yarn.lock");
|
18
|
+
return parseLockfile(data).object;
|
19
|
+
}
|
20
|
+
|
21
|
+
module.exports = { parse };
|
@@ -0,0 +1,130 @@
|
|
1
|
+
/* PEER DEPENDENCY CHECKER
|
2
|
+
*
|
3
|
+
* Inputs:
|
4
|
+
* - directory containing a package.json and a yarn.lock
|
5
|
+
* - dependency name
|
6
|
+
* - new dependency version
|
7
|
+
* - requirements for this dependency
|
8
|
+
*
|
9
|
+
* Outputs:
|
10
|
+
* - successful completion, or an error if there are peer dependency warnings
|
11
|
+
*/
|
12
|
+
const path = require("path");
|
13
|
+
const { Add } = require("@dependabot/yarn-lib/lib/cli/commands/add");
|
14
|
+
const Config = require("@dependabot/yarn-lib/lib/config").default;
|
15
|
+
const { BufferReporter } = require("@dependabot/yarn-lib/lib/reporters");
|
16
|
+
const Lockfile = require("@dependabot/yarn-lib/lib/lockfile").default;
|
17
|
+
const { isString } = require("./helpers");
|
18
|
+
const fetcher = require("@dependabot/yarn-lib/lib/package-fetcher.js");
|
19
|
+
|
20
|
+
// Check peer dependencies without downloading node_modules or updating
|
21
|
+
// package/lockfiles
|
22
|
+
//
|
23
|
+
// Logic copied from the import command
|
24
|
+
class LightweightAdd extends Add {
|
25
|
+
async bailout() {
|
26
|
+
const manifests = await fetcher.fetch(
|
27
|
+
this.resolver.getManifests(),
|
28
|
+
this.config
|
29
|
+
);
|
30
|
+
this.resolver.updateManifests(manifests);
|
31
|
+
await this.linker.resolvePeerModules();
|
32
|
+
return true;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
function devRequirement(requirements) {
|
37
|
+
const groups = requirements.groups;
|
38
|
+
return (
|
39
|
+
groups.indexOf("devDependencies") > -1 &&
|
40
|
+
groups.indexOf("dependencies") == -1
|
41
|
+
);
|
42
|
+
}
|
43
|
+
|
44
|
+
function optionalRequirement(requirements) {
|
45
|
+
const groups = requirements.groups;
|
46
|
+
return (
|
47
|
+
groups.indexOf("optionalDependencies") > -1 &&
|
48
|
+
groups.indexOf("dependencies") == -1
|
49
|
+
);
|
50
|
+
}
|
51
|
+
|
52
|
+
function installArgsWithVersion(depName, desiredVersion, requirements) {
|
53
|
+
const source =
|
54
|
+
"source" in requirements
|
55
|
+
? requirements.source
|
56
|
+
: (requirements.find(req => req.source) || {}).source;
|
57
|
+
const req =
|
58
|
+
"requirement" in requirements
|
59
|
+
? requirements.requirement
|
60
|
+
: (requirements.find(req => req.requirement) || {}).requirement;
|
61
|
+
|
62
|
+
if (source && source.type === "git") {
|
63
|
+
if (desiredVersion) {
|
64
|
+
return [`${depName}@${source.url}#${desiredVersion}`];
|
65
|
+
} else {
|
66
|
+
return [`${depName}@${source.url}`];
|
67
|
+
}
|
68
|
+
} else {
|
69
|
+
return [`${depName}@${desiredVersion || req}`];
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
async function checkPeerDependencies(
|
74
|
+
directory,
|
75
|
+
depName,
|
76
|
+
desiredVersion,
|
77
|
+
requirements
|
78
|
+
) {
|
79
|
+
for (let req of requirements) {
|
80
|
+
await checkPeerDepsForReq(directory, depName, desiredVersion, req);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
async function checkPeerDepsForReq(
|
85
|
+
directory,
|
86
|
+
depName,
|
87
|
+
desiredVersion,
|
88
|
+
requirement
|
89
|
+
) {
|
90
|
+
const flags = {
|
91
|
+
ignoreScripts: true,
|
92
|
+
ignoreWorkspaceRootCheck: true,
|
93
|
+
ignoreEngines: true,
|
94
|
+
dev: devRequirement(requirement),
|
95
|
+
optional: optionalRequirement(requirement)
|
96
|
+
};
|
97
|
+
const reporter = new BufferReporter();
|
98
|
+
const config = new Config(reporter);
|
99
|
+
|
100
|
+
await config.init({
|
101
|
+
cwd: path.join(directory, path.dirname(requirement.file)),
|
102
|
+
nonInteractive: true,
|
103
|
+
enableDefaultRc: true
|
104
|
+
});
|
105
|
+
|
106
|
+
const lockfile = await Lockfile.fromDirectory(directory, reporter);
|
107
|
+
|
108
|
+
// Returns dep name and version for yarn add, example: ["react@16.6.0"]
|
109
|
+
let args = installArgsWithVersion(depName, desiredVersion, requirement);
|
110
|
+
|
111
|
+
// Just as if we'd run `yarn add package@version`, but using our lightweight
|
112
|
+
// implementation of Add that doesn't actually download and install packages
|
113
|
+
const add = new LightweightAdd(args, flags, config, reporter, lockfile);
|
114
|
+
|
115
|
+
await add.init();
|
116
|
+
|
117
|
+
const eventBuffer = reporter.getBuffer();
|
118
|
+
const peerDependencyWarnings = eventBuffer
|
119
|
+
.map(({ data }) => data)
|
120
|
+
.filter(data => {
|
121
|
+
// Guard against event.data sometimes being an object
|
122
|
+
return isString(data) && data.match(/(unmet|incorrect) peer dependency/);
|
123
|
+
});
|
124
|
+
|
125
|
+
if (peerDependencyWarnings.length) {
|
126
|
+
throw new Error(peerDependencyWarnings.join("\n"));
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
module.exports = { checkPeerDependencies };
|
@@ -0,0 +1,57 @@
|
|
1
|
+
const parse = require("@dependabot/yarn-lib/lib/lockfile/parse").default;
|
2
|
+
const stringify = require("@dependabot/yarn-lib/lib/lockfile/stringify")
|
3
|
+
.default;
|
4
|
+
|
5
|
+
// Get an array of a dependency's requested version ranges from a lockfile
|
6
|
+
function getRequestedVersions(depName, lockfileJson) {
|
7
|
+
const requestedVersions = [];
|
8
|
+
// TODO: Rethink this regex matching, for example, we don't currently match:
|
9
|
+
// @dependabot/pack-core@^git+ssh://git@github.com:dependabot/pack-core.git
|
10
|
+
const re = /^(.*)@([^@]*?)$/;
|
11
|
+
|
12
|
+
Object.entries(lockfileJson).forEach(([name, _]) => {
|
13
|
+
if (name.match(re)) {
|
14
|
+
const [_, packageName, requestedVersion] = name.match(re);
|
15
|
+
if (packageName === depName) {
|
16
|
+
requestedVersions.push(requestedVersion);
|
17
|
+
}
|
18
|
+
}
|
19
|
+
});
|
20
|
+
|
21
|
+
return requestedVersions;
|
22
|
+
}
|
23
|
+
|
24
|
+
module.exports = (
|
25
|
+
oldLockfileContent,
|
26
|
+
newLockfileContent,
|
27
|
+
depName,
|
28
|
+
newVersionRequirement,
|
29
|
+
existingVersionRequirement
|
30
|
+
) => {
|
31
|
+
const oldJson = parse(oldLockfileContent).object;
|
32
|
+
const newJson = parse(newLockfileContent).object;
|
33
|
+
|
34
|
+
const enableLockfileVersions = Boolean(
|
35
|
+
oldLockfileContent.match(/^# yarn v/m)
|
36
|
+
);
|
37
|
+
const noHeader = !Boolean(oldLockfileContent.match(/^# THIS IS AN AU/m));
|
38
|
+
|
39
|
+
const oldPackageReqs = getRequestedVersions(depName, oldJson);
|
40
|
+
const newPackageReqs = getRequestedVersions(depName, newJson);
|
41
|
+
|
42
|
+
const reqToReplace = newPackageReqs.find(pattern => {
|
43
|
+
return !oldPackageReqs.includes(pattern);
|
44
|
+
});
|
45
|
+
|
46
|
+
// If the new lockfile has entries that don't exist in the old lockfile,
|
47
|
+
// replace these version requirements with a range (will currently be an
|
48
|
+
// exact version because we tell yarn to install a specific version)
|
49
|
+
if (reqToReplace) {
|
50
|
+
newJson[
|
51
|
+
`${depName}@${newVersionRequirement || existingVersionRequirement}`
|
52
|
+
] = newJson[`${depName}@${reqToReplace}`];
|
53
|
+
delete newJson[`${depName}@${reqToReplace}`];
|
54
|
+
}
|
55
|
+
|
56
|
+
return stringify(newJson, noHeader, enableLockfileVersions);
|
57
|
+
};
|
@@ -0,0 +1,69 @@
|
|
1
|
+
/* DEPENDENCY FILE UPDATER
|
2
|
+
*
|
3
|
+
* Inputs:
|
4
|
+
* - directory containing a package.json and a yarn.lock
|
5
|
+
* - dependency name
|
6
|
+
*
|
7
|
+
* Outputs:
|
8
|
+
* - yarn.lock file
|
9
|
+
*
|
10
|
+
* Update the sub-dependency versions for this dependency to that latest
|
11
|
+
* possible versions, without unlocking any other dependencies
|
12
|
+
*/
|
13
|
+
const fs = require("fs");
|
14
|
+
const path = require("path");
|
15
|
+
const { Install } = require("@dependabot/yarn-lib/lib/cli/commands/install");
|
16
|
+
const Config = require("@dependabot/yarn-lib/lib/config").default;
|
17
|
+
const { EventReporter } = require("@dependabot/yarn-lib/lib/reporters");
|
18
|
+
const Lockfile = require("@dependabot/yarn-lib/lib/lockfile").default;
|
19
|
+
|
20
|
+
class LightweightInstall extends Install {
|
21
|
+
async bailout(patterns, workspaceLayout) {
|
22
|
+
await this.saveLockfileAndIntegrity(patterns, workspaceLayout);
|
23
|
+
return true;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
// Replace the version comments in the new lockfile with the ones from the old
|
28
|
+
// lockfile. If they weren't present in the old lockfile, delete them.
|
29
|
+
function recoverVersionComments(oldLockfile, newLockfile) {
|
30
|
+
const yarnRegex = /^# yarn v(\S+)\n/gm;
|
31
|
+
const nodeRegex = /^# node v(\S+)\n/gm;
|
32
|
+
const oldMatch = regex => [].concat(oldLockfile.match(regex))[0];
|
33
|
+
return newLockfile
|
34
|
+
.replace(yarnRegex, () => oldMatch(yarnRegex) || "")
|
35
|
+
.replace(nodeRegex, () => oldMatch(nodeRegex) || "");
|
36
|
+
}
|
37
|
+
|
38
|
+
async function updateDependencyFile(directory, lockfileName) {
|
39
|
+
const readFile = fileName =>
|
40
|
+
fs.readFileSync(path.join(directory, fileName)).toString();
|
41
|
+
const originalYarnLock = readFile(lockfileName);
|
42
|
+
|
43
|
+
const flags = {
|
44
|
+
ignoreScripts: true,
|
45
|
+
ignoreWorkspaceRootCheck: true,
|
46
|
+
ignoreEngines: true
|
47
|
+
};
|
48
|
+
const reporter = new EventReporter();
|
49
|
+
const config = new Config(reporter);
|
50
|
+
await config.init({
|
51
|
+
cwd: directory,
|
52
|
+
nonInteractive: true,
|
53
|
+
enableDefaultRc: true
|
54
|
+
});
|
55
|
+
config.enableLockfileVersions = Boolean(originalYarnLock.match(/^# yarn v/m));
|
56
|
+
|
57
|
+
const lockfile = await Lockfile.fromDirectory(directory, reporter);
|
58
|
+
const install = new LightweightInstall(flags, config, reporter, lockfile);
|
59
|
+
await install.init();
|
60
|
+
var updatedYarnLock = readFile(lockfileName);
|
61
|
+
|
62
|
+
updatedYarnLock = recoverVersionComments(originalYarnLock, updatedYarnLock);
|
63
|
+
|
64
|
+
return {
|
65
|
+
[lockfileName]: updatedYarnLock
|
66
|
+
};
|
67
|
+
}
|
68
|
+
|
69
|
+
module.exports = { updateDependencyFile };
|
@@ -0,0 +1,266 @@
|
|
1
|
+
/* DEPENDENCY FILE UPDATER
|
2
|
+
*
|
3
|
+
* Inputs:
|
4
|
+
* - directory containing a package.json and a yarn.lock
|
5
|
+
* - dependency name
|
6
|
+
* - new dependency version
|
7
|
+
* - new requirements for this dependency
|
8
|
+
*
|
9
|
+
* Outputs:
|
10
|
+
* - updated package.json and yarn.lock files
|
11
|
+
*
|
12
|
+
* Update the dependency to the version specified and rewrite the package.json
|
13
|
+
* and yarn.lock files.
|
14
|
+
*/
|
15
|
+
const fs = require("fs");
|
16
|
+
const path = require("path");
|
17
|
+
const { Add } = require("@dependabot/yarn-lib/lib/cli/commands/add");
|
18
|
+
const { Install } = require("@dependabot/yarn-lib/lib/cli/commands/install");
|
19
|
+
const {
|
20
|
+
cleanLockfile
|
21
|
+
} = require("@dependabot/yarn-lib/lib/cli/commands/upgrade");
|
22
|
+
const Config = require("@dependabot/yarn-lib/lib/config").default;
|
23
|
+
const { EventReporter } = require("@dependabot/yarn-lib/lib/reporters");
|
24
|
+
const Lockfile = require("@dependabot/yarn-lib/lib/lockfile").default;
|
25
|
+
const parse = require("@dependabot/yarn-lib/lib/lockfile/parse").default;
|
26
|
+
const fixDuplicates = require("./fix-duplicates");
|
27
|
+
const replaceDeclaration = require("./replace-lockfile-declaration");
|
28
|
+
const urlParse = require("url").parse;
|
29
|
+
|
30
|
+
// Add is a subclass of the Install CLI command, which is responsible for
|
31
|
+
// adding packages to a package.json and yarn.lock. Upgrading a package is
|
32
|
+
// exactly the same as adding, except the package already exists in the
|
33
|
+
// manifests.
|
34
|
+
//
|
35
|
+
// Usually, calling Add.init() would execute a series of steps: resolve, fetch,
|
36
|
+
// link, run lifecycle scripts, cleanup, then save new manifest (package.json).
|
37
|
+
// We only care about the first and last steps: resolve, then save the new
|
38
|
+
// manifest. Fotunately, overriding bailout() gives us an opportunity to skip
|
39
|
+
// over the intermediate steps in a relatively painless fashion.
|
40
|
+
class LightweightAdd extends Add {
|
41
|
+
// This method is called by init() at the end of the resolve step, and is
|
42
|
+
// responsible for checking if any dependnecies need to be updated locally.
|
43
|
+
// If everything is up to date, it'll save a new lockfile and return true,
|
44
|
+
// which causes init() to skip over the next few steps (fetching and
|
45
|
+
// installing packages). If there are packages that need updating, it'll
|
46
|
+
// return false, and init() will continue on to the fetching and installing
|
47
|
+
// steps.
|
48
|
+
//
|
49
|
+
// Add overrides Install's implementation to always return false - meaning
|
50
|
+
// that it will always continue to the fetch and install steps. We want to
|
51
|
+
// do the opposite - just save the new lockfile and stop there.
|
52
|
+
async bailout(patterns, workspaceLayout) {
|
53
|
+
// This is the only part of the original bailout implementation that
|
54
|
+
// matters: saving the new lockfile
|
55
|
+
await this.saveLockfileAndIntegrity(patterns, workspaceLayout);
|
56
|
+
|
57
|
+
// Skip over the unnecessary steps - fetching and linking packages, etc.
|
58
|
+
return true;
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
class LightweightInstall extends Install {
|
63
|
+
async bailout(patterns, workspaceLayout) {
|
64
|
+
await this.saveLockfileAndIntegrity(patterns, workspaceLayout);
|
65
|
+
return true;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
async function flattenAllDependencies(config) {
|
70
|
+
const manifest = await config.readRootManifest();
|
71
|
+
return Object.assign(
|
72
|
+
{},
|
73
|
+
manifest.optionalDependencies,
|
74
|
+
manifest.peerDependencies,
|
75
|
+
manifest.devDependencies,
|
76
|
+
manifest.dependencies
|
77
|
+
);
|
78
|
+
}
|
79
|
+
|
80
|
+
// Replace the version comments in the new lockfile with the ones from the old
|
81
|
+
// lockfile. If they weren't present in the old lockfile, delete them.
|
82
|
+
function recoverVersionComments(oldLockfile, newLockfile) {
|
83
|
+
const yarnRegex = /^# yarn v(\S+)\n/gm;
|
84
|
+
const nodeRegex = /^# node v(\S+)\n/gm;
|
85
|
+
const oldMatch = regex => [].concat(oldLockfile.match(regex))[0];
|
86
|
+
return newLockfile
|
87
|
+
.replace(yarnRegex, match => oldMatch(yarnRegex) || "")
|
88
|
+
.replace(nodeRegex, match => oldMatch(nodeRegex) || "");
|
89
|
+
}
|
90
|
+
|
91
|
+
function devRequirement(requirements) {
|
92
|
+
const groups = requirements.groups;
|
93
|
+
return (
|
94
|
+
groups.indexOf("devDependencies") > -1 &&
|
95
|
+
groups.indexOf("dependencies") == -1
|
96
|
+
);
|
97
|
+
}
|
98
|
+
|
99
|
+
function optionalRequirement(requirements) {
|
100
|
+
const groups = requirements.groups;
|
101
|
+
return (
|
102
|
+
groups.indexOf("optionalDependencies") > -1 &&
|
103
|
+
groups.indexOf("dependencies") == -1
|
104
|
+
);
|
105
|
+
}
|
106
|
+
|
107
|
+
const GIT_HOSTS = [
|
108
|
+
"github.com",
|
109
|
+
"gitlab.com",
|
110
|
+
"bitbucket.com",
|
111
|
+
"bitbucket.org"
|
112
|
+
];
|
113
|
+
|
114
|
+
function getGitShorthand(depName, url, lockfileJson) {
|
115
|
+
const { hostname, path } = urlParse(url);
|
116
|
+
const isSupportedHost = hostname && path && GIT_HOSTS.indexOf(hostname) >= 0;
|
117
|
+
if (!isSupportedHost) return;
|
118
|
+
|
119
|
+
const hostShorthand = hostname.replace(/\.[a-z]{3}$/, "");
|
120
|
+
const repoShorthand = path.replace(/^\//, "");
|
121
|
+
|
122
|
+
return [repoShorthand, `${hostShorthand}:${repoShorthand}`].find(
|
123
|
+
shorthand => {
|
124
|
+
return Object.keys(lockfileJson).some(name => {
|
125
|
+
return name.startsWith(`${depName}@${shorthand}`);
|
126
|
+
});
|
127
|
+
}
|
128
|
+
);
|
129
|
+
}
|
130
|
+
|
131
|
+
function installArgsWithVersion(
|
132
|
+
depName,
|
133
|
+
desiredVersion,
|
134
|
+
requirements,
|
135
|
+
lockfileJson
|
136
|
+
) {
|
137
|
+
const source = requirements.source;
|
138
|
+
|
139
|
+
// TODO: Use logic from npm updater to find original version instead of doing
|
140
|
+
// all this mad git shorthand logic
|
141
|
+
// e.g. const originalVersion = flattenAllDependencies(oldPackage)[depName];
|
142
|
+
if (source && source.type === "git") {
|
143
|
+
// Handle packages added using the github shorthand, e.g.
|
144
|
+
// - yarn add discord.js@discordjs/discord.js
|
145
|
+
//
|
146
|
+
// To keep the correct resolved url in the lockfile we need to explicitly
|
147
|
+
// tell yarn to install using the shorthand and not using the git url
|
148
|
+
//
|
149
|
+
// The resolved url from the shorthand case comes from
|
150
|
+
// https://codeload.github.com/org/repo.. whereas it comes from
|
151
|
+
// https://github.com/org/repo.. in the usual git install case:
|
152
|
+
// yarn add https://github.com/org/repo..
|
153
|
+
const repoShortHand = getGitShorthand(depName, source.url, lockfileJson);
|
154
|
+
if (repoShortHand) {
|
155
|
+
return [`${depName}@${repoShortHand}#${desiredVersion}`];
|
156
|
+
} else {
|
157
|
+
return [`${depName}@${source.url}#${desiredVersion}`];
|
158
|
+
}
|
159
|
+
} else {
|
160
|
+
return [`${depName}@${desiredVersion}`];
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
async function updateDependencyFiles(directory, dependencies) {
|
165
|
+
const readFile = fileName =>
|
166
|
+
fs.readFileSync(path.join(directory, fileName)).toString();
|
167
|
+
let updateRunResults = { "yarn.lock": readFile("yarn.lock") };
|
168
|
+
|
169
|
+
for (let dep of dependencies) {
|
170
|
+
for (let reqs of dep.requirements) {
|
171
|
+
updateRunResults = Object.assign(
|
172
|
+
updateRunResults,
|
173
|
+
await updateDependencyFile(directory, dep.name, dep.version, reqs)
|
174
|
+
);
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
return updateRunResults;
|
179
|
+
}
|
180
|
+
|
181
|
+
async function updateDependencyFile(
|
182
|
+
directory,
|
183
|
+
depName,
|
184
|
+
desiredVersion,
|
185
|
+
requirements
|
186
|
+
) {
|
187
|
+
const readFile = fileName =>
|
188
|
+
fs.readFileSync(path.join(directory, fileName)).toString();
|
189
|
+
const originalYarnLock = readFile("yarn.lock");
|
190
|
+
const originalPackageJson = readFile(requirements.file);
|
191
|
+
|
192
|
+
const flags = {
|
193
|
+
ignoreScripts: true,
|
194
|
+
ignoreWorkspaceRootCheck: true,
|
195
|
+
ignoreEngines: true,
|
196
|
+
dev: devRequirement(requirements),
|
197
|
+
optional: optionalRequirement(requirements)
|
198
|
+
};
|
199
|
+
const reporter = new EventReporter();
|
200
|
+
const config = new Config(reporter);
|
201
|
+
await config.init({
|
202
|
+
cwd: path.join(directory, path.dirname(requirements.file)),
|
203
|
+
nonInteractive: true,
|
204
|
+
enableDefaultRc: true
|
205
|
+
});
|
206
|
+
config.enableLockfileVersions = Boolean(originalYarnLock.match(/^# yarn v/m));
|
207
|
+
|
208
|
+
const lockfile = await Lockfile.fromDirectory(directory, reporter);
|
209
|
+
|
210
|
+
// Just as if we'd run `yarn add package@version`, but using our lightweight
|
211
|
+
// implementation of Add that doesn't actually download and install packages
|
212
|
+
const lockfileJson = parse(originalYarnLock).object;
|
213
|
+
const args = installArgsWithVersion(
|
214
|
+
depName,
|
215
|
+
desiredVersion,
|
216
|
+
requirements,
|
217
|
+
lockfileJson
|
218
|
+
);
|
219
|
+
|
220
|
+
const add = new LightweightAdd(args, flags, config, reporter, lockfile);
|
221
|
+
|
222
|
+
// Despite the innocent-sounding name, this actually does all the hard work
|
223
|
+
await add.init();
|
224
|
+
|
225
|
+
const dedupedYarnLock = fixDuplicates(readFile("yarn.lock"), depName);
|
226
|
+
|
227
|
+
const newVersionRequirement = requirements.requirement;
|
228
|
+
|
229
|
+
const flattenedDependencies = await flattenAllDependencies(config);
|
230
|
+
const existingVersionRequirement = flattenedDependencies[depName];
|
231
|
+
|
232
|
+
// Replace the version requirement in the lockfile (which will currently be an
|
233
|
+
// exact version, not a requirement range)
|
234
|
+
// If we don't have new requirement (e.g. git source) use the existing version
|
235
|
+
// requirement from the package manifest
|
236
|
+
const replacedDeclarationYarnLock = replaceDeclaration(
|
237
|
+
originalYarnLock,
|
238
|
+
dedupedYarnLock,
|
239
|
+
depName,
|
240
|
+
newVersionRequirement,
|
241
|
+
existingVersionRequirement
|
242
|
+
);
|
243
|
+
|
244
|
+
// Do a normal install to ensure the lockfile doesn't change when we do
|
245
|
+
fs.writeFileSync(
|
246
|
+
path.join(directory, "yarn.lock"),
|
247
|
+
replacedDeclarationYarnLock
|
248
|
+
);
|
249
|
+
fs.writeFileSync(
|
250
|
+
path.join(directory, requirements.file),
|
251
|
+
originalPackageJson
|
252
|
+
);
|
253
|
+
|
254
|
+
const lockfile2 = await Lockfile.fromDirectory(directory, reporter);
|
255
|
+
const install2 = new LightweightInstall(flags, config, reporter, lockfile2);
|
256
|
+
await install2.init();
|
257
|
+
|
258
|
+
let updatedYarnLock = readFile("yarn.lock");
|
259
|
+
updatedYarnLock = recoverVersionComments(originalYarnLock, updatedYarnLock);
|
260
|
+
|
261
|
+
return {
|
262
|
+
"yarn.lock": updatedYarnLock
|
263
|
+
};
|
264
|
+
}
|
265
|
+
|
266
|
+
module.exports = { updateDependencyFiles };
|