blob: 358179d337f72703b8f58d1dcf8d44b13d92f2a8 [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');
10
Jack Franklinfe65bc62021-03-16 11:23:20 +000011function getTestRunnerConfig() {
12 try {
13 return JSON.parse(process.env.TEST_RUNNER_JSON_CONFIG);
14 } catch {
15 // Return an empty object so any lookups return undefined
16 return {};
17 }
18}
19function getTestRunnerConfigSetting(settingKey, fallbackValue) {
20 const config = getTestRunnerConfig();
21 return config[settingKey] === undefined ? fallbackValue : config[settingKey];
22}
Jack Franklin1557a1c2020-06-08 15:22:13 +010023
Jack Franklinfe65bc62021-03-16 11:23:20 +000024const serverPort = parseInt(process.env.PORT, 10) || 8090;
Jack Franklin8742dc82021-02-26 09:17:59 +000025const target = argv.target || process.env.TARGET || 'Default';
26
27/**
28 * This configures the base of the URLs that are injected into each component
29 * doc example to load. By default it's /, so that we load /front_end/..., but
30 * this can be configured if you have a different file structure.
31 */
Jack Franklinfe65bc62021-03-16 11:23:20 +000032const sharedResourcesBase =
33 argv.sharedResourcesBase || getTestRunnerConfigSetting('component-server-shared-resources-path', '/');
Jack Franklin086ccd52020-11-27 11:00:14 +000034
35/**
Jack Franklin0155f7d2021-03-02 09:11:22 +000036 * The server assumes that examples live in
Jack Franklinfe65bc62021-03-16 11:23:20 +000037 * devtoolsRoot/out/Target/gen/front_end/component_docs, but if you need to add a
Jack Franklin0155f7d2021-03-02 09:11:22 +000038 * prefix you can pass this argument. Passing `foo` will redirect the server to
Jack Franklinfe65bc62021-03-16 11:23:20 +000039 * look in devtoolsRoot/out/Target/gen/foo/front_end/component_docs.
Jack Franklin0155f7d2021-03-02 09:11:22 +000040 */
Jack Franklinfe65bc62021-03-16 11:23:20 +000041const componentDocsBaseArg = argv.componentDocsBase || process.env.COMPONENT_DOCS_BASE ||
42 getTestRunnerConfigSetting('component-server-base-path', '');
Jack Franklin0155f7d2021-03-02 09:11:22 +000043
44/**
Jack Franklin086ccd52020-11-27 11:00:14 +000045 * When you run npm run components-server we run the script as is from scripts/,
46 * but when this server is run as part of a test suite it's run from
47 * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
48 * out where we are.
49 */
Jack Franklin8742dc82021-02-26 09:17:59 +000050const isRunningInGen = __dirname.includes(path.join('out', path.sep, target));
Jack Franklinb36ad7e2020-12-07 10:20:52 +000051
Jack Franklin8742dc82021-02-26 09:17:59 +000052let pathToOutTargetDir = __dirname;
53/**
54 * If we are in the gen directory, we need to find the out/Default folder to use
55 * as our base to find files from. We could do this with path.join(x, '..',
56 * '..') until we get the right folder, but that's brittle. It's better to
57 * search up for out/Default to be robust to any folder structures.
58 */
59while (isRunningInGen && !pathToOutTargetDir.endsWith(`out${path.sep}${target}`)) {
60 pathToOutTargetDir = path.resolve(pathToOutTargetDir, '..');
61}
Jack Franklin0943df42021-02-26 11:12:13 +000062
63/* 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 +000064const pathToBuiltOutTargetDirectory =
Jack Franklin0943df42021-02-26 11:12:13 +000065 isRunningInGen ? pathToOutTargetDir : path.resolve(path.join(process.cwd(), 'out', target));
Jack Franklin8742dc82021-02-26 09:17:59 +000066
Jack Franklin90b66132021-01-05 11:33:43 +000067const devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen'));
Jack Franklin0155f7d2021-03-02 09:11:22 +000068const componentDocsBaseFolder = path.join(devtoolsRootFolder, componentDocsBaseArg);
Jack Franklin1557a1c2020-06-08 15:22:13 +010069
Jack Franklin90b66132021-01-05 11:33:43 +000070if (!fs.existsSync(devtoolsRootFolder)) {
71 console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`);
Jack Franklin1557a1c2020-06-08 15:22:13 +010072 console.log(
73 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
74 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
75 process.exit(1);
76}
77
Jack Franklin086ccd52020-11-27 11:00:14 +000078const server = http.createServer(requestHandler);
79server.listen(serverPort);
80server.once('listening', () => {
81 if (process.send) {
82 process.send(serverPort);
83 }
84 console.log(`Started components server at http://localhost:${serverPort}\n`);
Jack Franklin0155f7d2021-03-02 09:11:22 +000085 console.log(`component_docs location: ${
86 path.relative(process.cwd(), path.join(componentDocsBaseFolder, 'front_end', 'component_docs'))}`);
Jack Franklin086ccd52020-11-27 11:00:14 +000087});
88
89server.once('error', error => {
90 if (process.send) {
91 process.send('ERROR');
92 }
93 throw error;
94});
Jack Franklin1557a1c2020-06-08 15:22:13 +010095
96function createComponentIndexFile(componentPath, componentExamples) {
Jack Franklin7a75e462021-01-08 16:17:13 +000097 const componentName = componentPath.replace('/front_end/component_docs/', '').replace(/_/g, ' ').replace('/', '');
Jack Franklin1557a1c2020-06-08 15:22:13 +010098 // clang-format off
99 return `<!DOCTYPE html>
100 <html>
101 <head>
102 <meta charset="UTF-8" />
103 <meta name="viewport" content="width=device-width" />
104 <title>DevTools component: ${componentName}</title>
105 <style>
106 h1 { text-transform: capitalize; }
107
108 .example {
109 padding: 5px;
110 margin: 10px;
111 }
Jack Franklin7a75e462021-01-08 16:17:13 +0000112
113 a:link,
114 a:visited {
115 color: blue;
116 text-decoration: underline;
117 }
118
119 a:hover {
120 text-decoration: none;
121 }
122 .example summary {
123 font-size: 20px;
124 }
125
126 .back-link {
127 padding-left: 5px;
128 font-size: 16px;
129 font-style: italic;
130 }
131
132 iframe { display: block; width: 100%; height: 400px; }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100133 </style>
134 </head>
135 <body>
Jack Franklin7a75e462021-01-08 16:17:13 +0000136 <h1>
137 ${componentName}
138 <a class="back-link" href="/">Back to index</a>
139 </h1>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100140 ${componentExamples.map(example => {
Jack Franklin90b66132021-01-05 11:33:43 +0000141 const fullPath = path.join(componentPath, example);
Jack Franklin7a75e462021-01-08 16:17:13 +0000142 return `<details class="example">
143 <summary><a href="${fullPath}">${example.replace('.html', '').replace(/-|_/g, ' ')}</a></summary>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100144 <iframe src="${fullPath}"></iframe>
Jack Franklin7a75e462021-01-08 16:17:13 +0000145 </details>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100146 }).join('\n')}
147 </body>
148 </html>`;
149 // clang-format on
150}
151
152function createServerIndexFile(componentNames) {
153 // clang-format off
154 return `<!DOCTYPE html>
155 <html>
156 <head>
157 <meta charset="UTF-8" />
158 <meta name="viewport" content="width=device-width" />
159 <title>DevTools components</title>
160 <style>
Jack Franklinb5997162020-11-25 17:28:51 +0000161 a:link, a:visited {
162 color: blue;
163 text-transform: capitalize;
164 text-decoration: none;
165 }
166 a:hover {
167 text-decoration: underline;
168 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100169 </style>
170 </head>
171 <body>
172 <h1>DevTools components</h1>
173 <ul>
174 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +0000175 const niceName = name.replace(/_/g, ' ');
Jack Franklin90b66132021-01-05 11:33:43 +0000176 return `<li><a href='/front_end/component_docs/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100177 }).join('\n')}
178 </ul>
179 </body>
180 </html>`;
181 // clang-format on
182}
183
184async function getExamplesForPath(filePath) {
Jack Franklin0155f7d2021-03-02 09:11:22 +0000185 const componentDirectory = path.join(componentDocsBaseFolder, filePath);
Jack Franklinc1501222020-10-02 09:42:08 +0100186 const allFiles = await fs.promises.readdir(componentDirectory);
187 const htmlExampleFiles = allFiles.filter(file => {
188 return path.extname(file) === '.html';
189 });
Jack Franklin1557a1c2020-06-08 15:22:13 +0100190
Jack Franklinc1501222020-10-02 09:42:08 +0100191 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100192}
193
194function respondWithHtml(response, html) {
195 response.setHeader('Content-Type', 'text/html; charset=utf-8');
196 response.writeHead(200);
197 response.write(html, 'utf8');
198 response.end();
199}
200
201function send404(response, message) {
202 response.writeHead(404);
203 response.write(message, 'utf8');
204 response.end();
205}
206
Jack Franklin279564e2020-07-06 15:25:18 +0100207async function checkFileExists(filePath) {
208 try {
209 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
210 return !errorsAccessingFile;
211 } catch (e) {
212 return false;
213 }
214}
215
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100216/**
217 * In Devtools-Frontend we load images without a leading slash, e.g.
Tim van der Lippe88ba72d2021-03-04 14:54:14 +0000218 * var(--image-file-checker). This works within devtools, but breaks this component
Jack Franklin4738bc72020-09-17 09:51:24 +0100219 * server as the path ends up as /component_docs/my_component/Images/checker.png.
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100220 * So we check if the path ends in Images/*.* and if so, remove anything before
221 * it. Then it will be resolved correctly.
222 */
223function normalizeImagePathIfRequired(filePath) {
224 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
225 const match = imagePathRegex.exec(filePath);
226 if (!match) {
227 return filePath;
228 }
229
230 const [, imageName, imageExt] = match;
Jack Franklin90b66132021-01-05 11:33:43 +0000231 const normalizedPath = path.join('front_end', 'Images', `${imageName}.${imageExt}`);
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100232 return normalizedPath;
233}
234
Jack Franklin1557a1c2020-06-08 15:22:13 +0100235async function requestHandler(request, response) {
236 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100237 if (filePath === '/favicon.ico') {
238 send404(response, '404, no favicon');
239 return;
240 }
241
242 if (filePath === '/' || filePath === '/index.html') {
Jack Franklin0155f7d2021-03-02 09:11:22 +0000243 const components = await fs.promises.readdir(path.join(componentDocsBaseFolder, 'front_end', 'component_docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000244 const html = createServerIndexFile(components.filter(filePath => {
Jack Franklin0155f7d2021-03-02 09:11:22 +0000245 const stats = fs.lstatSync(path.join(componentDocsBaseFolder, 'front_end', 'component_docs', filePath));
Jack Franklinb5997162020-11-25 17:28:51 +0000246 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
247 return stats.isDirectory();
248 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100249 respondWithHtml(response, html);
Jack Franklin90b66132021-01-05 11:33:43 +0000250 } else if (filePath.startsWith('/front_end/component_docs') && path.extname(filePath) === '') {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100251 // This means it's a component path like /breadcrumbs.
252 const componentHtml = await getExamplesForPath(filePath);
253 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000254 return;
255 } else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
256 /** This conditional checks if we are viewing an individual example's HTML
Jack Franklin90b66132021-01-05 11:33:43 +0000257 * file. e.g. localhost:8090/front_end/component_docs/data_grid/basic.html For each
Jack Franklin36429002020-11-25 15:07:26 +0000258 * example we inject themeColors.css into the page so all CSS variables
259 * that components use are available.
260 */
Jack Franklin8742dc82021-02-26 09:17:59 +0000261
Jack Franklin0155f7d2021-03-02 09:11:22 +0000262 /**
263 * We also let the user provide a different base path for any shared
264 * resources that we load. But if this is provided along with the
265 * componentDocsBaseArg, and the two are the same, we don't want to use the
266 * shared resources base, as it's part of the componentDocsBaseArg and
267 * therefore the URL is already correct.
268 *
269 * If we didn't get a componentDocsBaseArg or we did and it's different to
270 * the sharedResourcesBase, we use sharedResourcesBase.
271 */
272 const baseUrlForSharedResource =
273 componentDocsBaseArg && componentDocsBaseArg.endsWith(sharedResourcesBase) ? '/' : `/${sharedResourcesBase}`;
274 const fileContents = await fs.promises.readFile(path.join(componentDocsBaseFolder, filePath), {encoding: 'utf8'});
Jack Franklin8742dc82021-02-26 09:17:59 +0000275 const themeColoursLink = `<link rel="stylesheet" href="${
Jack Franklin0155f7d2021-03-02 09:11:22 +0000276 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'themeColors.css')}" type="text/css" />`;
Jack Franklin343c1412021-02-26 15:08:10 +0000277 const inspectorCommonLink = `<link rel="stylesheet" href="${
Jack Franklin0155f7d2021-03-02 09:11:22 +0000278 path.join(baseUrlForSharedResource, 'front_end', 'ui', 'inspectorCommon.css')}" type="text/css" />`;
Jack Franklin8742dc82021-02-26 09:17:59 +0000279 const toggleDarkModeScript = `<script type="module" src="${
Jack Franklin0155f7d2021-03-02 09:11:22 +0000280 path.join(baseUrlForSharedResource, 'front_end', 'component_docs', 'component_docs.js')}"></script>`;
Jack Franklin343c1412021-02-26 15:08:10 +0000281 const newFileContents = fileContents.replace('<style>', `${themeColoursLink}\n${inspectorCommonLink}\n<style>`)
Jack Franklin36429002020-11-25 15:07:26 +0000282 .replace('<script', toggleDarkModeScript + '\n<script');
283 respondWithHtml(response, newFileContents);
284
Jack Franklin1557a1c2020-06-08 15:22:13 +0100285 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100286 // This means it's an asset like a JS file or an image.
287 const normalizedPath = normalizeImagePathIfRequired(filePath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100288
Jack Franklin0155f7d2021-03-02 09:11:22 +0000289 let fullPath = path.join(componentDocsBaseFolder, normalizedPath);
Jack Franklind0345122020-12-21 09:12:04 +0000290 if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
291 // Rewrite this path so we can load up the locale in the component-docs
Jack Franklin0155f7d2021-03-02 09:11:22 +0000292 fullPath = path.join(componentDocsBaseFolder, 'front_end', 'i18n', 'locales', 'en-US.json');
Jack Franklind0345122020-12-21 09:12:04 +0000293 }
294
Jack Franklin90b66132021-01-05 11:33:43 +0000295 if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100296 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
297 process.exit(1);
298 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100299
Jack Franklin279564e2020-07-06 15:25:18 +0100300 const fileExists = await checkFileExists(fullPath);
301
302 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100303 send404(response, '404, File not found');
304 return;
305 }
306
307 let encoding = 'utf8';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100308 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg') ||
309 fullPath.endsWith('.avif')) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100310 encoding = 'binary';
311 }
312
313 const fileContents = await fs.promises.readFile(fullPath, encoding);
314
315 encoding = 'utf8';
316 if (fullPath.endsWith('.js')) {
317 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
318 } else if (fullPath.endsWith('.css')) {
319 response.setHeader('Content-Type', 'text/css; charset=utf-8');
320 } else if (fullPath.endsWith('.wasm')) {
321 response.setHeader('Content-Type', 'application/wasm');
322 encoding = 'binary';
323 } else if (fullPath.endsWith('.svg')) {
324 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
325 } else if (fullPath.endsWith('.png')) {
326 response.setHeader('Content-Type', 'image/png');
327 encoding = 'binary';
328 } else if (fullPath.endsWith('.jpg')) {
329 response.setHeader('Content-Type', 'image/jpg');
330 encoding = 'binary';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100331 } else if (fullPath.endsWith('.avif')) {
332 response.setHeader('Content-Type', 'image/avif');
333 encoding = 'binary';
Jack Franklin1557a1c2020-06-08 15:22:13 +0100334 }
335
336 response.writeHead(200);
337 response.write(fileContents, encoding);
338 response.end();
339 }
340}