Skip to content

Commit

Permalink
feat: use package manager to find peerDependencies
Browse files Browse the repository at this point in the history
Fixes #73

Removes a bunch of custom logic and leverages the "info"
command that all package managers provide for getting package
details from a remote registry. By using the package tool, we can
delegate proxy configs, private registry paths, and auth tokens
to the package manager. This is especially valuable in networks
that require complex proxy configurations, or when packages
have peerDependencies that may be in multiple other private
than the singular one that was specified

BREAKING_CHANGE: removes `--registry` `--auth` and `--proxy` options

Use `.npmrc` or `.yarnrc` to mange private registry paths, auth tokens, and proxy configs
  • Loading branch information
amclin authored and nathanhleung committed Jan 20, 2021
1 parent 770b881 commit 1b386d5
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 177 deletions.
31 changes: 1 addition & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ Options:
-Y, --yarn Install with Yarn
-P, --pnpm Install with pnpm
-n, --no-registry Do not use a remote registry to find dependencies list
-r, --registry <uri> Install from custom registry (defaults to NPM registry)
--dry-run Do not install packages, but show the install command that will be run
-a, --auth <token> Provide an NPM authToken for private packages.
-p, --proxy <http_proxy> Enable http proxy to connect to the registry
-x, --extra-args "<extra_args>" Extra arguments to pass through to NPM or Yarn
-h, --help output usage information
```
Expand All @@ -79,7 +76,7 @@ Only core Yarn and NPM arguments relating to package installation are officially

Here's how you'd use `--extra-args` to pass a custom NPM config option (in this case, disabling `strict-ssl` when accessing a custom registry over HTTPS):

`install-peerdeps <package> -p http://proxy:8080 --extra-args "--strict-ssl false"`
`install-peerdeps <package> --extra-args "--strict-ssl false"`

## Examples

Expand Down Expand Up @@ -118,32 +115,6 @@ yarn add v0.18.1
...
```

### Installing from a Custom Registry

To install from a custom registry, use the `--registry` option:

`install-peerdeps my-custom-package --registry https://registry.mycompany.com`.

### Installing a Private Package

To install a private npm package (either from npm or from a registry that uses an authorization header), use the auth option:

`install-peerdeps my-private-package --auth your-npm-auth-token`

### Proxies

