blob: a2d9bbfe0735315fb69fe1c526f52d7004bab55a [file] [log] [blame]
Simon Zünd9c429fb2021-02-01 09:12:24 +01001// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7const fs = require('fs');
8const https = require('https');
9const path = require('path');
10const ts = require('typescript');
11
12const readDirAsync = fs.promises.readdir;
13const readFileAsync = fs.promises.readFile;
14
15const ORIGIN_PATTERNS_TO_CHECK = [
16 new RegExp('^https://web.dev'),
17 new RegExp('^https://developers.google.com'),
18 new RegExp('^https://developer[s]?.chrome.com'),
19];
20
21const DIRECTORIES_TO_CHECK = [
22 'front_end',
23];
24
25const EXCLUDE_DIRECTORIES = [
26 'front_end/third_party',
27];
28
29const REQUEST_TIMEOUT = 5000;
30
31const REDIRECTS_CONSIDERED_ERROR = new Set([
32 /* Multiple Choices */ 300,
33 /* Moved permanently */ 301,
34 /* Permament redirect */ 308,
35]);
36
37const ROOT_REPOSITORY_PATH = path.resolve(__dirname, '..');
38const DIRECTORIES_TO_CHECK_PATHS = DIRECTORIES_TO_CHECK.map(directory => path.resolve(ROOT_REPOSITORY_PATH, directory));
39
40async function findAllSourceFiles(directory) {
41 if (EXCLUDE_DIRECTORIES.includes(path.relative(ROOT_REPOSITORY_PATH, directory))) {
42 return [];
43 }
44
45 const dirEntries = await readDirAsync(directory, {withFileTypes: true});
46 const files = await Promise.all(dirEntries.map(dirEntry => {
47 const resolvedPath = path.resolve(directory, dirEntry.name);
48 if (dirEntry.isDirectory()) {
49 return findAllSourceFiles(resolvedPath);
50 }
51 if (dirEntry.isFile() && /\.(js|ts)$/.test(dirEntry.name)) {
52 return resolvedPath;
53 }
54 return []; // Let Array#flat filter out files we are not interested in.
55 }));
56 return files.flat();
57}
58
59function collectUrlsToCheck(node) {
60 const nodesToVisit = [node];
61 const urlsToCheck = [];
62 while (nodesToVisit.length) {
63 const currentNode = nodesToVisit.shift();
64 if (currentNode.kind === ts.SyntaxKind.StringLiteral ||
65 currentNode.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
66 const checkUrl = ORIGIN_PATTERNS_TO_CHECK.some(originPattern => originPattern.test(currentNode.text));
67 if (checkUrl) {
68 urlsToCheck.push(currentNode.text);
69 }
70 }
71 nodesToVisit.push(...currentNode.getChildren());
72 }
73 return urlsToCheck;
74}
75
76async function collectUrlsToCheckFromFile(filePath) {
77 const content = await readFileAsync(filePath, 'utf8');
78 const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ESNext, true);
79 return collectUrlsToCheck(sourceFile);
80}
81
82async function checkUrls(urls) {
83 // clang-format off
84 const requestPromises = urls.map(url => new Promise(resolve => {
85 const request = https.request(url, {method: 'HEAD'}, response => {
86 resolve({url, statusCode: response.statusCode});
87 });
88
89 request.on('error', err => {
90 resolve({url, error: err});
91 });
92 request.setTimeout(REQUEST_TIMEOUT, _ => {
93 resolve({url, error: `Timed out after ${REQUEST_TIMEOUT}`});
94 });
95 request.end();
96 }));
97 // clang-format on
98
99 return Promise.all(requestPromises);
100}
101
102function includeRequestResultInOutput(requestResult) {
103 return requestResult.error || requestResult.statusCode !== 200;
104}
105
106function isErrorStatusCode(statusCode) {
107 return statusCode >= 400 || REDIRECTS_CONSIDERED_ERROR.has(statusCode);
108}
109
110function requestResultIsErronous(requestResult) {
111 return requestResult.error || isErrorStatusCode(requestResult.statusCode);
112}
113
114function printSelectedRequestResults(requestResults) {
115 const requestsToPrint = requestResults.filter(includeRequestResultInOutput);
116 if (requestsToPrint.length === 0) {
117 console.log('\nAll Urls are accessible and point to existing resources.\n');
118 return;
119 }
120
121 for (const requestResult of requestsToPrint) {
122 if (requestResult.error) {
123 console.error(`[Failure] ${requestResult.error} - ${requestResult.url}`);
124 } else if (isErrorStatusCode(requestResult.statusCode)) {
125 console.error(`[Failure] Status Code: ${requestResult.statusCode} - ${requestResult.url}`);
126 } else {
127 console.log(`Status Code: ${requestResult.statusCode} - ${requestResult.url}`);
128 }
129 }
130}
131
132async function main() {
133 process.stdout.write('Collecting JS/TS source files ... ');
134 const sourceFiles = (await Promise.all(DIRECTORIES_TO_CHECK_PATHS.map(findAllSourceFiles))).flat();
135 process.stdout.write(`${sourceFiles.length} files found.\n`);
136
137 process.stdout.write('Collecting Urls from files ... ');
138 const urlsToCheck = (await Promise.all(sourceFiles.map(collectUrlsToCheckFromFile))).flat();
139 const deduplicatedUrlsToCheck = new Set(urlsToCheck);
140 process.stdout.write(`${deduplicatedUrlsToCheck.size} unique Urls found.\n`);
141
142 process.stdout.write('Sending a HEAD request to each one ...\n');
143 const requestResults = await checkUrls([...deduplicatedUrlsToCheck]);
144 printSelectedRequestResults(requestResults);
145
146 const exitCode = requestResults.some(requestResultIsErronous) ? 1 : 0;
147 process.exit(exitCode);
148}
149
150main();