fresh start
This commit is contained in:
parent
62c3e2d368
commit
7f86647175
570 changed files with 4895 additions and 866 deletions
141
tools/build.js
Executable file
141
tools/build.js
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import yaml from 'js-yaml';
|
||||
import md from 'markdown-it';
|
||||
import sanitizer from 'markdown-it-sanitizer';
|
||||
import crypto from 'crypto';
|
||||
import nacl from 'tweetnacl';
|
||||
import { validate, detectCycles } from './foldlint.js';
|
||||
|
||||
const mdParser = md().use(sanitizer);
|
||||
|
||||
// Collect files async, deterministic sort
|
||||
async function collectFiles(dir) {
|
||||
let files = [];
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files = [...files, ...(await collectFiles(fullPath))];
|
||||
} else if (entry.name.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
// Generate HTML
|
||||
function generateHTML(meta, bodyHtml, exits) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${meta.title}</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<script src="/witness.js"></script>
|
||||
<meta name="description" content="${meta.summary}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="room">
|
||||
<h1>${meta.title}</h1>
|
||||
${bodyHtml}
|
||||
<div class="exits">
|
||||
${exits.map(exit => `<a href="/${exit.to.replace(':', '_').split('@')[0]}.html">${exit.label}</a>`).join('')}
|
||||
</div>
|
||||
<div id="chat"></div>
|
||||
<input id="msg"><button id="send">Send</button>
|
||||
</div>
|
||||
<script>witness.connect('${meta.id}');</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// Canonical hash (exclude id from frontmatter)
|
||||
function canonicalHash(meta, body) {
|
||||
const metaClone = { ...meta };
|
||||
delete metaClone.id; // Exclude id to avoid circularity
|
||||
const frontYaml = yaml.dump(metaClone, { noRefs: true }).trim();
|
||||
const content = frontYaml + '\n' + body.trim();
|
||||
const lines = content.split('\n');
|
||||
const trimmedLines = lines.map(line => line.replace(/\s+$/, ''));
|
||||
const normalized = trimmedLines.join('\n');
|
||||
return crypto.createHash('sha256').update(normalized).digest('hex');
|
||||
}
|
||||
|
||||
// Build
|
||||
async function build() {
|
||||
const atlasDir = path.join(process.cwd(), 'atlas');
|
||||
const distDir = path.join(process.cwd(), 'dist');
|
||||
await fs.mkdir(distDir, { recursive: true });
|
||||
|
||||
const files = await collectFiles(atlasDir);
|
||||
|
||||
// Validate all
|
||||
for (const file of files) {
|
||||
await validate(file);
|
||||
console.log(`Validated: ${file}`);
|
||||
}
|
||||
|
||||
// Build graph
|
||||
const graph = {};
|
||||
const idToFile = {};
|
||||
const slugToId = {};
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file, 'utf8');
|
||||
const { data: meta } = matter(content);
|
||||
graph[meta.id] = meta.exits ? meta.exits.map(e => e.to) : [];
|
||||
idToFile[meta.id] = file;
|
||||
slugToId[meta.id.split('@')[0]] = meta.id; // Map kind:slug to full id
|
||||
}
|
||||
|
||||
// Check broken links and cycles
|
||||
Object.keys(graph).forEach(id => {
|
||||
graph[id].forEach(to => {
|
||||
let resolvedTo = to;
|
||||
if (!graph[to] && slugToId[to]) {
|
||||
resolvedTo = slugToId[to]; // Resolve kind:slug to full id
|
||||
}
|
||||
if (!graph[resolvedTo]) {
|
||||
console.error(`Available IDs: ${Object.keys(graph).join(', ')}`);
|
||||
throw new Error(`Broken link: ${to} from ${id}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
detectCycles(graph); // Throws if cycle
|
||||
|
||||
// Generate HTML
|
||||
const buildManifest = { files: [] };
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file, 'utf8');
|
||||
const { data: meta, content: body } = matter(content);
|
||||
|
||||
// Hash verify
|
||||
const computed = canonicalHash(meta, body);
|
||||
const idHash = meta.id.split('@sha256:')[1];
|
||||
if (computed !== idHash) throw new Error(`Hash mismatch in ${file}: computed=${computed}, expected=${idHash}`);
|
||||
|
||||
const bodyHtml = mdParser.render(body);
|
||||
const html = generateHTML(meta, bodyHtml, meta.exits || []);
|
||||
|
||||
const slug = meta.id.split(':')[1].split('@')[0];
|
||||
const outPath = path.join(distDir, `${slug}.html`);
|
||||
await fs.writeFile(outPath, html);
|
||||
buildManifest.files.push({ path: outPath, hash: crypto.createHash('sha256').update(html).digest('hex') });
|
||||
}
|
||||
|
||||
// Copy public
|
||||
await fs.cp(path.join(process.cwd(), 'public'), distDir, { recursive: true });
|
||||
|
||||
// Sitemap stub
|
||||
await fs.writeFile(path.join(distDir, 'sitemap.xml'), '<xml>Stub</xml>');
|
||||
|
||||
// Write build manifest
|
||||
await fs.writeFile(path.join(distDir, 'manifest.json'), JSON.stringify(buildManifest));
|
||||
|
||||
console.log('Build complete.');
|
||||
}
|
||||
|
||||
build().catch(console.error);
|
||||
Loading…
Add table
Add a link
Reference in a new issue