b273bb7aa2
Add a MindSpace public HTML checker for missing companion docx/PDF paths, wire it into runtime packaging and release-portal-runtime-prod.sh, and document the workflow for agents and ops. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
4.5 KiB
JavaScript
152 lines
4.5 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const SKIP_REFERENCE = /^(?:https?:|mailto:|javascript:|data:|tel:|#|\/\/)/i;
|
|
const DOWNLOAD_EXTENSIONS = /\.(?:docx|doc|pdf|xlsx|xls|pptx|ppt|zip|rar|7z|csv)$/i;
|
|
|
|
export function isFilesystemPublicReference(raw) {
|
|
const value = String(raw ?? '').trim();
|
|
if (!value) return false;
|
|
if (SKIP_REFERENCE.test(value)) return false;
|
|
// /api/... and /MindSpace/... are server routes, not on-disk siblings of public HTML.
|
|
if (value.startsWith('/')) return false;
|
|
return true;
|
|
}
|
|
|
|
/** @deprecated use isFilesystemPublicReference */
|
|
export function isRelativePublicReference(raw) {
|
|
return isFilesystemPublicReference(raw);
|
|
}
|
|
|
|
export function isDownloadLikeReference(raw, source = '') {
|
|
if (!isFilesystemPublicReference(raw)) return false;
|
|
const ref = normalizeReferencePath(raw);
|
|
if (DOWNLOAD_EXTENSIONS.test(ref)) return true;
|
|
return /\bdownload\b/i.test(String(source ?? ''));
|
|
}
|
|
|
|
export function normalizeReferencePath(raw) {
|
|
const trimmed = String(raw ?? '')
|
|
.trim()
|
|
.split('#')[0]
|
|
.split('?')[0]
|
|
.trim();
|
|
if (!trimmed) return '';
|
|
try {
|
|
return decodeURIComponent(trimmed);
|
|
} catch {
|
|
return trimmed;
|
|
}
|
|
}
|
|
|
|
export function resolveReferencePath(htmlDir, raw) {
|
|
const ref = normalizeReferencePath(raw);
|
|
if (!ref) return null;
|
|
return path.resolve(htmlDir, ref);
|
|
}
|
|
|
|
function parseMindspaceCoverPaths(contentAttr) {
|
|
const paths = [];
|
|
if (!contentAttr) return paths;
|
|
let parsed = null;
|
|
try {
|
|
parsed = JSON.parse(contentAttr);
|
|
} catch {
|
|
for (const key of ['cover', 'image']) {
|
|
const match = contentAttr.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`));
|
|
if (match?.[1]) paths.push(match[1]);
|
|
}
|
|
return paths;
|
|
}
|
|
for (const key of ['cover', 'image']) {
|
|
if (parsed?.[key]) paths.push(String(parsed[key]));
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
export function collectPublicHtmlReferences(html, htmlDir, { downloadsOnly = false } = {}) {
|
|
const refs = [];
|
|
const seen = new Set();
|
|
|
|
const add = (raw, source) => {
|
|
if (downloadsOnly && !isDownloadLikeReference(raw, source)) return;
|
|
if (!downloadsOnly && !isFilesystemPublicReference(raw)) return;
|
|
const ref = normalizeReferencePath(raw);
|
|
if (!ref) return;
|
|
const key = `${source}\0${ref}`;
|
|
if (seen.has(key)) return;
|
|
seen.add(key);
|
|
refs.push({
|
|
ref,
|
|
source,
|
|
resolvedPath: resolveReferencePath(htmlDir, ref),
|
|
});
|
|
};
|
|
|
|
for (const match of String(html ?? '').matchAll(/\b(?:href|src)\s*=\s*["']([^"']+)["']/gi)) {
|
|
add(match[1], match[0]);
|
|
}
|
|
|
|
if (!downloadsOnly) {
|
|
for (const match of String(html ?? '').matchAll(
|
|
/<meta\b[^>]*\bname\s*=\s*["']mindspace-cover["'][^>]*>/gi,
|
|
)) {
|
|
const tag = match[0];
|
|
const contentMatch =
|
|
tag.match(/\bcontent\s*=\s*"([^"]*)"/i) ?? tag.match(/\bcontent\s*=\s*'([^']*)'/i);
|
|
for (const coverPath of parseMindspaceCoverPaths(contentMatch?.[1])) {
|
|
add(coverPath, 'mindspace-cover');
|
|
}
|
|
}
|
|
}
|
|
|
|
return refs;
|
|
}
|
|
|
|
export function findMissingPublicHtmlReferences(htmlPath, html = null, options = {}) {
|
|
const resolvedHtmlPath = path.resolve(htmlPath);
|
|
const content = html ?? fs.readFileSync(resolvedHtmlPath, 'utf8');
|
|
const htmlDir = path.dirname(resolvedHtmlPath);
|
|
const missing = [];
|
|
const seen = new Set();
|
|
|
|
for (const item of collectPublicHtmlReferences(content, htmlDir, options)) {
|
|
const key = item.ref;
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
if (!item.resolvedPath || !fs.existsSync(item.resolvedPath)) {
|
|
missing.push({ htmlPath: resolvedHtmlPath, ...item });
|
|
}
|
|
}
|
|
|
|
return missing;
|
|
}
|
|
|
|
export function listPublicHtmlFiles(publishRoot, { userId = null } = {}) {
|
|
const root = path.resolve(publishRoot);
|
|
if (!fs.existsSync(root)) return [];
|
|
|
|
const userDirs = userId
|
|
? [path.join(root, userId)]
|
|
: fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path.join(root, d.name));
|
|
|
|
const files = [];
|
|
for (const userDir of userDirs) {
|
|
const publicDir = path.join(userDir, 'public');
|
|
if (!fs.existsSync(publicDir) || !fs.statSync(publicDir).isDirectory()) continue;
|
|
for (const name of fs.readdirSync(publicDir)) {
|
|
if (!name.toLowerCase().endsWith('.html')) continue;
|
|
files.push(path.join(publicDir, name));
|
|
}
|
|
}
|
|
return files.sort();
|
|
}
|
|
|
|
export function scanPublicHtmlLinks(publishRoot, options = {}) {
|
|
const issues = [];
|
|
for (const htmlPath of listPublicHtmlFiles(publishRoot, options)) {
|
|
issues.push(...findMissingPublicHtmlReferences(htmlPath, null, options));
|
|
}
|
|
return issues;
|
|
}
|