Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 1 | // Copyright 2021 The Chromium Authors. All rights reserved. |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | const fs = require('fs'); |
| 6 | const http = require('http'); |
| 7 | const path = require('path'); |
| 8 | const parseURL = require('url').parse; |
| 9 | const {argv} = require('yargs'); |
Tim van der Lippe | 23283e1 | 2021-05-10 14:08:31 | [diff] [blame] | 10 | |
Jack Franklin | a3dd06a | 2021-03-18 10:47:16 | [diff] [blame] | 11 | const {getTestRunnerConfigSetting} = require('../test/test_config_helpers.js'); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 12 | |
Andrés Olivares | 6c66158 | 2023-05-22 09:57:26 | [diff] [blame] | 13 | const tracesMode = argv.traces || false; |
Jack Franklin | 2f2b9fc | 2023-05-25 13:39:17 | [diff] [blame] | 14 | const serverPort = parseInt(process.env.PORT, 10) || (tracesMode ? 11010 : 8090); |
Liviu Rau | cc4ed1e | 2024-05-07 12:53:38 | [diff] [blame] | 15 | |
| 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 | */ |
| 22 | const [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 Franklin | 8742dc8 | 2021-02-26 09:17:59 | [diff] [blame] | 30 | |
| 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 Franklin | fe65bc6 | 2021-03-16 11:23:20 | [diff] [blame] | 36 | const sharedResourcesBase = |
| 37 | argv.sharedResourcesBase || getTestRunnerConfigSetting('component-server-shared-resources-path', '/'); |
Jack Franklin | 086ccd5 | 2020-11-27 11:00:14 | [diff] [blame] | 38 | |
| 39 | /** |
Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 40 | * The server assumes that examples live in |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 41 | * devtoolsRoot/out/Target/gen/front_end/ui/components/docs, but if you need to add a |
Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 42 | * prefix you can pass this argument. Passing `foo` will redirect the server to |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 43 | * look in devtoolsRoot/out/Target/gen/foo/front_end/ui/components/docs. |
Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 44 | */ |
Jack Franklin | fe65bc6 | 2021-03-16 11:23:20 | [diff] [blame] | 45 | const componentDocsBaseArg = argv.componentDocsBase || process.env.COMPONENT_DOCS_BASE || |
| 46 | getTestRunnerConfigSetting('component-server-base-path', ''); |
Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 47 | |
Jack Franklin | 8742dc8 | 2021-02-26 09:17:59 | [diff] [blame] | 48 | let 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 | */ |
| 55 | while (isRunningInGen && !pathToOutTargetDir.endsWith(`out${path.sep}${target}`)) { |
| 56 | pathToOutTargetDir = path.resolve(pathToOutTargetDir, '..'); |
| 57 | } |
Jack Franklin | 0943df4 | 2021-02-26 11:12:13 | [diff] [blame] | 58 | |
| 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 Franklin | 8742dc8 | 2021-02-26 09:17:59 | [diff] [blame] | 60 | const pathToBuiltOutTargetDirectory = |
Jack Franklin | 0943df4 | 2021-02-26 11:12:13 | [diff] [blame] | 61 | isRunningInGen ? pathToOutTargetDir : path.resolve(path.join(process.cwd(), 'out', target)); |
Jack Franklin | 8742dc8 | 2021-02-26 09:17:59 | [diff] [blame] | 62 | |
Eric Leese | 460be15 | 2024-07-10 12:07:57 | [diff] [blame] | 63 | let devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen')); |
| 64 | const fullCheckoutDevtoolsRootFolder = path.join(devtoolsRootFolder, 'third_party', 'devtools-frontend', 'src'); |
| 65 | if (__dirname.startsWith(fullCheckoutDevtoolsRootFolder)) { |
| 66 | devtoolsRootFolder = fullCheckoutDevtoolsRootFolder; |
| 67 | } |
| 68 | |
Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 69 | const componentDocsBaseFolder = path.join(devtoolsRootFolder, componentDocsBaseArg); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 70 | |
Jack Franklin | 90b6613 | 2021-01-05 11:33:43 | [diff] [blame] | 71 | if (!fs.existsSync(devtoolsRootFolder)) { |
| 72 | console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 73 | 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ünd | 803ee56 | 2024-02-07 07:25:37 | [diff] [blame] | 79 | process.on('uncaughtException', error => { |
| 80 | console.error('uncaughtException', error); |
| 81 | }); |
| 82 | process.on('unhandledRejection', error => { |
| 83 | console.error('unhandledRejection', error); |
| 84 | }); |
| 85 | |
| 86 | const server = http.createServer((req, res) => requestHandler(req, res).catch(err => send500(res, err))); |
| 87 | server.listen(serverPort, 'localhost'); |
Jack Franklin | 086ccd5 | 2020-11-27 11:00:14 | [diff] [blame] | 88 | server.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 Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 93 | console.log(`ui/components/docs location: ${ |
| 94 | path.relative(process.cwd(), path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs'))}`); |
Jack Franklin | 086ccd5 | 2020-11-27 11:00:14 | [diff] [blame] | 95 | }); |
| 96 | |
| 97 | server.once('error', error => { |
| 98 | if (process.send) { |
| 99 | process.send('ERROR'); |
| 100 | } |
| 101 | throw error; |
| 102 | }); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 103 | |
Jack Franklin | e4379b4 | 2023-10-27 14:12:41 | [diff] [blame] | 104 | // 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 Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 106 | const styleSheetPaths = [ |
| 107 | 'front_end/ui/legacy/themeColors.css', |
| 108 | 'front_end/ui/legacy/tokens.css', |
| 109 | 'front_end/ui/legacy/applicationColorTokens.css', |
Kateryna Prokopenko | b63b015 | 2024-03-14 09:04:42 | [diff] [blame] | 110 | 'front_end/ui/legacy/designTokens.css', |
Andrés Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 111 | 'front_end/ui/legacy/inspectorCommon.css', |
Jack Franklin | e4379b4 | 2023-10-27 14:12:41 | [diff] [blame] | 112 | 'front_end/ui/legacy/inspectorSyntaxHighlight.css', |
Danil Somsikov | 45f6ad1 | 2024-03-05 15:28:22 | [diff] [blame] | 113 | 'front_end/ui/legacy/textButton.css', |
Andrés Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 114 | 'front_end/ui/components/docs/component_docs_styles.css', |
| 115 | ]; |
| 116 | |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 117 | function createComponentIndexFile(componentPath, componentExamples) { |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 118 | const componentName = componentPath.replace('/front_end/ui/components/docs/', '').replace(/_/g, ' ').replace('/', ''); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 119 | // 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 Franklin | 7a75e46 | 2021-01-08 16:17:13 | [diff] [blame] | 133 | |
| 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 Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 154 | </style> |
| 155 | </head> |
| 156 | <body> |
Jack Franklin | 7a75e46 | 2021-01-08 16:17:13 | [diff] [blame] | 157 | <h1> |
| 158 | ${componentName} |
| 159 | <a class="back-link" href="/">Back to index</a> |
| 160 | </h1> |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 161 | ${componentExamples.map(example => { |
Jack Franklin | 90b6613 | 2021-01-05 11:33:43 | [diff] [blame] | 162 | const fullPath = path.join(componentPath, example); |
Jack Franklin | 7a75e46 | 2021-01-08 16:17:13 | [diff] [blame] | 163 | return `<details class="example"> |
| 164 | <summary><a href="${fullPath}">${example.replace('.html', '').replace(/-|_/g, ' ')}</a></summary> |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 165 | <iframe src="${fullPath}"></iframe> |
Jack Franklin | 7a75e46 | 2021-01-08 16:17:13 | [diff] [blame] | 166 | </details>`; |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 167 | }).join('\n')} |
| 168 | </body> |
| 169 | </html>`; |
| 170 | // clang-format on |
| 171 | } |
| 172 | |
| 173 | function createServerIndexFile(componentNames) { |
Andrés Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 174 | const linksToStyleSheets = |
| 175 | styleSheetPaths |
Jack Franklin | 2d63337 | 2023-07-17 10:31:05 | [diff] [blame] | 176 | .map(link => `<link rel="stylesheet" href="${sharedResourcesBase}${path.join(...link.split('/'))}" />`) |
Andrés Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 177 | .join('\n'); |
| 178 | |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 179 | // 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 Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 186 | ${linksToStyleSheets} |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 187 | </head> |
Jack Franklin | 64f1c6f | 2021-12-20 16:05:28 | [diff] [blame] | 188 | <body id="index-page"> |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 189 | <h1>DevTools components</h1> |
Jack Franklin | 64f1c6f | 2021-12-20 16:05:28 | [diff] [blame] | 190 | <ul class="components-list"> |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 191 | ${componentNames.map(name => { |
Jack Franklin | b599716 | 2020-11-25 17:28:51 | [diff] [blame] | 192 | const niceName = name.replace(/_/g, ' '); |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 193 | return `<li><a href='/front_end/ui/components/docs/${name}'>${niceName}</a></li>`; |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 194 | }).join('\n')} |
| 195 | </ul> |
| 196 | </body> |
| 197 | </html>`; |
| 198 | // clang-format on |
| 199 | } |
| 200 | |
| 201 | async function getExamplesForPath(filePath) { |
Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 202 | const componentDirectory = path.join(componentDocsBaseFolder, filePath); |
Simon Zünd | b7b0e53 | 2024-02-05 09:52:01 | [diff] [blame] | 203 | if (!await checkFileExists(componentDirectory)) { |
| 204 | return null; |
| 205 | } |
Jack Franklin | c150122 | 2020-10-02 08:42:08 | [diff] [blame] | 206 | const allFiles = await fs.promises.readdir(componentDirectory); |
| 207 | const htmlExampleFiles = allFiles.filter(file => { |
| 208 | return path.extname(file) === '.html'; |
| 209 | }); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 210 | |
Jack Franklin | c150122 | 2020-10-02 08:42:08 | [diff] [blame] | 211 | return createComponentIndexFile(filePath, htmlExampleFiles); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 212 | } |
| 213 | |
| 214 | function 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 | |
| 221 | function send404(response, message) { |
| 222 | response.writeHead(404); |
| 223 | response.write(message, 'utf8'); |
| 224 | response.end(); |
| 225 | } |
| 226 | |
Simon Zünd | 803ee56 | 2024-02-07 07:25:37 | [diff] [blame] | 227 | function send500(response, error) { |
| 228 | response.writeHead(500); |
| 229 | response.write(error.toString(), 'utf8'); |
| 230 | response.end(); |
| 231 | } |
| 232 | |
Jack Franklin | 279564e | 2020-07-06 14:25:18 | [diff] [blame] | 233 | async 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 Lippe | 23283e1 | 2021-05-10 14:08:31 | [diff] [blame] | 242 | /** |
| 243 | * @param {http.IncomingMessage} request |
| 244 | * @param {http.ServerResponse} response |
| 245 | */ |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 246 | async function requestHandler(request, response) { |
| 247 | const filePath = parseURL(request.url).pathname; |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 248 | if (filePath === '/favicon.ico') { |
| 249 | send404(response, '404, no favicon'); |
| 250 | return; |
| 251 | } |
Paul Irish | 7de4937 | 2023-05-18 16:20:21 | [diff] [blame] | 252 | if (['/', '/index.html'].includes(filePath) && tracesMode === false) { |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 253 | const components = |
| 254 | await fs.promises.readdir(path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs')); |
Jack Franklin | b599716 | 2020-11-25 17:28:51 | [diff] [blame] | 255 | const html = createServerIndexFile(components.filter(filePath => { |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 256 | const stats = fs.lstatSync(path.join(componentDocsBaseFolder, 'front_end', 'ui', 'components', 'docs', filePath)); |
Jack Franklin | b599716 | 2020-11-25 17:28:51 | [diff] [blame] | 257 | // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories. |
| 258 | return stats.isDirectory(); |
| 259 | })); |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 260 | respondWithHtml(response, html); |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 261 | } else if (filePath.startsWith('/front_end/ui/components/docs') && path.extname(filePath) === '') { |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 262 | // This means it's a component path like /breadcrumbs. |
| 263 | const componentHtml = await getExamplesForPath(filePath); |
Simon Zünd | b7b0e53 | 2024-02-05 09:52:01 | [diff] [blame] | 264 | if (componentHtml !== null) { |
| 265 | respondWithHtml(response, componentHtml); |
| 266 | } else { |
| 267 | send404(response, '404, not a valid component'); |
| 268 | } |
Jack Franklin | 3642900 | 2020-11-25 15:07:26 | [diff] [blame] | 269 | return; |
Paul Irish | 7de4937 | 2023-05-18 16:20:21 | [diff] [blame] | 270 | } else if (tracesMode) { |
| 271 | return handleTracesModeRequest(request, response, filePath); |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 272 | } else if (/ui\/components\/docs\/(.+)\/(.+)\.html/.test(filePath)) { |
Jack Franklin | 3642900 | 2020-11-25 15:07:26 | [diff] [blame] | 273 | /** This conditional checks if we are viewing an individual example's HTML |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 274 | * file. e.g. localhost:8090/front_end/ui/components/docs/data_grid/basic.html For each |
Jack Franklin | 3642900 | 2020-11-25 15:07:26 | [diff] [blame] | 275 | * example we inject themeColors.css into the page so all CSS variables |
| 276 | * that components use are available. |
| 277 | */ |
Jack Franklin | 8742dc8 | 2021-02-26 09:17:59 | [diff] [blame] | 278 | |
Jack Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 279 | /** |
Jack Franklin | c8d5dd2 | 2022-12-21 14:58:15 | [diff] [blame] | 280 | * 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 Franklin | 0155f7d | 2021-03-02 09:11:22 | [diff] [blame] | 289 | const baseUrlForSharedResource = |
| 290 | componentDocsBaseArg && componentDocsBaseArg.endsWith(sharedResourcesBase) ? '/' : `/${sharedResourcesBase}`; |
Simon Zünd | 11a99d0 | 2024-02-07 06:38:33 | [diff] [blame] | 291 | 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 Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 297 | |
| 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 Franklin | 8742dc8 | 2021-02-26 09:17:59 | [diff] [blame] | 305 | const toggleDarkModeScript = `<script type="module" src="${ |
Tim van der Lippe | e622f55 | 2021-04-14 14:15:18 | [diff] [blame] | 306 | path.join(baseUrlForSharedResource, 'front_end', 'ui', 'components', 'docs', 'component_docs.js')}"></script>`; |
Andrés Olivares | f1d811c | 2023-07-13 10:05:57 | [diff] [blame] | 307 | const newFileContents = fileContents.replace('</head>', `${linksToStyleSheets}</head>`) |
| 308 | .replace('</body>', toggleDarkModeScript + '\n</body>'); |
Jack Franklin | 3642900 | 2020-11-25 15:07:26 | [diff] [blame] | 309 | respondWithHtml(response, newFileContents); |
| 310 | |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 311 | } else { |
Jack Franklin | 9d4ecf7 | 2020-09-16 13:04:30 | [diff] [blame] | 312 | // This means it's an asset like a JS file or an image. |
Jack Franklin | 49e33f9 | 2021-03-31 10:30:56 | [diff] [blame] | 313 | let fullPath = path.join(componentDocsBaseFolder, filePath); |
Jack Franklin | 2b60639 | 2021-08-20 14:32:20 | [diff] [blame] | 314 | 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 Franklin | d034512 | 2020-12-21 09:12:04 | [diff] [blame] | 323 | // Rewrite this path so we can load up the locale in the component-docs |
Jack Franklin | 2b60639 | 2021-08-20 14:32:20 | [diff] [blame] | 324 | 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 Franklin | d034512 | 2020-12-21 09:12:04 | [diff] [blame] | 329 | } |
Jack Franklin | 90b6613 | 2021-01-05 11:33:43 | [diff] [blame] | 330 | if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) { |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 331 | console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`); |
| 332 | process.exit(1); |
| 333 | } |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 334 | |
Jack Franklin | 279564e | 2020-07-06 14:25:18 | [diff] [blame] | 335 | const fileExists = await checkFileExists(fullPath); |
| 336 | |
| 337 | if (!fileExists) { |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 338 | send404(response, '404, File not found'); |
| 339 | return; |
| 340 | } |
| 341 | |
| 342 | let encoding = 'utf8'; |
Danil Somsikov | 65e958b | 2024-08-20 08:01:45 | [diff] [blame] | 343 | if (fullPath.endsWith('.js') || fullPath.endsWith('.mjs')) { |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 344 | 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 Bynens | c38abd4 | 2020-12-24 07:20:05 | [diff] [blame] | 358 | } else if (fullPath.endsWith('.avif')) { |
| 359 | response.setHeader('Content-Type', 'image/avif'); |
| 360 | encoding = 'binary'; |
Paul Lewis | d689287 | 2021-04-01 14:56:46 | [diff] [blame] | 361 | } else if (fullPath.endsWith('.gz')) { |
| 362 | response.setHeader('Content-Type', 'application/gzip'); |
| 363 | encoding = 'binary'; |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 364 | } |
| 365 | |
Alex Rudenko | cb4b968 | 2024-09-05 09:45:06 | [diff] [blame] | 366 | const fileContents = await fs.promises.readFile(fullPath, encoding); |
Tim van der Lippe | 23283e1 | 2021-05-10 14:08:31 | [diff] [blame] | 367 | |
Jack Franklin | 1557a1c | 2020-06-08 14:22:13 | [diff] [blame] | 368 | response.writeHead(200); |
| 369 | response.write(fileContents, encoding); |
| 370 | response.end(); |
| 371 | } |
| 372 | } |
Paul Irish | 7de4937 | 2023-05-18 16:20:21 | [diff] [blame] | 373 | |
| 374 | function 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 | */ |
| 465 | async function handleTracesModeRequest(request, response, filePath) { |
Andrés Olivares | 294a584 | 2024-02-28 19:02:57 | [diff] [blame] | 466 | const traceFolder = path.resolve(path.join(process.cwd(), 'front_end/panels/timeline/fixtures/traces/')); |
Paul Irish | 7de4937 | 2023-05-18 16:20:21 | [diff] [blame] | 467 | 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 | } |