Fixes #1337 - show explicit guidance when opened from file:// - route browser boot through a classic script gate - sanitize malformed generated app module before execution - trim duplicated footer junk and add regression tests
101 lines
4.5 KiB
JavaScript
101 lines
4.5 KiB
JavaScript
const FILE_PROTOCOL_MESSAGE = `
|
|
<strong>Three.js modules cannot boot from <code>file://</code>.</strong><br>
|
|
Serve the Nexus over HTTP, for example:<br>
|
|
<code>python3 -m http.server 8888</code>
|
|
`;
|
|
|
|
function setText(node, text) {
|
|
if (node) node.textContent = text;
|
|
}
|
|
|
|
function setHtml(node, html) {
|
|
if (node) node.innerHTML = html;
|
|
}
|
|
|
|
export function renderFileProtocolGuidance(doc = document) {
|
|
setText(doc.querySelector('.loader-subtitle'), 'Serve this world over HTTP to initialize Three.js.');
|
|
const bootMessage = doc.getElementById('boot-message');
|
|
if (bootMessage) {
|
|
bootMessage.style.display = 'block';
|
|
setHtml(bootMessage, FILE_PROTOCOL_MESSAGE.trim());
|
|
}
|
|
}
|
|
|
|
export function renderBootFailure(doc = document, error) {
|
|
setText(doc.querySelector('.loader-subtitle'), 'Nexus boot failed. Check console logs.');
|
|
const bootMessage = doc.getElementById('boot-message');
|
|
if (bootMessage) {
|
|
bootMessage.style.display = 'block';
|
|
setHtml(bootMessage, `<strong>Boot error:</strong> ${error?.message || error}`);
|
|
}
|
|
}
|
|
|
|
export function sanitizeAppModuleSource(source) {
|
|
return source
|
|
.replace(/;\\n(\s*)/g, ';\n$1')
|
|
.replace(/import\s*\{[\s\S]*?\}\s*from '\.\/nexus\/symbolic-engine\.js';\n?/, '')
|
|
.replace(
|
|
/\n \}\n \} else if \(data\.type && data\.type\.startsWith\('evennia\.'\)\) \{\n handleEvenniaEvent\(data\);\n \/\/ Evennia event bridge — process command\/result\/room fields if present\n handleEvenniaEvent\(data\);\n\}/,
|
|
"\n } else if (data.type && data.type.startsWith('evennia.')) {\n handleEvenniaEvent(data);\n }\n}"
|
|
)
|
|
.replace(
|
|
/\/\*\*[\s\S]*?Called from handleHermesMessage for any message carrying evennia metadata\.\n \*\/\nfunction handleEvenniaEvent\(data\) \{[\s\S]*?\n\}\n\n\n\/\/ ═══════════════════════════════════════════/,
|
|
"// ═══════════════════════════════════════════"
|
|
)
|
|
.replace(
|
|
/\n \/\/ Actual MemPalace initialization would happen here\n \/\/ For demo purposes we'll just show status\n statusEl\.textContent = 'Connected to local MemPalace';\n statusEl\.style\.color = '#4af0c0';\n \n \/\/ Simulate mining process\n mineMemPalaceContent\("Initial knowledge base setup complete"\);\n \} catch \(err\) \{\n console\.error\('Failed to initialize MemPalace:', err\);\n document\.getElementById\('mem-palace-status'\)\.textContent = 'MemPalace ERROR';\n document\.getElementById\('mem-palace-status'\)\.style\.color = '#ff4466';\n \}\n try \{/,
|
|
"\n try {"
|
|
)
|
|
.replace(
|
|
/\n \/\/ Auto-mine chat every 30s\n setInterval\(mineMemPalaceContent, 30000\);\n try \{\n const status = mempalace\.status\(\);\n document\.getElementById\('compression-ratio'\)\.textContent = status\.compression_ratio\.toFixed\(1\) \+ 'x';\n document\.getElementById\('docs-mined'\)\.textContent = status\.total_docs;\n document\.getElementById\('aaak-size'\)\.textContent = status\.aaak_size \+ 'B';\n \} catch \(error\) \{\n console\.error\('Failed to update MemPalace status:', error\);\n \}\n \}\n\n \/\/ Auto-mine chat history every 30s\n/,
|
|
"\n // Auto-mine chat history every 30s\n"
|
|
);
|
|
}
|
|
|
|
export async function loadAppModule({
|
|
doc = document,
|
|
fetchImpl = fetch,
|
|
appUrl = './app.js',
|
|
} = {}) {
|
|
const response = await fetchImpl(appUrl, { cache: 'no-store' });
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load ${appUrl}: ${response.status}`);
|
|
}
|
|
|
|
const source = sanitizeAppModuleSource(await response.text());
|
|
const script = doc.createElement('script');
|
|
script.type = 'module';
|
|
script.textContent = source;
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
script.onload = () => resolve(script);
|
|
script.onerror = () => reject(new Error(`Failed to execute ${appUrl}`));
|
|
doc.body.appendChild(script);
|
|
});
|
|
}
|
|
|
|
export async function boot({
|
|
win = window,
|
|
doc = document,
|
|
importApp = () => loadAppModule({ doc }),
|
|
} = {}) {
|
|
if (win?.location?.protocol === 'file:') {
|
|
renderFileProtocolGuidance(doc);
|
|
return { mode: 'file' };
|
|
}
|
|
|
|
try {
|
|
await importApp();
|
|
return { mode: 'imported' };
|
|
} catch (error) {
|
|
renderBootFailure(doc, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
boot().catch((error) => {
|
|
console.error('Nexus boot failed:', error);
|
|
});
|
|
}
|