1 |
|
2 |
|
3 |
|
4 |
|
5 | 'use strict';
|
6 |
|
7 | const fs = require('fs');
|
8 | const https = require('https');
|
9 | const path = require('path');
|
10 | const ts = require('typescript');
|
11 |
|
12 | const readDirAsync = fs.promises.readdir;
|
13 | const readFileAsync = fs.promises.readFile;
|
14 |
|
15 | const 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 |
|
21 | const DIRECTORIES_TO_CHECK = [
|
22 | 'front_end',
|
23 | ];
|
24 |
|
25 | const EXCLUDE_DIRECTORIES = [
|
26 | 'front_end/third_party',
|
27 | ];
|
28 |
|
29 | const REQUEST_TIMEOUT = 5000;
|
30 |
|
31 | const REDIRECTS_CONSIDERED_ERROR = new Set([
|
32 | 300,
|
33 | 301,
|
34 | 308,
|
35 | ]);
|
36 |
|
37 | const ROOT_REPOSITORY_PATH = path.resolve(__dirname, '..');
|
38 | const DIRECTORIES_TO_CHECK_PATHS = DIRECTORIES_TO_CHECK.map(directory => path.resolve(ROOT_REPOSITORY_PATH, directory));
|
39 |
|
40 | async 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 [];
|
55 | }));
|
56 | return files.flat();
|
57 | }
|
58 |
|
59 | function 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 |
|
76 | async 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 |
|
82 | async function checkUrls(urls) {
|
83 |
|
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 |
|
98 |
|
99 | return Promise.all(requestPromises);
|
100 | }
|
101 |
|
102 | function includeRequestResultInOutput(requestResult) {
|
103 | return requestResult.error || requestResult.statusCode !== 200;
|
104 | }
|
105 |
|
106 | function isErrorStatusCode(statusCode) {
|
107 | return statusCode >= 400 || REDIRECTS_CONSIDERED_ERROR.has(statusCode);
|
108 | }
|
109 |
|
110 | function requestResultIsErronous(requestResult) {
|
111 | return requestResult.error || isErrorStatusCode(requestResult.statusCode);
|
112 | }
|
113 |
|
114 | function 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 |
|
132 | async 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 |
|
150 | main();
|