├── .npmrc ├── .travis.yml ├── LICENSE ├── bin └── cmd.js ├── example └── sort.js ├── index.js ├── package.json ├── readme.markdown └── test ├── dedupe-deps-of-deps.js ├── dedupe.js ├── dedupe_index.js ├── dedupe_undef.js ├── expose.js ├── expose_str.js ├── indexed.js └── sort.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "13" 4 | - "12" 5 | - "10" 6 | - "9" 7 | - "8" 8 | - "6" 9 | - "4" 10 | - "iojs" 11 | - "0.12" 12 | - "0.10" 13 | - "0.8" 14 | before_install: 15 | # Old npm certs are untrusted https://github.com/npm/npm/issues/20191 16 | - 'if [ "${TRAVIS_NODE_VERSION}" = "0.6" ] || [ "${TRAVIS_NODE_VERSION}" = "0.8" ]; then export NPM_CONFIG_STRICT_SSL=false; fi' 17 | - 'nvm install-latest-npm' 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) James Halliday and browserify contributors 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var argv = require('subarg')(process.argv.slice(2)); 4 | var JSONStream = require('JSONStream'); 5 | 6 | var sort = require('../')(argv); 7 | var parse = JSONStream.parse([ true ]); 8 | var stringify = JSONStream.stringify(); 9 | 10 | process.stdin.pipe(parse).pipe(sort).pipe(stringify).pipe(process.stdout); 11 | -------------------------------------------------------------------------------- /example/sort.js: -------------------------------------------------------------------------------- 1 | var sort = require('../')(); 2 | var JSONStream = require('JSONStream'); 3 | var parse = JSONStream.parse([ true ]); 4 | var stringify = JSONStream.stringify(); 5 | 6 | process.stdin.pipe(parse).pipe(sort).pipe(stringify).pipe(process.stdout); 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'); 2 | var shasum = require('shasum-object'); 3 | 4 | module.exports = function (opts) { 5 | if (!opts) opts = {}; 6 | var rows = []; 7 | return through.obj(write, end); 8 | 9 | function write (row, enc, next) { rows.push(row); next() } 10 | 11 | function end () { 12 | var tr = this; 13 | rows.sort(cmp); 14 | sorter(rows, tr, opts); 15 | } 16 | }; 17 | 18 | function sorter (rows, tr, opts) { 19 | var expose = opts.expose || {}; 20 | if (Array.isArray(expose)) { 21 | expose = expose.reduce(function (acc, key) { 22 | acc[key] = true; 23 | return acc; 24 | }, {}); 25 | } 26 | 27 | var hashes = {}, deduped = {}; 28 | var sameDeps = depCmp(); 29 | 30 | if (opts.dedupe) { 31 | rows.forEach(function (row) { 32 | var h = shasum(row.source); 33 | sameDeps.add(row, h); 34 | if (hashes[h]) { 35 | hashes[h].push(row); 36 | } else { 37 | hashes[h] = [row]; 38 | } 39 | }); 40 | Object.keys(hashes).forEach(function (h) { 41 | var rows = hashes[h]; 42 | while (rows.length > 1) { 43 | var row = rows.pop(); 44 | row.dedupe = rows[0].id; 45 | row.sameDeps = sameDeps.cmp(rows[0].deps, row.deps); 46 | deduped[row.id] = rows[0].id; 47 | } 48 | }); 49 | } 50 | 51 | if (opts.index) { 52 | var index = {}; 53 | var offset = 0; 54 | rows.forEach(function (row, ix) { 55 | if (has(expose, row.id)) { 56 | row.index = row.id; 57 | offset ++; 58 | if (expose[row.id] !== true) { 59 | index[expose[row.id]] = row.index; 60 | } 61 | } 62 | else { 63 | row.index = ix + 1 - offset; 64 | } 65 | index[row.id] = row.index; 66 | }); 67 | rows.forEach(function (row) { 68 | row.indexDeps = {}; 69 | Object.keys(row.deps).forEach(function (key) { 70 | var id = row.deps[key]; 71 | row.indexDeps[key] = index[id]; 72 | }); 73 | if (row.dedupe) { 74 | row.dedupeIndex = index[row.dedupe]; 75 | } 76 | tr.push(row); 77 | }); 78 | } 79 | else { 80 | rows.forEach(function (row) { tr.push(row) }); 81 | } 82 | tr.push(null); 83 | } 84 | 85 | function cmp (a, b) { 86 | return a.id + a.hash < b.id + b.hash ? -1 : 1; 87 | } 88 | 89 | function has (obj, key) { 90 | return Object.prototype.hasOwnProperty.call(obj, key); 91 | } 92 | 93 | function depCmp () { 94 | var deps = {}, hashes = {}; 95 | return { add: add, cmp: cmp } 96 | 97 | function add (row, hash) { 98 | deps[row.id] = row.deps; 99 | hashes[row.id] = hash; 100 | } 101 | function cmp (a, b, limit) { 102 | if (!a && !b) return true; 103 | if (!a || !b) return false; 104 | 105 | var keys = Object.keys(a); 106 | if (keys.length !== Object.keys(b).length) return false; 107 | 108 | for (var i = 0; i < keys.length; i++) { 109 | var k = keys[i], ka = a[k], kb = b[k]; 110 | var ha = hashes[ka]; 111 | var hb = hashes[kb]; 112 | var da = deps[ka]; 113 | var db = deps[kb]; 114 | 115 | if (ka === kb) continue; 116 | if (ha !== hb || (!limit && !cmp(da, db, 1))) { 117 | return false; 118 | } 119 | } 120 | return true; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deps-sort", 3 | "version": "2.0.1", 4 | "description": "sort module-deps output for deterministic browserify bundles", 5 | "main": "index.js", 6 | "bin": { 7 | "deps-sort": "bin/cmd.js" 8 | }, 9 | "dependencies": { 10 | "JSONStream": "^1.0.3", 11 | "shasum-object": "^1.0.0", 12 | "subarg": "^1.0.0", 13 | "through2": "^2.0.0" 14 | }, 15 | "devDependencies": { 16 | "tap": "^2.2.0" 17 | }, 18 | "scripts": { 19 | "test": "tap test/*.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/substack/deps-sort.git" 24 | }, 25 | "homepage": "https://github.com/substack/deps-sort", 26 | "keywords": [ 27 | "dependency", 28 | "graph", 29 | "browser", 30 | "browserify", 31 | "module-deps", 32 | "browser-pack", 33 | "sorted", 34 | "determinism" 35 | ], 36 | "author": { 37 | "name": "James Halliday", 38 | "email": "mail@substack.net", 39 | "url": "http://substack.net" 40 | }, 41 | "license": "MIT" 42 | } 43 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # deps-sort 2 | 3 | sort [module-deps](https://npmjs.org/package/module-deps) output for deterministic 4 | browserify bundles 5 | 6 | [![build status](https://secure.travis-ci.org/browserify/deps-sort.png)](http://travis-ci.org/browserify/deps-sort) 7 | 8 | # example 9 | 10 | ## command-line 11 | 12 | ``` 13 | $ for((i=0;i<5;i++)); do module-deps main.js | deps-sort | browser-pack | md5sum; done 14 | e9e630de2c62953140357db0444c3c3a - 15 | e9e630de2c62953140357db0444c3c3a - 16 | e9e630de2c62953140357db0444c3c3a - 17 | e9e630de2c62953140357db0444c3c3a - 18 | e9e630de2c62953140357db0444c3c3a - 19 | ``` 20 | 21 | or using `browserify --deps` on a [voxeljs][] project: 22 | 23 | ``` 24 | $ for((i=0;i<5;i++)); do browserify --deps browser.js | deps-sort | browser-pack | md5sum; done 25 | fb418c74b53ba2e4cef7d01808b848e6 - 26 | fb418c74b53ba2e4cef7d01808b848e6 - 27 | fb418c74b53ba2e4cef7d01808b848e6 - 28 | fb418c74b53ba2e4cef7d01808b848e6 - 29 | fb418c74b53ba2e4cef7d01808b848e6 - 30 | ``` 31 | 32 | ## api 33 | 34 | To use this module programmatically, write streaming object data and read 35 | streaming object data: 36 | 37 | ``` js 38 | var sort = require('../')(); 39 | var JSONStream = require('JSONStream'); 40 | var parse = JSONStream.parse([ true ]); 41 | var stringify = JSONStream.stringify(); 42 | 43 | process.stdin.pipe(parse).pipe(sort).pipe(stringify).pipe(process.stdout); 44 | ``` 45 | 46 | # methods 47 | 48 | ``` js 49 | var depsSort = require('deps-sort'); 50 | ``` 51 | 52 | ## var stream = depsSort(opts) 53 | 54 | Return a new through `stream` that should get written 55 | [module-deps](https://npmjs.org/package/module-deps) objects and will output 56 | sorted objects. 57 | 58 | `opts` can be: 59 | 60 | * `opts.index` - when true, for each module-deps row, insert `row.index` with 61 | the numeric index and `row.indexDeps` like `row.deps` but mapping require 62 | strings to row indices 63 | 64 | * `opts.expose` - array of names or object mapping names to `true` not to mangle 65 | with integer indexes when `opts.index` is turned on. If `opts.expose` maps names 66 | to strings, those strings will be used to resolve the indexed references. 67 | 68 | * `opts.dedupe` - set `row.dedupe` for files that match existing contents. Sets 69 | `row.dedupeIndex` when `opts.index` is enabled. When `row.dedupe` is set, 70 | `row.sameDeps` will be set to a boolean of whether the dependencies at the 71 | dedupe target match (true) or just the source content (false). 72 | 73 | # input objects 74 | 75 | Input objects are file objects in the [module-deps][] shape. They must at least 76 | have these properties: 77 | 78 | * `row.id` - a unique identifier for the file 79 | * `row.source` - the file contents 80 | * `row.deps` - dependencies for this file, mapping strings as used in 81 | `require()` to row IDs. 82 | 83 | # output objects 84 | 85 | All the input properties, and: 86 | 87 | * `row.index` - when `opts.index` is true, the sorted numeric index of the row 88 | * `row.indexDeps` - like `row.deps`, but mapping to `row.index` instead of 89 | `row.id` 90 | * `row.dedupe` - when `opts.dedupe` is true, contains the row ID of a file with 91 | identical contents 92 | * `row.dedupeIndex` - like `row.dedupe`, but contains the `row.index` instead 93 | of `row.id` 94 | 95 | # install 96 | 97 | With [npm](https://npmjs.org) do: 98 | 99 | ``` 100 | npm install deps-sort 101 | ``` 102 | 103 | # license 104 | 105 | MIT 106 | 107 | [module-deps]: https://github.com/browserify/module-deps#output-objects 108 | [voxeljs]: https://web.archive.org/web/20210307225301/http://www.voxeljs.com/ 109 | -------------------------------------------------------------------------------- /test/dedupe-deps-of-deps.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('dedupe-deps-of-deps', function (t) { 6 | t.plan(1); 7 | var s = sort({ dedupe: true }); 8 | var rows = []; 9 | function write (row, enc, next) { rows.push(row); next() } 10 | function end () { 11 | t.deepEqual(rows, [ 12 | { 13 | id: '/bar.js', 14 | deps: { baz: '/bar/baz.js' }, 15 | source: 'TWO' 16 | }, 17 | { 18 | id: '/bar/baz.js', 19 | deps: {}, 20 | source: 'THREE' 21 | }, 22 | { 23 | id: '/foo.js', 24 | deps: { baz: '/foo/baz.js' }, 25 | source: 'TWO', 26 | dedupe: '/bar.js', 27 | sameDeps: true 28 | }, 29 | { 30 | id: '/foo/baz.js', 31 | deps: {}, 32 | source: 'THREE', 33 | dedupe: '/bar/baz.js', 34 | sameDeps: true 35 | }, 36 | { 37 | id: '/main.js', 38 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 39 | source: 'ONE' 40 | } 41 | ]); 42 | } 43 | s.pipe(through.obj(write, end)); 44 | 45 | s.write({ 46 | id: '/main.js', 47 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 48 | source: 'ONE' 49 | }); 50 | s.write({ 51 | id: '/foo.js', 52 | deps: { baz : '/foo/baz.js' }, 53 | source: 'TWO' 54 | }); 55 | s.write({ 56 | id: '/bar.js', 57 | deps: { baz : '/bar/baz.js' }, 58 | source: 'TWO' 59 | }); 60 | s.write({ 61 | id: '/foo/baz.js', 62 | deps: {}, 63 | source: 'THREE' 64 | }); 65 | s.write({ 66 | id: '/bar/baz.js', 67 | deps: {}, 68 | source: 'THREE' 69 | }); 70 | s.end(); 71 | }); 72 | -------------------------------------------------------------------------------- /test/dedupe.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('dedupe', function (t) { 6 | t.plan(1); 7 | var s = sort({ dedupe: true }); 8 | var rows = []; 9 | function write (row, enc, next) { rows.push(row); next() } 10 | function end () { 11 | t.deepEqual(rows, [ 12 | { id: '/bar.js', deps: {}, source: 'TWO' }, 13 | { id: '/foo.js', deps: {}, source: 'TWO', dedupe: '/bar.js', sameDeps: true }, 14 | { 15 | id: '/main.js', 16 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 17 | source: 'ONE' 18 | } 19 | ]); 20 | } 21 | s.pipe(through.obj(write, end)); 22 | 23 | s.write({ 24 | id: '/main.js', 25 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 26 | source: 'ONE' 27 | }); 28 | s.write({ 29 | id: '/foo.js', 30 | deps: {}, 31 | source: 'TWO' 32 | }); 33 | s.write({ 34 | id: '/bar.js', 35 | deps: {}, 36 | source: 'TWO' 37 | }); 38 | s.end(); 39 | }); 40 | -------------------------------------------------------------------------------- /test/dedupe_index.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('dedupe index', function (t) { 6 | t.plan(1); 7 | var s = sort({ dedupe: true, index: true }); 8 | var rows = []; 9 | function write (row, enc, next) { rows.push(row); next() } 10 | function end () { 11 | t.deepEqual(rows, [ 12 | { 13 | id: '/bar.js', 14 | deps: {}, 15 | source: 'TWO', 16 | index: 1, 17 | indexDeps: {} 18 | }, 19 | { 20 | id: '/foo.js', 21 | deps: {}, 22 | source: 'TWO', 23 | dedupe: '/bar.js', 24 | index: 2, 25 | indexDeps: {}, 26 | dedupeIndex: 1, 27 | sameDeps: true 28 | }, 29 | { 30 | id: '/main.js', 31 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 32 | source: 'ONE', 33 | index: 3, 34 | indexDeps: { './foo': 2, './bar': 1 }, 35 | } 36 | ]); 37 | } 38 | s.pipe(through.obj(write, end)); 39 | 40 | s.write({ 41 | id: '/main.js', 42 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 43 | source: 'ONE' 44 | }); 45 | s.write({ 46 | id: '/foo.js', 47 | deps: {}, 48 | source: 'TWO' 49 | }); 50 | s.write({ 51 | id: '/bar.js', 52 | deps: {}, 53 | source: 'TWO' 54 | }); 55 | s.end(); 56 | }); 57 | -------------------------------------------------------------------------------- /test/dedupe_undef.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('dedupe undef', function (t) { 6 | t.plan(1); 7 | var s = sort({ dedupe: true }); 8 | var rows = []; 9 | function write (row, enc, next) { rows.push(row); next() } 10 | function end () { 11 | t.deepEqual(rows, [ 12 | { id: '/bar.js', source: 'TWO' }, 13 | { id: '/foo.js', source: 'TWO', dedupe: '/bar.js', sameDeps: true }, 14 | { 15 | id: '/main.js', 16 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 17 | source: 'ONE' 18 | } 19 | ]); 20 | } 21 | s.pipe(through.obj(write, end)); 22 | 23 | s.write({ 24 | id: '/main.js', 25 | deps: { './foo': '/foo.js', './bar': '/bar.js' }, 26 | source: 'ONE' 27 | }); 28 | s.write({ 29 | id: '/foo.js', 30 | source: 'TWO' 31 | }); 32 | s.write({ 33 | id: '/bar.js', 34 | source: 'TWO' 35 | }); 36 | s.end(); 37 | }); 38 | -------------------------------------------------------------------------------- /test/expose.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('expose true', function (t) { 6 | t.plan(1); 7 | var s = sort({ index: true, expose: [ '/foo.js', '/bar.js' ] }); 8 | var rows = []; 9 | function write (row, enc, next) { rows.push(row); next() } 10 | function end () { 11 | t.deepEqual(rows, [ 12 | { 13 | id: '/bar.js', 14 | deps: {}, 15 | index: '/bar.js', 16 | indexDeps: {} 17 | }, 18 | { 19 | id: '/foo.js', 20 | deps: { './bar': '/bar.js' }, 21 | index: '/foo.js', 22 | indexDeps: { './bar': '/bar.js' } 23 | }, 24 | { 25 | id: '/main.js', 26 | deps: { './foo': '/foo.js' }, 27 | index: 1, 28 | indexDeps: { './foo': '/foo.js' } 29 | }, 30 | ]); 31 | } 32 | s.pipe(through.obj(write, end)); 33 | 34 | s.write({ id: '/main.js', deps: { './foo': '/foo.js' } }); 35 | s.write({ id: '/foo.js', deps: { './bar': '/bar.js' } }); 36 | s.write({ id: '/bar.js', deps: {} }); 37 | s.end(); 38 | }); 39 | -------------------------------------------------------------------------------- /test/expose_str.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('expose string', function (t) { 6 | t.plan(1); 7 | var s = sort({ 8 | index: true, 9 | expose: { 10 | 'f': '/foo.js', 11 | 'b': '/bar.js' 12 | } 13 | }); 14 | var rows = []; 15 | function write (row, enc, next) { rows.push(row); next() } 16 | function end () { 17 | t.deepEqual(rows, [ 18 | { 19 | id: '/main.js', 20 | deps: { './foo': '/foo.js' }, 21 | index: 1, 22 | indexDeps: { './foo': 'f' } 23 | }, 24 | { 25 | id: 'b', 26 | deps: {}, 27 | index: 'b', 28 | indexDeps: {} 29 | }, 30 | { 31 | id: 'f', 32 | deps: { './bar': '/bar.js' }, 33 | index: 'f', 34 | indexDeps: { './bar': 'b' } 35 | } 36 | ]); 37 | } 38 | s.pipe(through.obj(write, end)); 39 | 40 | s.write({ id: '/main.js', deps: { './foo': '/foo.js' } }); 41 | s.write({ id: 'f', deps: { './bar': '/bar.js' } }); 42 | s.write({ id: 'b', deps: {} }); 43 | s.end(); 44 | }); 45 | -------------------------------------------------------------------------------- /test/indexed.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('indexed', function (t) { 6 | t.plan(1); 7 | var s = sort({ index: true }); 8 | var rows = []; 9 | function write (row, enc, next) { rows.push(row); next() } 10 | function end () { 11 | t.deepEqual(rows, [ 12 | { 13 | id: '/bar.js', 14 | deps: {}, 15 | index: 1, 16 | indexDeps: {} 17 | }, 18 | { 19 | id: '/foo.js', 20 | deps: { './bar': '/bar.js' }, 21 | index: 2, 22 | indexDeps: { './bar': 1 } 23 | }, 24 | { 25 | id: '/main.js', 26 | deps: { './foo': '/foo.js' }, 27 | index: 3, 28 | indexDeps: { './foo': 2 } 29 | }, 30 | ]); 31 | } 32 | s.pipe(through.obj(write, end)); 33 | 34 | s.write({ id: '/main.js', deps: { './foo': '/foo.js' } }); 35 | s.write({ id: '/foo.js', deps: { './bar': '/bar.js' } }); 36 | s.write({ id: '/bar.js', deps: {} }); 37 | s.end(); 38 | }); 39 | -------------------------------------------------------------------------------- /test/sort.js: -------------------------------------------------------------------------------- 1 | var sort = require('../'); 2 | var test = require('tap').test; 3 | var through = require('through2'); 4 | 5 | test('sort', function (t) { 6 | t.plan(1); 7 | var s = sort(); 8 | var rows = []; 9 | function write (row, enc, next) { rows.push(row); next() } 10 | function end () { 11 | t.deepEqual(rows, [ 12 | { id: '/bar.js', deps: {} }, 13 | { id: '/foo.js', deps: { './bar': '/bar.js' } }, 14 | { id: '/main.js', deps: { './foo': '/foo.js' } } 15 | ]); 16 | } 17 | s.pipe(through.obj(write, end)); 18 | 19 | s.write({ id: '/main.js', deps: { './foo': '/foo.js' } }); 20 | s.write({ id: '/foo.js', deps: { './bar': '/bar.js' } }); 21 | s.write({ id: '/bar.js', deps: {} }); 22 | s.end(); 23 | }); 24 | --------------------------------------------------------------------------------