├── .ebextensions └── 90_change_npm_permissions.config ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── favicon.png ├── server ├── cache.js ├── child-processes │ └── create-bundle.js ├── index.js ├── logger.js ├── serve-package.js ├── templates │ ├── 500.html │ └── index.html └── utils │ ├── findVersion.js │ ├── get.js │ ├── guid.js │ ├── makeLegalIdentifier.js │ ├── padRight.js │ └── responses.js └── test └── test.js /.ebextensions/90_change_npm_permissions.config: -------------------------------------------------------------------------------- 1 | files: 2 | "/opt/elasticbeanstalk/hooks/appdeploy/post/90_set_tmp_permissions.sh": 3 | mode: "000755" 4 | owner: root 5 | group: root 6 | content: | 7 | #!/usr/bin/env bash 8 | sudo mkdir /tmp/.{config,npm} 9 | 10 | sudo chown -R nodejs:nodejs /tmp/.config 11 | sudo chown -R nodejs:nodejs /tmp/.npm 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "rules": { 4 | "indent": [ 2, "tab", { "SwitchCase": 1 } ], 5 | "semi": [ 2, "always" ], 6 | "keyword-spacing": [ 2, { "before": true, "after": true } ], 7 | "space-before-blocks": [ 2, "always" ], 8 | "space-before-function-paren": [ 2, "always" ], 9 | "no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ], 10 | "no-cond-assign": 0, 11 | "no-unused-vars": 2, 12 | "object-shorthand": [ 2, "always" ], 13 | "no-const-assign": 2, 14 | "no-class-assign": 2, 15 | "no-this-before-super": 2, 16 | "no-var": 2, 17 | "no-unreachable": 2, 18 | "valid-typeof": 2, 19 | "quote-props": [ 2, "as-needed" ], 20 | "one-var": [ 2, "never" ], 21 | "prefer-arrow-callback": 2, 22 | "prefer-const": [ 2, { "destructuring": "all" } ], 23 | "arrow-spacing": 2, 24 | "no-inner-declarations": 0 25 | }, 26 | "env": { 27 | "es6": true, 28 | "node": true 29 | }, 30 | "extends": [ 31 | "eslint:recommended" 32 | ], 33 | "parserOptions": { 34 | "ecmaVersion": 8, 35 | "sourceType": "module" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tmp 4 | # Elastic Beanstalk Files 5 | .elasticbeanstalk/* 6 | !.elasticbeanstalk/*.cfg.yml 7 | !.elasticbeanstalk/*.global.yml 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | env: 6 | global: 7 | - BUILD_TIMEOUT=10000 8 | - NODE_ENV=production 9 | install: npm install 10 | 11 | deploy: 12 | provider: elasticbeanstalk 13 | access_key_id: AKIAIKAPGM6VB7OMQXOA 14 | secret_access_key: 15 | secure: RQlUkKVgCOL+JBaPiKVwzKCpo1mFZC678O+MJnIlPF/W8fqynOneJHXWJo1kBwRAZBMbtzOxEknHCS1+kqvhHJ0zESMwv/LdkJ8rlHQ4wgWhFZZ39HNNDiSnSQlT4+gL9Jex4vFodKqWy6VU8duCp0eCsgGt8/C0NhVj9XoHPJXt0RyV5uEOQuCo1WVg21+NjqPRnqQrYur/vXVEKgo9okIGV4+HMAS9vJSYQPQBL8/6LqryJDxtLLy2aM1vZBCGIagEJOR4XNpq0mfZey/goKIlSeLzLL3Gi0c15DfFgpDJoGv485HSIxw4ubC/GcUXGGpO93gtcShZFdNuyXlRCs9oRywjaM8hUoZVM0QzgOdCNZWXcyKEzDUpyZAGH3k4eSKHmcYdUiPXRUfv5J5b7UBXrj/kaVYQrSkZ7vpvu04CeaojWuj7arB1pZwZu2jqQ50tLiP74R5ou99Qq5fbPTz7atikO4+zQBHCKIrLxQ0idTL6HX+bJzyL0nRP43YoEZlBchCAbdD+K4koC985tLYTm7PfQSZrWZTWCKUbaAS48fGlkGY+3Oderf7vENR/wmSHvY4umGI/48w3lBONbBwcNrbEPc6h22i4GGosNiAEYtErIkxAgcOS8ISVVs8P6mdt6Hn1U7HvyX9rWu4TjRzvrVS93b2Sdj9BhLG3KcE= 16 | region: us-east-1 17 | app: packd 18 | env: packd-prod 19 | bucket_name: elasticbeanstalk-us-east-1-161663720085 20 | bucket_path: packd 21 | on: 22 | tags: true 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # packd changelog 2 | 3 | ## 2.8.0 4 | 5 | * Add `?format=esm` query parameter ([#102](https://github.com/Rich-Harris/packd/pull/102)) 6 | 7 | ## 2.7.0 8 | 9 | * Update Rollup to 1.0, replace Uglify with Terser ([#65](https://github.com/Rich-Harris/packd/pull/65)) 10 | 11 | ## 2.6.1 12 | 13 | * Add UTF8 header ([#36](https://github.com/Rich-Harris/packd/pull/36)) 14 | 15 | ## 2.6.0 16 | 17 | * Fix registry URL for scoped packages ([#35](https://github.com/Rich-Harris/packd/pull/35)) 18 | 19 | ## 2.5.3 20 | 21 | * Use `tar` module ([#34](https://github.com/Rich-Harris/packd/pull/34)) 22 | 23 | ## 2.5.2 24 | 25 | * Bump tag to trigger AWS deploy 26 | 27 | ## 2.5.1 28 | 29 | * Don't try to spellcheck package names ([#29](https://github.com/Rich-Harris/packd/pull/29)) 30 | 31 | ## 2.5.0 32 | 33 | * Improve caching ([#26](https://github.com/Rich-Harris/packd/pull/26)) 34 | 35 | ## 2.4.0 36 | 37 | * Show error message on 500 page ([#24](https://github.com/Rich-Harris/packd/pull/24)) 38 | 39 | ## 2.3.4 40 | 41 | * Fix package-lock.json 42 | 43 | ## 2.3.3 44 | 45 | * Move some `devDependencies` to `dependencies`, where they belong ([#20](https://github.com/Rich-Harris/packd/pull/20)) 46 | 47 | ## 2.3.2 48 | 49 | * Update to new Uglify API 50 | 51 | ## 2.3.1 52 | 53 | * Use `pushState` instead of `replaceState` on `/` 54 | * Update to new Rollup API 55 | 56 | ## 2.3.0 57 | 58 | * Do bundling and minification in a child process ([#11](https://github.com/Rich-Harris/packd/issues/11)) 59 | 60 | ## 2.2.1 61 | 62 | * Only send `start` message if process is a fork 63 | 64 | ## 2.2.0 65 | 66 | * Expose port ([#16](https://github.com/Rich-Harris/packd/pull/16)) 67 | * Fix UglifyJS dependency ([#16](https://github.com/Rich-Harris/packd/pull/16)) 68 | * Fix content type on `/_cache` 69 | 70 | ## 2.1.4 71 | 72 | * Update rollup-plugin-node-resolve — fixes #6 73 | 74 | ## 2.1.3 75 | 76 | * Fix queries with deep imports 77 | 78 | ## 2.1.2 79 | 80 | * Fix query string parsing 81 | 82 | ## 2.1.1 83 | 84 | * Fix bug with route matching regex 85 | 86 | ## 2.1.0 87 | 88 | * Support semver ranges 89 | * Add message encouraging package authors to use pkg.module 90 | 91 | ## 2.0.0 92 | 93 | * Shorten URLs — `/bundle/foo` is now just `/foo` 94 | * Move `/log` to `_log` (npm package names cannot start with underscores) 95 | * Add `/_cache` route for inspecting the cache 96 | * Filter logs by package name: `/_log?filter=left-pad` 97 | * Include yarn output in logs 98 | * Use `modulesOnly` option in rollup-plugin-node-resolve to increase reliability for packages that import CommonJS from ESM ([#2](https://github.com/Rich-Harris/packd/issues/2)) 99 | * Allow deep imports (e.g. `/lodash-es/range.js`) 100 | 101 | ## 1.1.0 102 | 103 | * Handle packages with `peerDependencies`, e.g. react-dom ([#1](https://github.com/Rich-Harris/packd/issues/1)) 104 | 105 | ## 1.0.4 106 | 107 | * Timeout after 10 seconds 108 | 109 | ## 1.0.3 110 | 111 | * Add a loading indicator 112 | * Use favicon middleware 113 | 114 | ## 1.0.2 115 | 116 | * Fix bug that was prevent certain tarballs from being extracted 117 | 118 | ## 1.0.1 119 | 120 | * Serve unminified bundles if minification fails 121 | * Update homepage to include version number and link 122 | * Better server error messages 123 | 124 | ## 1.0.0 125 | 126 | * First release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 [these people](https://github.com/Rich-Harris/packd/graphs/contributors) 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # packd 2 | 3 | [Rollup](https://rollupjs.org) as a service (with a little help from [Browserify](http://browserify.org/)). 4 | 5 | * [bundle.run](https://bundle.run) — CDN 6 | * [packd.now.sh](https://packd.now.sh) — demo instance 7 | 8 | This is a simple app for generating UMD bundles of npm packages, similar to [browserify-cdn](https://github.com/jfhbrook/wzrd.in) aka [wzrd.in](https://wzrd.in/). I made it because wzrd.in sometimes goes offline, and I need its functionality for the [Svelte REPL](https://svelte.technology/repl). Unfortunately I couldn't get browserify-cdn to run on [now.sh](https://zeit.co/now), so I decided to roll my own. 9 | 10 | And since I was *roll*ing my own, it made sense to use [Rollup](https://rollupjs.org). (Feel free to roll your eyes.) For npm packages that expose [`pkg.module`](https://github.com/rollup/rollup/wiki/pkg.module), such as the [D3 modules](https://github.com/d3), this means you get smaller, more efficient bundles than with browserify-cdn. packd also gzips the files it serves, typically resulting in much smaller requests. 11 | 12 | Since Rollup can't handle all of the CommonJS code on npm, packd will use Browserify (or a combination of Rollup and Browserify) where appropriate. 13 | 14 | 15 | ## Using packd 16 | 17 | You can try a hosted version of packd at https://bundle.run. 18 | 19 | Bundles can be accessed [like so](https://bundle.run/left-pad). Bear in mind that if a bundle isn't cached, it needs to be installed and built before it can be served, which may take a little while: 20 | 21 | ``` 22 | /[name] 23 | ``` 24 | 25 | You can specify a tag (e.g. 'latest') or a version (e.g. '1.2.3'): 26 | 27 | ``` 28 | /[name]@latest 29 | ``` 30 | 31 | If you're using these URLs with ` 129 | 130 | 131 | -------------------------------------------------------------------------------- /server/utils/findVersion.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | 3 | module.exports = function findVersion(meta, tag) { 4 | // already a valid version? 5 | if (semver.valid(tag)) return meta.versions[tag] && tag; 6 | 7 | // dist tag 8 | if (tag in meta['dist-tags']) return meta['dist-tags'][tag]; 9 | 10 | // semver range 11 | return semver.maxSatisfying(Object.keys(meta.versions), tag); 12 | }; 13 | -------------------------------------------------------------------------------- /server/utils/get.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | 3 | module.exports = function get(url) { 4 | return new Promise((fulfil, reject) => { 5 | https.get(url, response => { 6 | let body = ''; 7 | 8 | response.on('data', chunk => { 9 | body += chunk; 10 | }); 11 | 12 | response.on('end', () => { 13 | fulfil(body); 14 | }); 15 | 16 | response.on('error', reject); 17 | }); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /server/utils/guid.js: -------------------------------------------------------------------------------- 1 | module.exports = function guid() { 2 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 3 | let r = (Math.random() * 16) | 0, 4 | v = c == 'x' ? r : (r & 0x3) | 0x8; 5 | return v.toString(16); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /server/utils/makeLegalIdentifier.js: -------------------------------------------------------------------------------- 1 | const reservedWords = 'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public'.split( 2 | ' ' 3 | ); 4 | const builtins = 'Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl'.split( 5 | ' ' 6 | ); 7 | 8 | const blacklisted = Object.create(null); 9 | reservedWords.concat(builtins).forEach(word => (blacklisted[word] = true)); 10 | 11 | module.exports = function makeLegalIdentifier(str) { 12 | str = str 13 | .replace(/-(\w)/g, (_, letter) => letter.toUpperCase()) 14 | .replace(/[^$_a-zA-Z0-9]/g, '_'); 15 | 16 | if (/\d/.test(str[0]) || blacklisted[str]) str = `_${str}`; 17 | 18 | return str; 19 | }; 20 | -------------------------------------------------------------------------------- /server/utils/padRight.js: -------------------------------------------------------------------------------- 1 | module.exports = function padRight(str, num, char = ' ') { 2 | while (str.length < num) str += char; 3 | return str; 4 | }; 5 | -------------------------------------------------------------------------------- /server/utils/responses.js: -------------------------------------------------------------------------------- 1 | module.exports.sendBadRequest = function sendBadRequest(res, msg) { 2 | res.status(400); 3 | res.end(msg); 4 | }; 5 | 6 | module.exports.sendError = function sendError(res, msg) { 7 | res.status(500); 8 | res.set('Content-Type', 'text/html'); 9 | res.end(msg); 10 | }; 11 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const { fork } = require('child_process'); 2 | const request = require('request-promise'); 3 | const { gunzipSync } = require('zlib'); 4 | const assert = require('assert'); 5 | 6 | const server = fork('server/index.js', ['start'], { 7 | stdio: 'inherit' 8 | }); 9 | 10 | async function getPackage (id) { 11 | const zipped = await request(`http://localhost:9000/${id}`, { 12 | encoding: null 13 | }); 14 | 15 | const source = gunzipSync(zipped).toString(); 16 | 17 | const fn = new Function('module', 'exports', source); 18 | const mod = { exports: {} }; 19 | fn(mod, mod.exports); 20 | 21 | return mod.exports; 22 | } 23 | 24 | function success (message) { 25 | console.log(`\u001B[32m✓\u001B[39m ${message}`); 26 | } 27 | 28 | server.on('message', async message => { 29 | if (message !== 'start') return; 30 | 31 | const leftPad = await getPackage('left-pad'); 32 | assert.equal(leftPad('x', 3), ' x'); 33 | 34 | const theAnswer = await getPackage('the-answer'); 35 | assert.equal(theAnswer, 42); 36 | 37 | success('all tests pass'); 38 | server.kill(); 39 | }); --------------------------------------------------------------------------------