blob: 1ea215fc160d4f0adcde57db411df7400c69aa55 [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
Jack Franklin8742dc82021-02-26 09:17:59 +000013const target = argv.target || process.env.TARGET || 'Default';
14
15/**
16 * This configures the base of the URLs that are injected into each component
17 * doc example to load. By default it's /, so that we load /front_end/..., but
18 * this can be configured if you have a different file structure.
19 */
20const sharedResourcesBase = argv.sharedResourcesBase || '/';
Jack Franklin086ccd52020-11-27 11:00:14 +000021
22/**
23 * When you run npm run components-server we run the script as is from scripts/,
24 * but when this server is run as part of a test suite it's run from
25 * out/Default/gen/scripts, so we have to do a bit of path mangling to figure
26 * out where we are.
27 */
Jack Franklin8742dc82021-02-26 09:17:59 +000028const isRunningInGen = __dirname.includes(path.join('out', path.sep, target));
Jack Franklinb36ad7e2020-12-07 10:20:52 +000029
Jack Franklin8742dc82021-02-26 09:17:59 +000030let pathToOutTargetDir = __dirname;
31/**
32 * If we are in the gen directory, we need to find the out/Default folder to use
33 * as our base to find files from. We could do this with path.join(x, '..',
34 * '..') until we get the right folder, but that's brittle. It's better to
35 * search up for out/Default to be robust to any folder structures.
36 */
37while (isRunningInGen && !pathToOutTargetDir.endsWith(`out${path.sep}${target}`)) {
38 pathToOutTargetDir = path.resolve(pathToOutTargetDir, '..');
39}
Jack Franklin0943df42021-02-26 11:12:13 +000040
41/* 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 +000042const pathToBuiltOutTargetDirectory =
Jack Franklin0943df42021-02-26 11:12:13 +000043 isRunningInGen ? pathToOutTargetDir : path.resolve(path.join(process.cwd(), 'out', target));
Jack Franklin8742dc82021-02-26 09:17:59 +000044
Jack Franklin90b66132021-01-05 11:33:43 +000045const devtoolsRootFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, 'gen'));
Jack Franklin1557a1c2020-06-08 15:22:13 +010046
Jack Franklin90b66132021-01-05 11:33:43 +000047if (!fs.existsSync(devtoolsRootFolder)) {
48 console.error(`ERROR: Generated front_end folder (${devtoolsRootFolder}) does not exist.`);
Jack Franklin1557a1c2020-06-08 15:22:13 +010049 console.log(
50 'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
51 console.log('If you build to a target other than default, you need to pass --target=X as an argument');
52 process.exit(1);
53}
54
Jack Franklin086ccd52020-11-27 11:00:14 +000055const server = http.createServer(requestHandler);
56server.listen(serverPort);
57server.once('listening', () => {
58 if (process.send) {
59 process.send(serverPort);
60 }
61 console.log(`Started components server at http://localhost:${serverPort}\n`);
62});
63
64server.once('error', error => {
65 if (process.send) {
66 process.send('ERROR');
67 }
68 throw error;
69});
Jack Franklin1557a1c2020-06-08 15:22:13 +010070
71function createComponentIndexFile(componentPath, componentExamples) {
Jack Franklin7a75e462021-01-08 16:17:13 +000072 const componentName = componentPath.replace('/front_end/component_docs/', '').replace(/_/g, ' ').replace('/', '');
Jack Franklin1557a1c2020-06-08 15:22:13 +010073 // clang-format off
74 return `<!DOCTYPE html>
75 <html>
76 <head>
77 <meta charset="UTF-8" />
78 <meta name="viewport" content="width=device-width" />
79 <title>DevTools component: ${componentName}</title>
80 <style>
81 h1 { text-transform: capitalize; }
82
83 .example {
84 padding: 5px;
85 margin: 10px;
86 }
Jack Franklin7a75e462021-01-08 16:17:13 +000087
88 a:link,
89 a:visited {
90 color: blue;
91 text-decoration: underline;
92 }
93
94 a:hover {
95 text-decoration: none;
96 }
97 .example summary {
98 font-size: 20px;
99 }
100
101 .back-link {
102 padding-left: 5px;
103 font-size: 16px;
104 font-style: italic;
105 }
106
107 iframe { display: block; width: 100%; height: 400px; }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100108 </style>
109 </head>
110 <body>
Jack Franklin7a75e462021-01-08 16:17:13 +0000111 <h1>
112 ${componentName}
113 <a class="back-link" href="/">Back to index</a>
114 </h1>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100115 ${componentExamples.map(example => {
Jack Franklin90b66132021-01-05 11:33:43 +0000116 const fullPath = path.join(componentPath, example);
Jack Franklin7a75e462021-01-08 16:17:13 +0000117 return `<details class="example">
118 <summary><a href="${fullPath}">${example.replace('.html', '').replace(/-|_/g, ' ')}</a></summary>
Jack Franklin1557a1c2020-06-08 15:22:13 +0100119 <iframe src="${fullPath}"></iframe>
Jack Franklin7a75e462021-01-08 16:17:13 +0000120 </details>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100121 }).join('\n')}
122 </body>
123 </html>`;
124 // clang-format on
125}
126
127function createServerIndexFile(componentNames) {
128 // clang-format off
129 return `<!DOCTYPE html>
130 <html>
131 <head>
132 <meta charset="UTF-8" />
133 <meta name="viewport" content="width=device-width" />
134 <title>DevTools components</title>
135 <style>
Jack Franklinb5997162020-11-25 17:28:51 +0000136 a:link, a:visited {
137 color: blue;
138 text-transform: capitalize;
139 text-decoration: none;
140 }
141 a:hover {
142 text-decoration: underline;
143 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100144 </style>
145 </head>
146 <body>
147 <h1>DevTools components</h1>
148 <ul>
149 ${componentNames.map(name => {
Jack Franklinb5997162020-11-25 17:28:51 +0000150 const niceName = name.replace(/_/g, ' ');
Jack Franklin90b66132021-01-05 11:33:43 +0000151 return `<li><a href='/front_end/component_docs/${name}'>${niceName}</a></li>`;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100152 }).join('\n')}
153 </ul>
154 </body>
155 </html>`;
156 // clang-format on
157}
158
159async function getExamplesForPath(filePath) {
Jack Franklin90b66132021-01-05 11:33:43 +0000160 const componentDirectory = path.join(devtoolsRootFolder, filePath);
Jack Franklinc1501222020-10-02 09:42:08 +0100161 const allFiles = await fs.promises.readdir(componentDirectory);
162 const htmlExampleFiles = allFiles.filter(file => {
163 return path.extname(file) === '.html';
164 });
Jack Franklin1557a1c2020-06-08 15:22:13 +0100165
Jack Franklinc1501222020-10-02 09:42:08 +0100166 return createComponentIndexFile(filePath, htmlExampleFiles);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100167}
168
169function respondWithHtml(response, html) {
170 response.setHeader('Content-Type', 'text/html; charset=utf-8');
171 response.writeHead(200);
172 response.write(html, 'utf8');
173 response.end();
174}
175
176function send404(response, message) {
177 response.writeHead(404);
178 response.write(message, 'utf8');
179 response.end();
180}
181
Jack Franklin279564e2020-07-06 15:25:18 +0100182async function checkFileExists(filePath) {
183 try {
184 const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
185 return !errorsAccessingFile;
186 } catch (e) {
187 return false;
188 }
189}
190
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100191/**
192 * In Devtools-Frontend we load images without a leading slash, e.g.
193 * url(Images/checker.png). This works within devtools, but breaks this component
Jack Franklin4738bc72020-09-17 09:51:24 +0100194 * server as the path ends up as /component_docs/my_component/Images/checker.png.
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100195 * So we check if the path ends in Images/*.* and if so, remove anything before
196 * it. Then it will be resolved correctly.
197 */
198function normalizeImagePathIfRequired(filePath) {
199 const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
200 const match = imagePathRegex.exec(filePath);
201 if (!match) {
202 return filePath;
203 }
204
205 const [, imageName, imageExt] = match;
Jack Franklin90b66132021-01-05 11:33:43 +0000206 const normalizedPath = path.join('front_end', 'Images', `${imageName}.${imageExt}`);
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100207 return normalizedPath;
208}
209
Jack Franklin1557a1c2020-06-08 15:22:13 +0100210async function requestHandler(request, response) {
211 const filePath = parseURL(request.url).pathname;
Jack Franklin1557a1c2020-06-08 15:22:13 +0100212 if (filePath === '/favicon.ico') {
213 send404(response, '404, no favicon');
214 return;
215 }
216
217 if (filePath === '/' || filePath === '/index.html') {
Jack Franklin90b66132021-01-05 11:33:43 +0000218 const components = await fs.promises.readdir(path.join(devtoolsRootFolder, 'front_end', 'component_docs'));
Jack Franklinb5997162020-11-25 17:28:51 +0000219 const html = createServerIndexFile(components.filter(filePath => {
Jack Franklin90b66132021-01-05 11:33:43 +0000220 const stats = fs.lstatSync(path.join(devtoolsRootFolder, 'front_end', 'component_docs', filePath));
Jack Franklinb5997162020-11-25 17:28:51 +0000221 // Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
222 return stats.isDirectory();
223 }));
Jack Franklin1557a1c2020-06-08 15:22:13 +0100224 respondWithHtml(response, html);
Jack Franklin90b66132021-01-05 11:33:43 +0000225 } else if (filePath.startsWith('/front_end/component_docs') && path.extname(filePath) === '') {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100226 // This means it's a component path like /breadcrumbs.
227 const componentHtml = await getExamplesForPath(filePath);
228 respondWithHtml(response, componentHtml);
Jack Franklin36429002020-11-25 15:07:26 +0000229 return;
230 } else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
231 /** This conditional checks if we are viewing an individual example's HTML
Jack Franklin90b66132021-01-05 11:33:43 +0000232 * file. e.g. localhost:8090/front_end/component_docs/data_grid/basic.html For each
Jack Franklin36429002020-11-25 15:07:26 +0000233 * example we inject themeColors.css into the page so all CSS variables
234 * that components use are available.
235 */
Jack Franklin8742dc82021-02-26 09:17:59 +0000236
237 // server --base=.
238
239 // server --base=devtools-frontend
240
Jack Franklin90b66132021-01-05 11:33:43 +0000241 const fileContents = await fs.promises.readFile(path.join(devtoolsRootFolder, filePath), {encoding: 'utf8'});
Jack Franklin8742dc82021-02-26 09:17:59 +0000242 const themeColoursLink = `<link rel="stylesheet" href="${
243 path.join(sharedResourcesBase, 'front_end', 'ui', 'themeColors.css')}" type="text/css" />`;
244 const inspectorStyleLink = `<link rel="stylesheet" href="${
245 path.join(sharedResourcesBase, 'front_end', 'ui', 'inspectorStyle.css')}" type="text/css" />`;
246 const toggleDarkModeScript = `<script type="module" src="${
247 path.join(sharedResourcesBase, 'front_end', 'component_docs', 'component_docs.js')}"></script>`;
Jack Frankline6dcd242021-01-18 16:09:22 +0000248 const newFileContents = fileContents.replace('<style>', `${themeColoursLink}\n${inspectorStyleLink}\n<style>`)
Jack Franklin36429002020-11-25 15:07:26 +0000249 .replace('<script', toggleDarkModeScript + '\n<script');
250 respondWithHtml(response, newFileContents);
251
Jack Franklin1557a1c2020-06-08 15:22:13 +0100252 } else {
Jack Franklin9d4ecf72020-09-16 14:04:30 +0100253 // This means it's an asset like a JS file or an image.
254 const normalizedPath = normalizeImagePathIfRequired(filePath);
Jack Franklin1557a1c2020-06-08 15:22:13 +0100255
Jack Franklin90b66132021-01-05 11:33:43 +0000256 let fullPath = path.join(devtoolsRootFolder, normalizedPath);
Jack Franklind0345122020-12-21 09:12:04 +0000257 if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
258 // Rewrite this path so we can load up the locale in the component-docs
Jack Franklin90b66132021-01-05 11:33:43 +0000259 fullPath = path.join(devtoolsRootFolder, 'front_end', 'i18n', 'locales', 'en-US.json');
Jack Franklind0345122020-12-21 09:12:04 +0000260 }
261
Jack Franklin90b66132021-01-05 11:33:43 +0000262 if (!fullPath.startsWith(devtoolsRootFolder) && !fileIsInTestFolder) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100263 console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
264 process.exit(1);
265 }
Jack Franklin1557a1c2020-06-08 15:22:13 +0100266
Jack Franklin279564e2020-07-06 15:25:18 +0100267 const fileExists = await checkFileExists(fullPath);
268
269 if (!fileExists) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100270 send404(response, '404, File not found');
271 return;
272 }
273
274 let encoding = 'utf8';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100275 if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg') ||
276 fullPath.endsWith('.avif')) {
Jack Franklin1557a1c2020-06-08 15:22:13 +0100277 encoding = 'binary';
278 }
279
280 const fileContents = await fs.promises.readFile(fullPath, encoding);
281
282 encoding = 'utf8';
283 if (fullPath.endsWith('.js')) {
284 response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
285 } else if (fullPath.endsWith('.css')) {
286 response.setHeader('Content-Type', 'text/css; charset=utf-8');
287 } else if (fullPath.endsWith('.wasm')) {
288 response.setHeader('Content-Type', 'application/wasm');
289 encoding = 'binary';
290 } else if (fullPath.endsWith('.svg')) {
291 response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
292 } else if (fullPath.endsWith('.png')) {
293 response.setHeader('Content-Type', 'image/png');
294 encoding = 'binary';
295 } else if (fullPath.endsWith('.jpg')) {
296 response.setHeader('Content-Type', 'image/jpg');
297 encoding = 'binary';
Mathias Bynensc38abd42020-12-24 08:20:05 +0100298 } else if (fullPath.endsWith('.avif')) {
299 response.setHeader('Content-Type', 'image/avif');
300 encoding = 'binary';
Jack Franklin1557a1c2020-06-08 15:22:13 +0100301 }
302
303 response.writeHead(200);
304 response.write(fileContents, encoding);
305 response.end();
306 }
307}