dependabot-npm_and_yarn 0.91.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +14 -0
  3. data/helpers/npm/.eslintrc +14 -0
  4. data/helpers/npm/bin/run.js +34 -0
  5. data/helpers/npm/lib/helpers.js +25 -0
  6. data/helpers/npm/lib/peer-dependency-checker.js +102 -0
  7. data/helpers/npm/lib/subdependency-updater.js +48 -0
  8. data/helpers/npm/lib/updater.js +101 -0
  9. data/helpers/npm/package-lock.json +8868 -0
  10. data/helpers/npm/package.json +17 -0
  11. data/helpers/npm/test/fixtures/npm-left-pad.json +1 -0
  12. data/helpers/npm/test/fixtures/updater/original/package-lock.json +16 -0
  13. data/helpers/npm/test/fixtures/updater/original/package.json +9 -0
  14. data/helpers/npm/test/fixtures/updater/updated/package-lock.json +16 -0
  15. data/helpers/npm/test/helpers.js +7 -0
  16. data/helpers/npm/test/updater.test.js +50 -0
  17. data/helpers/npm/yarn.lock +6176 -0
  18. data/helpers/yarn/.eslintrc +14 -0
  19. data/helpers/yarn/bin/run.js +36 -0
  20. data/helpers/yarn/lib/fix-duplicates.js +78 -0
  21. data/helpers/yarn/lib/helpers.js +5 -0
  22. data/helpers/yarn/lib/lockfile-parser.js +21 -0
  23. data/helpers/yarn/lib/peer-dependency-checker.js +130 -0
  24. data/helpers/yarn/lib/replace-lockfile-declaration.js +57 -0
  25. data/helpers/yarn/lib/subdependency-updater.js +69 -0
  26. data/helpers/yarn/lib/updater.js +266 -0
  27. data/helpers/yarn/package.json +17 -0
  28. data/helpers/yarn/test/fixtures/updater/original/package.json +6 -0
  29. data/helpers/yarn/test/fixtures/updater/original/yarn.lock +11 -0
  30. data/helpers/yarn/test/fixtures/updater/updated/yarn.lock +12 -0
  31. data/helpers/yarn/test/fixtures/updater/with-version-comments/package.json +5 -0
  32. data/helpers/yarn/test/fixtures/updater/with-version-comments/yarn.lock +13 -0
  33. data/helpers/yarn/test/fixtures/yarnpkg-is-positive.json +1 -0
  34. data/helpers/yarn/test/fixtures/yarnpkg-left-pad.json +1 -0
  35. data/helpers/yarn/test/helpers.js +7 -0
  36. data/helpers/yarn/test/updater.test.js +93 -0
  37. data/helpers/yarn/yarn.lock +4760 -0
  38. data/lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb +146 -0
  39. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +332 -0
  40. data/lib/dependabot/npm_and_yarn/file_parser.rb +397 -0
  41. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +527 -0
  42. data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +190 -0
  43. data/lib/dependabot/npm_and_yarn/file_updater/package_json_preparer.rb +87 -0
  44. data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +218 -0
  45. data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +471 -0
  46. data/lib/dependabot/npm_and_yarn/file_updater.rb +189 -0
  47. data/lib/dependabot/npm_and_yarn/metadata_finder.rb +217 -0
  48. data/lib/dependabot/npm_and_yarn/native_helpers.rb +28 -0
  49. data/lib/dependabot/npm_and_yarn/requirement.rb +145 -0
  50. data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +340 -0
  51. data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +67 -0
  52. data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +224 -0
  53. data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +193 -0
  54. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +223 -0
  55. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +495 -0
  56. data/lib/dependabot/npm_and_yarn/update_checker.rb +282 -0
  57. data/lib/dependabot/npm_and_yarn/version.rb +34 -0
  58. data/lib/dependabot/npm_and_yarn.rb +11 -0
  59. metadata +226 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "plugins": [
3
+ "prettier"
4
+ ],
5
+ "rules": {
6
+ "prettier/prettier": "error"
7
+ },
8
+ "env": {
9
+ "node": true
10
+ },
11
+ "parserOptions": {
12
+ "ecmaVersion": 8
13
+ }
14
+ }
@@ -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,5 @@
1
+ function isString(value) {
2
+ return Object.prototype.toString.call(value) === "[object String]";
3
+ }
4
+
5
+ module.exports = { isString };
@@ -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 };