Skip to content

Commit

Permalink
Merge pull request #2 from UmbrellaDocs/export-as-func
Browse files Browse the repository at this point in the history
Feat: Spinner, JS API, check files
  • Loading branch information
gaurav-nelson authored Sep 18, 2023
2 parents 73b86f6 + 657d656 commit b756c23
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 112 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

name: Publish on NPM

on:
on:
push:
tags:
- '*'
Expand All @@ -16,6 +16,7 @@ jobs:
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm install && rm -rf node_modules
- run: npm ci
- run: npm publish --access public
env:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ Follow these guidelines for commit messages:

## License

By contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE.md).
By contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE).
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ If there are no errors, linkspector shows the following message:
```
## What's planned
- [ ] Spinner for local runs.
- [x] Spinner for local runs.
- [ ] Create a GitHub action.
- [ ] Modified files only check in pull requests.
- [ ] Asciidoc support.
- [x] Asciidoc support.
- [ ] ReStructured Text support.
- [ ] Disable binary files downlaod.
- [ ] JSON output for `failed-only` or `all` links.
Expand Down
Binary file modified bun.lockb
Binary file not shown.
131 changes: 34 additions & 97 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,129 +1,66 @@
#!/usr/bin/env node

import { readFileSync, existsSync } from "fs";
import path from "path";
import yaml from "js-yaml";
import { program } from "commander";
import kleur from "kleur";
import { validateConfig } from "./lib/validate-config.js";
import { prepareFilesList } from "./lib/prepare-file-list.js";
import { extractMarkdownHyperlinks } from "./lib/extract-markdown-hyperlinks.js";
import { extractAsciiDocLinks } from "./lib/extract-asciidoc-hyperlinks.js";
import { getUniqueLinks } from "./lib/get-unique-links.js";
import { checkHyperlinks } from "./lib/batch-check-links.js";
import { updateLinkstatusObj } from "./lib/update-linkstatus-obj.js";
import ora from "ora";
import { linkspector } from "./linkspector.js";

// Define the program and its options
program
.version("0.1.0")
.description("Check for dead hyperlinks in your markup language files.")
.version("0.2.1")
.description("🔍 Uncover broken links in your content.")
.command("check")
.description("Check hyperlinks based on the configuration file.")
.option("-c, --config <path>", "Specify a custom configuration file path")
.action(async (cmd) => {
const configFile = cmd.config || ".linkspector.yml"; // Use custom config file path if provided

// Check if the config file exists
if (!existsSync(configFile)) {
console.error(
kleur.red(
"Error: Configuration file not found. Create a '.linkspector.yml' file at the root of your project or use the '--config' option to specify another configuration file."
)
);
process.exit(1);
}
// Start the loading spinner
const spinner = ora().start();

// Read and validate the config file
try {
// Read the YAML file
const configContent = readFileSync(configFile, "utf-8");

// Check if the YAML content is empty
if (!configContent.trim()) {
console.error("Error: The configuration file is empty.");
return false;
}

// Parse the YAML content
const config = yaml.load(configContent);

// Check if the parsed YAML object is null or lacks properties
if (config === null || Object.keys(config).length === 0) {
console.error("Error: Failed to parse the YAML content.");
return false;
}

if (!validateConfig(config)) {
console.error(kleur.red("Error: Invalid config file."));
process.exit(1);
}

let hasErrorLinks = false;

// Prepare the list of files to check
const filesToCheck = prepareFilesList(config);

// Initialize an array to store link status objects
let linkStatusObjects = [];

// Process each file
for (const file of filesToCheck) {
const relativeFilePath = path.relative(process.cwd(), file);

// Get the file extension
const fileExtension = path.extname(file).substring(1).toLowerCase(); // Get the file extension without the leading dot and convert to lowercase

let astNodes;

// Check the file extension and use the appropriate function to extract links
if (
["asciidoc", "adoc", "asc"].includes(fileExtension) &&
config.fileExtensions &&
config.fileExtensions.includes(fileExtension)
) {
astNodes = await extractAsciiDocLinks(file);
} else {
const fileContent = readFileSync(file, "utf8");
astNodes = extractMarkdownHyperlinks(fileContent);
}

// Get unique hyperlinks
const uniqueLinks = getUniqueLinks(astNodes);

// Check the status of hyperlinks
const linkStatus = await checkHyperlinks(uniqueLinks);
// Update linkStatusObjects with information about removed links

linkStatusObjects = await updateLinkstatusObj(
linkStatusObjects,
linkStatus
);

const errorLinks = linkStatusObjects.filter(
(link) => link.status === "error"
);

if (errorLinks.length > 0) {
for (const item of errorLinks) {
for await (const { file, result } of linkspector(configFile)) {
for (const linkStatusObj of result) {
if (linkStatusObj.status === "error") {
hasErrorLinks = true;
// Stop the spinner before printing an error message
spinner.stop();
console.error(
kleur.red(
`${relativeFilePath}, ${item.link}, ${item.status_code}, ${item.line_number}, ${item.error_message}`
`🚫 ${file}, ${linkStatusObj.link}, ${linkStatusObj.status_code}, ${linkStatusObj.line_number}, ${linkStatusObj.error_message}`
)
);
// Start the spinner again after printing an error message
spinner.start();
}
hasErrorLinks = true;
}
}
if (hasErrorLinks) {
console.error(kleur.red("❌ Found link errors in one or more files."));
process.exit(1);

spinner.stop();

if (!hasErrorLinks) {
console.log(
kleur.green(
"✨ Success: All hyperlinks in the specified files are valid."
)
);
process.exit(0);
} else {
console.log(kleur.green("✅ All links are working."));
console.error(
kleur.red(
"❌ Error: Some links in the specified files are not valid."
)
);
process.exit(1);
}
} catch (error) {
console.error(kleur.red(`Error: ${error.message}`));
spinner.fail(kleur.red(`💥 Error: ${error.message}`));
process.exit(1);
}
});

// Parse the command line arguments
program.parse(process.argv);

44 changes: 34 additions & 10 deletions lib/batch-check-links.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import puppeteer from "puppeteer";
import url from "url";
import fs from "fs";

function isUrl(s) {
try {
new url.URL(s);
return true;
} catch (err) {
return false;
}
}

async function processLink(link, page, aliveStatusCodes) {
let linkStatus = {
Expand All @@ -11,22 +22,35 @@ async function processLink(link, page, aliveStatusCodes) {
};

try {
//const start = Date.now();
const response = await page.goto(link.url, { waitUntil: "load" });
//console.log("Link:", link.url, " - Took", Date.now() - start, "ms");
const statusCode = response.status();
linkStatus.status_code = statusCode;

if (aliveStatusCodes && aliveStatusCodes.includes(statusCode)) {
linkStatus.status = "assumed alive";
if (isUrl(link.url)) {
const response = await page.goto(link.url, { waitUntil: "load" });
const statusCode = response.status();
linkStatus.status_code = statusCode;

if (aliveStatusCodes && aliveStatusCodes.includes(statusCode)) {
linkStatus.status = "assumed alive";
} else {
linkStatus.status = response.ok() ? "alive" : "error";
}
} else {
linkStatus.status = response.ok() ? "alive" : "dead";
try {
if (fs.existsSync(link.url)) {
linkStatus.status_code = "200";
linkStatus.status = "alive";
} else {
linkStatus.status_code = "404";
linkStatus.status = "error";
linkStatus.error_message = `Cannot find: ${link.url}.`
}
} catch (err){
console.error(`Error in checking if file ${link.url} exist!`)
}

}
} catch (error) {
linkStatus.status = "error";
linkStatus.error_message = error.message;
}

return linkStatus;
}

Expand Down
3 changes: 3 additions & 0 deletions lib/validate-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@ async function validateConfig(config) {
} catch (err) {
if (err instanceof ValidationError) {
console.error(err.message, err.details);
throw err;
} else if (err.message.includes("ENOENT: no such file or directory")) {
console.error("Error reading file:", err.message);
throw err;
} else {
console.error("Error:", err.message);
throw err;
}
return false;
}
Expand Down
93 changes: 93 additions & 0 deletions linkspector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { readFileSync, existsSync } from "fs";
import path from "path";
import yaml from "js-yaml";
import { validateConfig } from "./lib/validate-config.js";
import { prepareFilesList } from "./lib/prepare-file-list.js";
import { extractMarkdownHyperlinks } from "./lib/extract-markdown-hyperlinks.js";
import { extractAsciiDocLinks } from "./lib/extract-asciidoc-hyperlinks.js";
import { getUniqueLinks } from "./lib/get-unique-links.js";
import { checkHyperlinks } from "./lib/batch-check-links.js";
import { updateLinkstatusObj } from "./lib/update-linkstatus-obj.js";

export async function* linkspector(configFile) {
// Check if the config file exists
if (!existsSync(configFile)) {
throw new Error(
"Configuration file not found. Create a '.linkspector.yml' file at the root of your project or use the '--config' option to specify another configuration file."
);
}

// Read and validate the config file
const configContent = readFileSync(configFile, "utf-8");

// Check if the YAML content is empty
if (!configContent.trim()) {
throw new Error("The configuration file is empty.");
}

// Parse the YAML content
const config = yaml.load(configContent);

// Check if the parsed YAML object is null or lacks properties
if (config === null || Object.keys(config).length === 0) {
throw new Error("Failed to parse the YAML content.");
}

try {
const isValid = await validateConfig(config);
if (!isValid) {
console.error("Validation failed!")
process.exit(1);
}
} catch (error) {
console.error(`💥 Error: Please check your configuration file.`)
process.exit(1);
}

// Prepare the list of files to check
const filesToCheck = prepareFilesList(config);

// Initialize an array to store link status objects
let linkStatusObjects = [];

// Process each file
for (const file of filesToCheck) {
const relativeFilePath = path.relative(process.cwd(), file);

// Get the file extension
const fileExtension = path.extname(file).substring(1).toLowerCase(); // Get the file extension without the leading dot and convert to lowercase

let astNodes;

// Check the file extension and use the appropriate function to extract links
if (
["asciidoc", "adoc", "asc"].includes(fileExtension) &&
config.fileExtensions &&
config.fileExtensions.includes(fileExtension)
) {
astNodes = await extractAsciiDocLinks(file);
} else {
const fileContent = readFileSync(file, "utf8");
astNodes = extractMarkdownHyperlinks(fileContent);
}

// Get unique hyperlinks
const uniqueLinks = getUniqueLinks(astNodes);
//console.log(JSON.stringify(uniqueLinks))

// Check the status of hyperlinks
const linkStatus = await checkHyperlinks(uniqueLinks);

// Update linkStatusObjects with information about removed links
linkStatusObjects = await updateLinkstatusObj(
linkStatusObjects,
linkStatus
);

// Yield an object with the relative file path and its result
yield {
file: relativeFilePath,
result: linkStatusObjects,
};
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@umbrelladocs/linkspector",
"version": "0.2.0",
"version": "0.2.1",
"description": "Uncover broken links in your content.",
"type": "module",
"main": "index.js",
Expand All @@ -27,6 +27,7 @@
"joi": "^17.10.1",
"js-yaml": "^4.1.0",
"kleur": "^4.1.5",
"ora": "^7.0.1",
"puppeteer": "^21.1.1",
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.2",
Expand Down

0 comments on commit b756c23

Please # to comment.