diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..245f9ad9 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + commit-message: + prefix: chore diff --git a/README.md b/README.md index cfb45ac1..d5ed49e4 100644 --- a/README.md +++ b/README.md @@ -21,30 +21,30 @@ $ npm install --global google-rank To retrieve the rank of a website for a specific keyword, run the `google-rank` tool followed by the website URL and the search keyword: ``` -$ google-rank wikipedia.org krakatoa +$ google-rank wikipedia.org --keywords krakatoa Ranks for wikipedia.org website: -1 krakatoa +page 1 rank 1 krakatoa ``` Multiple keywords can also be specified: ``` -$ google-rank wikipedia.org krakatoa mit 'social media' +$ google-rank wikipedia.org --keywords krakatoa facebook 'social media' Ranks for wikipedia.org website: -1 krakatoa -2 mit -1 social media +page 1 rank 1 krakatoa +page 2 rank 2 facebook +page 1 rank 1 social media ``` If the website is not found for the specified keywords, it will output the rank as `?`: ``` -$ google-rank wikipedia.org 'best city to travel' +$ google-rank wikipedia.org --keywords 'best city to travel' Ranks for wikipedia.org website: -? best city to travel +page ? rank ? best city to travel ``` ## License diff --git a/lib/google-rank.js b/lib/google-rank.js index 2978cc7b..cea979c9 100755 --- a/lib/google-rank.js +++ b/lib/google-rank.js @@ -1,26 +1,54 @@ #!/usr/bin/env node "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); -const commander_1 = require("commander"); -const utils_1 = require("./utils"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const utils = __importStar(require("./utils")); async function run() { - commander_1.program - .argument("", "website name") - .arguments("") - .parse(); - const website = commander_1.program.args[0]; - const keywords = commander_1.program.args.slice(1); + const parser = new utils.ArgumentsParser(); + const args = await parser.parse(); const rankByKeywords = []; - for (const keyword of keywords) { - const prom = (0, utils_1.getWebsiteRank)(website, keyword); + for (const keyword of args.keywords) { + const prom = utils.googleGetWebsiteRank(args.website, keyword, { + maxPage: args.maxPage, + }); rankByKeywords.push([keyword, prom]); } - process.stdout.write(`Ranks for ${website} website:\n`); + process.stdout.write(`Ranks for ${chalk_1.default.blueBright(args.website)} website:\n`); + const loading = (0, ora_1.default)("Getting ranks..."); + loading.start(); for (const [keyword, prom] of rankByKeywords) { - const rank = await prom; - const rankStr = rank > 0 ? `${rank}` : "?"; - process.stdout.write(`${rankStr} ${keyword}\n`); + loading.text = `Getting ranks of ${chalk_1.default.blueBright(keyword)} keyword...`; + const str = utils.formatKeywordRank(keyword, await prom); + process.stdout.write(`\r\x1b[K${str}\n`); } + loading.stop(); } run(); //# sourceMappingURL=google-rank.js.map \ No newline at end of file diff --git a/lib/google-rank.js.map b/lib/google-rank.js.map index 1f268c89..f67799df 100644 --- a/lib/google-rank.js.map +++ b/lib/google-rank.js.map @@ -1 +1 @@ -{"version":3,"file":"google-rank.js","sourceRoot":"","sources":["../src/google-rank.ts"],"names":[],"mappings":";;;AAEA,yCAAoC;AACpC,mCAAyC;AAEzC,KAAK,UAAU,GAAG;IAChB,mBAAO;SACJ,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC;SACrC,SAAS,CAAC,eAAe,CAAC;SAC1B,KAAK,EAAE,CAAC;IAEX,MAAM,OAAO,GAAG,mBAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,mBAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEvC,MAAM,cAAc,GAAgC,EAAE,CAAC;IACvD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE;QAC9B,MAAM,IAAI,GAAG,IAAA,sBAAc,EAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC9C,cAAc,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;KACtC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,OAAO,aAAa,CAAC,CAAC;IACxD,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,cAAc,EAAE;QAC5C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;QACxB,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,OAAO,IAAI,CAAC,CAAC;KACjD;AACH,CAAC;AAED,GAAG,EAAE,CAAC"} \ No newline at end of file +{"version":3,"file":"google-rank.js","sourceRoot":"","sources":["../src/google-rank.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,kDAA0B;AAC1B,8CAAsB;AACtB,+CAAiC;AAIjC,KAAK,UAAU,GAAG;IAChB,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,eAAe,EAAE,CAAC;IAC3C,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IAElC,MAAM,cAAc,GAA4B,EAAE,CAAC;IACnD,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE;QACnC,MAAM,IAAI,GAAG,KAAK,CAAC,oBAAoB,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE;YAC7D,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB,CAAC,CAAC;QACH,cAAc,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;KACtC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,aAAa,eAAK,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CACzD,CAAC;IAEF,MAAM,OAAO,GAAG,IAAA,aAAG,EAAC,kBAAkB,CAAC,CAAC;IACxC,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,cAAc,EAAE;QAC5C,OAAO,CAAC,IAAI,GAAG,oBAAoB,eAAK,CAAC,UAAU,CAAC,OAAO,CAAC,aAAa,CAAC;QAC1E,MAAM,GAAG,GAAG,KAAK,CAAC,iBAAiB,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,CAAC;QACzD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;KAC1C;IACD,OAAO,CAAC,IAAI,EAAE,CAAC;AACjB,CAAC;AAED,GAAG,EAAE,CAAC"} \ No newline at end of file diff --git a/lib/utils.d.ts b/lib/utils.d.ts deleted file mode 100644 index eb4a94c4..00000000 --- a/lib/utils.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Retrieves a list of websites from Google search results based on the provided keyword. - * @param keyword The keyword to search for. - * @returns A promise that resolves to an array of website URLs. - */ -export declare function listWebsites(keyword: string): Promise; -/** - * Retrieves the rank of a website in Google search results for a specific keyword. - * @param website The website URL to check the rank for. - * @param keyword The keyword to search for. - * @returns A promise that resolves to the rank of the website (1-based index). Returns 0 if the website is not found in the search results. - */ -export declare function getWebsiteRank(website: string, keyword: string): Promise; diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index bdafdff5..00000000 --- a/lib/utils.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getWebsiteRank = exports.listWebsites = void 0; -const googlethis_1 = __importDefault(require("googlethis")); -/** - * Retrieves a list of websites from Google search results based on the provided keyword. - * @param keyword The keyword to search for. - * @returns A promise that resolves to an array of website URLs. - */ -async function listWebsites(keyword) { - const res = await googlethis_1.default.search(keyword, { parse_ads: false }); - const websites = []; - let prevWebsite = ""; - for (const result of res.results) { - const website = new URL(result.url).hostname; - if (website !== prevWebsite) - websites.push(website); - prevWebsite = website; - } - return websites; -} -exports.listWebsites = listWebsites; -/** - * Retrieves the rank of a website in Google search results for a specific keyword. - * @param website The website URL to check the rank for. - * @param keyword The keyword to search for. - * @returns A promise that resolves to the rank of the website (1-based index). Returns 0 if the website is not found in the search results. - */ -async function getWebsiteRank(website, keyword) { - const websites = await listWebsites(keyword); - for (let i = 0; i < websites.length; ++i) { - if (websites[i].includes(website)) - return i + 1; - } - return 0; -} -exports.getWebsiteRank = getWebsiteRank; -//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/lib/utils.js.map b/lib/utils.js.map deleted file mode 100644 index 7506cb21..00000000 --- a/lib/utils.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;;;;AAAA,4DAAgC;AAEhC;;;;GAIG;AACI,KAAK,UAAU,YAAY,CAAC,OAAe;IAChD,MAAM,GAAG,GAAG,MAAM,oBAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE;QAChC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;QAC7C,IAAI,OAAO,KAAK,WAAW;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpD,WAAW,GAAG,OAAO,CAAC;KACvB;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAVD,oCAUC;AAED;;;;;GAKG;AACI,KAAK,UAAU,cAAc,CAClC,OAAe,EACf,OAAe;IAEf,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;IAC7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE;QACxC,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC;KACjD;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AATD,wCASC"} \ No newline at end of file diff --git a/lib/utils/arguments.d.ts b/lib/utils/arguments.d.ts new file mode 100644 index 00000000..2227bd68 --- /dev/null +++ b/lib/utils/arguments.d.ts @@ -0,0 +1,27 @@ +/** + * Represents the arguments and options of the program. + */ +export interface Arguments { + /** Website name. */ + website: string; + /** Keywords to search for. */ + keywords: string[]; + /** Maximum page to search for. */ + maxPage: number; +} +/** + * Represents the arguments and options parser of the program. + */ +export declare class ArgumentsParser { + #private; + /** + * Constructs a new instance of the arguments and options parser of the program. + */ + constructor(); + /** + * Parses the arguments and options of the program. + * @param argv - An optional array of strings representing the command-line arguments. + * @returns A promise that resolves to the arguments and options. + */ + parse(argv?: readonly string[]): Promise; +} diff --git a/lib/utils/arguments.js b/lib/utils/arguments.js new file mode 100644 index 00000000..707bd212 --- /dev/null +++ b/lib/utils/arguments.js @@ -0,0 +1,102 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var _ArgumentsParser_program; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ArgumentsParser = void 0; +const commander_1 = require("commander"); +const fs = __importStar(require("fs")); +const readline = __importStar(require("readline")); +/** + * Represents the arguments and options parser of the program. + */ +class ArgumentsParser { + /** + * Constructs a new instance of the arguments and options parser of the program. + */ + constructor() { + _ArgumentsParser_program.set(this, void 0); + __classPrivateFieldSet(this, _ArgumentsParser_program, new commander_1.Command(), "f"); + // Sets up the arguments and options available in the program + __classPrivateFieldGet(this, _ArgumentsParser_program, "f") + .argument("", "website name") + .option("--keywords ", "keywords to search for") + .option("--file ", "file to read keywords from") + .option("--max-page ", "maximum page to search for", "3"); + } + /** + * Parses the arguments and options of the program. + * @param argv - An optional array of strings representing the command-line arguments. + * @returns A promise that resolves to the arguments and options. + */ + async parse(argv) { + __classPrivateFieldGet(this, _ArgumentsParser_program, "f").parse(argv); + const opts = __classPrivateFieldGet(this, _ArgumentsParser_program, "f").opts(); + const args = { + website: __classPrivateFieldGet(this, _ArgumentsParser_program, "f").args[0], + keywords: opts.keywords ?? [], + maxPage: parseInt(opts.maxPage, 10), + }; + // Parse additional keywords from the given file, if specified. + if (opts.file !== undefined) { + const keywords = await readKeywordsFromFile(opts.file); + args.keywords = args.keywords.concat(keywords); + } + // If no keywords are provided, use the website as the keyword. + if (args.keywords.length < 1) { + args.keywords = [args.website]; + } + return args; + } +} +exports.ArgumentsParser = ArgumentsParser; +_ArgumentsParser_program = new WeakMap(); +/** + * Reads keywords from the specified file. + * @param filename - The file to read keywords from. + * @returns A promise that resolves to a list of keywords. + */ +async function readKeywordsFromFile(filename) { + const file = fs.createReadStream(filename); + const read = readline.createInterface({ input: file }); + const keywords = []; + for await (const line of read) { + const trimmedLine = line.trim(); + if (trimmedLine.length > 0) + keywords.push(trimmedLine); + } + return keywords; +} +//# sourceMappingURL=arguments.js.map \ No newline at end of file diff --git a/lib/utils/arguments.js.map b/lib/utils/arguments.js.map new file mode 100644 index 00000000..f36a52a8 --- /dev/null +++ b/lib/utils/arguments.js.map @@ -0,0 +1 @@ +{"version":3,"file":"arguments.js","sourceRoot":"","sources":["../../src/utils/arguments.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yCAAoC;AACpC,uCAAyB;AACzB,mDAAqC;AAgBrC;;GAEG;AACH,MAAa,eAAe;IAG1B;;OAEG;IACH;QALA,2CAAkB;QAMhB,uBAAA,IAAI,4BAAY,IAAI,mBAAO,EAAE,MAAA,CAAC;QAE9B,6DAA6D;QAC7D,uBAAA,IAAI,gCAAS;aACV,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC;aACrC,MAAM,CAAC,wBAAwB,EAAE,wBAAwB,CAAC;aAC1D,MAAM,CAAC,iBAAiB,EAAE,4BAA4B,CAAC;aACvD,MAAM,CAAC,qBAAqB,EAAE,4BAA4B,EAAE,GAAG,CAAC,CAAC;IACtE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,IAAwB;QAClC,uBAAA,IAAI,gCAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAE1B,MAAM,IAAI,GAAG,uBAAA,IAAI,gCAAS,CAAC,IAAI,EAAE,CAAC;QAClC,MAAM,IAAI,GAAc;YACtB,OAAO,EAAE,uBAAA,IAAI,gCAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC9B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;YAC7B,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;SACpC,CAAC;QAEF,+DAA+D;QAC/D,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE;YAC3B,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;SAChD;QAED,+DAA+D;QAC/D,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE;YAC5B,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;SAChC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AA7CD,0CA6CC;;AAED;;;;GAIG;AACH,KAAK,UAAU,oBAAoB,CAAC,QAAgB;IAClD,MAAM,IAAI,GAAG,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,EAAE;QAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAChC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;KACxD;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"} \ No newline at end of file diff --git a/lib/utils/format.d.ts b/lib/utils/format.d.ts new file mode 100644 index 00000000..e9f3b975 --- /dev/null +++ b/lib/utils/format.d.ts @@ -0,0 +1,8 @@ +import { GoogleWebsiteRank } from "./google"; +/** + * Formats the rank of a keyword as a string. + * @param keyword - The keyword string. + * @param rank - The rank of the keyword. + * @returns A formatted string. The rank will be displayed as a question mark if it is undefined. + */ +export declare function formatKeywordRank(keyword: string, rank?: GoogleWebsiteRank): string; diff --git a/lib/utils/format.js b/lib/utils/format.js new file mode 100644 index 00000000..f6a9ad26 --- /dev/null +++ b/lib/utils/format.js @@ -0,0 +1,32 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.formatKeywordRank = void 0; +const chalk_1 = __importDefault(require("chalk")); +function formatPageRank(rank) { + if (rank === undefined) + return `page ${chalk_1.default.blackBright("?")}`; + return rank.page <= 0 + ? `page ${chalk_1.default.greenBright(rank.page + 1)}` + : `page ${chalk_1.default.redBright(rank.page + 1)}`; +} +function formatRank(rank) { + if (rank === undefined) + return `rank ${chalk_1.default.blackBright("?")}`; + return rank.page <= 0 && rank.rank <= 2 + ? `rank ${chalk_1.default.greenBright(rank.rank + 1)}` + : `rank ${chalk_1.default.redBright(rank.rank + 1)}`; +} +/** + * Formats the rank of a keyword as a string. + * @param keyword - The keyword string. + * @param rank - The rank of the keyword. + * @returns A formatted string. The rank will be displayed as a question mark if it is undefined. + */ +function formatKeywordRank(keyword, rank) { + return `${formatPageRank(rank)} ${formatRank(rank)} ${keyword}`; +} +exports.formatKeywordRank = formatKeywordRank; +//# sourceMappingURL=format.js.map \ No newline at end of file diff --git a/lib/utils/format.js.map b/lib/utils/format.js.map new file mode 100644 index 00000000..9c168f38 --- /dev/null +++ b/lib/utils/format.js.map @@ -0,0 +1 @@ +{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA0B;AAG1B,SAAS,cAAc,CAAC,IAAwB;IAC9C,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,QAAQ,eAAK,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;IAChE,OAAO,IAAI,CAAC,IAAI,IAAI,CAAC;QACnB,CAAC,CAAC,QAAQ,eAAK,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE;QAC5C,CAAC,CAAC,QAAQ,eAAK,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,UAAU,CAAC,IAAwB;IAC1C,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,QAAQ,eAAK,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;IAChE,OAAO,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC;QACrC,CAAC,CAAC,QAAQ,eAAK,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE;QAC5C,CAAC,CAAC,QAAQ,eAAK,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAC/B,OAAe,EACf,IAAwB;IAExB,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;AACpE,CAAC;AALD,8CAKC"} \ No newline at end of file diff --git a/lib/utils/google.d.ts b/lib/utils/google.d.ts new file mode 100644 index 00000000..de41e7d1 --- /dev/null +++ b/lib/utils/google.d.ts @@ -0,0 +1,28 @@ +/** + * Retrieves a list of websites from Google search results based on the provided keyword. + * @param keyword - The keyword to search for. + * @param opts - Additional options. + * @returns A promise that resolves to an array of website URLs. + */ +export declare function googleListWebsites(keyword: string, opts?: { + page?: number; +}): Promise; +/** + * Represents the ranking of a website in Google Search. + */ +export interface GoogleWebsiteRank { + /** The search page ranking of the website. */ + page: number; + /** The ranking of the website on the specified page. */ + rank: number; +} +/** + * Retrieves the rank of a website in Google search results for a specific keyword. + * @param website - The website URL to check the rank for. + * @param keyword - The keyword to search for. + * @param opts - Additional options. + * @returns A promise that resolves to the rank of the website. Returns `undefined` if the website is not found in the search results. + */ +export declare function googleGetWebsiteRank(website: string, keyword: string, opts?: { + maxPage?: number; +}): Promise; diff --git a/lib/utils/google.js b/lib/utils/google.js new file mode 100644 index 00000000..6acaa59b --- /dev/null +++ b/lib/utils/google.js @@ -0,0 +1,50 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.googleGetWebsiteRank = exports.googleListWebsites = void 0; +const googlethis_1 = __importDefault(require("googlethis")); +/** + * Retrieves a list of websites from Google search results based on the provided keyword. + * @param keyword - The keyword to search for. + * @param opts - Additional options. + * @returns A promise that resolves to an array of website URLs. + */ +async function googleListWebsites(keyword, opts) { + const res = await googlethis_1.default.search(keyword, { + page: opts?.page ?? 0, + parse_ads: false, + }); + const websites = []; + let prevWebsite = ""; + for (const result of res.results) { + const website = new URL(result.url).hostname; + if (website !== prevWebsite) + websites.push(website); + prevWebsite = website; + } + return websites; +} +exports.googleListWebsites = googleListWebsites; +/** + * Retrieves the rank of a website in Google search results for a specific keyword. + * @param website - The website URL to check the rank for. + * @param keyword - The keyword to search for. + * @param opts - Additional options. + * @returns A promise that resolves to the rank of the website. Returns `undefined` if the website is not found in the search results. + */ +async function googleGetWebsiteRank(website, keyword, opts) { + const maxPage = opts?.maxPage ?? 1; + for (let page = 0; page < maxPage; ++page) { + const websites = await googleListWebsites(keyword, { page }); + for (let rank = 0; rank < websites.length; ++rank) { + if (websites[rank].includes(website)) { + return { page, rank }; + } + } + } + return undefined; +} +exports.googleGetWebsiteRank = googleGetWebsiteRank; +//# sourceMappingURL=google.js.map \ No newline at end of file diff --git a/lib/utils/google.js.map b/lib/utils/google.js.map new file mode 100644 index 00000000..747bef3f --- /dev/null +++ b/lib/utils/google.js.map @@ -0,0 +1 @@ +{"version":3,"file":"google.js","sourceRoot":"","sources":["../../src/utils/google.ts"],"names":[],"mappings":";;;;;;AAAA,4DAAgC;AAEhC;;;;;GAKG;AACI,KAAK,UAAU,kBAAkB,CACtC,OAAe,EACf,IAAwB;IAExB,MAAM,GAAG,GAAG,MAAM,oBAAM,CAAC,MAAM,CAAC,OAAO,EAAE;QACvC,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC;QACrB,SAAS,EAAE,KAAK;KACjB,CAAC,CAAC;IACH,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE;QAChC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;QAC7C,IAAI,OAAO,KAAK,WAAW;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpD,WAAW,GAAG,OAAO,CAAC;KACvB;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAhBD,gDAgBC;AAaD;;;;;;GAMG;AACI,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,OAAe,EACf,IAA2B;IAE3B,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,EAAE,EAAE,IAAI,EAAE;QACzC,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE;YACjD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;gBACpC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;aACvB;SACF;KACF;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAfD,oDAeC"} \ No newline at end of file diff --git a/lib/utils/index.d.ts b/lib/utils/index.d.ts new file mode 100644 index 00000000..83ef58ab --- /dev/null +++ b/lib/utils/index.d.ts @@ -0,0 +1,3 @@ +export { Arguments, ArgumentsParser } from "./arguments"; +export { formatKeywordRank } from "./format"; +export { googleGetWebsiteRank, GoogleWebsiteRank } from "./google"; diff --git a/lib/utils/index.js b/lib/utils/index.js new file mode 100644 index 00000000..0aba6f3b --- /dev/null +++ b/lib/utils/index.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.googleGetWebsiteRank = exports.formatKeywordRank = exports.ArgumentsParser = void 0; +var arguments_1 = require("./arguments"); +Object.defineProperty(exports, "ArgumentsParser", { enumerable: true, get: function () { return arguments_1.ArgumentsParser; } }); +var format_1 = require("./format"); +Object.defineProperty(exports, "formatKeywordRank", { enumerable: true, get: function () { return format_1.formatKeywordRank; } }); +var google_1 = require("./google"); +Object.defineProperty(exports, "googleGetWebsiteRank", { enumerable: true, get: function () { return google_1.googleGetWebsiteRank; } }); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/utils/index.js.map b/lib/utils/index.js.map new file mode 100644 index 00000000..88ca6ac2 --- /dev/null +++ b/lib/utils/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;AAAA,yCAAyD;AAArC,4GAAA,eAAe,OAAA;AACnC,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAC1B,mCAAmE;AAA1D,8GAAA,oBAAoB,OAAA"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d51b503a..58d36c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,18 @@ { "name": "google-rank", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "google-rank", - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "dependencies": { + "chalk": "^4.1.2", "commander": "^11.0.0", - "googlethis": "^1.7.1" + "googlethis": "^1.7.1", + "ora": "^5.4.1" }, "bin": { "google-rank": "lib/google-rank.js" @@ -1827,7 +1829,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1836,7 +1837,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2113,6 +2113,35 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2193,6 +2222,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2296,7 +2348,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2393,6 +2444,28 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2407,6 +2480,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2427,7 +2508,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2438,8 +2518,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -2608,6 +2687,17 @@ "node": ">=0.10.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -3912,7 +4002,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -4022,6 +4111,25 @@ "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -4100,8 +4208,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.5", @@ -4243,6 +4350,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -4386,6 +4501,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -5252,6 +5378,21 @@ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "dev": true }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5401,7 +5542,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "engines": { "node": ">=6" } @@ -5633,7 +5773,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -5661,6 +5800,28 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6225,6 +6386,19 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/redent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", @@ -6329,6 +6503,18 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6426,6 +6612,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -6511,8 +6716,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/sisteransi": { "version": "1.0.5", @@ -6607,6 +6811,14 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6710,7 +6922,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6780,7 +6991,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7137,6 +7347,11 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -7182,6 +7397,14 @@ "makeerror": "1.0.12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 8704176e..9f38f4bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-rank", - "version": "0.1.1", + "version": "0.2.0", "description": "Retrieve the Google search ranking of your website for specific keywords ", "keywords": [ "cli", @@ -11,7 +11,7 @@ "google-search", "search-ranking" ], - "homepage": "https://github.com/threeal/google-rank", + "homepage": "https://github.com/threeal/google-rank#readme", "bugs": { "url": "https://github.com/threeal/google-rank/issues", "email": "alfi.maulana.f@gmail.com" @@ -24,10 +24,7 @@ "bin": { "google-rank": "lib/google-rank.js" }, - "repository": { - "type": "git", - "url": "git+https://github.com/threeal/google-rank.git" - }, + "repository": "github:threeal/google-rank.git", "scripts": { "build": "dev build && chmod-cli lib/google-rank.js -m 0o777", "clean": "dev clean", @@ -36,8 +33,10 @@ "test": "dev test" }, "dependencies": { + "chalk": "^4.1.2", "commander": "^11.0.0", - "googlethis": "^1.7.1" + "googlethis": "^1.7.1", + "ora": "^5.4.1" }, "devDependencies": { "@actions-kit/dev": "^0.2.0", diff --git a/src/google-rank.ts b/src/google-rank.ts index 3e6ce5b0..6339e6c1 100644 --- a/src/google-rank.ts +++ b/src/google-rank.ts @@ -1,29 +1,35 @@ #!/usr/bin/env node -import { program } from "commander"; -import { getWebsiteRank } from "./utils"; +import chalk from "chalk"; +import ora from "ora"; +import * as utils from "./utils"; -async function run() { - program - .argument("", "website name") - .arguments("") - .parse(); +type RankPromise = Promise; - const website = program.args[0]; - const keywords = program.args.slice(1); +async function run() { + const parser = new utils.ArgumentsParser(); + const args = await parser.parse(); - const rankByKeywords: [string, Promise][] = []; - for (const keyword of keywords) { - const prom = getWebsiteRank(website, keyword); + const rankByKeywords: [string, RankPromise][] = []; + for (const keyword of args.keywords) { + const prom = utils.googleGetWebsiteRank(args.website, keyword, { + maxPage: args.maxPage, + }); rankByKeywords.push([keyword, prom]); } - process.stdout.write(`Ranks for ${website} website:\n`); + process.stdout.write( + `Ranks for ${chalk.blueBright(args.website)} website:\n` + ); + + const loading = ora("Getting ranks..."); + loading.start(); for (const [keyword, prom] of rankByKeywords) { - const rank = await prom; - const rankStr = rank > 0 ? `${rank}` : "?"; - process.stdout.write(`${rankStr} ${keyword}\n`); + loading.text = `Getting ranks of ${chalk.blueBright(keyword)} keyword...`; + const str = utils.formatKeywordRank(keyword, await prom); + process.stdout.write(`\r\x1b[K${str}\n`); } + loading.stop(); } run(); diff --git a/src/utils.test.ts b/src/utils.test.ts deleted file mode 100644 index 3aaff2ba..00000000 --- a/src/utils.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { getWebsiteRank, listWebsites } from "./utils"; - -it("should list websites", async () => { - const websites = await listWebsites("googlethis"); - expect(websites.length).toBeGreaterThan(0); - expect(websites[0]).toBe("www.npmjs.com"); -}); - -describe("rank a website", () => { - it("should rank a found website", async () => { - const rank = await getWebsiteRank("github.com", "googlethis"); - expect(rank).toBeGreaterThan(0); - }); - - it("should not rank a website that is not found", async () => { - const rank = await getWebsiteRank("randomsite.con", "googlethis"); - expect(rank).toBe(0); - }); -}); diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 2f579ed1..00000000 --- a/src/utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import google from "googlethis"; - -/** - * Retrieves a list of websites from Google search results based on the provided keyword. - * @param keyword The keyword to search for. - * @returns A promise that resolves to an array of website URLs. - */ -export async function listWebsites(keyword: string): Promise { - const res = await google.search(keyword, { parse_ads: false }); - const websites: string[] = []; - let prevWebsite = ""; - for (const result of res.results) { - const website = new URL(result.url).hostname; - if (website !== prevWebsite) websites.push(website); - prevWebsite = website; - } - return websites; -} - -/** - * Retrieves the rank of a website in Google search results for a specific keyword. - * @param website The website URL to check the rank for. - * @param keyword The keyword to search for. - * @returns A promise that resolves to the rank of the website (1-based index). Returns 0 if the website is not found in the search results. - */ -export async function getWebsiteRank( - website: string, - keyword: string -): Promise { - const websites = await listWebsites(keyword); - for (let i = 0; i < websites.length; ++i) { - if (websites[i].includes(website)) return i + 1; - } - return 0; -} diff --git a/src/utils/arguments.test.ts b/src/utils/arguments.test.ts new file mode 100644 index 00000000..06e3b159 --- /dev/null +++ b/src/utils/arguments.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { Readable } from "stream"; +import { ArgumentsParser } from "./arguments"; + +jest.mock("fs", () => ({ + ...jest.requireActual("fs"), + createReadStream: () => { + return Readable.from(["googlethis\n\n\ngooglethat\n", "googlethis github"]); + }, +})); + +describe("parse program arguments and options", () => { + it("should parse nothing", async () => { + const parser = new ArgumentsParser(); + const cmd = "node test github.com"; + const args = await parser.parse(cmd.split(" ")); + expect(args).toStrictEqual({ + website: "github.com", + keywords: ["github.com"], + maxPage: 3, + }); + }); + + it("should parse keywords correctly", async () => { + const parser = new ArgumentsParser(); + const cmd = "node test github.com --keywords github gihtub"; + const args = await parser.parse(cmd.split(" ")); + expect(args).toStrictEqual({ + website: "github.com", + keywords: ["github", "gihtub"], + maxPage: 3, + }); + }); + + it("should parse file correctly", async () => { + const parser = new ArgumentsParser(); + const cmd = "node test github.com --file keywords.txt"; + const args = await parser.parse(cmd.split(" ")); + expect(args).toStrictEqual({ + website: "github.com", + keywords: ["googlethis", "googlethat", "googlethis github"], + maxPage: 3, + }); + }); + + it("should parse arguments and options correctly", async () => { + const parser = new ArgumentsParser(); + const cmd = + "node test github.com --keywords github gihtub --file keywords.txt --max-page 7"; + const args = await parser.parse(cmd.split(" ")); + expect(args).toStrictEqual({ + website: "github.com", + keywords: [ + "github", + "gihtub", + "googlethis", + "googlethat", + "googlethis github", + ], + maxPage: 7, + }); + }); +}); diff --git a/src/utils/arguments.ts b/src/utils/arguments.ts new file mode 100644 index 00000000..bb5d5401 --- /dev/null +++ b/src/utils/arguments.ts @@ -0,0 +1,84 @@ +import { Command } from "commander"; +import * as fs from "fs"; +import * as readline from "readline"; + +/** + * Represents the arguments and options of the program. + */ +export interface Arguments { + /** Website name. */ + website: string; + + /** Keywords to search for. */ + keywords: string[]; + + /** Maximum page to search for. */ + maxPage: number; +} + +/** + * Represents the arguments and options parser of the program. + */ +export class ArgumentsParser { + #program: Command; + + /** + * Constructs a new instance of the arguments and options parser of the program. + */ + constructor() { + this.#program = new Command(); + + // Sets up the arguments and options available in the program + this.#program + .argument("", "website name") + .option("--keywords ", "keywords to search for") + .option("--file ", "file to read keywords from") + .option("--max-page ", "maximum page to search for", "3"); + } + + /** + * Parses the arguments and options of the program. + * @param argv - An optional array of strings representing the command-line arguments. + * @returns A promise that resolves to the arguments and options. + */ + async parse(argv?: readonly string[]): Promise { + this.#program.parse(argv); + + const opts = this.#program.opts(); + const args: Arguments = { + website: this.#program.args[0], + keywords: opts.keywords ?? [], + maxPage: parseInt(opts.maxPage, 10), + }; + + // Parse additional keywords from the given file, if specified. + if (opts.file !== undefined) { + const keywords = await readKeywordsFromFile(opts.file); + args.keywords = args.keywords.concat(keywords); + } + + // If no keywords are provided, use the website as the keyword. + if (args.keywords.length < 1) { + args.keywords = [args.website]; + } + + return args; + } +} + +/** + * Reads keywords from the specified file. + * @param filename - The file to read keywords from. + * @returns A promise that resolves to a list of keywords. + */ +async function readKeywordsFromFile(filename: string): Promise { + const file = fs.createReadStream(filename); + const read = readline.createInterface({ input: file }); + + const keywords: string[] = []; + for await (const line of read) { + const trimmedLine = line.trim(); + if (trimmedLine.length > 0) keywords.push(trimmedLine); + } + return keywords; +} diff --git a/src/utils/format.test.ts b/src/utils/format.test.ts new file mode 100644 index 00000000..8c95094d --- /dev/null +++ b/src/utils/format.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "@jest/globals"; +import chalk from "chalk"; +import { formatKeywordRank } from "./format"; + +describe("format the rank of a keyword as a string", () => { + it("should format undefined rank correctly", () => { + const str = formatKeywordRank("a keyword", undefined); + const mark = chalk.blackBright("?"); + expect(str).toBe(`page ${mark} rank ${mark} a keyword`); + }); + + it("should format green rank correctly", () => { + const str = formatKeywordRank("a keyword", { page: 0, rank: 2 }); + expect(str).toBe( + `page ${chalk.greenBright(1)} rank ${chalk.greenBright(3)} a keyword` + ); + }); + + it("should format green page and red rank correctly", () => { + const str = formatKeywordRank("a keyword", { page: 0, rank: 3 }); + expect(str).toBe( + `page ${chalk.greenBright(1)} rank ${chalk.redBright(4)} a keyword` + ); + }); + + it("should format red rank correctly", () => { + const str = formatKeywordRank("a keyword", { page: 1, rank: 0 }); + expect(str).toBe( + `page ${chalk.redBright(2)} rank ${chalk.redBright(1)} a keyword` + ); + }); +}); diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 00000000..095d850e --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,29 @@ +import chalk from "chalk"; +import { GoogleWebsiteRank } from "./google"; + +function formatPageRank(rank?: GoogleWebsiteRank): string { + if (rank === undefined) return `page ${chalk.blackBright("?")}`; + return rank.page <= 0 + ? `page ${chalk.greenBright(rank.page + 1)}` + : `page ${chalk.redBright(rank.page + 1)}`; +} + +function formatRank(rank?: GoogleWebsiteRank): string { + if (rank === undefined) return `rank ${chalk.blackBright("?")}`; + return rank.page <= 0 && rank.rank <= 2 + ? `rank ${chalk.greenBright(rank.rank + 1)}` + : `rank ${chalk.redBright(rank.rank + 1)}`; +} + +/** + * Formats the rank of a keyword as a string. + * @param keyword - The keyword string. + * @param rank - The rank of the keyword. + * @returns A formatted string. The rank will be displayed as a question mark if it is undefined. + */ +export function formatKeywordRank( + keyword: string, + rank?: GoogleWebsiteRank +): string { + return `${formatPageRank(rank)} ${formatRank(rank)} ${keyword}`; +} diff --git a/src/utils/google.test.ts b/src/utils/google.test.ts new file mode 100644 index 00000000..ae87ba25 --- /dev/null +++ b/src/utils/google.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "@jest/globals"; +import { googleGetWebsiteRank, googleListWebsites } from "./google"; + +describe("list websites in Google Search", () => { + it("should list websites", async () => { + const websites = await googleListWebsites("googlethis"); + expect(websites.length).toBeGreaterThan(0); + expect(websites[0]).toBe("www.npmjs.com"); + }); + + it("should list websites on a specific page", async () => { + const websites = await googleListWebsites("googlethis", { page: 1 }); + expect(websites.length).toBeGreaterThan(0); + expect(websites).not.toContain("www.npmjs.com"); + }); +}); + +describe("rank a website in Google Search", () => { + it("should rank a website that is found", async () => { + const rank = await googleGetWebsiteRank("github.com", "googlethis"); + expect(rank).toBeDefined(); + if (rank !== undefined) { + expect(rank.page).toBe(0); + expect(rank.rank).toBeGreaterThanOrEqual(0); + } + }); + + it("should not rank a website that is not found", async () => { + const rank = await googleGetWebsiteRank("randomsite.con", "googlethis"); + expect(rank).toBeUndefined(); + }); + + it("should rank a website that is found on a specific page", async () => { + const rank = await googleGetWebsiteRank("facebook.com", "googlethis", { + maxPage: 10, + }); + expect(rank).toBeDefined(); + if (rank !== undefined) { + expect(rank.page).toBeGreaterThan(0); + expect(rank.rank).toBeGreaterThanOrEqual(0); + } + }); +}); diff --git a/src/utils/google.ts b/src/utils/google.ts new file mode 100644 index 00000000..b1ae32ca --- /dev/null +++ b/src/utils/google.ts @@ -0,0 +1,60 @@ +import google from "googlethis"; + +/** + * Retrieves a list of websites from Google search results based on the provided keyword. + * @param keyword - The keyword to search for. + * @param opts - Additional options. + * @returns A promise that resolves to an array of website URLs. + */ +export async function googleListWebsites( + keyword: string, + opts?: { page?: number } +): Promise { + const res = await google.search(keyword, { + page: opts?.page ?? 0, + parse_ads: false, + }); + const websites: string[] = []; + let prevWebsite = ""; + for (const result of res.results) { + const website = new URL(result.url).hostname; + if (website !== prevWebsite) websites.push(website); + prevWebsite = website; + } + return websites; +} + +/** + * Represents the ranking of a website in Google Search. + */ +export interface GoogleWebsiteRank { + /** The search page ranking of the website. */ + page: number; + + /** The ranking of the website on the specified page. */ + rank: number; +} + +/** + * Retrieves the rank of a website in Google search results for a specific keyword. + * @param website - The website URL to check the rank for. + * @param keyword - The keyword to search for. + * @param opts - Additional options. + * @returns A promise that resolves to the rank of the website. Returns `undefined` if the website is not found in the search results. + */ +export async function googleGetWebsiteRank( + website: string, + keyword: string, + opts?: { maxPage?: number } +): Promise { + const maxPage = opts?.maxPage ?? 1; + for (let page = 0; page < maxPage; ++page) { + const websites = await googleListWebsites(keyword, { page }); + for (let rank = 0; rank < websites.length; ++rank) { + if (websites[rank].includes(website)) { + return { page, rank }; + } + } + } + return undefined; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..83ef58ab --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export { Arguments, ArgumentsParser } from "./arguments"; +export { formatKeywordRank } from "./format"; +export { googleGetWebsiteRank, GoogleWebsiteRank } from "./google";