Skip to content
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

Add support for npm 7 lockfiles #3011

Merged
merged 52 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
3bc2310
Add npm7 helpers (using npm6)
feelepxyz Jan 18, 2021
4112870
WIP: npm7:update script
feelepxyz Jan 18, 2021
ec04bd9
Add npm7 js tests
feelepxyz Jan 18, 2021
269724a
Fix vscode debugging
feelepxyz Jan 18, 2021
32b7460
Add npm7 file updater spec
feelepxyz Jan 18, 2021
e145400
Share conflicting dependency parser between npm 6 and 7
feelepxyz Jan 18, 2021
0de3c29
Fix conflicting dependency checker
feelepxyz Jan 18, 2021
6b7c875
Add jest config for debugging
feelepxyz Jan 18, 2021
eb1fd16
Update peer dependency checker for npm 7
feelepxyz Jan 19, 2021
a987c03
set jest config
feelepxyz Jan 19, 2021
2acf9a6
Fix jest config
feelepxyz Jan 19, 2021
dde264b
Fix bad merge
feelepxyz Jan 20, 2021
f9b1e6c
Add npm 7 version resolver specs
feelepxyz Jan 20, 2021
0acc128
Update npm 7 file updater spec
feelepxyz Jan 20, 2021
ceb5048
Update subdependencies using npm7
jurre Jan 19, 2021
9212623
Add npm file updater error spec
feelepxyz Jan 20, 2021
b22b1be
Test NpmLockfileUpdater with npm7
jurre Jan 26, 2021
bc2e1cf
Ensure invalid refs return a sensible error in npm7
jurre Jan 27, 2021
27cab22
Use npm error message util to format errors
feelepxyz Feb 4, 2021
5ea67c0
remove old error message check
feelepxyz Jan 28, 2021
3a1b38c
Remove unused variable
feelepxyz Jan 28, 2021
cfaccd0
Set root dir to exclude helper tests
feelepxyz Jan 29, 2021
8cf8b7b
Fix npm lockfile specs
feelepxyz Jan 29, 2021
99bc5ad
Clarify comments and restore npm 6 spec
feelepxyz Jan 29, 2021
8880965
Fix jest config path
feelepxyz Feb 2, 2021
43af391
Add project fixtures for npm and yarn
feelepxyz Feb 4, 2021
911560d
Unlock git deps in npm 7 lockfiles
feelepxyz Feb 2, 2021
e95a541
Use project fixtures in file updater
feelepxyz Feb 4, 2021
2f86f95
Disable platform checks in the npm 7 updater
feelepxyz Feb 2, 2021
769298e
Fix npm 7 workspace support
feelepxyz Feb 2, 2021
8cdd85a
Install npm 7.5.2 in dockerfile
feelepxyz Feb 4, 2021
e87959d
Execute npm 7 cli directly
feelepxyz Feb 3, 2021
1ffee28
Hack install npm 7 after elm
feelepxyz Feb 4, 2021
957a1f4
Execute npm cli with execa to escape arguments
feelepxyz Feb 4, 2021
119145f
Improve comment updating npm in dockerfile
feelepxyz Feb 3, 2021
b590cc8
Remove unused helpers
feelepxyz Feb 3, 2021
0e4a39b
Update code comment
feelepxyz Feb 3, 2021
17ff842
Fix js spec
feelepxyz Feb 4, 2021
4d96a49
Remove npm 6 fix from npm 7 updater
feelepxyz Feb 4, 2021
272d4ca
Add git dependency specs for npm 7
feelepxyz Feb 4, 2021
6a79463
Fix npm 7 lockfile packages entries
feelepxyz Feb 4, 2021
2b0fcb5
Fix npm 7 js specs
feelepxyz Feb 4, 2021
d2f9810
Refactor restoring npm 7 lockfile packages
feelepxyz Feb 5, 2021
0e52ae8
Copy npm 6 specs to npm 7
feelepxyz Feb 5, 2021
3bd9de4
npm 7: package-lock-only updates ✨
feelepxyz Feb 5, 2021
7bea0c1
Merge remote-tracking branch 'origin/main' into feelepxyz/use-npm7
feelepxyz Feb 5, 2021
94daecf
Use shared helper to run npm 7 updates
feelepxyz Feb 8, 2021
bfbc137
Extract npm 7 updaters to their own methods
feelepxyz Feb 8, 2021
80c9954
Improve comments for npm lockfile updater
feelepxyz Feb 8, 2021
b74b038
Merge remote-tracking branch 'origin/main' into feelepxyz/use-npm7
feelepxyz Feb 8, 2021
720e663
Use memoized updated package json content for npm 7 install args
feelepxyz Feb 8, 2021
7971b82
Extract npm 7 check to method
feelepxyz Feb 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions npm_and_yarn/helpers/lib/npm6/updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ function flattenAllDependencies(manifest) {
);
}

