blob: f9ad2319e6f93a8045acfd1fcb2ac291f057f8d0 [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');
Jack Franklina3dd06a2021-03-18 10:47:16 +000010const {getTestRunnerConfigSetting} = require('../test/test_config_helpers.js');
Jack Franklin1557a1c2020-06-08 15:22:13 +010011
Jack Franklin1557a1c2020-06-08 15:22:13 +010012
Jack Franklinfe65bc62021-03-16 11:23:20 +000013const serverPort = parseInt(process.env.PORT, 10) || 8090;
Jack Franklin8742dc82021-02-26 09:17:59 +000014const target = argv.target || process.env.TARGET || 'Default';
15
16/**
17 * This configures the base of the URLs that are injected into each component
18 * doc example to load. By default it's /, so that we load /front_end/..., but
19 * this can be configured if you have a different file structure.
20 */
Jack Franklinfe65bc62021-03-16 11:23:20 +000021const sharedResourcesBase =
22 argv.sharedResourcesBase || getTestRunnerConfigSetting('component-server-shared-resources-path', '/');
Jack Franklin086ccd52020-11-27 11:00:14 +000023
24/**
Jack Franklin0155f7d2021-03-02 09:11:22 +000025 * The server assumes that examples live in
Jack Franklinfe65bc62021-03-16 11:23:20 +000026 * devtoolsRoot/out/Target/gen/front_end/component_docs, but if you need to add a
Jack Franklin0155f7d2021-03-02 09:11:22 +000027 * prefix you can pass this argument. Passing `foo` will redirect the server to
Jack Franklinfe65bc62021-03-16 11:23:20 +000028 * look in devtoolsRoot/out/Target/gen/foo/front_end/component_docs.
Jack Franklin0155f7d2021-03-02 09:11:22 +000029 */
Jack Franklinfe65bc62021-03-16 11:23:20 +000030const componentDocsBaseArg = argv.componentDocsBase || process.env.COMPONENT_DOCS_BASE ||
31 getTestRunnerConfigSetting('component-server-base-path', '');
Jack Franklin0155f7d2021-03-02 09:11:22 +000032
33/**
Jack Franklin086ccd52020-11-27 11:00:14 +000034 * When you run npm run components-server we run the script as is from scripts/,
35 * but when this server is run as part of a test suite it's run from
36 * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
37 * out where we are.
38 */
Jack Franklin8742dc82021-02-26 09:17:59 +000039const isRunningInGen = __dirname.includes(path.join('out', path.sep, target));
Jack Franklinb36ad7e2020-12-07 10:20:52 +000040
Jack Franklin8742dc82021-02-26 09:17:59 +000041let pathToOutTargetDir = __dirname;
42/**
43 * If we are in the gen directory, we need to find the out/Default folder to use
44 * as our base to find files from. We could do this with path.join(x, '..',
45 * '..') until we get the right folder, but that's brittle. It's better to
46 * search up for out/Default to be robust to any folder structures.
47 */
48while (isRunningInGen && !pathToOutTargetDir.endsWith(`out${path.sep}${target}`)) {
49 pathToOutTargetDir = path.resolve(pathToOutTargetDir, '..');
50}
Jack Franklin0943df42021-02-26 11:12:13 +000051
52/* 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 +000053const pathToBuiltOutTargetDirectory =
Jack Franklin0943df42021-02-26 11:12:13 +000054 isRunningInGen ? pathToOutTargetDir : path.resolve(path.join(process.cwd(), 'out', target));
Jack Franklin8742dc82021-02-26 09:17:59 +000055
Jack Franklin90b66132021-01-05 11:33:43 +000056const devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen'));
Jack Franklin0155f7d2021-03-02 09:11:22 +000057const componentDocsBaseFolder = path.join(devtoolsRootFolder, componentDocsBaseArg);
Jack Franklin1557a1c2020-06-08 15:22:13 +010058
Jack Franklin90b66132021-01-05 11:33:43 +000059if (!fs.existsSync(devtoolsRootFolder)) {
60 console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`);
Jack Franklin1557a1c2020-06-08 15:22:13 +010061 console.log(
62 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
63 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
64 process.exit(1);
65}
66
Jack Franklin086ccd52020-11-27 11:00:14 +000067const server = http.createServer(requestHandler);
68server.listen(serverPort);
69server.once('listening', () => {
70 if (process.send) {
71 process.send(serverPort);
72 }
73 console.log(`Started components server at http://localhost:${serverPort}\n`);
Jack Franklin0155f7d2021-03-02 09:11:22 +000074 console.log(`component_docs location: ${
75 path.relative(process.cwd(), path.join(componentDocsBaseFolder, 'front_end', 'component_docs'))}`);
Jack Franklin086ccd52020-11-27 11:00:14 +000076});
77
78server.once('error', error => {
79 if (process.send) {
80 process.send('ERROR');
81 }
82 throw error;
83});
Jack Franklin1557a1c2020-06-08 15:22:13 +010084
85function createComponentIndexFile(componentPath, componentExamples) {
Jack Franklin7a75e462021-01-08 16:17:13 +000086 const componentName = componentPath.replace('/front_end/component_docs/', '').replace(/_/g, ' ').replace('/', '');
Jack Franklin1557a1c2020-06-08 15:22:13 +010087 // clang-format off
88 return `<!DOCTYPE html>
89 <html>
90 <head>
91 <meta charset="UTF-8" />
92 <meta name="viewport" content="width=device-width" />
93 <title>DevTools component: ${componentName}</title>
94 <style>
95 h1 { text-transform: capitalize; }
96
97 .example {
98 padding: 5px;
99 margin: 10px;
100 }
Jack Franklin7a75e462021-01-08 16:17:13 +0000101
102 a:link,
103 a:visited {
104 color: blue;
105 text-decoration: underline;
106 }
107
108 a:hover {
109 text-decoration: none;
110 }
111 .example summary {
112 font-size: 20px;
113 }
114
115 .back-link {
116 padding-left: 5px;
117 font-size: 16px;
118 font-style: italic;
119 }
120
121 iframe { display: block; width: 100%; height: 400px; }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100122 </style>
123 </head>
124 <body>
Jack Franklin7a75e462021-01-08 16:17:13 +0000125 <h1>
126 ${componentName}
127 <a class="back-link" href="/">Back to index</a>
128 </h1>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100129 ${componentExamples.map(example => {
Jack Franklin90b66132021-01-05 11:33:43 +0000130 const fullPath = path.join(componentPath, example);
Jack Franklin7a75e462021-01-08 16:17:13 +0000131 return `<details class="example">
132 <summary><a href="${fullPath}">${example.replace('.html', '').replace(/-|_/g, ' ')}</a></summary>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100133 <iframe src="${fullPath}"></iframe>
Jack Franklin7a75e462021-01-08 16:17:13 +0000134 </details>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100135 }).join('\n')}
136 </body>
137 </html>`;
138 // clang-format on
139}
140
141function createServerIndexFile(componentNames) {
142 // clang-format off
143 return `<!DOCTYPE html>
144 <html>
145 <head>
146 <meta charset="UTF-8" />
147 <meta name="viewport" content="width=device-width" />
148 <title>DevTools components</title>
149 <style>
Jack Franklinb5997162020-11-25 17:28:51 +0000150 a:link, a:visited {
151 color: blue;
152 text-transform: capitalize;
153 text-decoration: none;
154 }
155 a:hover {
156 text-decoration: underline;
157 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100158 </style>
159 </head>
160 <body>
161 <h1>DevTools components</h1>
162 <ul>
163 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +0000164 const niceName = name.replace(/_/g, ' ');
Jack Franklin90b66132021-01-05 11:33:43 +0000165 return `<li><a href='/front_end/component_docs/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100166 }).join('\n')}
167 </ul>
168 </body>
169 </html>`;
170 // clang-format on
171}
172
173async function getExamplesForPath(filePath) {
Jack Franklin0155f7d2021-03-02 09:11:22 +0000174 const componentDirectory = path.join(componentDocsBaseFolder, filePath);
Jack Franklinc1501222020-10-02 09:42:08 +0100175 const allFiles = await fs.promises.readdir(componentDirectory);
176 const htmlExampleFiles = allFiles.filter(file => {
177 return path.extname(file) === '.html';
178 });
Jack Franklin1557a1c2020-06-08 15:22:13 +0100179
Jack Franklinc1501222020-10-02 09:42:08 +0100180 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100181}
182
183function respondWithHtml(response, html) {
184 response.setHeader('Content-Type', 'text/html; charset=utf-8');
185 response.writeHead(200);
186 response.write(html, 'utf8');
187 response.end();
188}
189
190function send404(response, message) {
191 response.writeHead(404);
192 response.write(message, 'utf8');
193 response.end();
194}
195
Jack Franklin279564e2020-07-06 15:25:18 +0100196async function checkFileExists(filePath) {
197 try {
198 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
199 return !errorsAccessingFile;
200 } catch (e) {
201 return false;
202 }
203}
204
Jack Franklin1557a1c2020-06-08 15:22:13 +0100205async function requestHandler(request, response) {
206 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100207 if (filePath === '/favicon.ico') {
208 send404(response, '404, no favicon');
209 return;
210 }
211
212 if (filePath === '/' || filePath === '/index.html') {
Jack Franklin0155f7d2021-03-02 09:11:22 +0000213 const components = await fs.promises.readdir(path.join(componentDocsBaseFolder, 'front_end', 'component_docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000214 const html = createServerIndexFile(components.filter(filePath => {
Jack Franklin0155f7d2021-03-02 09:11:22 +0000215 const stats = fs.lstatSync(path.join(componentDocsBaseFolder, 'front_end', 'component_docs', filePath));
Jack Franklinb5997162020-11-25 17:28:51 +0000216 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
217 return stats.isDirectory();
218 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100219 respondWithHtml(response, html);
Jack Franklin90b66132021-01-05 11:33:43 +0000220 } else if (filePath.startsWith('/front_end/component_docs') && path.extname(filePath) === '') {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100221 // This means it's a component path like /breadcrumbs.
222 const componentHtml = await getExamplesForPath(filePath);
223 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000224 return;
225 } else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
226 /** This conditional checks if we are viewing an individual example's HTML
Jack Franklin90b66132021-01-05 11:33:43 +0000227 * file. e.g. localhost:8090/front_end/component_docs/data_grid/basic.html For each
Jack Franklin36429002020-11-25 15:07:26 +0000228 * example we inject themeColors.css into the page so all CSS variables
229 * that components use are available.
230 */
Jack Franklin8742dc82021-02-26 09:17:59 +0000231
Jack Franklin0155f7d2021-03-02 09:11:22 +0000232 /**
233 * We also let the user provide a different base path for any shared
234 * resources that we load. But if this is provided along with the
235 * componentDocsBaseArg, and the two are the same, we don't want to use the
236 * shared resources base, as it's part of the componentDocsBaseArg and
237 * therefore the URL is already correct.
238 *
239 * If we didn't get a componentDocsBaseArg or we did and it's different to
240 * the sharedResourcesBase, we use sharedResourcesBase.
241 */
242 const baseUrlForSharedResource =
243 componentDocsBaseArg && componentDocsBaseArg.endsWith(sharedResourcesBase) ? '/' : `/${sharedResourcesBase}`;
244 const fileContents = await fs.promises.readFile(path.join(componentDocsBaseFolder, filePath), {encoding: 'utf8'});
Jack Franklin8742dc82021-02-26 09:17:59 +0000245 const themeColoursLink = `<link rel="stylesheet" href="${
Jack Franklin0155f7d2021-03-02 09:11:22 +0000246 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'themeColors.css')}" type="text/css" />`;
Jack Franklin343c1412021-02-26 15:08:10 +0000247 const inspectorCommonLink = `<link rel="stylesheet" href="${
Jack Franklin0155f7d2021-03-02 09:11:22 +0000248 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'inspectorCommon.css')}" type="text/css" />`;
Jack Franklin8742dc82021-02-26 09:17:59 +0000249 const toggleDarkModeScript = `<script type="module" src="${
Jack Franklin0155f7d2021-03-02 09:11:22 +0000250 path.join(baseUrlForSharedResource, 'front_end', 'component_docs', 'component_docs.js')}"></script>`;
Jack Franklin343c1412021-02-26 15:08:10 +0000251 const newFileContents = fileContents.replace('<style>', `${themeColoursLink}\n${inspectorCommonLink}\n<style>`)
Jack Franklin36429002020-11-25 15:07:26 +0000252 .replace('<script', toggleDarkModeScript + '\n<script');
253 respondWithHtml(response, newFileContents);
254
Jack Franklin1557a1c2020-06-08 15:22:13 +0100255 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100256 // This means it's an asset like a JS file or an image.
Jack Franklin49e33f92021-03-31 10:30:56 +0000257 let fullPath = path.join(componentDocsBaseFolder, filePath);
Jack Franklind0345122020-12-21 09:12:04 +0000258 if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
259 // Rewrite this path so we can load up the locale in the component-docs
Jack Franklin0155f7d2021-03-02 09:11:22 +0000260 fullPath = path.join(componentDocsBaseFolder, 'front_end', 'i18n', 'locales', 'en-US.json');
Jack Franklind0345122020-12-21 09:12:04 +0000261 }
262
Jack Franklin90b66132021-01-05 11:33:43 +0000263 if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100264 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
265 process.exit(1);
266 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100267
Jack Franklin279564e2020-07-06 15:25:18 +0100268 const fileExists = await checkFileExists(fullPath);
269
270 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100271 send404(response, '404, File not found');
272 return;
273 }
274
275 let encoding = 'utf8';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100276 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg') ||
277 fullPath.endsWith('.avif')) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100278 encoding = 'binary';
279 }
280
281 const fileContents = await fs.promises.readFile(fullPath, encoding);
282
283 encoding = 'utf8';
284 if (fullPath.endsWith('.js')) {
285 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
286 } else if (fullPath.endsWith('.css')) {
287 response.setHeader('Content-Type', 'text/css; charset=utf-8');
288 } else if (fullPath.endsWith('.wasm')) {
289 response.setHeader('Content-Type', 'application/wasm');
290 encoding = 'binary';
291 } else if (fullPath.endsWith('.svg')) {
292 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
293 } else if (fullPath.endsWith('.png')) {
294 response.setHeader('Content-Type', 'image/png');
295 encoding = 'binary';
296 } else if (fullPath.endsWith('.jpg')) {
297 response.setHeader('Content-Type', 'image/jpg');
298 encoding = 'binary';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100299 } else if (fullPath.endsWith('.avif')) {
300 response.setHeader('Content-Type', 'image/avif');
301 encoding = 'binary';
Jack Franklin1557a1c2020-06-08 15:22:13 +0100302 }
303
304 response.writeHead(200);
305 response.write(fileContents, encoding);
306 response.end();
307 }
308}