mirror of https://github.com/rancher/dashboard.git
306 lines
8.5 KiB
JavaScript
Executable File
306 lines
8.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* This script reads and parses all of the source code files, reads the US English
|
|
* translation files, extracts all http/s links and checks for broken links.
|
|
*
|
|
* This script is used in the PR gate to check for broken links.
|
|
*
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const yaml = require('js-yaml');
|
|
const axios = require('axios');
|
|
|
|
const base = path.resolve(__dirname, '..');
|
|
const srcFolder = path.resolve(base, 'shell');
|
|
const pkgFolder = path.resolve(base, 'pkg');
|
|
|
|
// Simple shell colors
|
|
const reset = '\x1b[0m';
|
|
const cyan = `\x1b[96m`;
|
|
const yellow = `\x1b[33m`;
|
|
const bold = `\x1b[1m`;
|
|
|
|
const DOCS_BASE_REGEX = /export const DOCS_BASE = `([^']*v)\${ CURRENT_RANCHER_VERSION }`/;
|
|
const VERSION_REGEX = /export const CURRENT_RANCHER_VERSION = '([0-9]\.[0-9]+)';/;
|
|
|
|
const CATEGORIES = [
|
|
{
|
|
name: 'Rancher Manager Documentation',
|
|
regex: /^https:\/\/.*rancher\.com\/.*/
|
|
},
|
|
{
|
|
name: 'RKE2 Documentation',
|
|
regex: /^https:\/\/.*rke2\.io\/.*/
|
|
},
|
|
{
|
|
name: 'K3S Documentation',
|
|
regex: /^https:\/\/.*k3s\.io\/.*/
|
|
}
|
|
];
|
|
|
|
let docsBaseUrl = '';
|
|
|
|
// -x flag will cause script to return 0, even if there are errors
|
|
let doNotReturnError = false;
|
|
|
|
// Simple arg parsing
|
|
if (process.argv.length > 2) {
|
|
process.argv.shift();
|
|
process.argv.shift();
|
|
|
|
process.argv.forEach((arg) => {
|
|
if (arg === '-x') {
|
|
doNotReturnError = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Read file to parse out the docs base
|
|
const docsBaseFile = fs.readFileSync(path.join(srcFolder, 'config', 'private-label.js'), 'utf8');
|
|
const docsBaseFileMatches = docsBaseFile.match(DOCS_BASE_REGEX);
|
|
|
|
if (docsBaseFileMatches && docsBaseFileMatches.length === 2) {
|
|
docsBaseUrl = docsBaseFileMatches[1];
|
|
}
|
|
|
|
if (docsBaseUrl.length === 0) {
|
|
console.log('Could not parse documentation base URL'); // eslint-disable-line no-console
|
|
process.exit(1);
|
|
}
|
|
|
|
const versionFile = fs.readFileSync(path.join(srcFolder, 'config', 'version.js'), 'utf8');
|
|
const versionFileMatches = versionFile.match(VERSION_REGEX);
|
|
|
|
if (versionFileMatches && versionFileMatches.length === 2) {
|
|
docsBaseUrl = `${ docsBaseUrl }${ versionFileMatches[1] }`;
|
|
} else {
|
|
console.log('Could not parse version number from the version file'); // eslint-disable-line no-console
|
|
process.exit(1);
|
|
}
|
|
|
|
function readAndParseTranslations(filePath) {
|
|
const data = fs.readFileSync(filePath, 'utf8');
|
|
|
|
try {
|
|
const i18n = yaml.load(fs.readFileSync(filePath), 'utf8');
|
|
|
|
return parseTranslations(i18n);
|
|
} catch (e) {
|
|
console.log('Can not read i18n file'); // eslint-disable-line no-console
|
|
console.log(e); // eslint-disable-line no-console
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function parseTranslations(obj, parent) {
|
|
let res = {};
|
|
|
|
Object.keys(obj).forEach((key) => {
|
|
const v = obj[key];
|
|
const pKey = parent ? `${ parent }.${ key }` : key;
|
|
|
|
if (v === null) {
|
|
// ignore
|
|
} else if (typeof v === 'object') {
|
|
res = {
|
|
...res,
|
|
...parseTranslations(v, pKey)
|
|
};
|
|
} else {
|
|
// Ensure empty strings work
|
|
res[pKey] = v.length === 0 ? '[empty]' : v;
|
|
}
|
|
});
|
|
|
|
return res;
|
|
}
|
|
|
|
const LINK_REGEX = /<[aA]\s[^>]*>/g;
|
|
const ATTR_REGEX = /(([a-zA-Z]*)=["']([^"']*))/g;
|
|
|
|
function parseLinks(str) {
|
|
const a = [...str.matchAll(LINK_REGEX)];
|
|
|
|
const links = [];
|
|
|
|
if (a && a.length) {
|
|
a.forEach((m) => {
|
|
const attrs = [...m[0].matchAll(ATTR_REGEX)];
|
|
|
|
attrs.forEach((attr) => {
|
|
if (attr.length === 4 && attr[2] === 'href') {
|
|
const link = attr[3].replace('{docsBase}', docsBaseUrl);
|
|
|
|
if (link.startsWith('http')) {
|
|
links.push(link);
|
|
} else if (!(link.startsWith('{') && link.endsWith('}'))) {
|
|
console.log(`${ yellow }${bold}Skipping link: ${ link }${ reset }`); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
return links;
|
|
}
|
|
|
|
function loadI18nFiles(folder) {
|
|
let res = {};
|
|
|
|
// Find all of the test files
|
|
fs.readdirSync(folder).forEach((file) => {
|
|
const filePath = path.resolve(folder, file);
|
|
const isFolder = fs.lstatSync(filePath).isDirectory();
|
|
|
|
if (isFolder) {
|
|
res = {
|
|
...res,
|
|
...loadI18nFiles(filePath)
|
|
};
|
|
} else if (file === 'en-us.yaml') {
|
|
console.log(` ... ${ path.relative(base, filePath) }`); // eslint-disable-line no-console
|
|
|
|
const translations = readAndParseTranslations(filePath);
|
|
|
|
res = {
|
|
...res,
|
|
...translations
|
|
};
|
|
}
|
|
});
|
|
|
|
return res;
|
|
}
|
|
|
|
console.log('======================================'); // eslint-disable-line no-console
|
|
console.log(`${ cyan }Checking source files for i18n strings${ reset }`); // eslint-disable-line no-console
|
|
console.log('======================================'); // eslint-disable-line no-console
|
|
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
console.log(`Using documentation base URL: ${ cyan }${ docsBaseUrl }${ reset }`); // eslint-disable-line no-console
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
|
|
console.log('Reading translation files:'); // eslint-disable-line no-console
|
|
|
|
let i18n = loadI18nFiles(srcFolder);
|
|
|
|
i18n = { ...i18n, ...loadI18nFiles(pkgFolder) };
|
|
|
|
console.log(`Read ${ cyan }${ Object.keys(i18n).length }${ reset } translations`); // eslint-disable-line no-console
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
const links = [];
|
|
|
|
// Parse all of the links from each translation file
|
|
Object.keys(i18n).forEach((str) => {
|
|
const link = parseLinks(i18n[str]);
|
|
|
|
links.push(...link);
|
|
});
|
|
|
|
console.log(`Discovered ${ cyan }${ links.length }${ reset } links`); // eslint-disable-line no-console
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
console.log(`${ cyan }Links:${ reset }`); // eslint-disable-line no-console
|
|
console.log(`${ cyan }======${ reset }`); // eslint-disable-line no-console
|
|
|
|
showByCategory(links, 'Links in', 'Other Links', cyan);
|
|
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
function showByCategory(linksToShow, prefixLabel, otherLabel, color) {
|
|
const others = [];
|
|
const byCategory = {};
|
|
|
|
linksToShow.forEach((link) => {
|
|
let found = false;
|
|
|
|
CATEGORIES.forEach((category) => {
|
|
byCategory[category.name] = byCategory[category.name] || [];
|
|
|
|
if (!found && category.regex.test(link)) {
|
|
byCategory[category.name].push(link);
|
|
found = true;
|
|
}
|
|
});
|
|
|
|
if (!found) {
|
|
others.push(link);
|
|
}
|
|
});
|
|
|
|
CATEGORIES.forEach((category) => {
|
|
if (byCategory[category.name]?.length) {
|
|
console.log(`${ color }${ prefixLabel } ${ category.name }${ reset }`); // eslint-disable-line no-console
|
|
byCategory[category.name].forEach((link) => console.log(` ${ link }`)); // eslint-disable-line no-console
|
|
}
|
|
});
|
|
|
|
if (others.length) {
|
|
console.log(`${ color }${ otherLabel }${ reset }`); // eslint-disable-line no-console
|
|
others.forEach((link) => console.log(` ${ link }`));
|
|
}
|
|
}
|
|
|
|
async function check(links) {
|
|
const brokenLinks = [];
|
|
|
|
for (let i = 0; i < links.length; i++) {
|
|
const link = links[i];
|
|
let statusCode;
|
|
let statusMessage;
|
|
|
|
try {
|
|
const headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
Accept: 'text/html',
|
|
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
};
|
|
|
|
const r = await axios.get(link, { headers });
|
|
|
|
statusCode = r.status;
|
|
statusMessage = r.statusText;
|
|
} catch (e) {
|
|
statusCode = e.response ? e.response.status : e.status;
|
|
statusMessage = e.response ? e.response.statusText : e.statusText;
|
|
}
|
|
|
|
if (statusCode !== 200) {
|
|
const sc = `${ statusCode }`.padEnd(5);
|
|
|
|
console.log(` ${ link } : ${ sc } ${ statusMessage || '' }`); // eslint-disable-line no-console
|
|
|
|
brokenLinks.push(link);
|
|
}
|
|
}
|
|
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
if (brokenLinks.length === 0) {
|
|
console.log(`${ cyan }${ bold }Links Checked - all links could be fetched okay${ reset }`); // eslint-disable-line no-console
|
|
} else {
|
|
console.log(`${ yellow }${ bold }Found ${ brokenLinks.length } link(s) that could not be fetched${ reset }`); // eslint-disable-line no-console
|
|
}
|
|
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
showByCategory(brokenLinks, 'Broken links in', 'Other Broken links', yellow);
|
|
|
|
console.log(''); // eslint-disable-line no-console
|
|
|
|
// Return with error code if broken links found
|
|
if (!doNotReturnError && brokenLinks.length > 0) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
console.log(`${ cyan }Checking doc links ...${ reset }`); // eslint-disable-line no-console
|
|
|
|
check(links);
|