blob: 788613672f822b5766852c4f9782ab36a2964b14 [file] [log] [blame]
Jack Franklin0155f7d2021-03-02 09:11:221// Copyright 2021 The Chromium Authors. All rights reserved.
Jack Franklin1557a1c2020-06-08 14:22:132// 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 14:08:3110
Jack Franklina3dd06a2021-03-18 10:47:1611const {getTestRunnerConfigSetting} = require('../test/test_config_helpers.js');
Jack Franklin1557a1c2020-06-08 14:22:1312
Andrés Olivares6c661582023-05-22 09:57:2613const tracesMode = argv.traces || false;
Jack Franklin2f2b9fc2023-05-25 13:39:1714const serverPort = parseInt(process.env.PORT, 10) || (tracesMode ? 11010 : 8090);
Liviu Raucc4ed1e2024-05-07 12:53:3815
16/**
17 * When you run npm run components-server we run the script as is from scripts/,
18 * but when this server is run as part of a test suite it's run from
19 * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
20 * out where we are.
21 */
22const [target, isRunningInGen] = (() => {
23 const regex = new RegExp(`out\\${path.sep}(.*)\\${path.sep}gen`);
24 const match = regex.exec(__dirname);
25 if (match) {
26 return [match[1], true];
27 }
28 return [argv.target || process.env.TARGET || 'Default', false];
29})();
Jack Franklin8742dc82021-02-26 09:17:5930
31/**
32 * This configures the base of the URLs that are injected into each component
33 * doc example to load. By default it's /, so that we load /front_end/..., but
34 * this can be configured if you have a different file structure.
35 */
Jack Franklinfe65bc62021-03-16 11:23:2036const sharedResourcesBase =
37 argv.sharedResourcesBase || getTestRunnerConfigSetting('component-server-shared-resources-path', '/');
Jack Franklin086ccd52020-11-27 11:00:1438
39/**
Jack Franklin0155f7d2021-03-02 09:11:2240 * The server assumes that examples live in
Tim van der Lippee622f552021-04-14 14:15:1841 * devtoolsRoot/out/Target/gen/front_end/ui/components/docs, but if you need to add a
Jack Franklin0155f7d2021-03-02 09:11:2242 * prefix you can pass this argument. Passing `foo` will redirect the server to
Tim van der Lippee622f552021-04-14 14:15:1843 * look in devtoolsRoot/out/Target/gen/foo/front_end/ui/components/docs.
Jack Franklin0155f7d2021-03-02 09:11:2244 */
Jack Franklinfe65bc62021-03-16 11:23:2045const componentDocsBaseArg = argv.componentDocsBase || process.env.COMPONENT_DOCS_BASE ||
46 getTestRunnerConfigSetting('component-server-base-path', '');
Jack Franklin0155f7d2021-03-02 09:11:2247
Jack Franklin8742dc82021-02-26 09:17:5948let pathToOutTargetDir = __dirname;
49/**
50 * If we are in the gen directory, we need to find the out/Default folder to use
51 * as our base to find files from. We could do this with path.join(x, '..',
52 * '..') until we get the right folder, but that's brittle. It's better to
53 * search up for out/Default to be robust to any folder structures.
54 */
55while (isRunningInGen && !pathToOutTargetDir.endsWith(`out${path.sep}${target}`)) {
56 pathToOutTargetDir = path.resolve(pathToOutTargetDir, '..');
57}
Jack Franklin0943df42021-02-26 11:12:1358
59/* 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:5960const pathToBuiltOutTargetDirectory =
Jack Franklin0943df42021-02-26 11:12:1361 isRunningInGen ? pathToOutTargetDir : path.resolve(path.join(process.cwd(), 'out', target));
Jack Franklin8742dc82021-02-26 09:17:5962
Eric Leese460be152024-07-10 12:07:5763let devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen'));
64const fullCheckoutDevtoolsRootFolder = path.join(devtoolsRootFolder, 'third_party', 'devtools-frontend', 'src');
65if (__dirname.startsWith(fullCheckoutDevtoolsRootFolder)) {
66 devtoolsRootFolder = fullCheckoutDevtoolsRootFolder;
67}
68
Jack Franklin0155f7d2021-03-02 09:11:2269const componentDocsBaseFolder = path.join(devtoolsRootFolder, componentDocsBaseArg);
Jack Franklin1557a1c2020-06-08 14:22:1370
Jack Franklin90b66132021-01-05 11:33:4371if (!fs.existsSync(devtoolsRootFolder)) {
72 console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`);
Jack Franklin1557a1c2020-06-08 14:22:1373 console.log(
74 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
75 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
76 process.exit(1);
77}
78
Simon Zünd803ee562024-02-07 07:25:3779process.on('uncaughtException', error => {
80 console.error('uncaughtException', error);
81});
82process.on('unhandledRejection', error => {
83 console.error('unhandledRejection', error);
84});
85
86const server = http.createServer((req, res) => requestHandler(req, res).catch(err => send500(res, err)));
87server.listen(serverPort, 'localhost');
Jack Franklin086ccd52020-11-27 11:00:1488server.once('listening', () => {
89 if (process.send) {
90 process.send(serverPort);
91 }
92 console.log(`Started components server at http://localhost:${serverPort}\n`);
Tim van der Lippee622f552021-04-14 14:15:1893 console.log(`ui/components/docs location: ${
94 path.relative(process.cwd(), path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs'))}`);
Jack Franklin086ccd52020-11-27 11:00:1495});
96
97server.once('error', error => {
98 if (process.send) {
99 process.send('ERROR');
100 }
101 throw error;
102});
Jack Franklin1557a1c2020-06-08 14:22:13103
Jack Frankline4379b42023-10-27 14:12:41104// All paths that are injected globally into real DevTools, so we do the same
105// to avoid styles being broken in the component server.
Andrés Olivaresf1d811c2023-07-13 10:05:57106const styleSheetPaths = [
107 'front_end/ui/legacy/themeColors.css',
108 'front_end/ui/legacy/tokens.css',
109 'front_end/ui/legacy/applicationColorTokens.css',
Kateryna Prokopenkob63b0152024-03-14 09:04:42110 'front_end/ui/legacy/designTokens.css',
Andrés Olivaresf1d811c2023-07-13 10:05:57111 'front_end/ui/legacy/inspectorCommon.css',
Jack Frankline4379b42023-10-27 14:12:41112 'front_end/ui/legacy/inspectorSyntaxHighlight.css',
Danil Somsikov45f6ad12024-03-05 15:28:22113 'front_end/ui/legacy/textButton.css',
Andrés Olivaresf1d811c2023-07-13 10:05:57114 'front_end/ui/components/docs/component_docs_styles.css',
115];
116
Jack Franklin1557a1c2020-06-08 14:22:13117function createComponentIndexFile(componentPath, componentExamples) {
Tim van der Lippee622f552021-04-14 14:15:18118 const componentName = componentPath.replace('/front_end/ui/components/docs/', '').replace(/_/g, ' ').replace('/', '');
Jack Franklin1557a1c2020-06-08 14:22:13119 // clang-format off
120 return `<!DOCTYPE html>
121 <html>
122 <head>
123 <meta charset="UTF-8" />
124 <meta name="viewport" content="width=device-width" />
125 <title>DevTools component: ${componentName}</title>
126 <style>
127 h1 { text-transform: capitalize; }
128
129 .example {
130 padding: 5px;
131 margin: 10px;
132 }
Jack Franklin7a75e462021-01-08 16:17:13133
134 a:link,
135 a:visited {
136 color: blue;
137 text-decoration: underline;
138 }
139
140 a:hover {
141 text-decoration: none;
142 }
143 .example summary {
144 font-size: 20px;
145 }
146
147 .back-link {
148 padding-left: 5px;
149 font-size: 16px;
150 font-style: italic;
151 }
152
153 iframe { display: block; width: 100%; height: 400px; }
Jack Franklin1557a1c2020-06-08 14:22:13154 </style>
155 </head>
156 <body>
Jack Franklin7a75e462021-01-08 16:17:13157 <h1>
158 ${componentName}
159 <a class="back-link" href="/">Back to index</a>
160 </h1>
Jack Franklin1557a1c2020-06-08 14:22:13161 ${componentExamples.map(example => {
Jack Franklin90b66132021-01-05 11:33:43162 const fullPath = path.join(componentPath, example);
Jack Franklin7a75e462021-01-08 16:17:13163 return `<details class="example">
164 <summary><a href="${fullPath}">${example.replace('.html', '').replace(/-|_/g, ' ')}</a></summary>
Jack Franklin1557a1c2020-06-08 14:22:13165 <iframe src="${fullPath}"></iframe>
Jack Franklin7a75e462021-01-08 16:17:13166 </details>`;
Jack Franklin1557a1c2020-06-08 14:22:13167 }).join('\n')}
168 </body>
169 </html>`;
170 // clang-format on
171}
172
173function createServerIndexFile(componentNames) {
Andrés Olivaresf1d811c2023-07-13 10:05:57174 const linksToStyleSheets =
175 styleSheetPaths
Jack Franklin2d633372023-07-17 10:31:05176 .map(link => `<link rel="stylesheet" href="${sharedResourcesBase}${path.join(...link.split('/'))}" />`)
Andrés Olivaresf1d811c2023-07-13 10:05:57177 .join('\n');
178
Jack Franklin1557a1c2020-06-08 14:22:13179 // clang-format off
180 return `<!DOCTYPE html>
181 <html>
182 <head>
183 <meta charset="UTF-8" />
184 <meta name="viewport" content="width=device-width" />
185 <title>DevTools components</title>
Andrés Olivaresf1d811c2023-07-13 10:05:57186 ${linksToStyleSheets}
Jack Franklin1557a1c2020-06-08 14:22:13187 </head>
Jack Franklin64f1c6f2021-12-20 16:05:28188 <body id="index-page">
Jack Franklin1557a1c2020-06-08 14:22:13189 <h1>DevTools components</h1>
Jack Franklin64f1c6f2021-12-20 16:05:28190 <ul class="components-list">
Jack Franklin1557a1c2020-06-08 14:22:13191 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51192 const niceName = name.replace(/_/g, ' ');
Tim van der Lippee622f552021-04-14 14:15:18193 return `<li><a href='/front_end/ui/components/docs/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 14:22:13194 }).join('\n')}
195 </ul>
196 </body>
197 </html>`;
198 // clang-format on
199}
200
201async function getExamplesForPath(filePath) {
Jack Franklin0155f7d2021-03-02 09:11:22202 const componentDirectory = path.join(componentDocsBaseFolder, filePath);
Simon Zündb7b0e532024-02-05 09:52:01203 if (!await checkFileExists(componentDirectory)) {
204 return null;
205 }
Jack Franklinc1501222020-10-02 08:42:08206 const allFiles = await fs.promises.readdir(componentDirectory);
207 const htmlExampleFiles = allFiles.filter(file => {
208 return path.extname(file) === '.html';
209 });
Jack Franklin1557a1c2020-06-08 14:22:13210
Jack Franklinc1501222020-10-02 08:42:08211 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 14:22:13212}
213
214function respondWithHtml(response, html) {
215 response.setHeader('Content-Type', 'text/html; charset=utf-8');
216 response.writeHead(200);
217 response.write(html, 'utf8');
218 response.end();
219}
220
221function send404(response, message) {
222 response.writeHead(404);
223 response.write(message, 'utf8');
224 response.end();
225}
226
Simon Zünd803ee562024-02-07 07:25:37227function send500(response, error) {
228 response.writeHead(500);
229 response.write(error.toString(), 'utf8');
230 response.end();
231}
232
Jack Franklin279564e2020-07-06 14:25:18233async function checkFileExists(filePath) {
234 try {
235 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
236 return !errorsAccessingFile;
237 } catch (e) {
238 return false;
239 }
240}
241
Tim van der Lippe23283e12021-05-10 14:08:31242/**
243 * @param {http.IncomingMessage} request
244 * @param {http.ServerResponse} response
245 */
Jack Franklin1557a1c2020-06-08 14:22:13246async function requestHandler(request, response) {
247 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 14:22:13248 if (filePath === '/favicon.ico') {
249 send404(response, '404, no favicon');
250 return;
251 }
Paul Irish7de49372023-05-18 16:20:21252 if (['/', '/index.html'].includes(filePath) && tracesMode === false) {
Tim van der Lippee622f552021-04-14 14:15:18253 const components =
254 await fs.promises.readdir(path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs'));
Jack Franklinb5997162020-11-25 17:28:51255 const html = createServerIndexFile(components.filter(filePath => {
Tim van der Lippee622f552021-04-14 14:15:18256 const stats = fs.lstatSync(path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs', filePath));
Jack Franklinb5997162020-11-25 17:28:51257 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
258 return stats.isDirectory();
259 }));
Jack Franklin1557a1c2020-06-08 14:22:13260 respondWithHtml(response, html);
Tim van der Lippee622f552021-04-14 14:15:18261 } else if (filePath.startsWith('/front_end/ui/components/docs') && path.extname(filePath) === '') {
Jack Franklin1557a1c2020-06-08 14:22:13262 // This means it's a component path like /breadcrumbs.
263 const componentHtml = await getExamplesForPath(filePath);
Simon Zündb7b0e532024-02-05 09:52:01264 if (componentHtml !== null) {
265 respondWithHtml(response, componentHtml);
266 } else {
267 send404(response, '404, not a valid component');
268 }
Jack Franklin36429002020-11-25 15:07:26269 return;
Paul Irish7de49372023-05-18 16:20:21270 } else if (tracesMode) {
271 return handleTracesModeRequest(request, response, filePath);
Tim van der Lippee622f552021-04-14 14:15:18272 } else if (/ui\/components\/docs\/(.+)\/(.+)\.html/.test(filePath)) {
Jack Franklin36429002020-11-25 15:07:26273 /** This conditional checks if we are viewing an individual example's HTML
Tim van der Lippee622f552021-04-14 14:15:18274 * file. e.g. localhost:8090/front_end/ui/components/docs/data_grid/basic.html For each
Jack Franklin36429002020-11-25 15:07:26275 * example we inject themeColors.css into the page so all CSS variables
276 * that components use are available.
277 */
Jack Franklin8742dc82021-02-26 09:17:59278
Jack Franklin0155f7d2021-03-02 09:11:22279 /**
Jack Franklinc8d5dd22022-12-21 14:58:15280 * We also let the user provide a different base path for any shared
281 * resources that we load. But if this is provided along with the
282 * componentDocsBaseArg, and the two are the same, we don't want to use the
283 * shared resources base, as it's part of the componentDocsBaseArg and
284 * therefore the URL is already correct.
285 *
286 * If we didn't get a componentDocsBaseArg or we did and it's different to
287 * the sharedResourcesBase, we use sharedResourcesBase.
288 */
Jack Franklin0155f7d2021-03-02 09:11:22289 const baseUrlForSharedResource =
290 componentDocsBaseArg && componentDocsBaseArg.endsWith(sharedResourcesBase) ? '/' : `/${sharedResourcesBase}`;
Simon Zünd11a99d02024-02-07 06:38:33291 const fullPath = path.join(componentDocsBaseFolder, filePath);
292 if (!(await checkFileExists(fullPath))) {
293 send404(response, '404, File not found');
294 return;
295 }
296 const fileContents = await fs.promises.readFile(fullPath, {encoding: 'utf8'});
Andrés Olivaresf1d811c2023-07-13 10:05:57297
298 const linksToStyleSheets =
299 styleSheetPaths
300 .map(
301 link => `<link rel="stylesheet" href="${
302 path.join(baseUrlForSharedResource, ...link.split('/'))}" type="text/css" />`)
303 .join('\n');
304
Jack Franklin8742dc82021-02-26 09:17:59305 const toggleDarkModeScript = `<script type="module" src="${
Tim van der Lippee622f552021-04-14 14:15:18306 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'components', 'docs', 'component_docs.js')}"></script>`;
Andrés Olivaresf1d811c2023-07-13 10:05:57307 const newFileContents = fileContents.replace('</head>', `${linksToStyleSheets}</head>`)
308 .replace('</body>', toggleDarkModeScript + '\n</body>');
Jack Franklin36429002020-11-25 15:07:26309 respondWithHtml(response, newFileContents);
310
Jack Franklin1557a1c2020-06-08 14:22:13311 } else {
Jack Franklin9d4ecf72020-09-16 13:04:30312 // This means it's an asset like a JS file or an image.
Jack Franklin49e33f92021-03-31 10:30:56313 let fullPath = path.join(componentDocsBaseFolder, filePath);
Jack Franklin2b606392021-08-20 14:32:20314 if (fullPath.endsWith(path.join('locales', 'en-US.json')) &&
315 !componentDocsBaseFolder.includes(sharedResourcesBase)) {
316 /**
317 * If the path is for locales/en-US.json we special case the loading of that to fix the path so it works properly in the server.
318 * We also make sure that we take into account the shared resources base;
319 * but if the base folder already contains the shared resources base, we don't
320 * add it to the path, because otherwise that would cause the shared resources
321 * base to be duplicated in the fullPath.
322 */
Jack Franklind0345122020-12-21 09:12:04323 // Rewrite this path so we can load up the locale in the component-docs
Jack Franklin2b606392021-08-20 14:32:20324 let prefix = componentDocsBaseFolder;
325 if (sharedResourcesBase && !componentDocsBaseFolder.includes(sharedResourcesBase)) {
326 prefix = path.join(componentDocsBaseFolder, sharedResourcesBase);
327 }
328 fullPath = path.join(prefix, 'front_end', 'core', 'i18n', 'locales', 'en-US.json');
Jack Franklind0345122020-12-21 09:12:04329 }
Jack Franklin90b66132021-01-05 11:33:43330 if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) {
Jack Franklin1557a1c2020-06-08 14:22:13331 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
332 process.exit(1);
333 }
Jack Franklin1557a1c2020-06-08 14:22:13334
Jack Franklin279564e2020-07-06 14:25:18335 const fileExists = await checkFileExists(fullPath);
336
337 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 14:22:13338 send404(response, '404, File not found');
339 return;
340 }
341
342 let encoding = 'utf8';
Danil Somsikov65e958b2024-08-20 08:01:45343 if (fullPath.endsWith('.js') || fullPath.endsWith('.mjs')) {
Jack Franklin1557a1c2020-06-08 14:22:13344 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
345 } else if (fullPath.endsWith('.css')) {
346 response.setHeader('Content-Type', 'text/css; charset=utf-8');
347 } else if (fullPath.endsWith('.wasm')) {
348 response.setHeader('Content-Type', 'application/wasm');
349 encoding = 'binary';
350 } else if (fullPath.endsWith('.svg')) {
351 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
352 } else if (fullPath.endsWith('.png')) {
353 response.setHeader('Content-Type', 'image/png');
354 encoding = 'binary';
355 } else if (fullPath.endsWith('.jpg')) {
356 response.setHeader('Content-Type', 'image/jpg');
357 encoding = 'binary';
Mathias Bynensc38abd42020-12-24 07:20:05358 } else if (fullPath.endsWith('.avif')) {
359 response.setHeader('Content-Type', 'image/avif');
360 encoding = 'binary';
Paul Lewisd6892872021-04-01 14:56:46361 } else if (fullPath.endsWith('.gz')) {
362 response.setHeader('Content-Type', 'application/gzip');
363 encoding = 'binary';
Jack Franklin1557a1c2020-06-08 14:22:13364 }
365
Alex Rudenkocb4b9682024-09-05 09:45:06366 const fileContents = await fs.promises.readFile(fullPath, encoding);
Tim van der Lippe23283e12021-05-10 14:08:31367
Jack Franklin1557a1c2020-06-08 14:22:13368 response.writeHead(200);
369 response.write(fileContents, encoding);
370 response.end();
371 }
372}
Paul Irish7de49372023-05-18 16:20:21373
374function createTracesIndexFile(traceFilenames) {
375 function pageFunction() {
376 const origin = new URL(location.href).origin;
377
378 document.body.addEventListener('click', async e => {
379 if (!e.target.matches('button')) {
380 return;
381 }
382 const filename = e.target.textContent;
383 const traceUrl = `${origin}/t/${filename}`;
384 const devtoolsLoadingTraceUrl = `devtools://devtools/bundled/devtools_app.html?loadTimelineFromURL=${traceUrl}`;
385
386 try {
387 await navigator.clipboard.writeText(devtoolsLoadingTraceUrl);
388 e.target.classList.add('clicked');
389 setTimeout(() => e.target.classList.remove('clicked'), 1000);
390 } catch (e) {
391 console.error(e);
392 }
393 });
394 }
395
396 // clang-format off
397 return `<!doctype html>
398 <html>
399 <head>
400 <meta charset="utf-8" />
401 <meta name="viewport" content="width=device-width" />
402 <title>Traces</title>
403 <style>
404 button {
405 appearance: none;
406 border: 0;
407 background: transparent;
408 font-size: 18px;
409 padding: 5px;
410 text-align: left;
411 }
412 button:hover {
413 background: aliceblue;
414 cursor: pointer;
415 }
416 button:active {cursor: copy;}
417 button.clicked {animation: 600ms cubic-bezier(0.65, 0.05, 0.36, 1) bam;}
418 button.clicked::after {
419 content: "Copied URL";
420 background-color: #e0e0e0;
421 margin-left: 6px;
422 font-size: 70%;
423 padding: 1px 3px;
424 animation: 500ms fadeOut ease 700ms forwards;
425 }
426 form {
427 display: grid;
428 grid-template-columns: 1fr 1fr;
429 gap: 5px;
430 }
431 @keyframes bam {
432 from {background-color: #66bb6a;}
433 to {background-color: aliceblue;}
434 }
435 @keyframes fadeOut {
436 from {opacity: 1;}
437 to {opacity: 0;}
438 }
439 </style>
440 </head>
441 <body id="traces-page">
442 <h1>First</h1>
443 <p><textarea cols=100>devtools://devtools/bundled/devtools_app.html</textarea>
444 <h1>Load OPP with fixture traces:</h1>
445
446 <form>
447 ${traceFilenames.map(filename => {
448 return `<button type=button>${filename}</button>`;
449 }).join('\n')}
450 </form>
451
452 <script>
453 (${pageFunction.toString()})();
454 </script>
455 </body>
456 </html>`;
457 // clang-format on
458}
459
460/**
461 * @param {http.IncomingMessage} request
462 * @param {http.ServerResponse} response
463 * @param {string|null} filePath
464 */
465async function handleTracesModeRequest(request, response, filePath) {
Andrés Olivares294a5842024-02-28 19:02:57466 const traceFolder = path.resolve(path.join(process.cwd(), 'front_end/panels/timeline/fixtures/traces/'));
Paul Irish7de49372023-05-18 16:20:21467 if (filePath === '/') {
468 const traceFilenames = fs.readdirSync(traceFolder).filter(f => f.includes('json'));
469 const html = createTracesIndexFile(traceFilenames);
470 respondWithHtml(response, html);
471 } else if (filePath.startsWith('/t/')) {
472 const fileName = filePath.replace('/t/', '');
473 const fullPath = path.resolve(path.join(traceFolder, fileName));
474
475 if (!fullPath.startsWith(traceFolder)) {
476 console.error(`Path ${fullPath} is outside trace fixtures folder.`);
477 process.exit(1);
478 }
479
480 const fileExists = await checkFileExists(fullPath);
481 if (!fileExists) {
482 return send404(response, '404, File not found');
483 }
484
485 let encoding = 'utf8';
486 if (fullPath.endsWith('.json')) {
487 response.setHeader('Content-Type', 'application/json');
488 } else if (fullPath.endsWith('.gz')) {
489 response.setHeader('Content-Type', 'application/gzip');
490 encoding = 'binary';
491 }
492 // Traces need CORS to be loaded by devtools:// origin
493 response.setHeader('Access-Control-Allow-Origin', '*');
494
495 const fileContents = await fs.promises.readFile(fullPath, encoding);
496 response.writeHead(200);
497 response.write(fileContents, encoding);
498 response.end();
499 } else {
500 console.error(`Unhandled traces mode request: ${filePath}`);
501 process.exit(1);
502 }
503}