blob: 015f2735c385c808fca709715e69cc07cc3e68c7 [file] [log] [blame]
Jack Franklin1557a1c2020-06-08 15:22:13 +01001// Copyright (c) 2020 The Chromium Authors. All rights reserved.
2// 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
11const serverPort = parseInt(process.env.PORT, 10) || 8090;
12
13const target = argv.target || 'Default';
Jack Franklin086ccd52020-11-27 11:00:14 +000014
15/**
16 * When you run npm run components-server we run the script as is from scripts/,
17 * but when this server is run as part of a test suite it's run from
18 * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
19 * out where we are.
20 */
21const isRunningInGen = __dirname.includes(path.join(path.sep, 'gen', path.sep, 'scripts'));
Jack Franklinb36ad7e2020-12-07 10:20:52 +000022
23const pathToBuiltOutTargetDirectory = isRunningInGen ? path.resolve(path.join(__dirname), '..', '..', '..') :
24 path.resolve(path.join(__dirname, '..', '..', 'out', target));
Jack Franklin90b66132021-01-05 11:33:43 +000025const devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen'));
Jack Franklin1557a1c2020-06-08 15:22:13 +010026
Jack Franklin90b66132021-01-05 11:33:43 +000027if (!fs.existsSync(devtoolsRootFolder)) {
28 console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`);
Jack Franklin1557a1c2020-06-08 15:22:13 +010029 console.log(
30 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
31 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
32 process.exit(1);
33}
34
Jack Franklin086ccd52020-11-27 11:00:14 +000035const server = http.createServer(requestHandler);
36server.listen(serverPort);
37server.once('listening', () => {
38 if (process.send) {
39 process.send(serverPort);
40 }
41 console.log(`Started components server at http://localhost:${serverPort}\n`);
42});
43
44server.once('error', error => {
45 if (process.send) {
46 process.send('ERROR');
47 }
48 throw error;
49});
Jack Franklin1557a1c2020-06-08 15:22:13 +010050
51function createComponentIndexFile(componentPath, componentExamples) {
Jack Franklin7a75e462021-01-08 16:17:13 +000052 const componentName = componentPath.replace('/front_end/component_docs/', '').replace(/_/g, ' ').replace('/', '');
Jack Franklin1557a1c2020-06-08 15:22:13 +010053 // clang-format off
54 return `<!DOCTYPE html>
55 <html>
56 <head>
57 <meta charset="UTF-8" />
58 <meta name="viewport" content="width=device-width" />
59 <title>DevTools component: ${componentName}</title>
60 <style>
61 h1 { text-transform: capitalize; }
62
63 .example {
64 padding: 5px;
65 margin: 10px;
66 }
Jack Franklin7a75e462021-01-08 16:17:13 +000067
68 a:link,
69 a:visited {
70 color: blue;
71 text-decoration: underline;
72 }
73
74 a:hover {
75 text-decoration: none;
76 }
77 .example summary {
78 font-size: 20px;
79 }
80
81 .back-link {
82 padding-left: 5px;
83 font-size: 16px;
84 font-style: italic;
85 }
86
87 iframe { display: block; width: 100%; height: 400px; }
Jack Franklin1557a1c2020-06-08 15:22:13 +010088 </style>
89 </head>
90 <body>
Jack Franklin7a75e462021-01-08 16:17:13 +000091 <h1>
92 ${componentName}
93 <a class="back-link" href="/">Back to index</a>
94 </h1>
Jack Franklin1557a1c2020-06-08 15:22:13 +010095 ${componentExamples.map(example => {
Jack Franklin90b66132021-01-05 11:33:43 +000096 const fullPath = path.join(componentPath, example);
Jack Franklin7a75e462021-01-08 16:17:13 +000097 return `<details class="example">
98 <summary><a href="${fullPath}">${example.replace('.html', '').replace(/-|_/g, ' ')}</a></summary>
Jack Franklin1557a1c2020-06-08 15:22:13 +010099 <iframe src="${fullPath}"></iframe>
Jack Franklin7a75e462021-01-08 16:17:13 +0000100 </details>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100101 }).join('\n')}
102 </body>
103 </html>`;
104 // clang-format on
105}
106
107function createServerIndexFile(componentNames) {
108 // clang-format off
109 return `<!DOCTYPE html>
110 <html>
111 <head>
112 <meta charset="UTF-8" />
113 <meta name="viewport" content="width=device-width" />
114 <title>DevTools components</title>
115 <style>
Jack Franklinb5997162020-11-25 17:28:51 +0000116 a:link, a:visited {
117 color: blue;
118 text-transform: capitalize;
119 text-decoration: none;
120 }
121 a:hover {
122 text-decoration: underline;
123 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100124 </style>
125 </head>
126 <body>
127 <h1>DevTools components</h1>
128 <ul>
129 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +0000130 const niceName = name.replace(/_/g, ' ');
Jack Franklin90b66132021-01-05 11:33:43 +0000131 return `<li><a href='/front_end/component_docs/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100132 }).join('\n')}
133 </ul>
134 </body>
135 </html>`;
136 // clang-format on
137}
138
139async function getExamplesForPath(filePath) {
Jack Franklin90b66132021-01-05 11:33:43 +0000140 const componentDirectory = path.join(devtoolsRootFolder, filePath);
Jack Franklinc1501222020-10-02 09:42:08 +0100141 const allFiles = await fs.promises.readdir(componentDirectory);
142 const htmlExampleFiles = allFiles.filter(file => {
143 return path.extname(file) === '.html';
144 });
Jack Franklin1557a1c2020-06-08 15:22:13 +0100145
Jack Franklinc1501222020-10-02 09:42:08 +0100146 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100147}
148
149function respondWithHtml(response, html) {
150 response.setHeader('Content-Type', 'text/html; charset=utf-8');
151 response.writeHead(200);
152 response.write(html, 'utf8');
153 response.end();
154}
155
156function send404(response, message) {
157 response.writeHead(404);
158 response.write(message, 'utf8');
159 response.end();
160}
161
Jack Franklin279564e2020-07-06 15:25:18 +0100162async function checkFileExists(filePath) {
163 try {
164 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
165 return !errorsAccessingFile;
166 } catch (e) {
167 return false;
168 }
169}
170
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100171/**
172 * In Devtools-Frontend we load images without a leading slash, e.g.
173 * url(Images/checker.png). This works within devtools, but breaks this component
Jack Franklin4738bc72020-09-17 09:51:24 +0100174 * server as the path ends up as /component_docs/my_component/Images/checker.png.
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100175 * So we check if the path ends in Images/*.* and if so, remove anything before
176 * it. Then it will be resolved correctly.
177 */
178function normalizeImagePathIfRequired(filePath) {
179 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
180 const match = imagePathRegex.exec(filePath);
181 if (!match) {
182 return filePath;
183 }
184
185 const [, imageName, imageExt] = match;
Jack Franklin90b66132021-01-05 11:33:43 +0000186 const normalizedPath = path.join('front_end', 'Images', `${imageName}.${imageExt}`);
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100187 return normalizedPath;
188}
189
Jack Franklin1557a1c2020-06-08 15:22:13 +0100190async function requestHandler(request, response) {
191 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100192 if (filePath === '/favicon.ico') {
193 send404(response, '404, no favicon');
194 return;
195 }
196
197 if (filePath === '/' || filePath === '/index.html') {
Jack Franklin90b66132021-01-05 11:33:43 +0000198 const components = await fs.promises.readdir(path.join(devtoolsRootFolder, 'front_end', 'component_docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000199 const html = createServerIndexFile(components.filter(filePath => {
Jack Franklin90b66132021-01-05 11:33:43 +0000200 const stats = fs.lstatSync(path.join(devtoolsRootFolder, 'front_end', 'component_docs', filePath));
Jack Franklinb5997162020-11-25 17:28:51 +0000201 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
202 return stats.isDirectory();
203 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100204 respondWithHtml(response, html);
Jack Franklin90b66132021-01-05 11:33:43 +0000205 } else if (filePath.startsWith('/front_end/component_docs') && path.extname(filePath) === '') {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100206 // This means it's a component path like /breadcrumbs.
207 const componentHtml = await getExamplesForPath(filePath);
208 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000209 return;
210 } else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
211 /** This conditional checks if we are viewing an individual example's HTML
Jack Franklin90b66132021-01-05 11:33:43 +0000212 * file. e.g. localhost:8090/front_end/component_docs/data_grid/basic.html For each
Jack Franklin36429002020-11-25 15:07:26 +0000213 * example we inject themeColors.css into the page so all CSS variables
214 * that components use are available.
215 */
Jack Franklin90b66132021-01-05 11:33:43 +0000216 const fileContents = await fs.promises.readFile(path.join(devtoolsRootFolder, filePath), {encoding: 'utf8'});
217 const themeColoursLink = '<link rel="stylesheet" href="/front_end/ui/themeColors.css" type="text/css" />';
Jack Frankline6dcd242021-01-18 16:09:22 +0000218 const inspectorStyleLink = '<link rel="stylesheet" href="/front_end/ui/inspectorStyle.css" type="text/css" />';
Jack Franklin90b66132021-01-05 11:33:43 +0000219 const toggleDarkModeScript = '<script type="module" src="/front_end/component_docs/component_docs.js"></script>';
Jack Frankline6dcd242021-01-18 16:09:22 +0000220 const newFileContents = fileContents.replace('<style>', `${themeColoursLink}\n${inspectorStyleLink}\n<style>`)
Jack Franklin36429002020-11-25 15:07:26 +0000221 .replace('<script', toggleDarkModeScript + '\n<script');
222 respondWithHtml(response, newFileContents);
223
Jack Franklin1557a1c2020-06-08 15:22:13 +0100224 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100225 // This means it's an asset like a JS file or an image.
226 const normalizedPath = normalizeImagePathIfRequired(filePath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100227
Jack Franklin90b66132021-01-05 11:33:43 +0000228 let fullPath = path.join(devtoolsRootFolder, normalizedPath);
Jack Franklind0345122020-12-21 09:12:04 +0000229 if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
230 // Rewrite this path so we can load up the locale in the component-docs
Jack Franklin90b66132021-01-05 11:33:43 +0000231 fullPath = path.join(devtoolsRootFolder, 'front_end', 'i18n', 'locales', 'en-US.json');
Jack Franklind0345122020-12-21 09:12:04 +0000232 }
233
Jack Franklin90b66132021-01-05 11:33:43 +0000234 if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100235 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
236 process.exit(1);
237 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100238
Jack Franklin279564e2020-07-06 15:25:18 +0100239 const fileExists = await checkFileExists(fullPath);
240
241 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100242 send404(response, '404, File not found');
243 return;
244 }
245
246 let encoding = 'utf8';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100247 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg') ||
248 fullPath.endsWith('.avif')) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100249 encoding = 'binary';
250 }
251
252 const fileContents = await fs.promises.readFile(fullPath, encoding);
253
254 encoding = 'utf8';
255 if (fullPath.endsWith('.js')) {
256 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
257 } else if (fullPath.endsWith('.css')) {
258 response.setHeader('Content-Type', 'text/css; charset=utf-8');
259 } else if (fullPath.endsWith('.wasm')) {
260 response.setHeader('Content-Type', 'application/wasm');
261 encoding = 'binary';
262 } else if (fullPath.endsWith('.svg')) {
263 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
264 } else if (fullPath.endsWith('.png')) {
265 response.setHeader('Content-Type', 'image/png');
266 encoding = 'binary';
267 } else if (fullPath.endsWith('.jpg')) {
268 response.setHeader('Content-Type', 'image/jpg');
269 encoding = 'binary';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100270 } else if (fullPath.endsWith('.avif')) {
271 response.setHeader('Content-Type', 'image/avif');
272 encoding = 'binary';
Jack Franklin1557a1c2020-06-08 15:22:13 +0100273 }
274
275 response.writeHead(200);
276 response.write(fileContents, encoding);
277 response.end();
278 }
279}