blob: 9238c2210c471d9f3b48b10324167df0c4a114f4 [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));
Jan Schefflerc2ad98d2020-12-23 07:27:09 +000025const devtoolsFrontendFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen', 'front_end'));
Jack Franklin1557a1c2020-06-08 15:22:13 +010026
Jan Schefflerc2ad98d2020-12-23 07:27:09 +000027if (!fs.existsSync(devtoolsFrontendFolder)) {
28 console.error(`ERROR: Generated front_end folder (${devtoolsFrontendFolder}) 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) {
Jan Schefflerc2ad98d2020-12-23 07:27:09 +000052 const componentName = componentPath.replace('/', '').replace(/_/g, ' ');
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 }
67 iframe { display: block; width: 100%; }
68 </style>
69 </head>
70 <body>
71 <h1>${componentName}</h1>
72 ${componentExamples.map(example => {
Jan Schefflerc2ad98d2020-12-23 07:27:09 +000073 const fullPath = path.join('component_docs', componentPath, example);
Jack Franklin1557a1c2020-06-08 15:22:13 +010074 return `<div class="example">
75 <h3><a href="${fullPath}">${example}</a></h3>
76 <iframe src="${fullPath}"></iframe>
77 </div>`;
78 }).join('\n')}
79 </body>
80 </html>`;
81 // clang-format on
82}
83
84function createServerIndexFile(componentNames) {
85 // clang-format off
86 return `<!DOCTYPE html>
87 <html>
88 <head>
89 <meta charset="UTF-8" />
90 <meta name="viewport" content="width=device-width" />
91 <title>DevTools components</title>
92 <style>
Jack Franklinb5997162020-11-25 17:28:51 +000093 a:link, a:visited {
94 color: blue;
95 text-transform: capitalize;
96 text-decoration: none;
97 }
98 a:hover {
99 text-decoration: underline;
100 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100101 </style>
102 </head>
103 <body>
104 <h1>DevTools components</h1>
105 <ul>
106 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +0000107 const niceName = name.replace(/_/g, ' ');
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000108 return `<li><a href='/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100109 }).join('\n')}
110 </ul>
111 </body>
112 </html>`;
113 // clang-format on
114}
115
116async function getExamplesForPath(filePath) {
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000117 const componentDirectory = path.join(devtoolsFrontendFolder, 'component_docs', filePath);
Jack Franklinc1501222020-10-02 09:42:08 +0100118 const allFiles = await fs.promises.readdir(componentDirectory);
119 const htmlExampleFiles = allFiles.filter(file => {
120 return path.extname(file) === '.html';
121 });
Jack Franklin1557a1c2020-06-08 15:22:13 +0100122
Jack Franklinc1501222020-10-02 09:42:08 +0100123 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100124}
125
126function respondWithHtml(response, html) {
127 response.setHeader('Content-Type', 'text/html; charset=utf-8');
128 response.writeHead(200);
129 response.write(html, 'utf8');
130 response.end();
131}
132
133function send404(response, message) {
134 response.writeHead(404);
135 response.write(message, 'utf8');
136 response.end();
137}
138
Jack Franklin279564e2020-07-06 15:25:18 +0100139async function checkFileExists(filePath) {
140 try {
141 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
142 return !errorsAccessingFile;
143 } catch (e) {
144 return false;
145 }
146}
147
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100148/**
149 * In Devtools-Frontend we load images without a leading slash, e.g.
150 * url(Images/checker.png). This works within devtools, but breaks this component
Jack Franklin4738bc72020-09-17 09:51:24 +0100151 * server as the path ends up as /component_docs/my_component/Images/checker.png.
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100152 * So we check if the path ends in Images/*.* and if so, remove anything before
153 * it. Then it will be resolved correctly.
154 */
155function normalizeImagePathIfRequired(filePath) {
156 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
157 const match = imagePathRegex.exec(filePath);
158 if (!match) {
159 return filePath;
160 }
161
162 const [, imageName, imageExt] = match;
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000163 const normalizedPath = path.join('Images', `${imageName}.${imageExt}`);
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100164 return normalizedPath;
165}
166
Jack Franklin1557a1c2020-06-08 15:22:13 +0100167async function requestHandler(request, response) {
168 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100169 if (filePath === '/favicon.ico') {
170 send404(response, '404, no favicon');
171 return;
172 }
173
174 if (filePath === '/' || filePath === '/index.html') {
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000175 const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000176 const html = createServerIndexFile(components.filter(filePath => {
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000177 const stats = fs.lstatSync(path.join(devtoolsFrontendFolder, 'component_docs', filePath));
Jack Franklinb5997162020-11-25 17:28:51 +0000178 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
179 return stats.isDirectory();
180 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100181 respondWithHtml(response, html);
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000182 } else if (path.extname(filePath) === '') {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100183 // This means it's a component path like /breadcrumbs.
184 const componentHtml = await getExamplesForPath(filePath);
185 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000186 return;
187 } else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
188 /** This conditional checks if we are viewing an individual example's HTML
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000189 * file. e.g. localhost:8090/component_docs/data_grid/basic.html For each
Jack Franklin36429002020-11-25 15:07:26 +0000190 * example we inject themeColors.css into the page so all CSS variables
191 * that components use are available.
192 */
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000193 const fileContents = await fs.promises.readFile(path.join(devtoolsFrontendFolder, filePath), {encoding: 'utf8'});
194 const themeColoursLink = '<link rel="stylesheet" href="/ui/themeColors.css" type="text/css" />';
195 const toggleDarkModeScript = '<script type="module" src="/component_docs/component_docs.js"></script>';
Jack Franklin36429002020-11-25 15:07:26 +0000196 const newFileContents = fileContents.replace('<style>', themeColoursLink + '\n<style>')
197 .replace('<script', toggleDarkModeScript + '\n<script');
198 respondWithHtml(response, newFileContents);
199
Jack Franklin1557a1c2020-06-08 15:22:13 +0100200 } else {
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000201 if (filePath.startsWith('/front_end')) {
202 /**
203 * We load files from the test directory whose paths will often start with
204 * /front_end if they load in frontend code. However we also get requests
205 * from within the front_end directory which do not start with /front_end.
206 * This means we might get two requests for the same file:
207 *
208 * 1) /front_end/ui/ui.js
209 * 2) /ui/ui.js
210 *
211 * If we serve them both it will mean we load ui/ui.js twice. So instead
212 * we redirect permanently to the non-/front_end prefixed URL so that the
213 * browser only loads each module once.
214 */
215 response.writeHead(301, {
216 Location: filePath.replace('/front_end', ''),
217 });
218 response.end();
219 return;
220 }
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100221 // This means it's an asset like a JS file or an image.
222 const normalizedPath = normalizeImagePathIfRequired(filePath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100223
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000224 let fullPath = path.join(devtoolsFrontendFolder, normalizedPath);
Jack Franklind0345122020-12-21 09:12:04 +0000225 if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
226 // Rewrite this path so we can load up the locale in the component-docs
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000227 fullPath = path.join(devtoolsFrontendFolder, 'i18n', 'locales', 'en-US.json');
228 }
229 /**
230 * Component docs can also load files from the test directory, so we ensure
231 * that we allow that here, and also then deal with the relative imports
232 * that come from the test directory, which will contain the front_end
233 * folder already. In which case we take the devToolsFrontendFolder, and go
234 * up one directory with '..' to make sure we import the right file from the
235 * right place.
236 */
237 const fileIsInTestFolder = normalizedPath.startsWith('/test/');
238 if (fileIsInTestFolder) {
239 fullPath = path.join(devtoolsFrontendFolder, '..', normalizedPath);
Jack Franklind0345122020-12-21 09:12:04 +0000240 }
241
Jan Schefflerc2ad98d2020-12-23 07:27:09 +0000242 if (!fullPath.startsWith(devtoolsFrontendFolder) && !fileIsInTestFolder) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100243 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
244 process.exit(1);
245 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100246
Jack Franklin279564e2020-07-06 15:25:18 +0100247 const fileExists = await checkFileExists(fullPath);
248
249 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100250 send404(response, '404, File not found');
251 return;
252 }
253
254 let encoding = 'utf8';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100255 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg') ||
256 fullPath.endsWith('.avif')) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100257 encoding = 'binary';
258 }
259
260 const fileContents = await fs.promises.readFile(fullPath, encoding);
261
262 encoding = 'utf8';
263 if (fullPath.endsWith('.js')) {
264 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
265 } else if (fullPath.endsWith('.css')) {
266 response.setHeader('Content-Type', 'text/css; charset=utf-8');
267 } else if (fullPath.endsWith('.wasm')) {
268 response.setHeader('Content-Type', 'application/wasm');
269 encoding = 'binary';
270 } else if (fullPath.endsWith('.svg')) {
271 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
272 } else if (fullPath.endsWith('.png')) {
273 response.setHeader('Content-Type', 'image/png');
274 encoding = 'binary';
275 } else if (fullPath.endsWith('.jpg')) {
276 response.setHeader('Content-Type', 'image/jpg');
277 encoding = 'binary';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100278 } else if (fullPath.endsWith('.avif')) {
279 response.setHeader('Content-Type', 'image/avif');
280 encoding = 'binary';
Jack Franklin1557a1c2020-06-08 15:22:13 +0100281 }
282
283 response.writeHead(200);
284 response.write(fileContents, encoding);
285 response.end();
286 }
287}