mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 09:47:56 +08:00
Now the component docs have some TS that gets compiled, the docs server was listing out tsconfig and build files when it was supposed to only list component directories. This fixes that and also tidies up the list of links by removing underscores - a quick drive by fix to make it nicer to look at! Change-Id: Id9f05894a235f376cfd8a7ea98c5bf179c006869 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2560945 Auto-Submit: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org>
223 lines
7.7 KiB
JavaScript
223 lines
7.7 KiB
JavaScript
// Copyright (c) 2020 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
const fs = require('fs');
|
|
const http = require('http');
|
|
const path = require('path');
|
|
const parseURL = require('url').parse;
|
|
const {argv} = require('yargs');
|
|
|
|
const serverPort = parseInt(process.env.PORT, 10) || 8090;
|
|
|
|
const target = argv.target || 'Default';
|
|
const devtoolsFrontendFolder = path.resolve(path.join(__dirname, '..', '..', 'out', target, 'gen', 'front_end'));
|
|
|
|
if (!fs.existsSync(devtoolsFrontendFolder)) {
|
|
console.error(`ERROR: Generated front_end folder (${devtoolsFrontendFolder}) does not exist.`);
|
|
console.log(
|
|
'The components server works from the built Ninja output; you may need to run Ninja to update your built DevTools.');
|
|
console.log('If you build to a target other than default, you need to pass --target=X as an argument');
|
|
process.exit(1);
|
|
}
|
|
|
|
http.createServer(requestHandler).listen(serverPort);
|
|
console.log(`Started components server at http://localhost:${serverPort}\n`);
|
|
|
|
function createComponentIndexFile(componentPath, componentExamples) {
|
|
const componentName = componentPath.replace('/', '').replace(/_/g, ' ');
|
|
// clang-format off
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width" />
|
|
<title>DevTools component: ${componentName}</title>
|
|
<style>
|
|
h1 { text-transform: capitalize; }
|
|
|
|
.example {
|
|
padding: 5px;
|
|
margin: 10px;
|
|
}
|
|
iframe { display: block; width: 100%; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>${componentName}</h1>
|
|
${componentExamples.map(example => {
|
|
const fullPath = path.join('component_docs', componentPath, example);
|
|
return `<div class="example">
|
|
<h3><a href="${fullPath}">${example}</a></h3>
|
|
<iframe src="${fullPath}"></iframe>
|
|
</div>`;
|
|
}).join('\n')}
|
|
</body>
|
|
</html>`;
|
|
// clang-format on
|
|
}
|
|
|
|
function createServerIndexFile(componentNames) {
|
|
// clang-format off
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width" />
|
|
<title>DevTools components</title>
|
|
<style>
|
|
a:link, a:visited {
|
|
color: blue;
|
|
text-transform: capitalize;
|
|
text-decoration: none;
|
|
}
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>DevTools components</h1>
|
|
<ul>
|
|
${componentNames.map(name => {
|
|
const niceName = name.replace(/_/g, ' ');
|
|
return `<li><a href='/${name}'>${niceName}</a></li>`;
|
|
}).join('\n')}
|
|
</ul>
|
|
</body>
|
|
</html>`;
|
|
// clang-format on
|
|
}
|
|
|
|
async function getExamplesForPath(filePath) {
|
|
const componentDirectory = path.join(devtoolsFrontendFolder, 'component_docs', filePath);
|
|
const allFiles = await fs.promises.readdir(componentDirectory);
|
|
const htmlExampleFiles = allFiles.filter(file => {
|
|
return path.extname(file) === '.html';
|
|
});
|
|
|
|
return createComponentIndexFile(filePath, htmlExampleFiles);
|
|
}
|
|
|
|
function respondWithHtml(response, html) {
|
|
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
response.writeHead(200);
|
|
response.write(html, 'utf8');
|
|
response.end();
|
|
}
|
|
|
|
function send404(response, message) {
|
|
response.writeHead(404);
|
|
response.write(message, 'utf8');
|
|
response.end();
|
|
}
|
|
|
|
async function checkFileExists(filePath) {
|
|
try {
|
|
const errorsAccessingFile = await fs.promises.access(filePath, fs.constants.R_OK);
|
|
return !errorsAccessingFile;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* In Devtools-Frontend we load images without a leading slash, e.g.
|
|
* url(Images/checker.png). This works within devtools, but breaks this component
|
|
* server as the path ends up as /component_docs/my_component/Images/checker.png.
|
|
* So we check if the path ends in Images/*.* and if so, remove anything before
|
|
* it. Then it will be resolved correctly.
|
|
*/
|
|
function normalizeImagePathIfRequired(filePath) {
|
|
const imagePathRegex = /\/Images\/(\S+)\.(\w{3})/;
|
|
const match = imagePathRegex.exec(filePath);
|
|
if (!match) {
|
|
return filePath;
|
|
}
|
|
|
|
const [, imageName, imageExt] = match;
|
|
const normalizedPath = path.join('Images', `${imageName}.${imageExt}`);
|
|
return normalizedPath;
|
|
}
|
|
|
|
async function requestHandler(request, response) {
|
|
const filePath = parseURL(request.url).pathname;
|
|
if (filePath === '/favicon.ico') {
|
|
send404(response, '404, no favicon');
|
|
return;
|
|
}
|
|
|
|
if (filePath === '/' || filePath === '/index.html') {
|
|
const components = await fs.promises.readdir(path.join(devtoolsFrontendFolder, 'component_docs'));
|
|
const html = createServerIndexFile(components.filter(filePath => {
|
|
const stats = fs.lstatSync(path.join(devtoolsFrontendFolder, 'component_docs', filePath));
|
|
// Filter out some build config files (tsconfig, d.ts, etc), and just list the directories.
|
|
return stats.isDirectory();
|
|
}));
|
|
respondWithHtml(response, html);
|
|
} else if (path.extname(filePath) === '') {
|
|
// This means it's a component path like /breadcrumbs.
|
|
const componentHtml = await getExamplesForPath(filePath);
|
|
respondWithHtml(response, componentHtml);
|
|
return;
|
|
} else if (/component_docs\/(.+)\/(.+)\.html/.test(filePath)) {
|
|
/** This conditional checks if we are viewing an individual example's HTML
|
|
* file. e.g. localhost:8090/component_docs/data_grid/basic.html For each
|
|
* example we inject themeColors.css into the page so all CSS variables
|
|
* that components use are available.
|
|
*/
|
|
const fileContents = await fs.promises.readFile(path.join(devtoolsFrontendFolder, filePath), {encoding: 'utf8'});
|
|
const themeColoursLink = '<link rel="stylesheet" href="/ui/themeColors.css" type="text/css" />';
|
|
const toggleDarkModeScript = '<script type="module" src="/component_docs/component_docs.js"></script>';
|
|
const newFileContents = fileContents.replace('<style>', themeColoursLink + '\n<style>')
|
|
.replace('<script', toggleDarkModeScript + '\n<script');
|
|
respondWithHtml(response, newFileContents);
|
|
|
|
} else {
|
|
// This means it's an asset like a JS file or an image.
|
|
const normalizedPath = normalizeImagePathIfRequired(filePath);
|
|
const fullPath = path.join(devtoolsFrontendFolder, normalizedPath);
|
|
|
|
if (!fullPath.startsWith(devtoolsFrontendFolder)) {
|
|
console.error(`Path ${fullPath} is outside the DevTools Frontend root dir.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const fileExists = await checkFileExists(fullPath);
|
|
|
|
if (!fileExists) {
|
|
send404(response, '404, File not found');
|
|
return;
|
|
}
|
|
|
|
let encoding = 'utf8';
|
|
if (fullPath.endsWith('.wasm') || fullPath.endsWith('.png') || fullPath.endsWith('.jpg')) {
|
|
encoding = 'binary';
|
|
}
|
|
|
|
const fileContents = await fs.promises.readFile(fullPath, encoding);
|
|
|
|
encoding = 'utf8';
|
|
if (fullPath.endsWith('.js')) {
|
|
response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
|
|
} else if (fullPath.endsWith('.css')) {
|
|
response.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
} else if (fullPath.endsWith('.wasm')) {
|
|
response.setHeader('Content-Type', 'application/wasm');
|
|
encoding = 'binary';
|
|
} else if (fullPath.endsWith('.svg')) {
|
|
response.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
|
} else if (fullPath.endsWith('.png')) {
|
|
response.setHeader('Content-Type', 'image/png');
|
|
encoding = 'binary';
|
|
} else if (fullPath.endsWith('.jpg')) {
|
|
response.setHeader('Content-Type', 'image/jpg');
|
|
encoding = 'binary';
|
|
}
|
|
|
|
response.writeHead(200);
|
|
response.write(fileContents, encoding);
|
|
response.end();
|
|
}
|
|
}
|