├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── HISTORY.md
├── LICENSE.md
├── Readme.md
├── package.json
├── server.js
├── src
├── client.js
├── codemod-repo.js
├── console.js
├── db.js
├── get-repos-for-user.js
├── index.js
├── process-all-users.js
├── process-user.js
└── read-client.js
└── test
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["forbeslindesay"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | server.js
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: "forbeslindesay",
3 | rules: {
4 | 'no-unused-vars': [0],
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Coverage directory used by tools like istanbul
11 | coverage
12 |
13 | # Compiled binary addons (http://nodejs.org/api/addons.html)
14 | build/Release
15 |
16 | # Dependency directory
17 | node_modules
18 |
19 | # Users Environment Variables
20 | .lock-wscript
21 |
22 | # Babel build output
23 | /lib
24 |
25 | # Config files
26 | environment.toml
27 |
28 | # Temporrary file for repos being code-modded
29 | /temp
30 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | sudo: false
4 |
5 | node_js:
6 | - "6.5.0"
7 |
8 | deploy:
9 | provider: script
10 | script: npm run deploy
11 | on:
12 | branch: master
13 | node_js: 6.5.0
14 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.0.1: 2016-xx-xx
4 |
5 | - Initial release
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright (c) 2016 [Forbes Lindesay](https://github.com/ForbesLindesay)
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
13 | > all 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 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # unpkg-bot
2 |
3 | Bot to convert npmcdn urls to unpkg.
4 |
5 | Log in to https://unpkg-bot.herokuapp.com/ to:
6 |
7 | 1. get pull requests for any of your repos that need updating
8 | 2. lend your access tokens/rate limit towards updating all the other github users
9 |
10 | [](https://travis-ci.org/ForbesLindesay/unpkg-bot)
11 | [](http://david-dm.org/ForbesLindesay/unpkg-bot)
12 | [](https://www.npmjs.org/package/unpkg-bot)
13 |
14 | ## Installation
15 |
16 | ```
17 | npm install unpkg-bot --save
18 | ```
19 |
20 | ## Usage
21 |
22 | ```js
23 | var unpkgBot = require('unpkg-bot');
24 |
25 | // ...
26 | ```
27 |
28 | ## License
29 |
30 | MIT
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unpkg-bot",
3 | "private": true,
4 | "version": "0.0.0",
5 | "main": "lib/index.js",
6 | "description": "Bot to convert npmcdn urls to unpkg",
7 | "keywords": [],
8 | "files": [
9 | "lib/"
10 | ],
11 | "dependencies": {
12 | "babel-runtime": "^6.3.19",
13 | "cookie-session": "^2.0.0-alpha.1",
14 | "deck": "0.0.4",
15 | "express": "^4.14.0",
16 | "gethub": "^2.0.2",
17 | "github-basic": "^6.0.0",
18 | "lsr": "^1.0.0",
19 | "ms": "^0.7.1",
20 | "passport": "^0.3.2",
21 | "passport-github2": "^0.1.10",
22 | "prepare-response": "^1.1.3",
23 | "promise": "^7.1.1",
24 | "rimraf": "^2.5.4",
25 | "then-mongo": "^2.3.2",
26 | "then-request": "^2.2.0",
27 | "throat": "^3.0.0"
28 | },
29 | "devDependencies": {
30 | "babel-cli": "*",
31 | "babel-preset-forbeslindesay": "*",
32 | "babel-register": "^6.14.0",
33 | "babelify": "^7.3.0",
34 | "browserify": "^13.1.0",
35 | "browserify-middleware": "^7.0.0",
36 | "envify": "^3.4.1",
37 | "eslint": "*",
38 | "eslint-config-forbeslindesay": "*",
39 | "react": "^15.3.1",
40 | "react-dom": "^15.3.1",
41 | "testit": "*",
42 | "uglify-js": "^2.7.3"
43 | },
44 | "scripts": {
45 | "deploy": "npm install && npm run build && npm prune --prod && npm i heroku-release && heroku-release --app unpkg-bot",
46 | "build": "NODE_ENV=production babel src --out-dir lib && NODE_ENV=production browserify --global-transform envify lib/client.js | uglifyjs --compress --mangle > lib/bundle.js",
47 | "lint": "eslint src",
48 | "test": "babel-node test/index.js && npm run lint"
49 | },
50 | "repository": {
51 | "type": "git",
52 | "url": "https://github.com/ForbesLindesay/unpkg-bot.git"
53 | },
54 | "author": {
55 | "name": "Forbes Lindesay",
56 | "url": "http://github.com/ForbesLindesay"
57 | },
58 | "license": "MIT",
59 | "engines": {
60 | "node": "6.5.0"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | require('./lib');
3 | } else {
4 | require('babel-register');
5 | require('./src');
6 | }
7 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import request from 'then-request';
4 | import ms from 'ms';
5 |
6 | const colors = [
7 | '#3d9df2',
8 | '#e673cf',
9 | '#8800cc',
10 | '#005fb3',
11 | '#a69d7c',
12 | '#397358',
13 | '#ffbffb',
14 | '#ffd580',
15 | '#00e2f2',
16 | '#9173e6',
17 | '#a099cc',
18 | '#5995b3',
19 | '#994d6b',
20 | '#2d2080',
21 | '#736039',
22 | '#0c0059',
23 | '#00401a',
24 | '#1a3320',
25 | '#f240ff',
26 | '#ff8800',
27 | '#00f2c2',
28 | '#e59173',
29 | '#3347cc',
30 | '#18b300',
31 | '#269954',
32 | '#205380',
33 | '#733d00',
34 | '#161f59',
35 | '#364010',
36 | '#332b1a',
37 | '#0000ff',
38 | '#ffc8bf',
39 | '#79f299',
40 | '#0000d9',
41 | '#99cca7',
42 | '#b29559',
43 | '#8c004b',
44 | '#7f7700',
45 | '#730000',
46 | '#305900',
47 | '#402200',
48 | '#331a1a',
49 | '#bfe1ff',
50 | '#ff0000',
51 | '#baf279',
52 | '#a3d5d9',
53 | '#cc8533',
54 | '#b38686',
55 | '#59468c',
56 | '#7f4840',
57 | '#66001b',
58 | '#134d49',
59 | '#401100',
60 | '#40bfff',
61 | '#ff8080',
62 | '#def2b6',
63 | '#d94c36',
64 | '#cc5200',
65 | '#a67c98',
66 | '#23858c',
67 | '#6d1d73',
68 | '#005266',
69 | '#4d3e39',
70 | '#330014',
71 | '#73ff40',
72 | '#f2b6c6',
73 | '#f2d6b6',
74 | '#cc3347',
75 | '#0000bf',
76 | '#29a68d',
77 | '#8c6246',
78 | '#565a73',
79 | '#57664d',
80 | '#400033',
81 | '#331a31',
82 | '#ccff00',
83 | '#f279aa',
84 | '#f20000',
85 | '#cc0088',
86 | '#b2bf00',
87 | '#95a653',
88 | '#8c7769',
89 | '#566973',
90 | '#592d39',
91 | '#002240',
92 | '#070033',
93 | ];
94 | let colorIndex = 0;
95 | function nextColor() {
96 | if (colorIndex < colors.length) {
97 | return colors[colorIndex++];
98 | } else {
99 | colorIndex = 0;
100 | return nextColor();
101 | }
102 | }
103 | let scrolledDown = false;
104 | window.addEventListener('scroll', () => {
105 | scrolledDown = document.body.scrollTop > document.getElementById('log-heading').offsetTop;
106 | }, false);
107 |
108 | const colorsByMessage = {};
109 | const App = React.createClass({
110 | _maxLogIndex: -1,
111 | getInitialState() {
112 | return {loading: true, maxUserIDProcessed: 0, rateLimits: [], log: []};
113 | },
114 | componentDidMount() {
115 | this._poll();
116 | },
117 | _poll() {
118 | request('get', '/ajax').getBody('utf8').then(JSON.parse).done(status => {
119 | this.setState({
120 | loading: false,
121 | maxUserIDProcessed: status.maxUserIDProcessed,
122 | rateLimits: status.rateLimits,
123 | log: (
124 | scrolledDown
125 | ? this.state.log
126 | : status.log.filter(logEntry => {
127 | if (logEntry.index > this._maxLogIndex) {
128 | this._maxLogIndex = logEntry.index;
129 | return true;
130 | } else {
131 | return false;
132 | }
133 | }).slice().reverse().concat(this.state.log)
134 | ),
135 | });
136 | setTimeout(this._poll, 3000);
137 | }, this._poll);
138 | },
139 | render() {
140 | if (this.state.loading) {
141 | return
Loading...
;
142 | }
143 | const now = Date.now() / 1000;
144 | return (
145 |
146 |
Code Mod Status
147 |
148 | Max User ID Processed: {this.state.maxUserIDProcessed}
149 |
150 |
Rate Limits Remaining
151 |
152 | {
153 | this.state.rateLimits.map((rateLimit, i) => {
154 | const reset = Math.floor(rateLimit.reset - now);
155 | return (
156 |
157 |
162 |
167 |
168 |
{reset <= 0 ? 'now' : ms(reset * 1000)}
169 |
{rateLimit.remaining}
170 |
171 |
172 | );
173 | })
174 | }
175 |
176 |
Log
177 |
178 | {this.state.log.map(log => {
179 | if (log.level === 'error') {
180 | return (
181 |
182 |
{log.date} {log.context}
183 |
{log.stack}
184 |
185 | );
186 | }
187 | if (log.level === 'warn') {
188 | return (
189 |
190 |
{log.date} {log.message}
191 |
192 | );
193 | }
194 | if (log.level === 'log') {
195 | const color = colorsByMessage[log.name] || (colorsByMessage[log.name] = nextColor());
196 | return (
197 |
198 |
{log.date} {log.name} {log.message}
199 |
200 | );
201 | }
202 | return {log.date}
;
203 | })}
204 |
205 |
206 | );
207 | },
208 | });
209 |
210 | ReactDOM.render(, document.getElementById('container'));
211 |
--------------------------------------------------------------------------------
/src/codemod-repo.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import Promise from 'promise';
3 | import github from 'github-basic';
4 | import gethub from 'gethub';
5 | import rimraf from 'rimraf';
6 | import lsr from 'lsr';
7 | import throat from 'throat';
8 | import {log, error} from './console';
9 | import {pushError} from './db';
10 |
11 | const client = github({version: 3, auth: process.env.GITHUB_BOT_TOKEN});
12 |
13 | const readFile = Promise.denodeify(fs.readFile);
14 | const rm = Promise.denodeify(rimraf);
15 | const directory = __dirname + '/../temp';
16 |
17 | // TODO: client.exists doesn't work because http-basic does not work with head requests that also support gzip
18 | client.exists = function (owner, repo) {
19 | return this.get('/repos/:owner/:repo', {owner, repo}).then(
20 | () => true,
21 | () => false,
22 | );
23 | };
24 |
25 | if (process.env.NODE_ENV !== 'production') {
26 | console.log('===== DRY RUN =====');
27 | console.log('');
28 | console.log('To actually run code-mod, set NODE_ENV=production');
29 | console.log('');
30 | client.exists = () => Promise.resolve(false);
31 | [
32 | 'fork',
33 | 'branch',
34 | 'commit',
35 | 'pull',
36 | ].forEach(method => {
37 | client[method] = () => Promise.resolve(null);
38 | });
39 | }
40 |
41 | const blackList = [
42 | 'qdot/gecko-hg',
43 | 'angular/code.angularjs.org',
44 | 'Belxjander/tenfourfox',
45 | 'tijuca/icedove',
46 | 'jsdelivr/jsdelivr',
47 | 'andrit/uiUXWebsite',
48 | 'WhoAmID/angular-whoamid',
49 | 'rubber-duckies/the-frank-tank',
50 | ];
51 | function codemodRepo(fullName) {
52 | if (blackList.includes(fullName)) {
53 | return Promise.resolve(null);
54 | }
55 | const [owner, name] = fullName.split('/');
56 |
57 | return client.exists('npmcdn-to-unpkg-bot', name).then(exists => {
58 | if (exists) {
59 | return;
60 | }
61 | log('Code Modding', fullName);
62 | return rm(directory).then(() => {
63 | log('Downloading', fullName);
64 | return gethub(owner, name, 'master', directory);
65 | }).then(() => {
66 | log('Fetched', fullName);
67 | return lsr(directory);
68 | }).then(entries => {
69 | log('Processing Files', fullName);
70 | return Promise.all(entries.map(entry => {
71 | if (entry.isFile()) {
72 | return readFile(entry.fullPath, 'utf8').then(content => {
73 | const newContent = content.replace(/\bnpmcdn\b/g, 'unpkg');
74 | if (newContent !== content) {
75 | return {
76 | path: entry.path.substr(2), // remove the `./` that entry paths start with
77 | content: newContent,
78 | };
79 | }
80 | }, err => {
81 | if (err.code === 'ENOENT') {
82 | return;
83 | }
84 | throw err;
85 | });
86 | }
87 | }));
88 | }).then(updates => {
89 | updates = updates.filter(Boolean);
90 | if (updates.length === 0) {
91 | log('No Changes Made', fullName);
92 | return;
93 | }
94 | log('Forking', fullName);
95 | return client.fork(owner, name).then(() => {
96 | return new Promise(resolve => setTimeout(resolve, 10000));
97 | }).then(() => {
98 | log('Branching', fullName);
99 | return client.branch('npmcdn-to-unpkg-bot', name, 'master', 'npmcdn-to-unpkg');
100 | }).then(() => {
101 | log('Committing', fullName);
102 | return client.commit('npmcdn-to-unpkg-bot', name, {
103 | branch: 'npmcdn-to-unpkg',
104 | message: 'Replace npmcdn.com with unpkg.com',
105 | updates,
106 | });
107 | }).then(() => {
108 | log('Submitting Pull Request', fullName);
109 | return client.pull(
110 | {user: 'npmcdn-to-unpkg-bot', repo: name, branch: 'npmcdn-to-unpkg'},
111 | {user: owner, repo: name},
112 | {
113 | title: 'Replace npmcdn.com with unpkg.com',
114 | body: (
115 | 'To avoid potential naming conflicts with npm, npmcdn.com is being renamed to unpkg.com. This is an ' +
116 | 'automated pull request to update your project to use the new domain.'
117 | ),
118 | },
119 | );
120 | }).then(() => {
121 | log('Codemod Complete', fullName);
122 | });
123 | });
124 | }).then(null, err => {
125 | error('Error processing ' + fullName, err.stack);
126 | return pushError(owner, name, err.message || err);
127 | });
128 | }
129 |
130 | // only allow codemodding one repo at a time
131 | export default throat(Promise)(1, codemodRepo);
132 |
--------------------------------------------------------------------------------
/src/console.js:
--------------------------------------------------------------------------------
1 | let index = 0;
2 | const logEntries = [];
3 | for (let i = 0; i < 500; i++) {
4 | logEntries.push(null);
5 | }
6 |
7 | export function getLog() {
8 | return logEntries;
9 | }
10 | export function log(name, message) {
11 | logEntries.shift();
12 | logEntries.push({
13 | index: index++,
14 | date: (new Date()).toISOString(),
15 | level: 'log',
16 | name,
17 | message,
18 | });
19 | }
20 | export function warn(message) {
21 | logEntries.shift();
22 | logEntries.push({
23 | index: index++,
24 | date: (new Date()).toISOString(),
25 | level: 'warn',
26 | message,
27 | });
28 | }
29 | export function error(context, stack = '') {
30 | if (
31 | /The listed users and repositories cannot be searched/.test(stack)
32 | ) {
33 | stack = 'The listed user cannot be searched';
34 | }
35 | logEntries.shift();
36 | logEntries.push({
37 | index: index++,
38 | date: (new Date()).toISOString(),
39 | level: 'error',
40 | context,
41 | stack,
42 | });
43 | console.error(context + '\n' + stack);
44 | }
45 |
--------------------------------------------------------------------------------
/src/db.js:
--------------------------------------------------------------------------------
1 | import mongo from 'then-mongo';
2 | import Promise from 'promise';
3 |
4 | const db = mongo(process.env.MONGO_DB, ['users', 'maxUserIDProcessed', 'errors']);
5 |
6 | export function saveUser(username, accessToken) {
7 | return db.users.update({_id: username}, {_id: username, username, accessToken}, {upsert: true});
8 | }
9 | export function getUsers() {
10 | return db.users.find();
11 | }
12 |
13 | export function getMaxUserIDProcessed() {
14 | return db.maxUserIDProcessed.findOne({_id: 'unpkg-bot'}).then(o => (o ? o.value : undefined));
15 | }
16 | export function setMaxUserIDProcessed(value) {
17 | if (process.env.NODE_ENV === 'production') {
18 | return db.maxUserIDProcessed.update({_id: 'unpkg-bot'}, {_id: 'unpkg-bot', value}, {upsert: true});
19 | } else {
20 | return Promise.resolve(null);
21 | }
22 | }
23 | export function pushError(username, repo, message) {
24 | return db.errors.insert({username, repo, message});
25 | }
26 |
--------------------------------------------------------------------------------
/src/get-repos-for-user.js:
--------------------------------------------------------------------------------
1 | import Promise from 'promise';
2 | import {get} from './read-client';
3 |
4 | export default function getReposForUser(username) {
5 | return new Promise((resolve, reject) => {
6 | const repos = [];
7 | processPage(get('/search/code', {q: 'npmcdn user:' + username}));
8 | function processPage(page) {
9 | page.done(p => {
10 | p.items.forEach(item => {
11 | const repo = item.repository.full_name;
12 | if (!repos.includes(repo)) {
13 | repos.push(repo);
14 | }
15 | });
16 | if (p.urlNext) {
17 | processPage(get(p.urlNext));
18 | } else {
19 | resolve(repos);
20 | }
21 | }, reject);
22 | }
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {readFileSync} from 'fs';
2 | import {createHash} from 'crypto';
3 | import passport from 'passport';
4 | import {Strategy as GitHubStrategy} from 'passport-github2';
5 | import cookieSession from 'cookie-session';
6 | import express from 'express';
7 | import prepareResponse from 'prepare-response';
8 | import {getLog} from './console';
9 | import {saveUser, getUsers, getMaxUserIDProcessed} from './db';
10 | import {addToken, getRateLimitStatus} from './read-client';
11 | import processUser from './process-user';
12 | import './process-all-users';
13 |
14 | const app = express();
15 |
16 | getUsers().done(users => {
17 | users.forEach(user => {
18 | addToken(user.accessToken);
19 | });
20 | });
21 |
22 | app.use(cookieSession({
23 | keys: [process.env.SESSION_KEY],
24 | // session expires after 1 hour
25 | maxAge: 60 * 60 * 1000,
26 | // session is not accessible from JavaScript
27 | httpOnly: true,
28 | }));
29 | passport.serializeUser((user, done) => {
30 | done(null, user);
31 | });
32 |
33 | passport.deserializeUser((user, done) => {
34 | done(null, user);
35 | });
36 |
37 | passport.use(new GitHubStrategy(
38 | {
39 | clientID: process.env.GITHUB_CLIENT_ID,
40 | clientSecret: process.env.GITHUB_CLIENT_SECRET,
41 | callbackURL: process.env.GITHUB_CALLBACK || 'http://localhost:3000/auth/github/callback',
42 | },
43 | (accessToken, refreshToken, profile, done) => {
44 | addToken(accessToken);
45 | processUser(profile.username).done();
46 | saveUser(profile.username, accessToken).done(
47 | () => done(null, {username: profile.username, accessToken}),
48 | done,
49 | );
50 | }
51 | ));
52 | app.use(passport.initialize());
53 | app.use(passport.session());
54 |
55 | app.get('/auth/github', passport.authenticate('github', {scope: []}));
56 |
57 | app.get('/auth/github/callback', passport.authenticate('github'), (req, res) => {
58 | res.redirect('/');
59 | });
60 | app.get('/auth/logout', passport.authenticate('github'), (req, res) => {
61 | res.logout();
62 | res.sendStatus(200);
63 | });
64 |
65 | app.get('/ajax', (req, res, next) => {
66 | getMaxUserIDProcessed().done(maxUserIDProcessed => {
67 | res.json({maxUserIDProcessed, log: getLog().filter(Boolean), rateLimits: getRateLimitStatus()});
68 | }, next);
69 | });
70 | let hash = 'development';
71 | if (process.env.NODE_ENV !== 'production') {
72 | app.get('/client/' + hash + '.js', require('browserify-middleware')(__dirname + '/client.js', {
73 | transform: [require('babelify')],
74 | }));
75 | } else {
76 | const src = readFileSync(__dirname + '/bundle.js');
77 | const response = prepareResponse(src, {
78 | 'content-type': 'js',
79 | 'cache-control': '1 year',
80 | });
81 | hash = createHash('md5').update(src).digest("hex");
82 | app.get('/client/' + hash + '.js', (req, res, next) => {
83 | response.send(req, res, next);
84 | });
85 | }
86 |
87 | app.get('/', (req, res, next) => {
88 | if (!req.isAuthenticated()) {
89 | return res.redirect('/auth/github');
90 | }
91 | res.send(
92 | `
93 |
94 | Code Mod
95 |
96 |
125 |
126 |
127 | `
128 | );
129 | });
130 |
131 | app.listen(process.env.PORT || 3000);
132 |
--------------------------------------------------------------------------------
/src/process-all-users.js:
--------------------------------------------------------------------------------
1 | import throat from 'throat';
2 | import Promise from 'promise';
3 | import {getMaxUserIDProcessed, setMaxUserIDProcessed, pushError} from './db';
4 | import {get} from './read-client';
5 | import processUser from './process-user';
6 | import {error} from './console';
7 |
8 | function getPage(since) {
9 | get('/users', {since}).done(users => {
10 | const maxID = users.reduce((id, user) => {
11 | return Math.max(id, user.id);
12 | }, since || -1);
13 | Promise.all(users.map(throat(Promise)(10, user => processUser(user.login)))).then(() => {
14 | return setMaxUserIDProcessed(maxID);
15 | }).done(() => getPage(maxID));
16 | }, err => {
17 | error('Error getting users', err.stack);
18 | pushError(null, null, err.message || err).done(
19 | () => setTimeout(() => getPage(since), 5000),
20 | );
21 | });
22 | }
23 | getMaxUserIDProcessed().done(getPage);
24 |
--------------------------------------------------------------------------------
/src/process-user.js:
--------------------------------------------------------------------------------
1 | import getReposForUser from './get-repos-for-user';
2 | import codemodRepo from './codemod-repo';
3 | import {pushError} from './db';
4 | import {log, error} from './console';
5 |
6 | // codemod repos for a given user one at a time
7 | export default function processUser(username) {
8 | return getReposForUser(username).then(repos => {
9 | log('Processing', username);
10 | return new Promise((resolve, reject) => {
11 | function next(i) {
12 | if (i >= repos.length) {
13 | return resolve();
14 | }
15 | codemodRepo(repos[i]).done(() => next(i + 1), reject);
16 | }
17 | next(0);
18 | });
19 | }).then(null, err => {
20 | error('Error processing ' + username, err.stack);
21 | return pushError(username, null, err.message || err);
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/src/read-client.js:
--------------------------------------------------------------------------------
1 | import github from 'github-basic';
2 | import Promise from 'promise';
3 | import deck from 'deck';
4 | import {warn} from './console';
5 |
6 | const tokens = [];
7 | const clients = [];
8 |
9 | export function addToken(token) {
10 | if (!tokens.includes(token)) {
11 | tokens.push(token);
12 | clients.push(github({version: 3, auth: token}));
13 | }
14 | }
15 | export function getRateLimitStatus() {
16 | return clients.map(client => client.rateLimit);
17 | }
18 |
19 | export function get(...args) {
20 | return new Promise((resolve, reject) => {
21 | function retry() {
22 | if (clients.length === 0) {
23 | warn('No clients available, waiting for a client to be added');
24 | setTimeout(retry, 5000);
25 | return;
26 | }
27 | const clientWeights = {};
28 | for (let i = 0; i < clients.length; i++) {
29 | clientWeights[i] = clients[i].rateLimit.remaining || 1;
30 | }
31 | const clientToUse = clients[deck.pick(clientWeights)];
32 | clientToUse.get(...args).done(resolve, err => {
33 | if (err.statusCode === 401) {
34 | console.error('=====================');
35 | console.error(err.stack);
36 | console.dir(clientToUse);
37 | console.error('=====================');
38 | warn('Client token seems to have expired!');
39 | const index = clients.indexOf(clientToUse);
40 | if (index !== -1) {
41 | clients.splice(index, 1);
42 | }
43 | setTimeout(retry, 1000);
44 | } else if (err.statusCode === 403) {
45 | warn('Rate limit exceeded, waiting 1 second then trying again');
46 | setTimeout(retry, 1000);
47 | } else {
48 | reject(err);
49 | }
50 | });
51 | }
52 | retry();
53 | });
54 | }
55 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | console.error('Oops, no tests :(');
2 |
--------------------------------------------------------------------------------