// NOTE: Re-used in npm 7 updater
function installArgs(
depName,
desiredVersion,
Expand Down
9 changes: 9 additions & 0 deletions npm_and_yarn/helpers/lib/npm7/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const updater = require("./updater");
const peerDependencyChecker = require("./peer-dependency-checker");
const subdependencyUpdater = require("./subdependency-updater");

module.exports = {
update: updater.updateDependencyFiles,
updateSubdependency: subdependencyUpdater.updateDependencyFile,
checkPeerDependencies: peerDependencyChecker.checkPeerDependencies,
};
77 changes: 77 additions & 0 deletions npm_and_yarn/helpers/lib/npm7/peer-dependency-checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* PEER DEPENDENCY CHECKER
*
* Inputs:
* - directory containing a package.json and a yarn.lock
* - dependency name
* - new dependency version
* - requirements for this dependency
*
* Outputs:
* - successful completion, or an error if there are peer dependency warnings
*/

const npm = require("npm7");
const Arborist = require("@npmcli/arborist");
const { promisify } = require("util");

async function checkPeerDependencies(
directory,
depName,
desiredVersion,
requirements,
_topLevelDependencies // included for compatibility with npm 6 implementation
) {
await promisify(npm.load).bind(npm)();

// `ignoreScripts` is used to disable prepare and prepack scripts which are
// run when installing git dependencies
const arb = new Arborist({
...npm.flatOptions,
path: directory,
packageLockOnly: true,
dryRun: true,
save: false,
ignoreScripts: true,
engineStrict: false,
// NOTE: there seems to be no way to disable platform checks in arborist
// without force installing invalid peer dependencies
//
// TODO: ignore platform checks
force: false,
});

// Returns dep name and version for npm install, example: ["react@16.6.0"]
let args = installArgsWithVersion(depName, desiredVersion, requirements);

return await arb
.buildIdealTree({
add: args,
})
.catch((er) => {
if (er.code === "ERESOLVE") {
// NOTE: Emulate the error message in npm 6 for compatibility with the
// version resolver
const conflictingDependencies = [
`${er.edge.from.name}@${er.edge.from.version} requires a peer of ${er.current.name}@${er.edge.spec} but none is installed.`,
];
throw new Error(conflictingDependencies.join("\n"));
} else {
// NOTE: Hand over exception handling to the file updater. This is
// consistent with npm6 behaviour.
return [];
}
})
.then(() => []);
}

function installArgsWithVersion(depName, desiredVersion, reqs) {
const source = (reqs.find((req) => req.source) || {}).source;

if (source && source.type === "git") {
return [`${depName}@${source.url}#${desiredVersion}`];
} else {
return [`${depName}@${desiredVersion}`];
}
}

module.exports = { checkPeerDependencies };
39 changes: 39 additions & 0 deletions npm_and_yarn/helpers/lib/npm7/subdependency-updater.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const fs = require("fs");
const path = require("path");
const execa = require("execa");

const updateDependencyFile = async (directory, lockfileName, dependencies) => {
const dependencyNames = dependencies.map((dep) => dep.name);

try {
// NOTE:
// - `--dry-run=false` the updater sets a global .npmrc with dry-run: true to
// work around an issue in npm 6, we don't want that here
// - `--force` ignores checks for platform (os, cpu) and engines
// - `--ignore-scripts` disables prepare and prepack scripts which are run
// when installing git dependencies
await execa(
"npm",
[
"update",
...dependencyNames,
"--force",
"--dry-run",
"false",
"--ignore-scripts",
"--package-lock-only",
],
{ cwd: directory }
);
} catch (e) {
throw new Error(e.stderr);
}

const updatedLockfile = fs
.readFileSync(path.join(directory, lockfileName))
.toString();

return { [lockfileName]: updatedLockfile };
};

module.exports = { updateDependencyFile };
110 changes: 110 additions & 0 deletions npm_and_yarn/helpers/lib/npm7/updater.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* DEPENDENCY FILE UPDATER
*
* Inputs:
* - directory containing an up-to-date package.json and a package-lock.json to
* be updated
* - the name of the lockfile (package-lock.json or npm-shrinkwrap.json)
* - array of dependencies to be updated [{name, version, requirements}]
*
* Outputs:
* - updated package-lock.json
*
* Update the dependency to the version specified and rewrite the
* package-lock.json files.
*/
const fs = require("fs");
const path = require("path");
const execa = require("execa");

const updateDependencyFiles = async (directory, lockfileName, dependencies) => {
const manifest = JSON.parse(
fs.readFileSync(path.join(directory, "package.json")).toString()
);
const flattenedDependencies = flattenAllDependencies(manifest);
const args = dependencies.map((dependency) => {
const existingVersionRequirement = flattenedDependencies[dependency.name];
return installArgs(
dependency.name,
dependency.version,
dependency.requirements,
existingVersionRequirement
);
});

try {
// NOTE:
// - `--dry-run=false` the updater sets a global .npmrc with dry-run: true to
// work around an issue in npm 6, we don't want that here
// - `--force` ignores checks for platform (os, cpu) and engines
// - `--ignore-scripts` disables prepare and prepack scripts which are run
// when installing git dependencies
await execa(
"npm",
[
"install",
...args,
feelepxyz marked this conversation as resolved.
Show resolved Hide resolved
"--force",
"--dry-run",
"false",
"--ignore-scripts",
"--package-lock-only",
],
{ cwd: directory }
);
} catch (e) {
throw new Error(e.stderr);
}

const updatedLockfile = fs
.readFileSync(path.join(directory, lockfileName))
.toString();

return { [lockfileName]: updatedLockfile };
};

function flattenAllDependencies(manifest) {
return Object.assign(
{},
manifest.optionalDependencies,
manifest.peerDependencies,
manifest.devDependencies,
manifest.dependencies
);
}

// NOTE: Copied from npm 6 updater
function installArgs(
depName,
desiredVersion,
requirements,
existingVersionRequirement
) {
const source = (requirements.find((req) => req.source) || {}).source;

if (source && source.type === "git") {
if (!existingVersionRequirement) {
existingVersionRequirement = source.url;
}

// Git is configured to auth over https while updating
existingVersionRequirement = existingVersionRequirement.replace(
/git\+ssh:\/\/git@(.*?)[:/]/,
"git+https://$1/"
);

// Keep any semver range that has already been updated in the package
// requirement when installing the new version
if (existingVersionRequirement.match(desiredVersion)) {
return `${depName}@${existingVersionRequirement}`;
} else {
return `${depName}@${existingVersionRequirement.replace(
/#.*/,
""
)}#${desiredVersion}`;
}
} else {
return `${depName}@${desiredVersion}`;
}
}

module.exports = { updateDependencyFiles };
1 change: 1 addition & 0 deletions npm_and_yarn/helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@dependabot/yarn-lib": "^1.21.1",
"@npmcli/arborist": "^2.2.0",
"detect-indent": "^6.0.0",
"execa": "^5.0.0",
"npm6": "npm:npm@6.14.11",
"npm7": "npm:npm@7.4.0",
"semver": "^7.3.4"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const path = require("path");
const os = require("os");
const fs = require("fs");
const rimraf = require("rimraf");
const {
findConflictingDependencies,
} = require("../../lib/npm/conflicting-dependency-parser");
const helpers = require("./helpers");

describe("findConflictingDependencies", () => {
let tempDir;
beforeEach(() => {
tempDir = fs.mkdtempSync(os.tmpdir() + path.sep);
});
afterEach(() => rimraf.sync(tempDir));

it("finds conflicting dependencies", async () => {
helpers.copyDependencies("conflicting-dependency-parser/simple", tempDir);

const result = await findConflictingDependencies(tempDir, "abind", "2.0.0");
expect(result).toEqual([
{
explanation: "objnest@4.1.2 requires abind@^1.0.0",
name: "objnest",
version: "4.1.2",
requirement: "^1.0.0",
},
]);
});

it("finds the top-level conflicting dependency", async () => {
helpers.copyDependencies("conflicting-dependency-parser/nested", tempDir);

const result = await findConflictingDependencies(tempDir, "abind", "2.0.0");
expect(result).toEqual([
{
explanation: "askconfig@4.0.4 requires abind@^1.0.5 via objnest@5.1.0",
name: "objnest",
version: "5.1.0",
requirement: "^1.0.5",
},
]);
});

it("explains a deeply nested dependency", async () => {
helpers.copyDependencies(
"conflicting-dependency-parser/deeply-nested",
tempDir
);

const result = await findConflictingDependencies(tempDir, "abind", "2.0.0");
expect(result).toEqual([
{
explanation: "apass@1.1.0 requires abind@^1.0.0 via cipherjson@2.1.0",
name: "cipherjson",
version: "2.1.0",
requirement: "^1.0.0",
},
{
explanation: `apass@1.1.0 requires abind@^1.0.0 via a transitive dependency on objnest@3.0.9`,
name: "objnest",
version: "3.0.9",
requirement: "^1.0.0",
},
]);
});
});
Loading