├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── sourcemaps ├── Chrome.png ├── Firefox.png ├── README.md ├── helloworld.coffee ├── helloworld.js ├── helloworld.js.map └── index.html ├── src └── index.js ├── test ├── decode.js ├── encode.js └── index.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tmp* 3 | node_modules 4 | /dist 5 | /types -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.disableLanguages": ["md"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | ## 2.0.3-4 4 | 5 | - Fix package entry points 6 | 7 | ## 2.0.2 8 | 9 | - Package admin 10 | 11 | ## 2.0.1 12 | 13 | - Fix build 14 | 15 | ## 2.0.0 16 | 17 | - Convert package to ESM, add `pkg.exports` ([#16](https://github.com/Rich-Harris/vlq/pull/16)) 18 | 19 | ## 1.0.1 20 | 21 | - Handle overflow cases ([#9](https://github.com/Rich-Harris/vlq/pull/9)) 22 | 23 | ## 1.0.0 24 | 25 | - Rewrite in TypeScript, include definitions in package ([#6](https://github.com/Rich-Harris/vlq/pull/6)) 26 | 27 | ## 0.2.3 28 | 29 | - Add LICENSE to npm package 30 | 31 | ## 0.2.2 32 | 33 | - Expose `pkg.module`, not `jsnext:main` 34 | 35 | ## 0.2.1 36 | 37 | - Performance boost - vlq no longer checks that you've passed a number or an array into `vlq.encode()`, making it significantly faster 38 | 39 | ## 0.2.0 40 | 41 | - Author as ES6 module, accessible to ES6-aware systems via the `jsnext:main` field in `package.json` 42 | 43 | ## 0.1.0 44 | 45 | - First release 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2021 [these people](https://github.com/Rich-Harris/vlq/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 | # vlq.js 2 | 3 | Convert integers to a Base64-encoded VLQ string, and vice versa. No dependencies, works in node.js or browsers, supports AMD. 4 | 5 | 6 | ## Why would you want to do that? 7 | 8 | Sourcemaps are the most likely use case. Mappings from original source to generated content are encoded as a sequence of VLQ strings. 9 | 10 | 11 | ## What is a VLQ string? 12 | 13 | A [variable-length quantity](http://en.wikipedia.org/wiki/Variable-length_quantity) is a compact way of encoding large integers in text (i.e. in situations where you can't transmit raw binary data). An integer represented as digits will always take up more space than the equivalent VLQ representation: 14 | 15 | | Integer | VLQ | 16 | | :------------------ | :--------- | 17 | | 0 | A | 18 | | 1 | C | 19 | | -1 | D | 20 | | 123 | 2H | 21 | | 123456789 | qxmvrH | 22 | 23 | 24 | ## Installation 25 | 26 | ```bash 27 | npm install vlq 28 | ``` 29 | 30 | 31 | ## Usage 32 | 33 | ### Encoding 34 | 35 | `vlq.encode` accepts an integer, or an array of integers, and returns a string: 36 | 37 | ```js 38 | vlq.encode(123); // '2H'; 39 | vlq.encode([123, 456, 789]); // '2HwcqxB' 40 | ``` 41 | 42 | ### Decoding 43 | 44 | `vlq.decode` accepts a string and always returns an array: 45 | 46 | ```js 47 | vlq.decode('2H'); // [123] 48 | vlq.decode('2HwcqxB'); // [123, 456, 789] 49 | ``` 50 | 51 | 52 | ## Limitations 53 | 54 | Since JavaScript bitwise operators work on 32 bit integers, the maximum value this library can handle is 2^30 - 1, or 1073741823. 55 | 56 | 57 | ## Using vlq.js with sourcemaps 58 | 59 | [See here for an example of using vlq.js with sourcemaps.](https://github.com/Rich-Harris/vlq/tree/master/sourcemaps) 60 | 61 | 62 | ## Credits 63 | 64 | Adapted from [murzwin.com/base64vlq.html](http://murzwin.com/base64vlq.html) by Alexander Pavlov. 65 | 66 | 67 | ## License 68 | 69 | [MIT](LICENSE). 70 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vlq", 3 | "version": "1.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.1", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "rollup": "^2.58.0", 12 | "typescript": "^4.4.4" 13 | } 14 | }, 15 | "node_modules/fsevents": { 16 | "version": "2.3.2", 17 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 18 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 19 | "dev": true, 20 | "hasInstallScript": true, 21 | "optional": true, 22 | "os": [ 23 | "darwin" 24 | ], 25 | "engines": { 26 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 27 | } 28 | }, 29 | "node_modules/rollup": { 30 | "version": "2.58.0", 31 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.0.tgz", 32 | "integrity": "sha512-NOXpusKnaRpbS7ZVSzcEXqxcLDOagN6iFS8p45RkoiMqPHDLwJm758UF05KlMoCRbLBTZsPOIa887gZJ1AiXvw==", 33 | "dev": true, 34 | "bin": { 35 | "rollup": "dist/bin/rollup" 36 | }, 37 | "engines": { 38 | "node": ">=10.0.0" 39 | }, 40 | "optionalDependencies": { 41 | "fsevents": "~2.3.2" 42 | } 43 | }, 44 | "node_modules/typescript": { 45 | "version": "4.4.4", 46 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", 47 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", 48 | "dev": true, 49 | "bin": { 50 | "tsc": "bin/tsc", 51 | "tsserver": "bin/tsserver" 52 | }, 53 | "engines": { 54 | "node": ">=4.2.0" 55 | } 56 | } 57 | }, 58 | "dependencies": { 59 | "fsevents": { 60 | "version": "2.3.2", 61 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 62 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 63 | "dev": true, 64 | "optional": true 65 | }, 66 | "rollup": { 67 | "version": "2.58.0", 68 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.0.tgz", 69 | "integrity": "sha512-NOXpusKnaRpbS7ZVSzcEXqxcLDOagN6iFS8p45RkoiMqPHDLwJm758UF05KlMoCRbLBTZsPOIa887gZJ1AiXvw==", 70 | "dev": true, 71 | "requires": { 72 | "fsevents": "~2.3.2" 73 | } 74 | }, 75 | "typescript": { 76 | "version": "4.4.4", 77 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", 78 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", 79 | "dev": true 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vlq", 3 | "description": "Generate, and decode, base64 VLQ mappings for source maps and other uses", 4 | "author": "Rich Harris", 5 | "repository": "https://github.com/Rich-Harris/vlq", 6 | "license": "MIT", 7 | "version": "2.0.4", 8 | "type": "module", 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": { 12 | "import": "./src/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "main": "dist/index.cjs", 17 | "module": "src/index.js", 18 | "types": "types/index.d.ts", 19 | "files": [ 20 | "src", 21 | "dist", 22 | "types" 23 | ], 24 | "devDependencies": { 25 | "rollup": "^2.58.0", 26 | "typescript": "^4.4.4" 27 | }, 28 | "scripts": { 29 | "build": "rm -rf dist && rollup -c && tsc", 30 | "test": "node test", 31 | "prepublishOnly": "npm test && npm run build" 32 | }, 33 | "keywords": [ 34 | "sourcemap", 35 | "sourcemaps", 36 | "base64", 37 | "vlq" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/index.js', 3 | output: { 4 | file: 'dist/index.cjs', 5 | format: 'umd', 6 | name: 'vlq' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /sourcemaps/Chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rich-Harris/vlq/e3009f5757abeb0b5b6233045f3bbdaf86435d08/sourcemaps/Chrome.png -------------------------------------------------------------------------------- /sourcemaps/Firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rich-Harris/vlq/e3009f5757abeb0b5b6233045f3bbdaf86435d08/sourcemaps/Firefox.png -------------------------------------------------------------------------------- /sourcemaps/README.md: -------------------------------------------------------------------------------- 1 | # Using vlq.js with source maps 2 | 3 | This library doesn't include any special magic for dealing with source maps, just the low-level encoding/decoding. But it's actually fairly straightforward to convert an incomprehensible-looking string like this... 4 | 5 | ``` 6 | AAAA;AAAA,EAAA,OAAO,CAAC,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA 7 | ``` 8 | 9 | ...into an array of mappings. Suppose we had some CoffeeScript code: 10 | 11 | **helloworld.coffee** 12 | ```coffee 13 | console.log 'hello world' 14 | ``` 15 | 16 | It would get transpiled into this: 17 | 18 | **helloworld.js** 19 | ```js 20 | (function() { 21 | console.log('hello world'); 22 | 23 | }).call(this); 24 | ``` 25 | 26 | And CoffeeScript would (if you asked it to) generate a sourcemap like this: 27 | 28 | **helloworld.js.map** 29 | ```js 30 | { 31 | "version": 3, 32 | "file": "helloworld.js", 33 | "sources": [ 34 | "helloworld.coffee" 35 | ], 36 | "names": [], 37 | "mappings": "AAAA;AAAA,EAAA,OAAO,CAAC,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA" 38 | } 39 | ``` 40 | 41 | (A source map simply a JSON object that adheres to a particular specification, [which you can find here](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1).) 42 | 43 | Each line in the generated JavaScript (`helloworld.js`) is represented as a series of VLQ-encoded *segments*, separated by the `,` character. The lines themselves are separated by `;` characters. So we could represent the mapping like so: 44 | 45 | ```js 46 | mappings = 'AAAA;AAAA,EAAA,OAAO,CAAC,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA'; 47 | vlqs = mappings.split(';').map(line => line.split(',')); 48 | 49 | [ 50 | // line 0 of helloworld.js (everything is zero-based) 51 | ['AAAA'], 52 | 53 | // line 1 54 | ['AAAA', 'EAAA', 'OAAO', 'CAAC', 'GAAR', 'CAAY', 'aAAZ', 'CAAA', 'CAAA'], 55 | 56 | // line 2 57 | ['AAAA'] 58 | ] 59 | ``` 60 | 61 | Using vlq.js to decode each segment, we can convert that into the following: 62 | 63 | ```js 64 | decoded = vlqs.map(line => line.map(vlq.decode)); 65 | 66 | [ 67 | // line 0 68 | [[0, 0, 0, 0]], 69 | 70 | // line 1 71 | [ 72 | [0, 0, 0, 0], 73 | [2, 0, 0, 0], 74 | [7, 0, 0, 7], 75 | [1, 0, 0, 1], 76 | [3, 0, 0, -8], 77 | [1, 0, 0, 12], 78 | [13, 0, 0, -12], 79 | [1, 0, 0, 0], 80 | [1, 0, 0, 0] 81 | ], 82 | 83 | // line 2 84 | [[0, 0, 0, 0]] 85 | ] 86 | ``` 87 | 88 | Each segment has 4 *fields* in this case, though the [spec](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1) allows segments to have either 1, 4 or 5 fields (in other words, 2, 3, 4 and 5 below are optional - in our CoffeeScript example, the fifth field is never used). They are: 89 | 90 | 1. The zero-based starting column of the current line. If this is the first segment of the line, it's absolute, otherwise it's relative to the same field in the previous segment. 91 | 2. The zero-based index of the original **source file**, as listed in the source map object's `sources` array (since the generated code may be the result of combining several files), *relative to the previous value*. 92 | 3. The zero-based starting **line** in the original source code that this segment corresponds to, *relative to the previous value*. 93 | 4. The zero-based starting **column** in the original source code that this segment corresponds to, *relative to the previous value*. 94 | 5. The zero-based index of the **name**, as listed in the source map object's `names` array, that this mapping corresponds to, *relative to the previous value*. (This isn't used here because no names are changed, but it's useful when minifying JavaScript, since `myVeryLongVarName` will get changed to `a` or similar.) 95 | 96 | We can now decode our mappings a bit further: 97 | 98 | ```js 99 | let sourceFileIndex = 0; // second field 100 | let sourceCodeLine = 0; // third field 101 | let sourceCodeColumn = 0; // fourth field 102 | let nameIndex = 0; // fifth field 103 | 104 | decoded = decoded.map(line => { 105 | let generatedCodeColumn = 0; // first field - reset each time 106 | 107 | return line.map(segment => { 108 | generatedCodeColumn += segment[0]; 109 | 110 | const result = [generatedCodeColumn]; 111 | 112 | if (segment.length === 1) { 113 | // only one field! 114 | return result; 115 | } 116 | 117 | sourceFileIndex += segment[1]; 118 | sourceCodeLine += segment[2]; 119 | sourceCodeColumn += segment[3]; 120 | 121 | result.push(sourceFileIndex, sourceCodeLine, sourceCodeColumn); 122 | 123 | if (segment.length === 5) { 124 | nameIndex += segment[4]; 125 | result.push(nameIndex); 126 | } 127 | 128 | return result; 129 | }); 130 | }); 131 | 132 | [ 133 | // line 0 134 | [[0, 0, 0, 0]], 135 | 136 | // line 1 137 | [ 138 | [0, 0, 0, 0], 139 | [2, 0, 0, 0], 140 | [9, 0, 0, 7], 141 | [10, 0, 0, 8], 142 | [13, 0, 0, 0], 143 | [14, 0, 0, 12], 144 | [27, 0, 0, 0], 145 | [28, 0, 0, 0], 146 | [29, 0, 0, 0] 147 | ], 148 | 149 | // line 2 150 | [[0, 0, 0, 0]] 151 | ] 152 | ``` 153 | 154 | The first and third lines don't really contain any interesting information. But the second line does. Let's take the first three segments: 155 | 156 | ```js 157 | // line 1 (the second line - still zero-based, remember) 158 | [ 159 | // Column 0 of line 1 corresponds to source file 0, line 0, column 0 160 | [0, 0, 0, 0], 161 | 162 | // Column 2 of line 1 also corresponds to 0, 0, 0! In other words, the 163 | // two spaces before `console` in helloworld.js don't correspond to 164 | // anything in helloworld.coffee 165 | [2, 0, 0, 0], 166 | 167 | // Column 9 of line 1 corresponds to 0, 0, 7. Taken together with the 168 | // previous segment, this means that columns 2-9 of line 1 in the 169 | // generated helloworld.js file correspond to columns 0-7 of line 0 170 | // in the original helloworld.coffee 171 | [9, 0, 0, 7], 172 | 173 | ... 174 | ] 175 | ``` 176 | 177 | It's through this fairly convoluted process that your browser (assuming it's one of the good ones) is able to read `helloworld.js` and an accompanying source map (typically `helloworld.js.map`) and do this: 178 | 179 | ### Chrome 180 | 181 |  182 | 183 | ### Firefox 184 | 185 |  186 | 187 | You can try this for yourself by cloning this repo and opening the `sourcemaps/index.html` file. -------------------------------------------------------------------------------- /sourcemaps/helloworld.coffee: -------------------------------------------------------------------------------- 1 | console.log 'hello world' -------------------------------------------------------------------------------- /sourcemaps/helloworld.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | console.log('hello world'); 3 | 4 | }).call(this); 5 | 6 | //# sourceMappingURL=helloworld.js.map -------------------------------------------------------------------------------- /sourcemaps/helloworld.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "helloworld.js", 4 | "sources": [ 5 | "helloworld.coffee" 6 | ], 7 | "names": [], 8 | "mappings": "AAAA;AAAA,EAAA,OAAO,CAAC,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA" 9 | } -------------------------------------------------------------------------------- /sourcemaps/index.html: -------------------------------------------------------------------------------- 1 |
Check the console! It should print 'hello world', and the source of the log should be reported as helloworld.coffee
, if you're in a modern browser (you may need to refresh the page with devtools open).
Clicking on helloworld.coffee
should open the original CoffeeScript file in the Sources pane of your devtools.