blob: d252bff8b72964f4da4a3e2e81c24692f9c887b7 [file] [log] [blame]
Jack Franklin0155f7d2021-03-02 09:11:22 +00001// Copyright 2021 The Chromium Authors. All rights reserved.
Jack Franklin1557a1c2020-06-08 15:22:13 +01002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5const fs = require('fs');
6const http = require('http');
7const path = require('path');
8const parseURL = require('url').parse;
9const {argv} = require('yargs');
Tim van der Lippe23283e12021-05-10 15:08:31 +010010
11const {createInstrumenter} = require('istanbul-lib-instrument');
12const convertSourceMap = require('convert-source-map');
13const defaultIstanbulSchema = require('@istanbuljs/schema');
14
Jack Franklina3dd06a2021-03-18 10:47:16 +000015const {getTestRunnerConfigSetting} = require('../test/test_config_helpers.js');
Jack Franklin1557a1c2020-06-08 15:22:13 +010016
Jack Franklin1557a1c2020-06-08 15:22:13 +010017
Jack Franklinfe65bc62021-03-16 11:23:20 +000018const serverPort = parseInt(process.env.PORT, 10) || 8090;
Jack Franklin8742dc82021-02-26 09:17:59 +000019const target = argv.target || process.env.TARGET || 'Default';
20
21/**
22 * This configures the base of the URLs that are injected into each component
23 * doc example to load. By default it's /, so that we load /front_end/..., but
24 * this can be configured if you have a different file structure.
25 */
Jack Franklinfe65bc62021-03-16 11:23:20 +000026const sharedResourcesBase =
27 argv.sharedResourcesBase || getTestRunnerConfigSetting('component-server-shared-resources-path', '/');
Jack Franklin086ccd52020-11-27 11:00:14 +000028
29/**
Jack Franklin0155f7d2021-03-02 09:11:22 +000030 * The server assumes that examples live in
Tim van der Lippee622f552021-04-14 15:15:18 +010031 * devtoolsRoot/out/Target/gen/front_end/ui/components/docs, but if you need to add a
Jack Franklin0155f7d2021-03-02 09:11:22 +000032 * prefix you can pass this argument. Passing `foo` will redirect the server to
Tim van der Lippee622f552021-04-14 15:15:18 +010033 * look in devtoolsRoot/out/Target/gen/foo/front_end/ui/components/docs.
Jack Franklin0155f7d2021-03-02 09:11:22 +000034 */
Jack Franklinfe65bc62021-03-16 11:23:20 +000035const componentDocsBaseArg = argv.componentDocsBase || process.env.COMPONENT_DOCS_BASE ||
36 getTestRunnerConfigSetting('component-server-base-path', '');
Jack Franklin0155f7d2021-03-02 09:11:22 +000037
38/**
Jack Franklin086ccd52020-11-27 11:00:14 +000039 * When you run npm run components-server we run the script as is from scripts/,
40 * but when this server is run as part of a test suite it's run from
41 * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
42 * out where we are.
43 */
Jack Franklin8742dc82021-02-26 09:17:59 +000044const isRunningInGen = __dirname.includes(path.join('out', path.sep, target));
Jack Franklinb36ad7e2020-12-07 10:20:52 +000045
Jack Franklin8742dc82021-02-26 09:17:59 +000046let pathToOutTargetDir = __dirname;
47/**
48 * If we are in the gen directory, we need to find the out/Default folder to use
49 * as our base to find files from. We could do this with path.join(x, '..',
50 * '..') until we get the right folder, but that's brittle. It's better to
51 * search up for out/Default to be robust to any folder structures.
52 */
53while (isRunningInGen && !pathToOutTargetDir.endsWith(`out${path.sep}${target}`)) {
54 pathToOutTargetDir = path.resolve(pathToOutTargetDir, '..');
55}
Jack Franklin0943df42021-02-26 11:12:13 +000056
57/* If we are not running in out/Default, we'll assume the script is running from the repo root, and navigate to {CWD}/out/Target */
Jack Franklin8742dc82021-02-26 09:17:59 +000058const pathToBuiltOutTargetDirectory =
Jack Franklin0943df42021-02-26 11:12:13 +000059 isRunningInGen ? pathToOutTargetDir : path.resolve(path.join(process.cwd(), 'out', target));
Jack Franklin8742dc82021-02-26 09:17:59 +000060
Jack Franklin90b66132021-01-05 11:33:43 +000061const devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen'));
Jack Franklin0155f7d2021-03-02 09:11:22 +000062const componentDocsBaseFolder = path.join(devtoolsRootFolder, componentDocsBaseArg);
Jack Franklin1557a1c2020-06-08 15:22:13 +010063
Jack Franklin90b66132021-01-05 11:33:43 +000064if (!fs.existsSync(devtoolsRootFolder)) {
65 console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`);
Jack Franklin1557a1c2020-06-08 15:22:13 +010066 console.log(
67 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
68 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
69 process.exit(1);
70}
71
Jack Franklin086ccd52020-11-27 11:00:14 +000072const server = http.createServer(requestHandler);
73server.listen(serverPort);
74server.once('listening', () => {
75 if (process.send) {
76 process.send(serverPort);
77 }
78 console.log(`Started components server at http://localhost:${serverPort}\n`);
Tim van der Lippee622f552021-04-14 15:15:18 +010079 console.log(`ui/components/docs location: ${
80 path.relative(process.cwd(), path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs'))}`);
Jack Franklin086ccd52020-11-27 11:00:14 +000081});
82
83server.once('error', error => {
84 if (process.send) {
85 process.send('ERROR');
86 }
87 throw error;
88});
Jack Franklin1557a1c2020-06-08 15:22:13 +010089
90function createComponentIndexFile(componentPath, componentExamples) {
Tim van der Lippee622f552021-04-14 15:15:18 +010091 const componentName = componentPath.replace('/front_end/ui/components/docs/', '').replace(/_/g, ' ').replace('/', '');
Jack Franklin1557a1c2020-06-08 15:22:13 +010092 // clang-format off
93 return `<!DOCTYPE html>
94 <html>
95 <head>
96 <meta charset="UTF-8" />
97 <meta name="viewport" content="width=device-width" />
98 <title>DevTools component: ${componentName}</title>
99 <style>
100 h1 { text-transform: capitalize; }
101
102 .example {
103 padding: 5px;
104 margin: 10px;
105 }
Jack Franklin7a75e462021-01-08 16:17:13 +0000106
107 a:link,
108 a:visited {
109 color: blue;
110 text-decoration: underline;
111 }
112
113 a:hover {
114 text-decoration: none;
115 }
116 .example summary {
117 font-size: 20px;
118 }
119
120 .back-link {
121 padding-left: 5px;
122 font-size: 16px;
123 font-style: italic;
124 }
125
126 iframe { display: block; width: 100%; height: 400px; }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100127 </style>
128 </head>
129 <body>
Jack Franklin7a75e462021-01-08 16:17:13 +0000130 <h1>
131 ${componentName}
132 <a class="back-link" href="/">Back to index</a>
133 </h1>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100134 ${componentExamples.map(example => {
Jack Franklin90b66132021-01-05 11:33:43 +0000135 const fullPath = path.join(componentPath, example);
Jack Franklin7a75e462021-01-08 16:17:13 +0000136 return `<details class="example">
137 <summary><a href="${fullPath}">${example.replace('.html', '').replace(/-|_/g, ' ')}</a></summary>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100138 <iframe src="${fullPath}"></iframe>
Jack Franklin7a75e462021-01-08 16:17:13 +0000139 </details>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100140 }).join('\n')}
141 </body>
142 </html>`;
143 // clang-format on
144}
145
146function createServerIndexFile(componentNames) {
147 // clang-format off
148 return `<!DOCTYPE html>
149 <html>
150 <head>
151 <meta charset="UTF-8" />
152 <meta name="viewport" content="width=device-width" />
153 <title>DevTools components</title>
154 <style>
Jack Franklinb5997162020-11-25 17:28:51 +0000155 a:link, a:visited {
156 color: blue;
157 text-transform: capitalize;
158 text-decoration: none;
159 }
160 a:hover {
161 text-decoration: underline;
162 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100163 </style>
164 </head>
165 <body>
166 <h1>DevTools components</h1>
167 <ul>
168 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +0000169 const niceName = name.replace(/_/g, ' ');
Tim van der Lippee622f552021-04-14 15:15:18 +0100170 return `<li><a href='/front_end/ui/components/docs/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100171 }).join('\n')}
172 </ul>
173 </body>
174 </html>`;
175 // clang-format on
176}
177
178async function getExamplesForPath(filePath) {
Jack Franklin0155f7d2021-03-02 09:11:22 +0000179 const componentDirectory = path.join(componentDocsBaseFolder, filePath);
Jack Franklinc1501222020-10-02 09:42:08 +0100180 const allFiles = await fs.promises.readdir(componentDirectory);
181 const htmlExampleFiles = allFiles.filter(file => {
182 return path.extname(file) === '.html';
183 });
Jack Franklin1557a1c2020-06-08 15:22:13 +0100184
Jack Franklinc1501222020-10-02 09:42:08 +0100185 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100186}
187
188function respondWithHtml(response, html) {
189 response.setHeader('Content-Type', 'text/html; charset=utf-8');
190 response.writeHead(200);
191 response.write(html, 'utf8');
192 response.end();
193}
194
195function send404(response, message) {
196 response.writeHead(404);
197 response.write(message, 'utf8');
198 response.end();
199}
200
Jack Franklin279564e2020-07-06 15:25:18 +0100201async function checkFileExists(filePath) {
202 try {
203 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
204 return !errorsAccessingFile;
205 } catch (e) {
206 return false;
207 }
208}
209
Tim van der Lippe23283e12021-05-10 15:08:31 +0100210const EXCLUDED_COVERAGE_FOLDERS = new Set(['third_party', 'ui/components/docs', 'Images']);
211
212/**
213 * @param {string} filePath
214 * @returns {boolean}
215 */
216function isIncludedForCoverageComputation(filePath) {
217 for (const excludedFolder of EXCLUDED_COVERAGE_FOLDERS) {
218 if (filePath.startsWith(`/front_end/${excludedFolder}/`)) {
219 return false;
220 }
221 }
222
223 return true;
224}
225
226const COVERAGE_INSTRUMENTER = createInstrumenter({
227 esModules: true,
228 parserPlugins: [
229 ...defaultIstanbulSchema.instrumenter.properties.parserPlugins.default,
230 'topLevelAwait',
231 ],
232});
233
234const instrumentedSourceCacheForFilePaths = new Map();
235
236const SHOULD_GATHER_COVERAGE_INFORMATION = process.env.COVERAGE === '1';
237
238/**
239 * @param {http.IncomingMessage} request
240 * @param {http.ServerResponse} response
241 */
Jack Franklin1557a1c2020-06-08 15:22:13 +0100242async function requestHandler(request, response) {
243 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100244 if (filePath === '/favicon.ico') {
245 send404(response, '404, no favicon');
246 return;
247 }
248
249 if (filePath === '/' || filePath === '/index.html') {
Tim van der Lippee622f552021-04-14 15:15:18 +0100250 const components =
251 await fs.promises.readdir(path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000252 const html = createServerIndexFile(components.filter(filePath => {
Tim van der Lippee622f552021-04-14 15:15:18 +0100253 const stats = fs.lstatSync(path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs', filePath));
Jack Franklinb5997162020-11-25 17:28:51 +0000254 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
255 return stats.isDirectory();
256 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100257 respondWithHtml(response, html);
Tim van der Lippee622f552021-04-14 15:15:18 +0100258 } else if (filePath.startsWith('/front_end/ui/components/docs') && path.extname(filePath) === '') {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100259 // This means it's a component path like /breadcrumbs.
260 const componentHtml = await getExamplesForPath(filePath);
261 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000262 return;
Tim van der Lippee622f552021-04-14 15:15:18 +0100263 } else if (/ui\/components\/docs\/(.+)\/(.+)\.html/.test(filePath)) {
Jack Franklin36429002020-11-25 15:07:26 +0000264 /** This conditional checks if we are viewing an individual example's HTML
Tim van der Lippee622f552021-04-14 15:15:18 +0100265 * file. e.g. localhost:8090/front_end/ui/components/docs/data_grid/basic.html For each
Jack Franklin36429002020-11-25 15:07:26 +0000266 * example we inject themeColors.css into the page so all CSS variables
267 * that components use are available.
268 */
Jack Franklin8742dc82021-02-26 09:17:59 +0000269
Jack Franklin0155f7d2021-03-02 09:11:22 +0000270 /**
271 * We also let the user provide a different base path for any shared
272 * resources that we load. But if this is provided along with the
273 * componentDocsBaseArg, and the two are the same, we don't want to use the
274 * shared resources base, as it's part of the componentDocsBaseArg and
275 * therefore the URL is already correct.
276 *
277 * If we didn't get a componentDocsBaseArg or we did and it's different to
278 * the sharedResourcesBase, we use sharedResourcesBase.
279 */
280 const baseUrlForSharedResource =
281 componentDocsBaseArg && componentDocsBaseArg.endsWith(sharedResourcesBase) ? '/' : `/${sharedResourcesBase}`;
282 const fileContents = await fs.promises.readFile(path.join(componentDocsBaseFolder, filePath), {encoding: 'utf8'});
Jack Franklin8742dc82021-02-26 09:17:59 +0000283 const themeColoursLink = `<link rel="stylesheet" href="${
Tim van der Lippee6583132021-04-08 10:10:57 +0100284 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'legacy', 'themeColors.css')}" type="text/css" />`;
Jack Franklin343c1412021-02-26 15:08:10 +0000285 const inspectorCommonLink = `<link rel="stylesheet" href="${
Tim van der Lippee6583132021-04-08 10:10:57 +0100286 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'legacy', 'inspectorCommon.css')}" type="text/css" />`;
Jack Franklin8742dc82021-02-26 09:17:59 +0000287 const toggleDarkModeScript = `<script type="module" src="${
Tim van der Lippee622f552021-04-14 15:15:18 +0100288 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'components', 'docs', 'component_docs.js')}"></script>`;
Jack Franklin0f019a02021-04-09 08:50:29 +0000289 const newFileContents = fileContents.replace('</head>', `${themeColoursLink}\n${inspectorCommonLink}\n</head>`)
290 .replace('</body>', toggleDarkModeScript + '\n</body>');
Jack Franklin36429002020-11-25 15:07:26 +0000291 respondWithHtml(response, newFileContents);
292
Jack Franklin1557a1c2020-06-08 15:22:13 +0100293 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100294 // This means it's an asset like a JS file or an image.
Jack Franklin49e33f92021-03-31 10:30:56 +0000295 let fullPath = path.join(componentDocsBaseFolder, filePath);
Jack Franklind0345122020-12-21 09:12:04 +0000296 if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
297 // Rewrite this path so we can load up the locale in the component-docs
Tim van der Lippebb352e62021-04-01 18:57:28 +0100298 fullPath = path.join(componentDocsBaseFolder, 'front_end', 'core', 'i18n', 'locales', 'en-US.json');
Jack Franklind0345122020-12-21 09:12:04 +0000299 }
300
Jack Franklin90b66132021-01-05 11:33:43 +0000301 if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100302 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
303 process.exit(1);
304 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100305
Jack Franklin279564e2020-07-06 15:25:18 +0100306 const fileExists = await checkFileExists(fullPath);
307
308 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100309 send404(response, '404, File not found');
310 return;
311 }
312
313 let encoding = 'utf8';
Jack Franklin1557a1c2020-06-08 15:22:13 +0100314 if (fullPath.endsWith('.js')) {
315 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
316 } else if (fullPath.endsWith('.css')) {
317 response.setHeader('Content-Type', 'text/css; charset=utf-8');
318 } else if (fullPath.endsWith('.wasm')) {
319 response.setHeader('Content-Type', 'application/wasm');
320 encoding = 'binary';
321 } else if (fullPath.endsWith('.svg')) {
322 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
323 } else if (fullPath.endsWith('.png')) {
324 response.setHeader('Content-Type', 'image/png');
325 encoding = 'binary';
326 } else if (fullPath.endsWith('.jpg')) {
327 response.setHeader('Content-Type', 'image/jpg');
328 encoding = 'binary';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100329 } else if (fullPath.endsWith('.avif')) {
330 response.setHeader('Content-Type', 'image/avif');
331 encoding = 'binary';
Paul Lewisd6892872021-04-01 15:56:46 +0100332 } else if (fullPath.endsWith('.gz')) {
333 response.setHeader('Content-Type', 'application/gzip');
334 encoding = 'binary';
Jack Franklin1557a1c2020-06-08 15:22:13 +0100335 }
336
Tim van der Lippe23283e12021-05-10 15:08:31 +0100337 let fileContents = await fs.promises.readFile(fullPath, encoding);
338 const isComputingCoverageRequest = request.headers['devtools-compute-coverage'] === '1';
339
340 if (SHOULD_GATHER_COVERAGE_INFORMATION && fullPath.endsWith('.js') && filePath.startsWith('/front_end/') &&
341 isIncludedForCoverageComputation(filePath)) {
342 const previouslyGeneratedInstrumentedSource = instrumentedSourceCacheForFilePaths.get(fullPath);
343
344 if (previouslyGeneratedInstrumentedSource) {
345 fileContents = previouslyGeneratedInstrumentedSource;
346 } else {
347 if (!isComputingCoverageRequest) {
348 response.writeHead(400);
349 response.write(`Invalid coverage request. Attempted to load ${request.url}.`, 'utf8');
350 response.end();
351
352 console.error(
353 `Invalid coverage request. Attempted to load ${request.url} which was not available in the ` +
354 'code coverage instrumentation cache. Make sure that you call `preloadForCodeCoverage` in the describe block ' +
355 'of your interactions test, before declaring any tests.\n');
356 return;
357 }
358
359 fileContents = await new Promise(async (resolve, reject) => {
360 let sourceMap = convertSourceMap.fromSource(fileContents);
361 if (!sourceMap) {
362 sourceMap = convertSourceMap.fromMapFileSource(fileContents, path.dirname(fullPath));
363 }
364
365 COVERAGE_INSTRUMENTER.instrument(fileContents, fullPath, (error, instrumentedSource) => {
366 if (error) {
367 reject(error);
368 } else {
369 resolve(instrumentedSource);
370 }
371 }, sourceMap ? sourceMap.sourcemap : undefined);
372 });
373
374 instrumentedSourceCacheForFilePaths.set(fullPath, fileContents);
375 }
376 }
377
Jack Franklin1557a1c2020-06-08 15:22:13 +0100378 response.writeHead(200);
379 response.write(fileContents, encoding);
380 response.end();
381 }
382}