702871a98c
Serve missing *.thumbnail.png requests when the SVG sidecar exists, and schedule async PNG rasterization after public HTML is materialized so WeChat og:image URLs stop 404ing. Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.7 KiB
JavaScript
138 lines
4.7 KiB
JavaScript
import fs from 'node:fs';
|
|
import fsPromises from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { ensureThumbnailPng } from './mindspace-thumbnail-png.mjs';
|
|
import { ensureHtmlThumbnail } from './mindspace-thumbnails.mjs';
|
|
|
|
export function workspaceThumbnailRelativePath(htmlRelativePath) {
|
|
const normalized = String(htmlRelativePath ?? '').replace(/^\/+/, '');
|
|
const dir = path.posix.dirname(normalized);
|
|
const base = path.basename(normalized, path.extname(normalized));
|
|
const thumb = `${base}.thumbnail.svg`;
|
|
return dir === '.' ? thumb : path.posix.join(dir, thumb);
|
|
}
|
|
|
|
export function workspaceThumbnailAbsolutePath(publishDir, htmlRelativePath) {
|
|
return path.join(publishDir, workspaceThumbnailRelativePath(htmlRelativePath));
|
|
}
|
|
|
|
function scheduleWorkspaceThumbnailPng(publishDir, htmlRelativePath) {
|
|
queueMicrotask(() => {
|
|
void Promise.resolve()
|
|
.then(() => {
|
|
const svgPath = workspaceThumbnailAbsolutePath(publishDir, htmlRelativePath);
|
|
if (fs.existsSync(svgPath)) ensureThumbnailPng(svgPath);
|
|
})
|
|
.catch(() => {});
|
|
});
|
|
}
|
|
|
|
/** Fire-and-forget SVG + PNG sidecars after public HTML is written (must not block Finish/chat). */
|
|
export function scheduleWorkspaceHtmlThumbnailSidecars(publishDir, htmlRelativePath) {
|
|
queueMicrotask(() => {
|
|
void ensureWorkspaceHtmlThumbnail(publishDir, htmlRelativePath).catch(() => {});
|
|
});
|
|
}
|
|
|
|
export async function ensureWorkspaceHtmlThumbnail(publishDir, htmlRelativePath, html, meta = {}) {
|
|
const htmlPath = path.join(publishDir, htmlRelativePath);
|
|
const content = html ?? (await fsPromises.readFile(htmlPath, 'utf8'));
|
|
const thumbRel = workspaceThumbnailRelativePath(htmlRelativePath);
|
|
const svg = await ensureHtmlThumbnail(publishDir, thumbRel, content, {
|
|
...meta,
|
|
contentBaseDir: path.dirname(htmlPath),
|
|
});
|
|
scheduleWorkspaceThumbnailPng(publishDir, htmlRelativePath);
|
|
return svg;
|
|
}
|
|
|
|
export async function readWorkspaceThumbnailIfExists(publishDir, htmlRelativePath) {
|
|
try {
|
|
return await fsPromises.readFile(workspaceThumbnailAbsolutePath(publishDir, htmlRelativePath), 'utf8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function scanPublishTree(publishRoot) {
|
|
if (!fs.existsSync(publishRoot)) return;
|
|
const entries = await fsPromises.readdir(publishRoot, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory() || entry.name === 'wiki' || entry.name.startsWith('.')) continue;
|
|
const userDir = path.join(publishRoot, entry.name);
|
|
await scanUserHtmlFiles(userDir);
|
|
}
|
|
}
|
|
|
|
async function scanUserHtmlFiles(userDir) {
|
|
const walk = async (dir) => {
|
|
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === '.agents' || entry.name === 'node_modules') continue;
|
|
await walk(full);
|
|
continue;
|
|
}
|
|
if (!entry.name.endsWith('.html') || entry.name.endsWith('.thumbnail.svg')) continue;
|
|
const rel = path.relative(userDir, full);
|
|
await ensureWorkspaceHtmlThumbnail(userDir, rel).catch(() => {});
|
|
}
|
|
};
|
|
await walk(userDir);
|
|
}
|
|
|
|
export function startWorkspaceThumbnailWatcher(publishRoot) {
|
|
if (!fs.existsSync(publishRoot)) {
|
|
fs.mkdirSync(publishRoot, { recursive: true });
|
|
}
|
|
|
|
void scanPublishTree(publishRoot);
|
|
|
|
const pending = new Map();
|
|
const schedule = (userDir, relativePath) => {
|
|
const key = path.join(userDir, relativePath);
|
|
const existing = pending.get(key);
|
|
if (existing) clearTimeout(existing);
|
|
pending.set(
|
|
key,
|
|
setTimeout(() => {
|
|
pending.delete(key);
|
|
void ensureWorkspaceHtmlThumbnail(userDir, relativePath).catch(() => {});
|
|
}, 400),
|
|
);
|
|
};
|
|
|
|
const attachUserWatcher = (userDir) => {
|
|
if (!fs.existsSync(userDir)) return;
|
|
void scanUserHtmlFiles(userDir);
|
|
try {
|
|
fs.watch(userDir, { recursive: true }, (_event, filename) => {
|
|
if (!filename || !String(filename).endsWith('.html')) return;
|
|
if (String(filename).endsWith('.thumbnail.svg')) return;
|
|
schedule(userDir, filename);
|
|
});
|
|
} catch {
|
|
// Some platforms disallow recursive watch; startup scan still runs.
|
|
}
|
|
};
|
|
|
|
for (const entry of fs.readdirSync(publishRoot, { withFileTypes: true })) {
|
|
if (entry.isDirectory() && entry.name !== 'wiki' && !entry.name.startsWith('.')) {
|
|
attachUserWatcher(path.join(publishRoot, entry.name));
|
|
}
|
|
}
|
|
|
|
try {
|
|
fs.watch(publishRoot, (_event, filename) => {
|
|
if (!filename) return;
|
|
const userDir = path.join(publishRoot, filename);
|
|
if (fs.existsSync(userDir) && fs.statSync(userDir).isDirectory()) {
|
|
attachUserWatcher(userDir);
|
|
}
|
|
});
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|