├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── cli.js ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 21 15 | - 20 16 | - 18 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from 'meow'; 4 | import {readPackageUp} from 'read-package-up'; 5 | import open from 'open'; 6 | import packageJson, {PackageNotFoundError} from 'package-json'; 7 | import repoUrlFromPackage from 'repo-url-from-package'; 8 | import logSymbols from 'log-symbols'; 9 | import pMap from 'p-map'; 10 | 11 | const cli = meow(` 12 | Usage 13 | $ npm-home [name] […] 14 | $ nh [name] […] 15 | 16 | Options 17 | --github -g Open the GitHub repo of the package 18 | --yarn -y Open the Yarn homepage of the package 19 | 20 | Examples 21 | $ npm-home 22 | $ npm-home chalk -g 23 | $ npm-home execa ava -y 24 | `, { 25 | importMeta: import.meta, 26 | flags: { 27 | github: { 28 | type: 'boolean', 29 | shortFlag: 'g', 30 | }, 31 | yarn: { 32 | type: 'boolean', 33 | shortFlag: 'y', 34 | }, 35 | }, 36 | }); 37 | 38 | const openNpm = async name => open(`https://www.npmjs.com/package/${name}`); 39 | const openYarn = async name => open(`https://yarnpkg.com/package/?name=${name}`); 40 | 41 | const openNpmOrYarn = cli.flags.yarn ? openYarn : openNpm; 42 | 43 | const openGitHub = async name => { 44 | try { 45 | const packageData = await packageJson(name, {fullMetadata: true}); 46 | const {repository} = packageData; 47 | 48 | if (!repository) { 49 | await openNpmOrYarn(name); 50 | return; 51 | } 52 | 53 | const {url, warnings} = repoUrlFromPackage(packageData); 54 | 55 | for (const warning of warnings) { 56 | console.error(`${logSymbols.error} ${warning}`); 57 | } 58 | 59 | if (url) { 60 | await open(url); 61 | return; 62 | } 63 | 64 | if (packageData.homepage) { 65 | console.log(`${logSymbols.warning} Falling back to \`homepage\` field.`); 66 | await open(packageData.homepage); 67 | } else { 68 | console.error(`${logSymbols.error} No \`repository\` or \`homepage\` field found in package.json. Please open an issue or pull request on ${name}`); 69 | } 70 | } catch (error) { 71 | if (error.code === 'ENOTFOUND') { 72 | console.error(`${logSymbols.error} No network connection detected!`); 73 | process.exitCode = 1; 74 | return; 75 | } 76 | 77 | if (error instanceof PackageNotFoundError) { 78 | console.error(`${logSymbols.error} ${name} - package not found!`); 79 | process.exitCode = 1; 80 | return; 81 | } 82 | 83 | throw error; 84 | } 85 | }; 86 | 87 | const openPackages = async names => pMap(names, async name => { 88 | if (cli.flags.github) { 89 | await openGitHub(name); 90 | return; 91 | } 92 | 93 | await openNpmOrYarn(name); 94 | }, {concurrency: 5}); 95 | 96 | if (cli.input.length > 0) { 97 | await openPackages(cli.input); 98 | } else { 99 | const result = await readPackageUp(); 100 | 101 | if (!result) { 102 | console.error('You\'re not in an npm package'); 103 | process.exit(1); 104 | } 105 | 106 | await openPackages([result.packageJson.name]); 107 | } 108 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-home", 3 | "version": "4.1.0", 4 | "description": "Open the npm page, Yarn page, or GitHub repo of a package", 5 | "license": "MIT", 6 | "repository": "sindresorhus/npm-home", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "bin": { 14 | "npm-home": "./cli.js", 15 | "nh": "./cli.js" 16 | }, 17 | "type": "module", 18 | "engines": { 19 | "node": ">=18" 20 | }, 21 | "scripts": { 22 | "test": "xo && ava" 23 | }, 24 | "files": [ 25 | "cli.js" 26 | ], 27 | "keywords": [ 28 | "cli-app", 29 | "cli", 30 | "npm", 31 | "package", 32 | "page", 33 | "home", 34 | "open", 35 | "cwd", 36 | "current", 37 | "directory", 38 | "github", 39 | "gh", 40 | "repo", 41 | "repository", 42 | "yarn" 43 | ], 44 | "dependencies": { 45 | "log-symbols": "^6.0.0", 46 | "meow": "^13.2.0", 47 | "open": "^10.1.0", 48 | "p-map": "^7.0.2", 49 | "package-json": "^10.0.1", 50 | "read-package-up": "^11.0.0", 51 | "repo-url-from-package": "^0.1.0" 52 | }, 53 | "devDependencies": { 54 | "ava": "^6.1.3", 55 | "execa": "^9.3.0", 56 | "xo": "^0.59.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # npm-home 2 | 3 | > Open the npm page, Yarn page, or GitHub repo of a package 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install --global npm-home 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```sh 14 | $ npm-home --help 15 | 16 | Usage 17 | $ npm-home [name] […] 18 | $ nh [name] […] 19 | 20 | Options 21 | --github -g Open the GitHub repo of the package 22 | --yarn -y Open the Yarn homepage of the package 23 | 24 | Examples 25 | $ npm-home 26 | $ npm-home chalk -g 27 | $ npm-home execa ava -y 28 | ``` 29 | 30 | ## Related 31 | 32 | - [gh-home](https://github.com/sindresorhus/gh-home) - Open the GitHub page of the given or current directory repo 33 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {execa} from 'execa'; 3 | 4 | // Tests only checks that opening doesn't return an error, not that the correct page was opened. 5 | // These have to be manually verified. 6 | 7 | const testCli = test.macro(async (t, arguments_ = []) => { 8 | await t.notThrowsAsync(execa('./cli.js', arguments_)); 9 | }); 10 | 11 | test('main', testCli); 12 | test('named package', testCli, ['chalk']); 13 | test('multiple packages', testCli, ['execa', 'ava']); 14 | 15 | for (const flag of ['--github', '-g']) { 16 | test(`github: ${flag}`, testCli, [flag]); 17 | test(`named package - github: ${flag}`, testCli, [flag, 'chalk']); 18 | test(`multiple packages - github: ${flag}`, testCli, [flag, 'execa', 'ava']); 19 | 20 | test(`github - does not error on missing package: ${flag}`, async t => { 21 | // https://github.com/npm/validate-npm-package-name#naming-rules 22 | const {stderr} = await t.throwsAsync(execa('./cli.js', [flag, '~invalid~'])); 23 | t.is(stderr, '✖ ~invalid~ - package not found!'); 24 | }); 25 | 26 | test(`github - invalid repository warning: ${flag}`, async t => { 27 | const {stderr} = await execa('./cli.js', [flag, 'babel-preset-minify']); // From #5 28 | t.is(stderr, '✖ The `repository` field in package.json should point to a Git repo and not a website. Please open an issue or pull request on `babel-preset-minify`.'); 29 | }); 30 | } 31 | 32 | for (const flag of ['--yarn', '-y']) { 33 | test(`yarn: ${flag}`, testCli, [flag]); 34 | test(`named package - yarn: ${flag}`, testCli, [flag, 'chalk']); 35 | test(`multiple packages - yarn: ${flag}`, testCli, [flag, 'execa', 'ava']); 36 | } 37 | --------------------------------------------------------------------------------