To use this tool with a proxy, set the `HTTPS_PROXY` environment variable (if you're using a custom registry and it is only accessible over HTTP, though, set the `HTTP_PROXY` environment variable).

Under the hood, this package uses the `request` module to get package information from the registry and it spawns an NPM or Yarn child process for the actual installation.

`request` respects the `HTTP_PROXY` and `HTTPS_PROXY` environment variables, and `spawn` passes environment variables to the child process, so if you have the `PROXY` environment variables correctly set, everything should work. Nonetheless, proxy support is a new addition to this tool (added in v1.4.0), so please leave an issue if you have any problems.

`HTTPS_PROXY=https://proxy.mycompany.com/ install-peerdeps my-company-package`

Alternatively, you may use the `--proxy` flag like so:

`install-peerdeps my-company-package --proxy https://proxy.mycompany.com/`

## Contributing

See [CONTRIBUTING.md](https://github.com/nathanhleung/install-peerdeps/blob/master/CONTRIBUTING.md)
Expand Down
27 changes: 1 addition & 26 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,10 @@ program
"-n, --no-registry",
"Use local node_modules instead of a remote registry to get the list of peerDependencies"
)
.option(
"-r, --registry <uri>",
"Install from custom registry (defaults to NPM registry)"
)
.option(
"--dry-run",
"Do not install packages, but show the install command that will be run"
)
.option(
"-a, --auth <token>",
"Provide an NPM authToken for private packages."
)
.option(
"-p, --proxy <http_proxy>",
"Enable http proxy to connect to the registry"
)
.option(
'-x, --extra-args "<extra_args>"',
"Extra arguments to pass through to NPM or Yarn"
Expand Down Expand Up @@ -145,25 +133,13 @@ if (devMode && program.silent) {
process.exit(9);
}

if (program.registry) {
const { registry } = program;
// Check if last character in registry is a trailing slash
if (registry.substr(-1) === "/") {
// If the last character is a trailing slash, remove it
program.registry = registry.slice(0, -1);
}
}

// Define options object to pass to
// the installPeerDeps function
const options = {
packageName,
// If packageVersion is undefined, default to "latest"
version: packageVersion || "latest",
noRegistry: program.noRegistry,
// If registry is undefined, default to the official NPM registry
// See: https://docs.npmjs.com/using-npm/registry.html
registry: program.registry || "https://registry.npmjs.org",
dev: devMode,
global: program.global,
onlyPeers: program.onlyPeers,
Expand All @@ -172,8 +148,7 @@ const options = {
dryRun: program.dryRun,
auth: program.auth,
// Args after -- will be passed through
extraArgs: program.extraArgs || "",
proxy: program.proxy
extraArgs: program.extraArgs || ""
};

// Disabled this rule so we can hoist the callback
Expand Down
1 change: 0 additions & 1 deletion src/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ async function getCliInstallCommand(extraArgs) {
}
// The command will be the last non-whitespace line written to
// stdout by the cli during a dry run

const lines = fullstdout
.join("")
.split(/\r?\n/g)
Expand Down
171 changes: 67 additions & 104 deletions src/install-peerdeps.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import "@babel/polyfill";

import fs from "fs";
import request from "request-promise-native";
import HttpsProxyAgent from "https-proxy-agent";
import { spawn } from "child_process";
import { valid, coerce, maxSatisfying } from "semver";
import * as C from "./constants";
Expand All @@ -25,57 +23,6 @@ function encodePackageName(packageName) {
return encodedPackageName;
}

/**
* Gets metadata about the package from the provided registry
* @param {Object} requestInfo - information needed to make the request for the data
* @param {string} requestInfo.packageName - the name of the package
* @param {string} requestInfo.registry - the URI of the registry on which the package is hosted
* @returns {Promise<Object>} - a Promise which resolves to the JSON response from the registry
*/
function getPackageData({ packageName, registry, auth, proxy }) {
const requestHeaders = {};
if (auth) {
requestHeaders.Authorization = `Bearer ${auth}`;
}

const options = {
uri: `${registry}/${encodePackageName(packageName)}`,
resolveWithFullResponse: true,
// When simple is true, all non-200 status codes throw an
// error. However, we want to handle status code errors in
// the .then(), so we make simple false.
simple: false,
headers: requestHeaders
};

// If any proxy setting were passed then include the http proxy agent.
const requestProxy =
process.env.HTTP_PROXY || process.env.http_proxy || `${proxy}`;
if (requestProxy !== "undefined") {
options.agent = new HttpsProxyAgent(requestProxy);
}

return request(options).then(response => {
const { statusCode } = response;
if (statusCode === 404) {
throw new Error(
"That package doesn't exist. Did you mean to specify a custom registry?"
);
}

// If the statusCode not 200 or 404, assume that something must
// have gone wrong with the connection
if (statusCode !== 200) {
throw new Error(
`There was a problem connecting to the registry: status code ${statusCode}`
);
}
const { body } = response;
const parsedData = JSON.parse(body);
return parsedData;
});
}

/**
* Spawns a command to the shell
* @param {string} command - the command to spawn
Expand All @@ -86,22 +33,17 @@ const spawnCommand = (command, args) => {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";

const cmdProcess = spawn(command, args, {
cwd: process.cwd(),
// Something to do with this, progress bar only shows if stdio is inherit
// https://github.com/yarnpkg/yarn/issues/2200
stdio: "inherit"
cwd: process.cwd()
});

cmdProcess.stdout.on("data", chunk => {
stdout += chunk;
});
cmdProcess.stderr.on("data", chunk => {
stderr += chunk;
});

cmdProcess.on("error", reject).on("close", code => {
cmdProcess.on("error", reject);
cmdProcess.on("exit", code => {
if (code === 0) {
resolve(stdout);
} else {
Expand All @@ -111,23 +53,58 @@ const spawnCommand = (command, args) => {
});
};

/**
* Parse a registry manifest to get the best matching version
* @param {Object} requestInfo - information needed to make the request for the data
* @param {string} requestInfo.data - the data from the remote registry
* @param {string} requestInfo.version - the version (or version tag) to try to find
* @returns {string} - The best matching version number
*/
function findPackageVersion({ data, version }) {
// Get the max satisfying semver version
const versionToInstall = maxSatisfying(data.versions, version);
if (versionToInstall) {
return versionToInstall;
}

// When no matching semver, try named tags, like "latest"
if (data["dist-tags"][version]) {
return data["dist-tags"][version];
}

// No match
throw new Error("That version or tag does not exist.");
}

/**
* Gets metadata about the package from the provided registry
* @param {Object} requestInfo - information needed to make the request for the data
* @param {string} requestInfo.packageName - the name of the package
* @param {string} requestInfo.packageManager - the package manager to use (Yarn or npm)
* @param {string} requestInfo.version - the version (or version tag) to attempt to install
* @returns {Promise<Object>} - a Promise which resolves to the JSON response from the registry
*/
function getPackageData({ packageName, packageManager, version }) {
const pkgString = version ? `${packageName}@${version}` : packageName;
const args = ["info", pkgString, "--json"];
return spawnCommand(packageManager, args).then(response => {
const parsed = JSON.parse(response);
// Yarn returns with an extra nested { data } that NPM doesn't
return parsed.data || parsed;
});
}

/**
* Gets the contents of the package.json for a package at a specific version
* @param {Object} requestInfo - information needed to make the request for the data
* @param {string} requestInfo.packageName - the name of the package
* @param {Boolean} requestInfo.noRegistry - Gets the package dependencies list from the local node_modules instead of remote registry
* @param {string} requestInfo.registry - the URI of the registry on which the package is hosted
* @param {string} requestInfo.packageManager - the package manager to use (Yarn or npm)
* @param {string} requestInfo.version - the version (or version tag) to attempt to install. Ignored if an installed version of the package is found in node_modules.
* @returns {Promise<Object>} - a Promise which resolves to the JSON response from the registry
*/
function getPackageJson({
packageName,
noRegistry,
registry,
auth,
proxy,
version
}) {
function getPackageJson({ packageName, noRegistry, packageManager, version }) {
// Local package.json
if (noRegistry) {
if (fs.existsSync(`node_modules/${packageName}`)) {
return Promise.resolve(
Expand All @@ -137,29 +114,30 @@ function getPackageJson({
);
}
}
return getPackageData({ packageName, registry, auth, proxy }).then(data => {
const versions = Object.keys(data.versions);
// Get max satisfying semver version
let versionToInstall = maxSatisfying(versions, version);
// If we didn't find a version, maybe it's a tag
if (versionToInstall === null) {
const tags = Object.keys(data["dist-tags"]);
// If it's not a valid tag, throw an error
if (tags.indexOf(version) === -1) {
throw new Error("That version or tag does not exist.");
}
// If the tag is valid, then find the version corresponding to the tag
versionToInstall = data["dist-tags"][version];
}

return data.versions[versionToInstall];
});
// Remote registry
return getPackageData({ packageName, packageManager, version })
.then(data => {
return Promise.resolve(
findPackageVersion({
data,
version
})
);
})
.then(version => {
return getPackageData({
packageName,
packageManager,
version
});
});
}

/**
* Builds the package install string based on the version
* @param {Object} options - information needed to build a package install string
* @param {string} optoins.name - name of the package
* @param {string} options.name - name of the package
* @param {string} options.version - version string of the package
* @returns {string} - the package name and version formatted for an install command
*/
Expand Down Expand Up @@ -190,7 +168,6 @@ const getPackageString = ({ name, version }) => {
* @param {string} options.version - the version of the package
* @param {string} options.packageManager - the package manager to use (Yarn or npm)
* @param {string} options.noRegistry - Disable going to a remote registry to find a list of peers. Use local node_modules instead
* @param {string} options.registry - the URI of the registry to install from
* @param {string} options.dev - whether to install the dependencies as devDependencies
* @param {boolean} options.onlyPeers - whether to install the package itself or only its peers
* @param {boolean} options.silent - whether to save the new dependencies to package.json (NPM only)
Expand All @@ -204,19 +181,16 @@ function installPeerDeps(
version,
packageManager,
noRegistry,
registry,
dev,
global,
onlyPeers,
silent,
dryRun,
auth,
extraArgs,
proxy
extraArgs
},
cb
) {
getPackageJson({ packageName, noRegistry, registry, auth, proxy, version })
getPackageJson({ packageName, noRegistry, packageManager, version })
// Catch before .then because the .then is so long
.catch(err => cb(err))
.then(data => {
Expand Down Expand Up @@ -264,12 +238,6 @@ function installPeerDeps(
}

let args = [];
// If any proxy setting were passed then include the http proxy agent.
const requestProxy =
process.env.HTTP_PROXY || process.env.http_proxy || `${proxy}`;
if (requestProxy !== "undefined") {
args = args.concat(["--proxy", String(requestProxy)]);
}
// I know I can push it, but I'll just
// keep concatenating for consistency
// global must preceed add in yarn; npm doesn't care
Expand Down Expand Up @@ -312,11 +280,6 @@ function installPeerDeps(
args = args.concat("--no-save");
}

// If any registry were passed then include it
if (registry) {
args = args.concat(["--registry", String(registry)]);
}

// Pass extra args through
if (extraArgs !== "") {
args = args.concat(extraArgs);
Expand Down
Loading

0 comments on commit 1b386d5

Please sign in to comment.