blob: 4ab64ed2d4431048f55009f715ec83f7f78d7789 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node*/
const globby = require('globby');
const lex = require('pug-lexer');
const parse = require('pug-parser');
const walk = require('pug-walk');
const compileAttrs = require('pug-attrs');
const {promisify} = require('util');
const fs = require('fs');
const readFileAsync = promisify(fs.readFile);
const fetch = require('node-fetch');
const ProgressBar = require('progress');
const chalk = require('chalk');
const tsm = require('teamcity-service-messages');
const slash = require('slash');
const appRoot = require('app-root-path').path;
const {argv} = require('yargs')
.option('directory', {
alias: 'd',
describe: 'parent directory to apply glob pattern from',
default: appRoot
})
.option('pugs', {
alias: 'p',
describe: 'glob pattern to select templates with',
default: '{app,views}/**/*.pug'
})
.usage('Usage: $0 [options]')
.example(
'$0 --pugs="./**/*.pug"',
`look for all invalid links in all pug files in current dir and it's subdirs`
)
.help();
const pugPathToAST = async(pugPath) => Object.assign(parse(lex(await readFileAsync(pugPath, 'utf8'))), {filePath: pugPath});
const findLinks = (acc, ast) => {
walk(ast, (node) => {
if (node.attrs) {
const href = node.attrs.find((attr) => attr.name === 'href');
if (href) {
const compiledAttr = compileAttrs([href], {
terse: false,
format: 'object',
runtime() {}
});
try {
acc.push([JSON.parse(compiledAttr).href, ast.filePath]);
}
catch (e) {
console.log(ast.filePath, e);
}
}
}
});
return acc;
};
const isDocsURL = (url) => url.startsWith('http');
const isInvalidURL = (url) => fetch(url, {redirect: 'manual'}).then((res) => res.status !== 200);
const first = ([value]) => value;
const checkDocLinks = async(pugsGlob, onProgress = () => {}) => {
const pugs = await globby(pugsGlob);
const allAST = await Promise.all(pugs.map(pugPathToAST));
const allLinks = allAST.reduce(findLinks, []).filter((pair) => isDocsURL(first(pair)));
onProgress(allLinks);
const tick = (v) => {onProgress(v); return v;};
const results = await Promise.all(allLinks.map((pair) => {
return isInvalidURL(first(pair))
.then((isInvalid) => [...pair, isInvalid])
.then(tick)
.catch(tick);
}));
const invalidLinks = results.filter(([,, isInvalid]) => isInvalid);
return {allLinks, invalidLinks};
};
module.exports.checkDocLinks = checkDocLinks;
const specReporter = (allLinks, invalidLinks) => {
const invalidCount = invalidLinks.length;
const format = ([link, at], i) => `\n${i + 1}. ${chalk.red(link)} ${chalk.dim('in')} ${chalk.yellow(at)}`;
console.log(`Total links: ${allLinks.length}`);
console.log(`Invalid links found: ${invalidCount ? chalk.red(invalidCount) : chalk.green(invalidCount)}`);
if (invalidCount) console.log(invalidLinks.map(format).join(''));
};
const teamcityReporter = (allLinks, invalidLinks) => {
const name = 'Checking docs links';
tsm.testStarted({ name });
if (invalidLinks.length > 0)
tsm.testFailed({ name, details: invalidLinks.map(([link, at]) => `\n ${link} in ${at}`).join('') });
else {
tsm.testStdOut(`All ${allLinks.length} are correct!`);
tsm.testFinished({ name: 'Checking docs links' });
}
};
const main = async() => {
let bar;
const updateBar = (value) => {
if (!bar)
bar = new ProgressBar('Checking links [:bar] :current/:total', {total: value.length});
else
bar.tick();
};
const unBackSlashedDirPath = slash(argv.directory);
const absolutePugGlob = `${unBackSlashedDirPath}/${argv.pugs}`;
console.log(`Looking for invalid links in ${chalk.cyan(absolutePugGlob)}.`);
const {allLinks, invalidLinks} = await checkDocLinks(absolutePugGlob, updateBar);
const reporter = process.env.TEAMCITY ? teamcityReporter : specReporter;
reporter(allLinks, invalidLinks);
};
main();