From 572656f8c48ea5be0e30a8a3f50cdcdfcfc18d29 Mon Sep 17 00:00:00 2001 From: omrilotan Date: Tue, 16 Jan 2024 13:11:35 +0000 Subject: [PATCH] Support custom headers --- .eslintrc | 5 - .github/workflows/release.yml | 28 ++-- .nvmrc | 2 +- CHANGELOG.md | 7 + README.md | 15 +- bin.js | 265 +++++++++++++++++++------------ index.js | 78 +++++---- lib/getSourceCodeMapUrl/index.js | 14 +- lib/listIndex/index.js | 3 +- lib/loader/index.js | 21 +-- lib/snippet/index.js | 47 +++--- lib/update/index.js | 25 +-- man | 15 ++ package.json | 21 ++- 14 files changed, 320 insertions(+), 226 deletions(-) delete mode 100644 .eslintrc create mode 100644 CHANGELOG.md create mode 100644 man diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 88f2f86..0000000 --- a/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "@omrilotan" - ] -} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea454a1..bb3d3a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,18 +7,18 @@ jobs: strategy: matrix: node-version: - - '16' + - "20" steps: - - uses: actions/checkout@v1 - - name: Install dependencies - run: npm i - - name: Lint code - run: npm run lint - - name: Add NPM token - if: github.ref == 'refs/heads/main' - run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Build and Publish - if: github.ref == 'refs/heads/main' - run: npx @lets/publish + - uses: actions/checkout@v1 + - name: Install dependencies + run: npm i + - name: Lint code + run: npm run lint + - name: Add NPM token + if: github.ref == 'refs/heads/main' + run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Build and Publish + if: github.ref == 'refs/heads/main' + run: npx @lets/publish diff --git a/.nvmrc b/.nvmrc index b6a7d89..209e3ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +20 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4d42160 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [2.0.0](https://github.com/omrilotan/isbot/compare/v1.3.7...v2.0.0) + +- Support adding custom headers to HTTP requests (see help) +- Drop support for EOL Node.js engines +- Change import to named (consuming as a module is now `import { colombo } from 'colombo'`) diff --git a/README.md b/README.md index d16c911..81776c5 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ## 🕵️‍♂️ Use source-map to view Javascript source code from CLI Run and follow instructions: + ```bash npx colombo ``` @@ -10,5 +11,17 @@ npx colombo ![](https://user-images.githubusercontent.com/516342/103102893-ef27da00-4626-11eb-928a-9c67c077520d.gif) ```bash -npx colombo [https://website.com/bundle.js:line:column] +$ colombo [file[:line[:column]]] [options] + +Examples: + +$ colombo --help +$ colombo https://cdn.example.com/app.d0892a20d266460d6c63.js +$ colombo https://cdn.example.com/app.d0892a20d266460d6c63.js:1:9694 +$ colombo https://cdn.example.com/app.d0892a20d266460d6c63.js:1:9694 -H "Access-Token: 1234" + +Options: + --header, -H "key: value" Add a header to the request + --version, -V Show version number + --help Show help ``` diff --git a/bin.js b/bin.js index 5cd61d7..0e2fd6e 100755 --- a/bin.js +++ b/bin.js @@ -1,135 +1,190 @@ #!/usr/bin/env node -const { prompt } = require('inquirer'); -const axios = require('axios'); -const { bold, red } = require('chalk'); -const { satisfies } = require('semver'); -const { getSourceCodeMapUrl } = require('./lib/getSourceCodeMapUrl'); -const { loader } = require('./lib/loader'); -const { update } = require('./lib/update'); -const { - version, - homepage, - dependencies: { axios: axiosVersion }, - engines: { node }, - bugs: { url: bugUrl }, -} = require('./package.json'); - -if (!satisfies(process.version, node)) { - console.error(new Error(`This node version (${process.version}) is not supported (${node}).`)); - process.exit(1); -} - -const colombo = require('.'); - -Object.assign( - axios.defaults.headers.common, - { 'User-Agent': `axios/${axiosVersion.replace(/\W/, '')}; (compatible; Colombo/${version}; bot; +${homepage})` }, -); - -start().then(console.log).catch(error => { - console.log('\n'); - if (typeof error.toJSON === 'function') { - console.error(error.toJSON()); - return; - } - error.message = red(error.message); - console.error(error); - - console.log(` -I can see you were not successful. Feel free to ${bold('submit an issue')} -${bugUrl}`); -}); +import { parseArgs } from "node:util"; +import { readFile } from "node:fs/promises"; +import inquirer from "inquirer"; +import chalk from "chalk"; +import semver from "semver"; +import { getSourceCodeMapUrl } from "./lib/getSourceCodeMapUrl/index.js"; +import { loader } from "./lib/loader/index.js"; +import { update } from "./lib/update/index.js"; + +const { satisfies } = semver; +const { bold, red } = chalk; +const { prompt } = inquirer; let updateMessage; +start(); async function start() { - try { - update().then(message => { - updateMessage = message; - }).catch(() => null); - - const [ arg = '' ] = process.argv.slice(2); + const packageJson = await readFile( + new URL("./package.json", import.meta.url), + "utf8", + ); + const { + name, + version, + homepage, + dependencies: {}, + engines: { node }, + bugs: { url: bugUrl }, + } = JSON.parse(packageJson); + + if (!satisfies(process.version, node)) { + console.error( + new Error( + `This node version (${process.version}) is not supported (${node}).`, + ), + ); + process.exit(1); + } - const { file } = await prompt( - [ - { - name: 'file', - message: 'File URL (optional)', - type: 'input', - default: arg, + try { + const { colombo } = await import("./index.js"); + + // Lazy call to update. Intentional race condition. + update({ name, version }) + .then( + (message) => message && process.on("exit", () => console.log(message)), + ) + .catch(() => null); + + const { + values: { header = [], help = false, version: showVersion = false }, + positionals: [arg = ""], + } = parseArgs({ + args: process.argv.slice(2), + options: { + header: { + type: "string", + short: "H", + multiple: true, }, - ], - ); + version: { + type: "boolean", + short: "V", + }, + help: { + type: "boolean", + }, + }, + allowPositionals: true, + strict: false, + }); + + if (help) { + console.log(await readFile(new URL("./man", import.meta.url), "utf8")); + process.exit(0); + } + if (showVersion) { + console.log([name, version].join("@")); + process.exit(0); + } + + const { file } = await prompt([ + { + name: "file", + message: "File URL (optional)", + type: "input", + default: arg, + }, + ]); const match = file.match(/:(?\d+):(?\d+)$/); - const { groups = { line: undefined, column: undefined, file } } = match || {}; - const clean = file.replace(/:\d+:\d+$/, '').replace(/\?.*/, ''); + const { groups = { line: undefined, column: undefined, file } } = + match || {}; + const clean = file.replace(/:\d+:\d+$/, "").replace(/\?.*/, ""); let url; - + const headers = new Headers( + header.map((header) => header.split(":").map((value) => value.trim())), + ); + if (!headers.has("User-Agent")) { + headers.set( + "User-Agent", + `node (compatible; Colombo/${version}; bot; +${homepage})`, + ); + } if (clean) { - loader.start('Load file'); - const { data: code } = await axios({ method: 'get', url: clean }); + loader.start("Load file"); + const response = await fetch(clean, { headers }); + if (!response.ok) { + throw new Error( + `Failed to load file ${clean}: ${response.status} ${response.statusText}`, + ); + } + const code = await response.text(); loader.end(); const mapUrl = getSourceCodeMapUrl(code, clean); if (mapUrl) { - ({ url } = await prompt([ { - name: 'url', - message: 'Source map (found)', - type: 'input', - default: mapUrl, - validate: Boolean, - } ]) - ); + ({ url } = await prompt([ + { + name: "url", + message: "Source map (found)", + type: "input", + default: mapUrl, + validate: Boolean, + }, + ])); } else { - ({ url } = await prompt([ { - name: 'url', - message: 'Source map (assumed)', - type: 'input', - default: clean + '.map', - validate: Boolean, - } ]) - ); + ({ url } = await prompt([ + { + name: "url", + message: "Source map (assumed)", + type: "input", + default: clean + ".map", + validate: Boolean, + }, + ])); } } else { - ({ url } = await prompt([ { - name: 'url', - message: 'Source map', - type: 'input', - validate: Boolean, - } ]) - ); + ({ url } = await prompt([ + { + name: "url", + message: "Source map", + type: "input", + validate: Boolean, + }, + ])); } if (!url) { - throw new Error('Source-map is a must'); + throw new Error("Source-map is a must"); } - const { column, line } = await prompt( - [ - { - name: 'line', - message: 'Line number', - type: 'number', - default: groups.line || 1, - }, - { - name: 'column', - message: 'Column number', - type: 'number', - default: groups.column || 1, - }, - ], - ); - - loader.start('Load source map'); + const { column, line } = await prompt([ + { + name: "line", + message: "Line number", + type: "number", + default: groups.line || 1, + }, + { + name: "column", + message: "Column number", + type: "number", + default: groups.column || 1, + }, + ]); + + loader.start("Load source map"); const result = await colombo({ url, line, column }); loader.end(); - - return [ result, updateMessage ].filter(Boolean).join('\n'); + console.log(result); + process.exit(0); } catch (error) { loader.end(); - throw error; + console.log("\n"); + if (typeof error.toJSON === "function") { + console.error(error.toJSON()); + process.exit(1); + } + error.message = red(error.message); + console.error(error); + + console.log(` +I can see you were not successful. Feel free to ${bold("submit an issue")} +${bugUrl}`); + process.exit(1); } } diff --git a/index.js b/index.js index a74c6fc..7e59278 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,17 @@ -const { SourceMapConsumer } = require('source-map'); -const axios = require('axios'); -const { yellow, red } = require('chalk'); -const { snippet } = require('./lib/snippet'); +import { SourceMapConsumer } from "source-map"; +import chalk from "chalk"; +import { snippet } from "./lib/snippet/index.js"; + +const { yellow, red } = chalk; /** - * @param {string} o.url Sourcemap URL - * @param {number} o.line Error Line - * @param {number} o.column Error column + * @param {string} o.url Sourcemap URL + * @param {number} o.line Error Line + * @param {number} o.column Error column + * @param {Headers} o.headers Request Headers * @returns {string} Source code visualisation */ -module.exports = async function colombo({ url, column, line }) { +export async function colombo({ url, column, line, headers }) { try { column = Number(column); line = Number(line); @@ -18,7 +20,7 @@ module.exports = async function colombo({ url, column, line }) { return '"column" and "line" must be numbers'; } - const data = await getData(url); + const data = await getData(url, { headers }); if (data instanceof Error) { throw red(`Could not find file at ${url}:\n${data.message}`); } @@ -26,25 +28,25 @@ module.exports = async function colombo({ url, column, line }) { const consumer = await new SourceMapConsumer(data); const source = consumer.originalPositionFor({ line, column }); if (!source.source) { - throw red(`Could not find source code from original position line ${line}, column ${column}`); + throw red( + `Could not find source code from original position line ${line}, column ${column}`, + ); } const sourceContent = consumer.sourceContentFor(source.source); if (!sourceContent) { - throw red(`Could not find source code for ${source.source.split('\n').pop()} position line ${line}, column ${column}`); + throw red( + `Could not find source code for ${source.source.split("\n").pop()} position line ${line}, column ${column}`, + ); } - const content = sourceContent.split('\n'); + const content = sourceContent.split("\n"); consumer.destroy(); return [ - yellow([ - source.source, - source.line, - source.column, - ].join(':')), - '\n------\n', + yellow([source.source, source.line, source.column].join(":")), + "\n------\n", snippet({ content, source }), - '\n------', - ].join(''); + "\n------", + ].join(""); } catch (error) { if (error.response) { error.message = `Request map file at ${url} resulted in ${error.response.status} ${error.response.statusText}`; @@ -52,19 +54,37 @@ module.exports = async function colombo({ url, column, line }) { throw error; } -}; +} -async function getData(url) { +async function getData(url, { headers } = {}) { try { - const result = await axios({ method: 'get', url }); - if (!result.status.toString().startsWith('2')) { - throw new Error(`${url} returned status ${result.status}`); + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error( + `Failed to load file ${url}: ${response.status} ${response.statusText}`, + ); + } + if (!response.ok) { + throw new Error( + `${url} returned status ${response.status} ${response.statusText}`, + ); + } + const data = await response.text(); + + if ( + typeof data === "string" && + data?.startsWith("<") && + response.headers["content-type"].includes("html") + ) { + throw new Error( + `${url} returns an HTML document with the title "${result.data.match(/(.*)<\/title>/)?.pop()}"`, + ); } - if (typeof result.data === 'string' && result.data?.startsWith('<') && result.headers['content-type'].includes('html')) { - throw new Error(`${url} returns an HTML document with the title "${result.data.match(/<title>(.*)<\/title>/)?.pop()}"`); + try { + return JSON.parse(data); + } catch (error) { + return error; } - const { data } = result; - return data; } catch (error) { return error; } diff --git a/lib/getSourceCodeMapUrl/index.js b/lib/getSourceCodeMapUrl/index.js index 9ebed17..83552a3 100644 --- a/lib/getSourceCodeMapUrl/index.js +++ b/lib/getSourceCodeMapUrl/index.js @@ -1,8 +1,8 @@ -const { dirname, join } = require('path'); -const { URL } = require('url'); -const sourceMappingURL = require('source-map-url'); +import { dirname, join } from "node:path"; +import { URL } from "node:url"; +import sourceMappingURL from "source-map-url"; -exports.getSourceCodeMapUrl = function (source, url) { +export function getSourceCodeMapUrl(source, url) { const file = sourceMappingURL.getFrom(source); if (!file) { @@ -22,8 +22,8 @@ exports.getSourceCodeMapUrl = function (source, url) { if (/^\/\w/.test(file)) { // Base of domain const { origin } = new URL(url); - return join( origin, file ); + return join(origin, file); } - return [ dirname(url), file ].join('/'); -}; + return [dirname(url), file].join("/"); +} diff --git a/lib/listIndex/index.js b/lib/listIndex/index.js index 9043ba5..57f1263 100644 --- a/lib/listIndex/index.js +++ b/lib/listIndex/index.js @@ -4,4 +4,5 @@ * @param {number} width * @returns {string} */ -exports.listIndex = (num, width) => num.toString().padStart(width, ' ') + '. '; +export const listIndex = (num, width) => + num.toString().padStart(width, " ") + ". "; diff --git a/lib/loader/index.js b/lib/loader/index.js index 89754f6..111ee62 100644 --- a/lib/loader/index.js +++ b/lib/loader/index.js @@ -1,23 +1,14 @@ -const { clear, update } = require('stdline'); +import { clear, update } from "stdline"; -const loader = [ - '◉○○○', - '◉○○○', - '○◉○○', - '○○◉○', - '○○○◉', - '○○○◉', - '○○◉○', - '○◉○○', -]; +const states = ["◉○○○", "◉○○○", "○◉○○", "○○◉○", "○○○◉", "○○○◉", "○○◉○", "○◉○○"]; let timer; -module.exports.loader = { +export const loader = { start: function start(message, index = 0) { clearTimeout(timer); - const graphics = loader[index]; - const next = index === loader.length - 1 ? 0 : index + 1; - update([ graphics, message ].join(' ')); + const graphics = states[index]; + const next = index === states.length - 1 ? 0 : index + 1; + update([graphics, message].join(" ")); timer = setTimeout(() => start(message, next), 50); }, end: function end() { diff --git a/lib/snippet/index.js b/lib/snippet/index.js index 28827fc..80dc958 100644 --- a/lib/snippet/index.js +++ b/lib/snippet/index.js @@ -1,10 +1,9 @@ -const { bold, dim, yellow } = require('chalk'); -const { listIndex } = require('../listIndex'); +import chalk from "chalk"; +import { listIndex } from "../listIndex/index.js"; -exports.snippet = function snippet({ - content, - source: { line, column }, -}) { +const { bold, dim, yellow } = chalk; + +export function snippet({ content, source: { line, column } }) { const index = line - 1; const start = Math.max(index - 5, 0); const end = Math.min(index + 5, content.length); @@ -12,26 +11,24 @@ exports.snippet = function snippet({ return content .slice(start, end) - .map( - (code, index) => { - const num = index + start + 1; - const prefix = listIndex(num, width); - const print = []; + .map((code, index) => { + const num = index + start + 1; + const prefix = listIndex(num, width); + const print = []; - if (num === line) { - const pad = prefix.length + column + 1; + if (num === line) { + const pad = prefix.length + column + 1; - // Add code line - print.push(yellow(prefix), bold(code)); + // Add code line + print.push(yellow(prefix), bold(code)); - // Add column marker - print.push('\n', '^'.padStart(pad, ' ')); - } else { - print.push(dim(prefix), code); - } + // Add column marker + print.push("\n", "^".padStart(pad, " ")); + } else { + print.push(dim(prefix), code); + } - return print.join(''); - }, - ) - .join('\n'); -}; + return print.join(""); + }) + .join("\n"); +} diff --git a/lib/update/index.js b/lib/update/index.js index 914bb44..c8ba87a 100644 --- a/lib/update/index.js +++ b/lib/update/index.js @@ -1,15 +1,16 @@ -const axios = require('axios'); -const { gt } = require('semver'); -const { name, version } = require('../../package.json'); +import semver from "semver"; +const { gt } = semver; -module.exports.update = async function update() { - const { data: { latest } } = await axios({ method: 'get', url: `https://registry.npmjs.com/-/package/${name}/dist-tags` }); +export async function update({ name, version }) { + const response = await fetch( + `https://registry.npmjs.com/-/package/${name}/dist-tags`, + ); + const { latest } = await response.json(); return gt(latest, version) ? [ - '📦', - `I can see you're running ${name} ${version}. Version ${latest} is available.`, - 'Install the latest version: "npm i colombo@latest -g"', - ].join('\n') - : '' - ; -}; + "📦", + `I can see you're running ${name} ${version}. Version ${latest} is available.`, + 'Install the latest version: "npm i colombo@latest -g"', + ].join("\n") + : ""; +} diff --git a/man b/man new file mode 100644 index 0000000..cbca3bf --- /dev/null +++ b/man @@ -0,0 +1,15 @@ +# colombo: Read source code from sourcemap location + +$ colombo [file[:line[:column]]] [options] + +Examples: + +$ colombo --help +$ colombo https://cdn.example.com/app.d0892a20d266460d6c63.js +$ colombo https://cdn.example.com/app.d0892a20d266460d6c63.js:1:9694 +$ colombo https://cdn.example.com/app.d0892a20d266460d6c63.js:1:9694 -H "Access-Token: 1234" + +Options: + --header, -H "key: value" Add a header to the request + --version, -V Show version number + --help Show help diff --git a/package.json b/package.json index a474945..32d1a66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "colombo", - "version": "1.3.7", + "version": "2.0.0", "description": "🕵️‍♂️ View Javascript source code using source-map from CLI", "keywords": [ "sourcemap", @@ -20,26 +20,25 @@ "url": "https://github.com/omrilotan/colombo/issues/new/choose" }, "engines": { - "node": ">=14" + "node": ">=18" }, + "type": "module", "main": "index.js", "bin": "bin.js", + "man": "man", "scripts": { - "lint": "eslint .", + "format": "prettier --write .", "start": "./bin.js" }, "dependencies": { - "axios": "^0.26.1", - "chalk": "^4.1.2", - "inquirer": "^8.2.1", - "semver": "^7.3.5", - "source-map": "^0.7.3", + "chalk": "^5.3.0", + "inquirer": "^9.2.12", + "semver": "^7.5.4", + "source-map": "^0.8.0-beta.0", "source-map-url": "^0.4.0", "stdline": "^1.1.1" }, "devDependencies": { - "@omrilotan/eslint-config": "^1.4.0", - "eslint": "^8.11.0", - "eslint-plugin-import": "^2.25.4" + "prettier": "^3.2.2" } }