├── .gcloudignore ├── .gitignore ├── LICENSE ├── app.yaml ├── custom-search.png ├── favicon.ico ├── package.json ├── readme.md └── server.js /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | key.json 3 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Stephen Sawchuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs10 -------------------------------------------------------------------------------- /custom-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenplusplus/gitnpm/7f49499f7c0e83dc1c55ec466159b27bca897ffd/custom-search.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenplusplus/gitnpm/7f49499f7c0e83dc1c55ec466159b27bca897ffd/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitnpm", 3 | "version": "1.0.0", 4 | "private": "true", 5 | "description": "git urls from npm packages", 6 | "main": "index.js", 7 | "author": "Stephen Sawchuk ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@google-cloud/bigquery": "^2.0.2", 11 | "@google-cloud/datastore": "^2.0.0", 12 | "express": "^4.13.3", 13 | "github-url-from-git": "^1.4.0", 14 | "google-cloud-kvstore": "^5.0.0", 15 | "package-json": "^1.2.0", 16 | "through2": "^2.0.0", 17 | "validate-npm-package-name": "^2.2.2" 18 | }, 19 | "scripts": { 20 | "deploy": "gcloud app deploy", 21 | "test": "standard" 22 | }, 23 | "devDependencies": { 24 | "standard": "^5.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [gitnpm](http://gitnpm.com) 2 | an app that takes you to an npm package's repo 3 | - - - 4 | 5 | Several times a day, I wind up going to npmjs.org/package/some-package to dig through and find the GitHub url. I thought it would be nice to just get booted right to the place I need to go from one url. 6 | 7 | ## places you can go 8 | 9 | ### gitnpm.com 10 | *Example: http://gitnpm.com* 11 | 12 | ### gitnpm.com/{pkgName} 13 | *Example: http://gitnpm.com/gcloud* 14 | 15 | ### gitnpm.com/{pkgName}/json 16 | *Example: http://gitnpm.com/gcloud/json* 17 | 18 | ### gitnpm.com/{pkgName}/[version]/json 19 | *Example: http://gitnpm.com/gcloud/json* 20 | *Example: http://gitnpm.com/gcloud/0.36.0/json* 21 | 22 | ### gitnpm.com/{pkgName}/json/[version]/{property} 23 | *Example: http://gitnpm.com/gcloud/json/dependencies* 24 | *Example: http://gitnpm.com/gcloud/0.36.0/json/dependencies* 25 | 26 | ### gitnpm.com/{pkgName}/hits 27 | *Example: http://gitnpm.com/gcloud/hits* 28 | 29 | ## make gitnpm a custom search for quicker results 30 | 31 | ![Add gitnpm as a custom search engine](custom-search.png) 32 | 33 | ## under the hood 34 | 35 | The app is hosted with Google App Engine [Managed VMs](https://cloud.google.com/appengine/docs/managed-vms). [gcloud-node](https://github.com/GoogleCloudPlatform/gcloud-node) is used to interact connect to [Google Cloud Datastore](https://cloud.google.com/datastore/docs) (used as a [key-value store](https://github.com/stephenplusplus/gcloud-kvstore)) and [Google BigQuery](https://cloud.google.com/bigquery/what-is-bigquery). 36 | 37 | Each time the app is used, an entry in a Datastore dataset is created as well as a row inserted into a BigQuery dataset. These rows can be queried by going to the `gitnpm.com{pkgName}/hits` route to see when searches have been made for a package. 38 | 39 | Also, the favicon is mah baby. 40 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {BigQuery} = require('@google-cloud/bigquery') 4 | const Datastore = require('@google-cloud/datastore') 5 | const {KVStore} = require('google-cloud-kvstore') 6 | const express = require('express') 7 | const githubUrl = require('github-url-from-git') 8 | const packageJson = require('package-json') 9 | const through = require('through2') 10 | const validateNpmPackageName = require('validate-npm-package-name') 11 | 12 | const config = {projectId: 'git-npm', keyFilename: './key.json'} 13 | const datastore = new Datastore(config) 14 | const bigQuery = new BigQuery(config) 15 | 16 | const logDataset = new KVStore(datastore) 17 | const logTable = bigQuery.dataset('gitnpm').table('npm_packages') 18 | 19 | function parseUrl(pkg) { 20 | const repository = pkg.repository 21 | 22 | if (!repository) return 'https://npmjs.org/package/' + pkg.name 23 | if (repository.url) return githubUrl(repository.url) 24 | 25 | const hosts = { 26 | gist: { 27 | pattern: /gist:(\w+)/, 28 | getUrl: function (match) { 29 | return 'https://gist.github.com/' + match[1] 30 | } 31 | }, 32 | bitbucket: { 33 | pattern: /bitbucket:([^/]+)\/(.+)/, 34 | getUrl: function (match) { 35 | return 'https://bitbucket.org/' + match[1] + '/' + match[2] 36 | } 37 | }, 38 | gitlab: { 39 | pattern: /gitlab:([^/]+)\/(.+)/, 40 | getUrl: function (match) { 41 | return 'https://gitlab.com/' + match[1] + '/' + match[2] 42 | } 43 | }, 44 | github: { 45 | pattern: /([^/]+)\/(.+)/, 46 | getUrl: function (match) { 47 | return 'https://github.com/' + match[1] + '/' + match[2] 48 | } 49 | } 50 | } 51 | 52 | for (const host in hosts) { 53 | const pattern = hosts[host].pattern 54 | const getUrl = hosts[host].getUrl 55 | 56 | if (pattern.test(repository)) return getUrl(pattern.exec(repository)) 57 | } 58 | } 59 | 60 | function validatePkgName(req, res, next) { 61 | const isNameValid = validateNpmPackageName(req.params.pkgName) 62 | if (!isNameValid.validForNewPackages && !isNameValid.validForOldPackages) { 63 | return res.end('this looks funky. try something else') 64 | } 65 | next() 66 | } 67 | 68 | function getPkgInfo(req, res, next) { 69 | const pkgName = req.params.pkgName 70 | 71 | packageJson(pkgName, function (err, json) { 72 | if (err) return res.end(pkgName + ' isn\'t a thing... go make it?') 73 | 74 | const latestVersion = json['dist-tags'] && json['dist-tags'].latest 75 | res._pkgInfo = { 76 | all: json, 77 | latest: latestVersion ? json.versions[latestVersion] : {} 78 | } 79 | 80 | next() 81 | }) 82 | } 83 | 84 | const app = express() 85 | 86 | app 87 | .set('json spaces', 2) 88 | 89 | // display a form to accept a package name 90 | .get('/', function (req, res) { 91 | if (req.query.pkgName) return res.redirect('/' + req.query.pkgName) 92 | 93 | res.write('
') 94 | res.write('
') 95 | res.write(' $ npm repo') 96 | res.write(' ') 97 | res.end() 98 | }) 99 | 100 | // redirect to a package's github 101 | .get('/:pkgName', validatePkgName, getPkgInfo, function (req, res) { 102 | const pkgName = req.params.pkgName 103 | const pkg = res._pkgInfo 104 | const url = parseUrl(pkg.latest) 105 | 106 | res.redirect(url) 107 | 108 | logDataset.set(pkgName, url, console.log) 109 | logTable.insert({ name: pkgName, url: url, created: (new Date()).toJSON() }, console.log) 110 | }) 111 | 112 | .get('/:pkgName/json', validatePkgName, getPkgInfo, function (req, res) { 113 | const pkg = res._pkgInfo 114 | res.json(pkg.latest || pkg.all) 115 | res.end() 116 | }) 117 | .get('/:pkgName/:version/json', validatePkgName, getPkgInfo, function (req, res) { 118 | const pkg = res._pkgInfo 119 | const version = req.params.version.replace(/^v/, '') 120 | 121 | const json = pkg.all.versions[version] 122 | 123 | if (!json) { 124 | res.json(new Error('Could not load requested version')) 125 | } else { 126 | res.json(json) 127 | } 128 | 129 | res.end() 130 | }) 131 | 132 | .get('/:pkgName/json/:prop', validatePkgName, getPkgInfo, function (req, res) { 133 | const pkg = res._pkgInfo 134 | const prop = req.params.prop 135 | 136 | if (!pkg.latest) { 137 | res.json(new Error('Could not parse property')) 138 | } else { 139 | res.json(pkg.latest[prop]) 140 | } 141 | 142 | res.end() 143 | }) 144 | .get('/:pkgName/:version/json/:prop', validatePkgName, getPkgInfo, function (req, res) { 145 | const pkg = res._pkgInfo 146 | const version = req.params.version.replace(/^v/, '') 147 | const prop = req.params.prop 148 | 149 | const json = pkg.all.versions[version] 150 | 151 | if (!json) { 152 | res.json(new Error('Could not load requested version')) 153 | } else { 154 | res.json(json[prop]) 155 | } 156 | 157 | res.end() 158 | }) 159 | 160 | .get('/:pkgName/hits', validatePkgName, function (req, res) { 161 | const pkgName = req.params.pkgName 162 | 163 | res.write('

redirects from gitnpm.com/' + pkgName + '

') 164 | res.write('running query... ') 165 | 166 | logTable 167 | .query('SELECT * FROM npm_packages WHERE name="' + pkgName + '" ORDER BY created DESC') 168 | .pipe(through.obj(function (row, enc, next) { 169 | next(null, '

' + new Date(row.created * 1000) + '

') 170 | })) 171 | .on('prefinish', res.write.bind(res, 'done.')) 172 | .pipe(res) 173 | }) 174 | 175 | app.listen(process.env.PORT || 8080) 176 | --------------------------------------------------------------------------------