mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-09-30 17:27:22 +08:00
This reverts commit 6bdb3622bd.
Reason for revert: Interaction tests started failing on mac.
https://ci.chromium.org/ui/p/devtools-frontend/builders/ci/Stand-alone%20Mac/2514/overview
Original change's description:
> Add header context menu to data grid
>
> This CL adds a default header context menu to the data grid that lists visible
> and sortable columns much like the legacy data grid.
>
> Bug: 1150793
> Change-Id: I84a8a9cc511d6be9fee1f89ba41a6e8649df54ec
> Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2600723
> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
> Reviewed-by: Paul Lewis <aerotwist@chromium.org>
TBR=aerotwist@chromium.org,jacktfranklin@chromium.org
Change-Id: Ic8ef16044716d922d3a37b6a8fb32e13a75dee72
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 1150793
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2601962
Reviewed-by: Jan Scheffler <janscheffler@chromium.org>
Commit-Queue: Jan Scheffler <janscheffler@chromium.org>
284 lines
10 KiB
JavaScript
284 lines
10 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';
|
|
|
|
/**
|
|
* When you run npm run components-server we run the script as is from scripts/,
|
|
* but when this server is run as part of a test suite it's run from
|
|
* out/Default/gen/scripts, so we have to do a bit of path mangling to figure
|
|
* out where we are.
|
|
*/
|
|
const isRunningInGen = __dirname.includes(path.join(path.sep, 'gen', path.sep, 'scripts'));
|
|
|
|
const pathToBuiltOutTargetDirectory = isRunningInGen ? path.resolve(path.join(__dirname), '..', '..', '..') :
|
|
path.resolve(path.join(__dirname, '..', '..', 'out', target));
|
|
const devtoolsFrontendFolder = path.resolve(path.join(pathToBuiltOutTargetDirectory, '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);
|
|
}
|
|
|
|
const server = http.createServer(requestHandler);
|
|
server.listen(serverPort);
|
|
server.once('listening', () => {
|
|
if (process.send) {
|
|
process.send(serverPort);
|
|
}
|
|
console.log(`Started components server at http://localhost:${serverPort}\n`);
|
|
});
|
|
|
|
server.once('error', error => {
|
|
if (process.send) {
|
|
process.send('ERROR');
|
|
}
|
|
throw error;
|
|
});
|
|
|
|
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 {
|
|
if (filePath.startsWith('/front_end')) {
|
|
/**
|
|
* We load files from the test directory whose paths will often start with
|
|
* /front_end if they load in frontend code. However we also get requests
|
|
* from within the front_end directory which do not start with /front_end.
|
|
* This means we might get two requests for the same file:
|
|
*
|
|
* 1) /front_end/ui/ui.js
|
|
* 2) /ui/ui.js
|
|
*
|
|
* If we serve them both it will mean we load ui/ui.js twice. So instead
|
|
* we redirect permanently to the non-/front_end prefixed URL so that the
|
|
* browser only loads each module once.
|
|
*/
|
|
response.writeHead(301, {
|
|
Location: filePath.replace('/front_end', ''),
|
|
});
|
|
response.end();
|
|
return;
|
|
}
|
|
// This means it's an asset like a JS file or an image.
|
|
const normalizedPath = normalizeImagePathIfRequired(filePath);
|
|
|
|
let fullPath = path.join(devtoolsFrontendFolder, normalizedPath);
|
|
if (fullPath.endsWith(path.join('locales', 'en-US.json'))) {
|
|
// Rewrite this path so we can load up the locale in the component-docs
|
|
fullPath = path.join(devtoolsFrontendFolder, 'i18n', 'locales', 'en-US.json');
|
|
}
|
|
/**
|
|
* Component docs can also load files from the test directory, so we ensure
|
|
* that we allow that here, and also then deal with the relative imports
|
|
* that come from the test directory, which will contain the front_end
|
|
* folder already. In which case we take the devToolsFrontendFolder, and go
|
|
* up one directory with '..' to make sure we import the right file from the
|
|
* right place.
|
|
*/
|
|
const fileIsInTestFolder = normalizedPath.startsWith('/test/');
|
|
if (fileIsInTestFolder) {
|
|
fullPath = path.join(devtoolsFrontendFolder, '..', normalizedPath);
|
|
}
|
|
|
|
if (!fullPath.startsWith(devtoolsFrontendFolder) && !fileIsInTestFolder) {
|
|
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();
|
|
}
|
|
}
|