Start an attempt at documenting V8's internals, from high-level concepts to low-level implementation details. These docs are intended to be readable to both humans and AI agents, with the hope that documentation like this can help both humans and AIs maintain a concept of the overall design and design constraints when making changes. It merges in the existing documentation we had along with a bunch of AI-assisted extra docs. Includes a web viewer that renders the markdown and mermaid diagrams. The viewer supports commenting, where comments are saved in docs/viewer/comments.json, and can be manually or agentically resolved. #ai-assisted Bug: 500280354 Change-Id: Ie9ffc2f2f85de1496b8266027ca8b5b455acdb63 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7761899 Auto-Submit: Leszek Swirski <leszeks@chromium.org> Commit-Queue: Leszek Swirski <leszeks@chromium.org> Reviewed-by: Toon Verwaest <verwaest@chromium.org> Cr-Commit-Position: refs/heads/main@{#106585}
188 lines
5.4 KiB
JavaScript
188 lines
5.4 KiB
JavaScript
// Copyright 2026 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import express from 'express';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { marked } from 'marked';
|
|
import { Graphviz } from '@hpcc-js/wasm-graphviz';
|
|
|
|
const app = express();
|
|
const PORT = 8081;
|
|
|
|
app.use(express.json());
|
|
|
|
// Resolve paths
|
|
const __dirname = path.resolve(); // Cwd is docs/viewer
|
|
const docsDir = path.resolve(__dirname, '..');
|
|
const distDir = path.resolve(__dirname, 'dist');
|
|
|
|
console.log('Loading Graphviz WASM...');
|
|
const g = await Graphviz.load();
|
|
|
|
function resolvePath(currentFile, relativePath) {
|
|
if (relativePath.startsWith('/')) return relativePath.slice(1);
|
|
|
|
const parts = currentFile.split('/');
|
|
parts.pop(); // Remove filename
|
|
|
|
const relParts = relativePath.split('/');
|
|
for (const part of relParts) {
|
|
if (part === '.') continue;
|
|
if (part === '..') parts.pop();
|
|
else parts.push(part);
|
|
}
|
|
return parts.join('/');
|
|
}
|
|
|
|
// Helper to render markdown to HTML with Graphviz
|
|
async function renderMarkdown(filePath, relPath) {
|
|
let content = fs.readFileSync(filePath, 'utf-8');
|
|
|
|
// Handle Graphviz blocks
|
|
const codeBlockRegex = /```(?:graphviz|dot)\n([\s\S]*?)\n```/g;
|
|
let match;
|
|
const replacements = [];
|
|
|
|
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
let dot = match[1];
|
|
const firstBrace = dot.indexOf('{');
|
|
if (firstBrace !== -1) {
|
|
dot = dot.slice(0, firstBrace + 1) +
|
|
'\n bgcolor="transparent";' +
|
|
'\n graph [fontname="Courier"];' +
|
|
'\n node [fontname="Courier"];' +
|
|
'\n edge [fontname="Courier"];' +
|
|
dot.slice(firstBrace + 1);
|
|
}
|
|
|
|
try {
|
|
const svg = g.layout(dot, "svg", "dot");
|
|
const modifiedSvg = svg.replaceAll('font-family="Courier,monospace"', 'font-family="JetBrains Mono"');
|
|
replacements.push({
|
|
original: match[0],
|
|
replacement: `<div class="graphviz">${modifiedSvg}</div>`
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to render graphviz:`, e);
|
|
replacements.push({
|
|
original: match[0],
|
|
replacement: `<div class="graphviz"><pre>${e.message}</pre></div>`
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const r of replacements) {
|
|
content = content.replace(r.original, r.replacement);
|
|
}
|
|
|
|
let html = await marked.parse(content);
|
|
|
|
// Rewrite links
|
|
html = html.replaceAll(/href="([^"]+)"/g, (match, href) => {
|
|
if (href.startsWith('http')) return match; // Skip external links
|
|
|
|
const resolvedPath = resolvePath(relPath, href);
|
|
|
|
if (href.startsWith('src/') || href.startsWith('include/') ||
|
|
resolvedPath.startsWith('src/') || resolvedPath.startsWith('include/')) {
|
|
|
|
const finalPath = href.startsWith('src/') || href.startsWith('include/') ? href : resolvedPath;
|
|
return `href="https://source.chromium.org/chromium/chromium/src/+/main:v8/${finalPath}" target="_blank"`;
|
|
}
|
|
|
|
if (href.endsWith('.md')) {
|
|
return `href="${href.replace(/\.md$/, '.html')}"`;
|
|
}
|
|
|
|
return match;
|
|
});
|
|
|
|
// Convert inline code paths to links
|
|
html = html.replaceAll(/<code>((?:src|include)\/[^<]+)<\/code>/g, '<a href="https://source.chromium.org/chromium/chromium/src/+/main:v8/$1" target="_blank"><code>$1</code></a>');
|
|
|
|
return html;
|
|
}
|
|
|
|
// Serve static files for the viewer itself
|
|
app.use('/viewer', express.static(__dirname));
|
|
|
|
// Handle requests for processed HTML files
|
|
app.get('*', async (req, res, next) => {
|
|
const relPath = req.path.slice(1); // Remove leading slash
|
|
|
|
if (!relPath.endsWith('.html')) {
|
|
return next();
|
|
}
|
|
|
|
const htmlPath = path.join(distDir, relPath);
|
|
const mdPath = path.join(docsDir, relPath.replace(/\.html$/, '.md'));
|
|
|
|
try {
|
|
if (fs.existsSync(htmlPath)) {
|
|
res.sendFile(htmlPath);
|
|
} else if (fs.existsSync(mdPath)) {
|
|
console.log(`Generating on the fly: ${relPath}`);
|
|
const html = await renderMarkdown(mdPath, relPath);
|
|
res.send(html);
|
|
} else {
|
|
res.status(404).send('File not found');
|
|
}
|
|
} catch (error) {
|
|
res.status(500).send(`Error: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
// Handle comments API
|
|
const commentsFile = path.resolve(__dirname, 'comments.json');
|
|
|
|
function readComments() {
|
|
if (fs.existsSync(commentsFile)) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(commentsFile, 'utf-8'));
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
app.get('/api/comments', (req, res) => {
|
|
const file = req.query.file;
|
|
const comments = readComments();
|
|
res.json(comments[file] || {});
|
|
});
|
|
|
|
app.post('/api/comments', (req, res) => {
|
|
const { file, blockIndex, text } = req.body;
|
|
|
|
if (!file || blockIndex === undefined || !text) {
|
|
return res.status(400).send('Bad Request: Missing fields');
|
|
}
|
|
|
|
const comments = readComments();
|
|
|
|
if (!comments[file]) comments[file] = {};
|
|
const blockIndexStr = String(blockIndex);
|
|
if (!comments[file][blockIndexStr]) comments[file][blockIndexStr] = [];
|
|
|
|
comments[file][blockIndexStr].push({
|
|
text,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
fs.writeFileSync(commentsFile, JSON.stringify(comments, null, 2));
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// Default route serves index.html
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'index.html'));
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running at http://localhost:${PORT}/`);
|
|
console.log(`Viewer available at http://localhost:${PORT}/viewer/`);
|
|
});
|