├── .npmignore ├── .gitignore ├── lib ├── index.d.ts ├── index.js ├── postinstall.js └── download.js ├── jsconfig.json ├── package.json ├── .vscode └── launch.json ├── README.md └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | _node_modules/ 3 | bin/ -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare const rgPath: string; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "lib": [ 5 | "esnext" 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports.rgPath = path.join(__dirname, `../bin/rg${process.platform === 'win32' ? '.exe' : ''}`); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-ripgrep", 3 | "version": "1.5.6", 4 | "description": "A module for using ripgrep in a Node project", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/microsoft/vscode-ripgrep" 10 | }, 11 | "scripts": { 12 | "postinstall": "node ./lib/postinstall.js" 13 | }, 14 | "author": "Rob Lourens", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/node": "^10.12.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Postinstall", 11 | "program": "${workspaceFolder}/lib/postinstall.js", 12 | "runtimeVersion": "10.12.0" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-ripgrep 2 | 3 | This is an npm module for using [ripgrep](https://github.com/BurntSushi/ripgrep) in a Node project. It's used by VS Code. 4 | 5 | ## How it works 6 | 7 | - Ripgrep is built in [microsoft/ripgrep-prebuilt](https://github.com/microsoft/ripgrep-prebuilt) and published to releases for each tag in that repo. 8 | - In this module's postinstall task, it determines which platform it is being installed on and downloads the correct binary from ripgrep-prebuilt for the platform. 9 | - The path to the ripgrep binary is exported as `rgPath`. 10 | 11 | ### Usage example 12 | 13 | ```js 14 | const { rgPath } = require('vscode-ripgrep'); 15 | 16 | // child_process.spawn(rgPath, ...) 17 | ``` 18 | 19 | ### Dev note 20 | 21 | Runtime dependencies are not allowed in this project. This code runs on postinstall, and any dependencies would only be needed for postinstall, but they would have to be declared as `dependencies`, not `devDependencies`. Then if they were not cleaned up manually, they would end up being included in any project that uses this. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | vscode-ripgrep 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | 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: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | 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. 14 | -------------------------------------------------------------------------------- /lib/postinstall.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict'; 3 | 4 | const os = require('os'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const util = require('util'); 8 | 9 | const download = require('./download'); 10 | 11 | const fsExists = util.promisify(fs.exists); 12 | const mkdir = util.promisify(fs.mkdir); 13 | 14 | const forceInstall = process.argv.includes('--force'); 15 | if (forceInstall) { 16 | console.log('--force, ignoring caches'); 17 | } 18 | 19 | const VERSION = 'v11.0.1-2'; 20 | const BIN_PATH = path.join(__dirname, '../bin'); 21 | 22 | process.on('unhandledRejection', (reason, promise) => { 23 | console.log('Unhandled rejection: ', promise, 'reason:', reason); 24 | }); 25 | 26 | function getTarget() { 27 | const arch = process.env.npm_config_arch || os.arch(); 28 | 29 | switch (os.platform()) { 30 | case 'darwin': 31 | return 'x86_64-apple-darwin'; 32 | case 'win32': 33 | return arch === 'x64' ? 34 | 'x86_64-pc-windows-msvc' : 35 | 'i686-pc-windows-msvc'; 36 | case 'linux': 37 | return arch === 'x64' ? 'x86_64-unknown-linux-musl' : 38 | arch === 'arm' ? 'arm-unknown-linux-gnueabihf' : 39 | arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 40 | arch === 'ppc64' ? 'powerpc64le-unknown-linux-gnu' : 41 | 'i686-unknown-linux-musl' 42 | default: throw new Error('Unknown platform: ' + os.platform()); 43 | } 44 | } 45 | 46 | async function main() { 47 | const binExists = await fsExists(BIN_PATH); 48 | if (!forceInstall && binExists) { 49 | console.log('bin/ folder already exists, exiting'); 50 | process.exit(0); 51 | } 52 | 53 | if (!binExists) { 54 | await mkdir(BIN_PATH); 55 | } 56 | 57 | const opts = { 58 | version: VERSION, 59 | token: process.env['GITHUB_TOKEN'], 60 | target: getTarget(), 61 | destDir: BIN_PATH, 62 | force: forceInstall 63 | }; 64 | try { 65 | await download(opts); 66 | } catch (err) { 67 | console.error(`Downloading ripgrep failed: ${err.stack}`); 68 | process.exit(1); 69 | } 70 | } 71 | 72 | main(); -------------------------------------------------------------------------------- /lib/download.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const os = require('os'); 7 | const https = require('https'); 8 | const util = require('util'); 9 | const url = require('url'); 10 | const child_process = require('child_process'); 11 | 12 | const packageVersion = require('../package.json').version; 13 | const tmpDir = path.join(os.tmpdir(), `vscode-ripgrep-cache-${packageVersion}`); 14 | 15 | const fsUnlink = util.promisify(fs.unlink); 16 | const fsExists = util.promisify(fs.exists); 17 | const fsMkdir = util.promisify(fs.mkdir); 18 | 19 | const isWindows = os.platform() === 'win32'; 20 | 21 | const REPO = 'microsoft/ripgrep-prebuilt'; 22 | 23 | function isGithubUrl(_url) { 24 | return url.parse(_url).hostname === 'api.github.com'; 25 | } 26 | 27 | function downloadWin(url, dest, opts) { 28 | return new Promise((resolve, reject) => { 29 | let userAgent; 30 | if (opts.headers['user-agent']) { 31 | userAgent = opts.headers['user-agent']; 32 | delete opts.headers['user-agent']; 33 | } 34 | const headerValues = Object.keys(opts.headers) 35 | .map(key => `\\"${key}\\"=\\"${opts.headers[key]}\\"`) 36 | .join('; '); 37 | const headers = `@{${headerValues}}`; 38 | console.log('Downloading with Invoke-WebRequest'); 39 | let iwrCmd = `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -URI ${url} -UseBasicParsing -OutFile ${dest} -Headers ${headers}`; 40 | if (userAgent) { 41 | iwrCmd += ' -UserAgent ' + userAgent; 42 | } 43 | 44 | iwrCmd = `powershell "${iwrCmd}"`; 45 | 46 | child_process.exec(iwrCmd, err => { 47 | if (err) { 48 | reject(err); 49 | return; 50 | } 51 | resolve(); 52 | }); 53 | }); 54 | } 55 | 56 | function download(_url, dest, opts) { 57 | if (isWindows) { 58 | // This alternative strategy shouldn't be necessary but sometimes on Windows the file does not get closed, 59 | // so unzipping it fails, and I don't know why. 60 | return downloadWin(_url, dest, opts); 61 | } 62 | 63 | if (opts.headers && opts.headers.authorization && !isGithubUrl(_url)) { 64 | delete opts.headers.authorization; 65 | } 66 | 67 | return new Promise((resolve, reject) => { 68 | console.log(`Download options: ${JSON.stringify(opts)}`); 69 | const outFile = fs.createWriteStream(dest); 70 | const mergedOpts = { 71 | ...url.parse(_url), 72 | ...opts 73 | }; 74 | https.get(mergedOpts, response => { 75 | console.log('statusCode: ' + response.statusCode); 76 | if (response.statusCode === 302) { 77 | console.log('Following redirect to: ' + response.headers.location); 78 | return download(response.headers.location, dest, opts) 79 | .then(resolve, reject); 80 | } else if (response.statusCode !== 200) { 81 | reject(new Error('Download failed with ' + response.statusCode)); 82 | return; 83 | } 84 | 85 | response.pipe(outFile); 86 | outFile.on('finish', () => { 87 | resolve(); 88 | }); 89 | }).on('error', async err => { 90 | await fsUnlink(dest); 91 | reject(err); 92 | }); 93 | }); 94 | } 95 | 96 | function get(_url, opts) { 97 | console.log(`GET ${_url}`); 98 | return new Promise((resolve, reject) => { 99 | let result = ''; 100 | opts = { 101 | ...url.parse(_url), 102 | ...opts 103 | }; 104 | https.get(opts, response => { 105 | if (response.statusCode !== 200) { 106 | reject(new Error('Request failed: ' + response.statusCode)); 107 | } 108 | 109 | response.on('data', d => { 110 | result += d.toString(); 111 | }); 112 | 113 | response.on('end', () => { 114 | resolve(result); 115 | }); 116 | 117 | response.on('error', e => { 118 | reject(e); 119 | }); 120 | }); 121 | }); 122 | } 123 | 124 | function getApiUrl(repo, tag) { 125 | return `https://api.github.com/repos/${repo}/releases/tags/${tag}`; 126 | } 127 | 128 | /** 129 | * @param {{ force: boolean; token: string; version: string; }} opts 130 | * @param {string} assetName 131 | * @param {string} downloadFolder 132 | */ 133 | async function getAssetFromGithubApi(opts, assetName, downloadFolder) { 134 | const assetDownloadPath = path.join(downloadFolder, assetName); 135 | 136 | // We can just use the cached binary 137 | if (!opts.force && await fsExists(assetDownloadPath)) { 138 | console.log('Using cached download: ' + assetDownloadPath); 139 | return assetDownloadPath; 140 | } 141 | 142 | const downloadOpts = { 143 | headers: { 144 | 'user-agent': 'vscode-ripgrep' 145 | } 146 | }; 147 | if (opts.token) { 148 | downloadOpts.headers.authorization = `token ${opts.token}`; 149 | } 150 | 151 | console.log(`Finding release for ${opts.version}`); 152 | const release = await get(getApiUrl(REPO, opts.version), downloadOpts); 153 | let jsonRelease; 154 | try { 155 | jsonRelease = JSON.parse(release); 156 | } catch (e) { 157 | throw new Error('Malformed API response: ' + e.stack); 158 | } 159 | 160 | if (!jsonRelease.assets) { 161 | throw new Error('Bad API response: ' + JSON.stringify(release)); 162 | } 163 | 164 | const asset = jsonRelease.assets.find(a => a.name === assetName); 165 | if (!asset) { 166 | throw new Error('Asset not found with name: ' + assetName); 167 | } 168 | 169 | console.log(`Downloading from ${asset.url}`); 170 | console.log(`Downloading to ${assetDownloadPath}`); 171 | 172 | downloadOpts.headers.accept = 'application/octet-stream'; 173 | await download(asset.url, assetDownloadPath, downloadOpts); 174 | } 175 | 176 | function unzipWindows(zipPath, destinationDir) { 177 | return new Promise((resolve, reject) => { 178 | zipPath = sanitizePathForPowershell(zipPath); 179 | destinationDir = sanitizePathForPowershell(destinationDir); 180 | const expandCmd = 'powershell -ExecutionPolicy Bypass -Command Expand-Archive ' + ['-Path', zipPath, '-DestinationPath', destinationDir, '-Force'].join(' '); 181 | child_process.exec(expandCmd, (err, _stdout, stderr) => { 182 | if (err) { 183 | reject(err); 184 | return; 185 | } 186 | 187 | if (stderr) { 188 | console.log(stderr); 189 | reject(new Error(stderr)); 190 | return; 191 | } 192 | 193 | console.log('Expand-Archive completed'); 194 | resolve(); 195 | }); 196 | }); 197 | } 198 | 199 | // Handle whitespace in filepath as powershell split's path with whitespaces 200 | function sanitizePathForPowershell(path) { 201 | path = path.replace(' ', '` '); // replace whitespace with "` " as solution provided here https://stackoverflow.com/a/18537344/7374562 202 | return path; 203 | } 204 | 205 | function untar(zipPath, destinationDir) { 206 | return new Promise((resolve, reject) => { 207 | const unzipProc = child_process.spawn('tar', ['xvf', zipPath, '-C', destinationDir], { stdio: 'inherit' }); 208 | unzipProc.on('error', err => { 209 | reject(err); 210 | }); 211 | unzipProc.on('close', code => { 212 | console.log(`tar xvf exited with ${code}`); 213 | if (code !== 0) { 214 | reject(new Error(`tar xvf exited with ${code}`)); 215 | return; 216 | } 217 | 218 | resolve(); 219 | }); 220 | }); 221 | } 222 | 223 | async function unzipRipgrep(zipPath, destinationDir) { 224 | if (isWindows) { 225 | await unzipWindows(zipPath, destinationDir); 226 | } else { 227 | await untar(zipPath, destinationDir); 228 | } 229 | 230 | const expectedName = path.join(destinationDir, 'rg'); 231 | if (await fsExists(expectedName)) { 232 | return expectedName; 233 | } 234 | 235 | if (await fsExists(expectedName + '.exe')) { 236 | return expectedName + '.exe'; 237 | } 238 | 239 | throw new Error(`Expecting rg or rg.exe unzipped into ${destinationDir}, didn't find one.`); 240 | } 241 | 242 | module.exports = async opts => { 243 | if (!opts.version) { 244 | return Promise.reject(new Error('Missing version')); 245 | } 246 | 247 | if (!opts.target) { 248 | return Promise.reject(new Error('Missing target')); 249 | } 250 | 251 | const extension = isWindows ? '.zip' : '.tar.gz'; 252 | const assetName = ['ripgrep', opts.version, opts.target].join('-') + extension; 253 | 254 | if (!await fsExists(tmpDir)) { 255 | await fsMkdir(tmpDir); 256 | } 257 | 258 | const assetDownloadPath = path.join(tmpDir, assetName); 259 | try { 260 | await getAssetFromGithubApi(opts, assetName, tmpDir) 261 | } catch (e) { 262 | console.log('Deleting invalid download cache'); 263 | try { 264 | await fsUnlink(assetDownloadPath); 265 | } catch (e) {} 266 | 267 | throw e; 268 | } 269 | 270 | console.log(`Unzipping to ${opts.destDir}`); 271 | try { 272 | const destinationPath = await unzipRipgrep(assetDownloadPath, opts.destDir); 273 | if (!isWindows) { 274 | await util.promisify(fs.chmod)(destinationPath, '755'); 275 | } 276 | } catch (e) { 277 | console.log('Deleting invalid download'); 278 | 279 | try { 280 | await fsUnlink(assetDownloadPath); 281 | } catch (e) {} 282 | 283 | throw e; 284 | } 285 | }; --------------------------------------------------------------------------------