feat: add default-editor + files plugins, bundle execution check, fix platform-test diagnostic priority
This commit is contained in:
parent
1d2190fc6c
commit
da49788504
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Default Editor</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
background: transparent;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="default-editor-root"></div>
|
||||||
|
<script type="module" src="./dist/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,440 @@
|
||||||
|
/* ===========================================================
|
||||||
|
Default Editor Plugin — Verstak v2 Frontend Bundle
|
||||||
|
Contract: window.VerstakPluginRegister(id, { components })
|
||||||
|
=========================================================== */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ── Style injection ─────────────────────────────────────── */
|
||||||
|
function injectStyles() {
|
||||||
|
if (document.getElementById('de-style-injected')) return;
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'de-style-injected';
|
||||||
|
style.textContent = STYLES;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
var STYLES = [
|
||||||
|
'.de-root { display:flex; flex-direction:column; height:100%; min-height:0; overflow:hidden; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; color:#e0e0e0; }',
|
||||||
|
'.de-toolbar { display:flex; align-items:center; gap:0.5rem; padding:0.5rem 0.75rem; border-bottom:1px solid #16213e; flex-shrink:0; background:#12122a; }',
|
||||||
|
'.de-toolbar-mode { font-size:0.75rem; color:#4ecca3; padding:0.15rem 0.5rem; border-radius:3px; background:#1a2a3a; }',
|
||||||
|
'.de-toolbar-context { font-size:0.7rem; color:#8b8ba8; margin-left:0.25rem; }',
|
||||||
|
'.de-toolbar-spacer { flex:1; }',
|
||||||
|
'.de-toolbar-btn { font-size:0.75rem; padding:0.25rem 0.6rem; border:1px solid #333; border-radius:4px; background:#1a1a2e; color:#ccc; cursor:pointer; }',
|
||||||
|
'.de-toolbar-btn:hover { background:#2a2a4e; border-color:#4ecca3; }',
|
||||||
|
'.de-toolbar-btn.active { background:#1a3a2a; border-color:#4ecca3; color:#4ecca3; }',
|
||||||
|
'.de-toolbar-btn:disabled { opacity:0.4; cursor:default; }',
|
||||||
|
'.de-status { font-size:0.7rem; color:#8b8ba8; padding:0.15rem 0.5rem; }',
|
||||||
|
'.de-status.saved { color:#4ecca3; }',
|
||||||
|
'.de-status.error { color:#e74c3c; }',
|
||||||
|
'.de-status.dirty { color:#f39c12; }',
|
||||||
|
'.de-editor-wrap { flex:1; display:flex; min-height:0; overflow:hidden; }',
|
||||||
|
'.de-textarea { flex:1; width:100%; height:100%; resize:none; border:none; outline:none; padding:0.75rem; font-family:"SF Mono","Fira Code","Cascadia Code",monospace; font-size:0.85rem; line-height:1.6; background:#0d0d1a; color:#e0e0e0; tab-size:2; }',
|
||||||
|
'.de-textarea:focus { outline:none; }',
|
||||||
|
'.de-preview { flex:1; height:100%; padding:0.75rem 1rem; overflow-y:auto; background:#0d0d1a; line-height:1.7; font-size:0.9rem; }',
|
||||||
|
'.de-preview h1,.de-preview h2,.de-preview h3,.de-preview h4,.de-preview h5,.de-preview h6 { color:#e0e0f0; margin:1rem 0 0.5rem; }',
|
||||||
|
'.de-preview h1 { font-size:1.5rem; border-bottom:1px solid #16213e; padding-bottom:0.3rem; }',
|
||||||
|
'.de-preview h2 { font-size:1.25rem; border-bottom:1px solid #16213e; padding-bottom:0.25rem; }',
|
||||||
|
'.de-preview h3 { font-size:1.1rem; }',
|
||||||
|
'.de-preview p { margin:0.5rem 0; }',
|
||||||
|
'.de-preview code { background:#1a1a2e; padding:0.15rem 0.35rem; border-radius:3px; font-size:0.85em; color:#4ecca3; }',
|
||||||
|
'.de-preview pre { background:#1a1a2e; padding:0.75rem; border-radius:4px; overflow-x:auto; margin:0.75rem 0; }',
|
||||||
|
'.de-preview pre code { background:none; padding:0; }',
|
||||||
|
'.de-preview ul,.de-preview ol { padding-left:1.5rem; margin:0.5rem 0; }',
|
||||||
|
'.de-preview li { margin:0.25rem 0; }',
|
||||||
|
'.de-preview blockquote { border-left:3px solid #4ecca3; margin:0.5rem 0; padding:0.25rem 0.75rem; color:#aaa; }',
|
||||||
|
'.de-preview a { color:#4ecca3; text-decoration:none; }',
|
||||||
|
'.de-preview a:hover { text-decoration:underline; }',
|
||||||
|
'.de-preview hr { border:none; border-top:1px solid #16213e; margin:1rem 0; }',
|
||||||
|
'.de-preview table { border-collapse:collapse; margin:0.75rem 0; }',
|
||||||
|
'.de-preview th,.de-preview td { border:1px solid #333; padding:0.35rem 0.6rem; text-align:left; }',
|
||||||
|
'.de-preview th { background:#1a1a2e; }',
|
||||||
|
'.de-notes-badge { font-size:0.65rem; padding:0.1rem 0.4rem; border-radius:3px; background:#2a1a3a; color:#b388ff; margin-left:0.5rem; }',
|
||||||
|
'.de-notes-info { padding:0.5rem 0.75rem; background:#1a1a2a; border-top:1px solid #16213e; font-size:0.75rem; color:#8b8ba8; flex-shrink:0; }',
|
||||||
|
'.de-loading { flex:1; display:flex; align-items:center; justify-content:center; color:#666; }',
|
||||||
|
'.de-error { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; color:#e74c3c; gap:0.5rem; }',
|
||||||
|
'.de-error-msg { font-size:0.85rem; color:#aaa; max-width:400px; text-align:center; }',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
/* ── Simple markdown renderer (no raw HTML) ──────────────── */
|
||||||
|
function renderMarkdown(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
var html = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
// Code blocks (``` ... ```)
|
||||||
|
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
|
||||||
|
return '<pre><code class="lang-' + (lang || 'text') + '">' + code.trimEnd() + '</code></pre>';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inline code
|
||||||
|
html = html.replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
||||||
|
|
||||||
|
// Headings
|
||||||
|
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
|
||||||
|
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
|
||||||
|
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
|
||||||
|
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
|
||||||
|
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
|
||||||
|
|
||||||
|
// Horizontal rule
|
||||||
|
html = html.replace(/^---+$/gm, '<hr>');
|
||||||
|
html = html.replace(/^\*\*\*+$/gm, '<hr>');
|
||||||
|
|
||||||
|
// Bold and italic
|
||||||
|
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||||
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// Strikethrough
|
||||||
|
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||||||
|
|
||||||
|
// Blockquote
|
||||||
|
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
||||||
|
|
||||||
|
// Unordered list items
|
||||||
|
html = html.replace(/^[\-\*]\s+(.+)$/gm, '<li>$1</li>');
|
||||||
|
|
||||||
|
// Ordered list items
|
||||||
|
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
||||||
|
|
||||||
|
// Wrap consecutive <li> in <ul>
|
||||||
|
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
||||||
|
|
||||||
|
// Links [text](url) — render as text since we can't navigate internally
|
||||||
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a title="Link: $2">$1</a>');
|
||||||
|
|
||||||
|
// Images  — render as placeholder
|
||||||
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<span title="Image: $2" style="color:#666">[image: $1]</span>');
|
||||||
|
|
||||||
|
// Paragraphs: double newlines
|
||||||
|
html = html.replace(/\n\n+/g, '</p><p>');
|
||||||
|
html = '<p>' + html + '</p>';
|
||||||
|
|
||||||
|
// Clean up empty paragraphs
|
||||||
|
html = html.replace(/<p>\s*<\/p>/g, '');
|
||||||
|
html = html.replace(/<p>\s*(<(?:h[1-6]|ul|ol|pre|blockquote|hr))/g, '$1');
|
||||||
|
html = html.replace(/(<\/(?:h[1-6]|ul|ol|pre|blockquote|hr)>)\s*<\/p>/g, '$1');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Utilities ──────────────────────────────────────────── */
|
||||||
|
function el(tag, attrs, children) {
|
||||||
|
var elem = document.createElement(tag);
|
||||||
|
if (attrs) {
|
||||||
|
Object.keys(attrs).forEach(function (k) {
|
||||||
|
if (k === 'className') elem.className = attrs[k];
|
||||||
|
else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(elem.style, attrs[k]);
|
||||||
|
else if (k.slice(0, 2) === 'on') elem.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
|
||||||
|
else if (k === 'innerHTML') elem.innerHTML = attrs[k];
|
||||||
|
else elem.setAttribute(k, attrs[k]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (children) {
|
||||||
|
(Array.isArray(children) ? children : [children]).forEach(function (c) {
|
||||||
|
if (c == null) return;
|
||||||
|
elem.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectMode(props) {
|
||||||
|
var ctx = props.request && props.request.context;
|
||||||
|
if (ctx && (ctx.notesMode || ctx.isInsideNotesFolder)) return 'notes-markdown';
|
||||||
|
var ext = (props.request && props.request.extension || '').toLowerCase();
|
||||||
|
if (ext === '.md' || ext === '.markdown') return 'generic-markdown';
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectContextLabel(mode) {
|
||||||
|
if (mode === 'notes-markdown') return 'notes';
|
||||||
|
if (mode === 'generic-markdown') return 'markdown';
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileName(path) {
|
||||||
|
if (!path) return '';
|
||||||
|
var parts = path.split('/');
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── DefaultEditor component ─────────────────────────────── */
|
||||||
|
var DefaultEditor = {
|
||||||
|
mount: function (containerEl, props, api) {
|
||||||
|
injectStyles();
|
||||||
|
containerEl.innerHTML = '';
|
||||||
|
containerEl.className = 'de-root';
|
||||||
|
|
||||||
|
var request = props.request || {};
|
||||||
|
var resourcePath = request.path || '';
|
||||||
|
var mode = request.mode || 'view';
|
||||||
|
var editorMode = detectMode(props);
|
||||||
|
var isMarkdown = editorMode === 'generic-markdown' || editorMode === 'notes-markdown';
|
||||||
|
var previewVisible = isMarkdown && mode === 'view';
|
||||||
|
|
||||||
|
var currentContent = '';
|
||||||
|
var savedContent = '';
|
||||||
|
var dirty = false;
|
||||||
|
var saveState = ''; // '' | 'saved' | 'error'
|
||||||
|
var saveTimer = null;
|
||||||
|
var disposed = false;
|
||||||
|
|
||||||
|
// ── Toolbar ──────────────────────────────────────────
|
||||||
|
var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]);
|
||||||
|
var contextLabel = el('span', { className: 'de-toolbar-context' }, [fileName(resourcePath)]);
|
||||||
|
|
||||||
|
var notesBadge = null;
|
||||||
|
if (editorMode === 'notes-markdown') {
|
||||||
|
notesBadge = el('span', { className: 'de-notes-badge' }, ['notes context']);
|
||||||
|
}
|
||||||
|
|
||||||
|
var spacer = el('span', { className: 'de-toolbar-spacer' });
|
||||||
|
|
||||||
|
var editBtn = null;
|
||||||
|
var previewBtn = null;
|
||||||
|
var saveBtn = el('button', { className: 'de-toolbar-btn' }, ['Save']);
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
|
||||||
|
if (isMarkdown) {
|
||||||
|
editBtn = el('button', { className: 'de-toolbar-btn' + (mode === 'edit' ? ' active' : '') }, ['Edit']);
|
||||||
|
previewBtn = el('button', { className: 'de-toolbar-btn' + (previewVisible ? ' active' : '') }, ['Preview']);
|
||||||
|
if (mode === 'edit') {
|
||||||
|
previewBtn.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusEl = el('span', { className: 'de-status' });
|
||||||
|
|
||||||
|
var toolbarChildren = [modeLabel, contextLabel];
|
||||||
|
if (notesBadge) toolbarChildren.push(notesBadge);
|
||||||
|
toolbarChildren.push(spacer);
|
||||||
|
if (editBtn) toolbarChildren.push(editBtn);
|
||||||
|
if (previewBtn) toolbarChildren.push(previewBtn);
|
||||||
|
toolbarChildren.push(saveBtn);
|
||||||
|
toolbarChildren.push(statusEl);
|
||||||
|
|
||||||
|
var toolbar = el('div', { className: 'de-toolbar' }, toolbarChildren);
|
||||||
|
containerEl.appendChild(toolbar);
|
||||||
|
|
||||||
|
// ── Editor area ──────────────────────────────────────
|
||||||
|
var editorWrap = el('div', { className: 'de-editor-wrap' });
|
||||||
|
containerEl.appendChild(editorWrap);
|
||||||
|
|
||||||
|
var textarea = null;
|
||||||
|
var previewEl = null;
|
||||||
|
|
||||||
|
if (mode === 'edit' || !isMarkdown) {
|
||||||
|
textarea = el('textarea', { className: 'de-textarea', spellcheck: 'false' });
|
||||||
|
textarea.value = '';
|
||||||
|
editorWrap.appendChild(textarea);
|
||||||
|
|
||||||
|
textarea.addEventListener('input', function () {
|
||||||
|
currentContent = textarea.value;
|
||||||
|
dirty = currentContent !== savedContent;
|
||||||
|
updateStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
textarea.addEventListener('keydown', function (e) {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
// Tab support
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMarkdown && previewVisible) {
|
||||||
|
previewEl = el('div', { className: 'de-preview' });
|
||||||
|
editorWrap.appendChild(previewEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notes info bar ───────────────────────────────────
|
||||||
|
if (editorMode === 'notes-markdown') {
|
||||||
|
var notesInfo = el('div', { className: 'de-notes-info' }, [
|
||||||
|
'Notes context active — internal links, backlinks, and widgets deferred.'
|
||||||
|
]);
|
||||||
|
containerEl.appendChild(notesInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Status helpers ───────────────────────────────────
|
||||||
|
function updateStatus() {
|
||||||
|
if (saveState === 'error') {
|
||||||
|
statusEl.textContent = 'Error saving';
|
||||||
|
statusEl.className = 'de-status error';
|
||||||
|
} else if (saveState === 'saved') {
|
||||||
|
statusEl.textContent = 'Saved';
|
||||||
|
statusEl.className = 'de-status saved';
|
||||||
|
} else if (dirty) {
|
||||||
|
statusEl.textContent = 'Modified';
|
||||||
|
statusEl.className = 'de-status dirty';
|
||||||
|
} else {
|
||||||
|
statusEl.textContent = '';
|
||||||
|
statusEl.className = 'de-status';
|
||||||
|
}
|
||||||
|
saveBtn.disabled = !dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
if (previewEl) {
|
||||||
|
previewEl.innerHTML = renderMarkdown(currentContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save ─────────────────────────────────────────────
|
||||||
|
function save() {
|
||||||
|
if (!dirty || disposed) return;
|
||||||
|
saveState = '';
|
||||||
|
updateStatus();
|
||||||
|
api.files.writeText(resourcePath, currentContent, {
|
||||||
|
createIfMissing: false,
|
||||||
|
overwrite: true
|
||||||
|
}).then(function () {
|
||||||
|
if (disposed) return;
|
||||||
|
savedContent = currentContent;
|
||||||
|
dirty = false;
|
||||||
|
saveState = 'saved';
|
||||||
|
updateStatus();
|
||||||
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(function () {
|
||||||
|
if (!disposed) { saveState = ''; updateStatus(); }
|
||||||
|
}, 2000);
|
||||||
|
}).catch(function (err) {
|
||||||
|
if (disposed) return;
|
||||||
|
saveState = 'error';
|
||||||
|
updateStatus();
|
||||||
|
console.error('[default-editor] save error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar events ───────────────────────────────────
|
||||||
|
saveBtn.addEventListener('click', save);
|
||||||
|
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.addEventListener('click', function () {
|
||||||
|
if (mode === 'edit') return;
|
||||||
|
mode = 'edit';
|
||||||
|
previewVisible = false;
|
||||||
|
rebuildEditorArea();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewBtn) {
|
||||||
|
previewBtn.addEventListener('click', function () {
|
||||||
|
if (previewVisible) return;
|
||||||
|
previewVisible = true;
|
||||||
|
mode = isMarkdown ? 'view' : mode;
|
||||||
|
rebuildEditorArea();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildEditorArea() {
|
||||||
|
editorWrap.innerHTML = '';
|
||||||
|
textarea = null;
|
||||||
|
previewEl = null;
|
||||||
|
|
||||||
|
if (mode === 'edit' || !isMarkdown) {
|
||||||
|
textarea = el('textarea', { className: 'de-textarea', spellcheck: 'false' });
|
||||||
|
textarea.value = currentContent;
|
||||||
|
editorWrap.appendChild(textarea);
|
||||||
|
|
||||||
|
textarea.addEventListener('input', function () {
|
||||||
|
currentContent = textarea.value;
|
||||||
|
dirty = currentContent !== savedContent;
|
||||||
|
updateStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
textarea.addEventListener('keydown', function (e) {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + 2;
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMarkdown && previewVisible) {
|
||||||
|
previewEl = el('div', { className: 'de-preview' });
|
||||||
|
editorWrap.appendChild(previewEl);
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update toolbar button states
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.className = 'de-toolbar-btn' + (mode === 'edit' ? ' active' : '');
|
||||||
|
}
|
||||||
|
if (previewBtn) {
|
||||||
|
previewBtn.className = 'de-toolbar-btn' + (previewVisible ? ' active' : '');
|
||||||
|
}
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Load file ────────────────────────────────────────
|
||||||
|
editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...']));
|
||||||
|
|
||||||
|
api.files.readText(resourcePath).then(function (content) {
|
||||||
|
if (disposed) return;
|
||||||
|
editorWrap.innerHTML = '';
|
||||||
|
currentContent = content;
|
||||||
|
savedContent = content;
|
||||||
|
dirty = false;
|
||||||
|
rebuildEditorArea();
|
||||||
|
}).catch(function (err) {
|
||||||
|
if (disposed) return;
|
||||||
|
editorWrap.innerHTML = '';
|
||||||
|
var msg = (err && err.message) ? err.message : String(err);
|
||||||
|
editorWrap.appendChild(el('div', { className: 'de-error' }, [
|
||||||
|
el('div', {}, ['Failed to load file']),
|
||||||
|
el('div', { className: 'de-error-msg' }, [msg])
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Cleanup ──────────────────────────────────────────
|
||||||
|
containerEl.__deCleanup = function () {
|
||||||
|
disposed = true;
|
||||||
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set data attributes for testability
|
||||||
|
containerEl.setAttribute('data-editor-mode', editorMode);
|
||||||
|
containerEl.setAttribute('data-resource-path', resourcePath);
|
||||||
|
containerEl.setAttribute('data-request-mode', mode);
|
||||||
|
},
|
||||||
|
|
||||||
|
unmount: function (containerEl) {
|
||||||
|
if (containerEl.__deCleanup) {
|
||||||
|
containerEl.__deCleanup();
|
||||||
|
containerEl.__deCleanup = null;
|
||||||
|
}
|
||||||
|
containerEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── Register ────────────────────────────────────────────── */
|
||||||
|
window.VerstakPluginRegister('verstak.default-editor', {
|
||||||
|
components: {
|
||||||
|
DefaultEditor: DefaultEditor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "verstak.default-editor",
|
||||||
|
"name": "Default Editor",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"apiVersion": "0.1.0",
|
||||||
|
"description": "Built-in text and markdown editor/viewer for Verstak. Provides openProviders for generic text, generic markdown, and notes-context markdown files.",
|
||||||
|
"source": "official",
|
||||||
|
"icon": "edit",
|
||||||
|
"provides": [
|
||||||
|
"verstak/default-editor/v1"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"verstak/core/files/v1",
|
||||||
|
"verstak/core/workbench/v1"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"files.read",
|
||||||
|
"files.write",
|
||||||
|
"workbench.open"
|
||||||
|
],
|
||||||
|
"frontend": {
|
||||||
|
"entry": "frontend/dist/index.js"
|
||||||
|
},
|
||||||
|
"contributes": {
|
||||||
|
"openProviders": [
|
||||||
|
{
|
||||||
|
"id": "verstak.default-editor.text",
|
||||||
|
"title": "Default Text Editor",
|
||||||
|
"priority": 50,
|
||||||
|
"component": "DefaultEditor",
|
||||||
|
"supports": [
|
||||||
|
{
|
||||||
|
"kind": "vault-file",
|
||||||
|
"extensions": [".txt", ".log", ".conf", ".ini", ".toml", ".yaml", ".yml", ".json", ".csv"],
|
||||||
|
"mime": ["text/plain", "application/json"],
|
||||||
|
"contexts": ["generic-text"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "verstak.default-editor.markdown",
|
||||||
|
"title": "Default Markdown Editor",
|
||||||
|
"priority": 50,
|
||||||
|
"component": "DefaultEditor",
|
||||||
|
"supports": [
|
||||||
|
{
|
||||||
|
"kind": "vault-file",
|
||||||
|
"extensions": [".md", ".markdown"],
|
||||||
|
"contexts": ["generic-markdown"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "verstak.default-editor.notes-markdown",
|
||||||
|
"title": "Default Notes Markdown Editor",
|
||||||
|
"priority": 50,
|
||||||
|
"component": "DefaultEditor",
|
||||||
|
"supports": [
|
||||||
|
{
|
||||||
|
"kind": "vault-file",
|
||||||
|
"extensions": [".md", ".markdown"],
|
||||||
|
"contexts": ["notes-markdown"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
/* ===========================================================
|
||||||
|
Files Plugin — Verstak v2 Frontend Bundle
|
||||||
|
Contract: window.VerstakPluginRegister(id, { components })
|
||||||
|
=========================================================== */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ── Style injection ─────────────────────────────────────── */
|
||||||
|
function injectStyles() {
|
||||||
|
if (document.getElementById('files-style-injected')) return;
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'files-style-injected';
|
||||||
|
style.textContent = STYLES;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
var STYLES = [
|
||||||
|
'.files-root{display:flex;flex-direction:column;height:100%;min-height:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a}',
|
||||||
|
'.files-toolbar{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a}',
|
||||||
|
'.files-toolbar-btn{font-size:0.75rem;padding:0.25rem 0.6rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}',
|
||||||
|
'.files-toolbar-btn:hover{background:#2a2a4e;border-color:#4ecca3}',
|
||||||
|
'.files-toolbar-btn:disabled{opacity:0.4;cursor:default}',
|
||||||
|
'.files-breadcrumb{display:flex;align-items:center;gap:0.25rem;font-size:0.8rem;color:#8b8ba8;flex:1;min-width:0;overflow:hidden}',
|
||||||
|
'.files-breadcrumb-item{color:#4ecca3;cursor:pointer;padding:0.1rem 0.3rem;border-radius:3px}',
|
||||||
|
'.files-breadcrumb-item:hover{background:#1a2a3a}',
|
||||||
|
'.files-breadcrumb-sep{color:#555}',
|
||||||
|
'.files-list{flex:1;overflow-y:auto;padding:0.5rem 0}',
|
||||||
|
'.files-item{display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0.75rem;cursor:pointer;font-size:0.85rem}',
|
||||||
|
'.files-item:hover{background:#1a1a2e}',
|
||||||
|
'.files-item.selected{background:#1a2a3a}',
|
||||||
|
'.files-item-icon{font-size:1rem;width:1.2rem;text-align:center;flex-shrink:0}',
|
||||||
|
'.files-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
|
'.files-item-meta{font-size:0.7rem;color:#666;flex-shrink:0}',
|
||||||
|
'.files-empty{flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:0.9rem}',
|
||||||
|
'.files-loading{flex:1;display:flex;align-items:center;justify-content:center;color:#666}',
|
||||||
|
'.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#e74c3c;gap:0.5rem;padding:1rem}',
|
||||||
|
'.files-error-msg{font-size:0.85rem;color:#aaa;max-width:400px;text-align:center}',
|
||||||
|
'.files-create-bar{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-top:1px solid #16213e;flex-shrink:0;background:#12122a}',
|
||||||
|
'.files-create-input{flex:1;font-size:0.8rem;padding:0.3rem 0.5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}',
|
||||||
|
'.files-create-input:focus{border-color:#4ecca3}',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
/* ── Utilities ──────────────────────────────────────────── */
|
||||||
|
function el(tag, attrs, children) {
|
||||||
|
var elem = document.createElement(tag);
|
||||||
|
if (attrs) {
|
||||||
|
Object.keys(attrs).forEach(function (k) {
|
||||||
|
if (k === 'className') elem.className = attrs[k];
|
||||||
|
else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(elem.style, attrs[k]);
|
||||||
|
else if (k.slice(0, 2) === 'on') elem.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
|
||||||
|
else if (k === 'innerHTML') elem.innerHTML = attrs[k];
|
||||||
|
else if (k === 'textContent') elem.textContent = attrs[k];
|
||||||
|
else elem.setAttribute(k, attrs[k]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (children) {
|
||||||
|
(Array.isArray(children) ? children : [children]).forEach(function (c) {
|
||||||
|
if (c == null) return;
|
||||||
|
elem.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileIcon(entry) {
|
||||||
|
if (entry.type === 'folder') return '\uD83D\uDCC1';
|
||||||
|
var ext = (entry.extension || '').toLowerCase();
|
||||||
|
if (ext === 'md' || ext === 'markdown') return '\uD83D\uDCDD';
|
||||||
|
if (ext === 'txt' || ext === 'log') return '\uD83D\uDCC4';
|
||||||
|
if (ext === 'json') return '{ }';
|
||||||
|
if (ext === 'yaml' || ext === 'yml' || ext === 'toml') return '\u2699\uFE0F';
|
||||||
|
return '\uD83D\uDCC1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (bytes == null || bytes === 0) return '';
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortEntries(entries) {
|
||||||
|
var folders = entries.filter(function (e) { return e.type === 'folder'; });
|
||||||
|
var files = entries.filter(function (e) { return e.type !== 'folder'; });
|
||||||
|
folders.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
||||||
|
files.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
||||||
|
return folders.concat(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── FilesView component ─────────────────────────────────── */
|
||||||
|
var FilesView = {
|
||||||
|
mount: function (containerEl, props, api) {
|
||||||
|
injectStyles();
|
||||||
|
containerEl.innerHTML = '';
|
||||||
|
containerEl.className = 'files-root';
|
||||||
|
|
||||||
|
var currentPath = '';
|
||||||
|
var entries = [];
|
||||||
|
var disposed = false;
|
||||||
|
|
||||||
|
var toolbar = el('div', { className: 'files-toolbar' });
|
||||||
|
var breadcrumb = el('div', { className: 'files-breadcrumb' });
|
||||||
|
var refreshBtn = el('button', { className: 'files-toolbar-btn' }, ['Refresh']);
|
||||||
|
var createFolderBtn = el('button', { className: 'files-toolbar-btn' }, ['+ Folder']);
|
||||||
|
var createFileBtn = el('button', { className: 'files-toolbar-btn' }, ['+ File']);
|
||||||
|
toolbar.appendChild(breadcrumb);
|
||||||
|
toolbar.appendChild(refreshBtn);
|
||||||
|
toolbar.appendChild(createFolderBtn);
|
||||||
|
toolbar.appendChild(createFileBtn);
|
||||||
|
containerEl.appendChild(toolbar);
|
||||||
|
|
||||||
|
var listContainer = el('div', { className: 'files-list' });
|
||||||
|
containerEl.appendChild(listContainer);
|
||||||
|
|
||||||
|
var createBar = el('div', { className: 'files-create-bar', style: { display: 'none' } });
|
||||||
|
var createInput = el('input', { className: 'files-create-input', placeholder: 'Name...' });
|
||||||
|
var createConfirmBtn = el('button', { className: 'files-toolbar-btn' }, ['Create']);
|
||||||
|
var createCancelBtn = el('button', { className: 'files-toolbar-btn' }, ['Cancel']);
|
||||||
|
createBar.appendChild(createInput);
|
||||||
|
createBar.appendChild(createConfirmBtn);
|
||||||
|
createBar.appendChild(createCancelBtn);
|
||||||
|
containerEl.appendChild(createBar);
|
||||||
|
|
||||||
|
var createMode = ''; // 'folder' | 'file' | ''
|
||||||
|
|
||||||
|
function updateBreadcrumb() {
|
||||||
|
breadcrumb.innerHTML = '';
|
||||||
|
var rootItem = el('span', { className: 'files-breadcrumb-item', onClick: function () { navigateTo(''); } }, ['Root']);
|
||||||
|
breadcrumb.appendChild(rootItem);
|
||||||
|
if (currentPath) {
|
||||||
|
var parts = currentPath.split('/');
|
||||||
|
var accumulated = '';
|
||||||
|
parts.forEach(function (part, i) {
|
||||||
|
breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-sep' }, [' / ']));
|
||||||
|
accumulated += (accumulated ? '/' : '') + part;
|
||||||
|
(function (path) {
|
||||||
|
breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-item', onClick: function () { navigateTo(path); } }, [part]));
|
||||||
|
})(accumulated);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
listContainer.innerHTML = '';
|
||||||
|
if (entries.length === 0) {
|
||||||
|
listContainer.appendChild(el('div', { className: 'files-empty' }, ['Empty folder']));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var sorted = sortEntries(entries);
|
||||||
|
sorted.forEach(function (entry) {
|
||||||
|
if (entry.isHidden || entry.isReserved) return;
|
||||||
|
var item = el('div', { className: 'files-item' }, [
|
||||||
|
el('span', { className: 'files-item-icon' }, [fileIcon(entry)]),
|
||||||
|
el('span', { className: 'files-item-name', textContent: entry.name }),
|
||||||
|
el('span', { className: 'files-item-meta', textContent: entry.type === 'folder' ? '' : formatSize(entry.size) }),
|
||||||
|
]);
|
||||||
|
if (entry.type === 'folder') {
|
||||||
|
item.addEventListener('dblclick', function () {
|
||||||
|
navigateTo(entry.relativePath);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
item.addEventListener('dblclick', function () {
|
||||||
|
openFile(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
listContainer.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(path) {
|
||||||
|
currentPath = path;
|
||||||
|
updateBreadcrumb();
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEntries() {
|
||||||
|
listContainer.innerHTML = '';
|
||||||
|
listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...']));
|
||||||
|
api.files.list(currentPath).then(function (result) {
|
||||||
|
if (disposed) return;
|
||||||
|
entries = result || [];
|
||||||
|
renderList();
|
||||||
|
}).catch(function (err) {
|
||||||
|
if (disposed) return;
|
||||||
|
listContainer.innerHTML = '';
|
||||||
|
var msg = (err && err.message) ? err.message : String(err);
|
||||||
|
listContainer.appendChild(el('div', { className: 'files-error' }, [
|
||||||
|
el('div', {}, ['Failed to load files']),
|
||||||
|
el('div', { className: 'files-error-msg' }, [msg]),
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFile(entry) {
|
||||||
|
var ext = entry.extension ? '.' + entry.extension : '';
|
||||||
|
var isMd = ext === '.md' || ext === '.markdown';
|
||||||
|
var isNotes = currentPath.split('/')[0] === 'Notes';
|
||||||
|
var context = { sourcePluginId: 'verstak.files', sourceView: 'files' };
|
||||||
|
if (isMd && isNotes) {
|
||||||
|
context.isInsideNotesFolder = true;
|
||||||
|
context.notesMode = true;
|
||||||
|
}
|
||||||
|
api.workbench.openResource({
|
||||||
|
kind: 'vault-file',
|
||||||
|
path: entry.relativePath,
|
||||||
|
mode: 'view',
|
||||||
|
extension: ext,
|
||||||
|
context: context,
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('[files] openResource error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCreate(mode) {
|
||||||
|
createMode = mode;
|
||||||
|
createInput.value = '';
|
||||||
|
createInput.placeholder = mode === 'folder' ? 'Folder name...' : 'File name (e.g. note.md)...';
|
||||||
|
createBar.style.display = 'flex';
|
||||||
|
createInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelCreate() {
|
||||||
|
createMode = '';
|
||||||
|
createBar.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmCreate() {
|
||||||
|
var name = createInput.value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
var path = currentPath ? currentPath + '/' + name : name;
|
||||||
|
var promise;
|
||||||
|
if (createMode === 'folder') {
|
||||||
|
promise = api.files.createFolder(path);
|
||||||
|
} else {
|
||||||
|
promise = api.files.writeText(path, '', { createIfMissing: true, overwrite: false });
|
||||||
|
}
|
||||||
|
promise.then(function () {
|
||||||
|
cancelCreate();
|
||||||
|
loadEntries();
|
||||||
|
}).catch(function (err) {
|
||||||
|
var msg = (err && err.message) ? err.message : String(err);
|
||||||
|
createInput.value = '';
|
||||||
|
createInput.placeholder = 'Error: ' + msg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('click', loadEntries);
|
||||||
|
createFolderBtn.addEventListener('click', function () { startCreate('folder'); });
|
||||||
|
createFileBtn.addEventListener('click', function () { startCreate('file'); });
|
||||||
|
createConfirmBtn.addEventListener('click', confirmCreate);
|
||||||
|
createCancelBtn.addEventListener('click', cancelCreate);
|
||||||
|
createInput.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter') confirmCreate();
|
||||||
|
if (e.key === 'Escape') cancelCreate();
|
||||||
|
});
|
||||||
|
|
||||||
|
loadEntries();
|
||||||
|
|
||||||
|
containerEl.__filesCleanup = function () {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
unmount: function (containerEl) {
|
||||||
|
if (containerEl.__filesCleanup) {
|
||||||
|
containerEl.__filesCleanup();
|
||||||
|
containerEl.__filesCleanup = null;
|
||||||
|
}
|
||||||
|
containerEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── Register ────────────────────────────────────────────── */
|
||||||
|
window.VerstakPluginRegister('verstak.files', {
|
||||||
|
components: {
|
||||||
|
FilesView: FilesView
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "verstak.files",
|
||||||
|
"name": "Files",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"apiVersion": "0.1.0",
|
||||||
|
"description": "Minimal vault file navigator. Shows folders and files, opens files through Workbench openResource.",
|
||||||
|
"source": "official",
|
||||||
|
"icon": "folder",
|
||||||
|
"provides": [
|
||||||
|
"verstak/files/v1"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"verstak/core/files/v1",
|
||||||
|
"verstak/core/workbench/v1"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"files.read",
|
||||||
|
"files.write",
|
||||||
|
"workbench.open",
|
||||||
|
"ui.register"
|
||||||
|
],
|
||||||
|
"frontend": {
|
||||||
|
"entry": "frontend/dist/index.js"
|
||||||
|
},
|
||||||
|
"contributes": {
|
||||||
|
"workspaceItems": [
|
||||||
|
{
|
||||||
|
"id": "verstak.files.workspace",
|
||||||
|
"title": "Files",
|
||||||
|
"icon": "folder",
|
||||||
|
"component": "FilesView"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,19 +86,13 @@
|
||||||
{
|
{
|
||||||
"id": "verstak.platform-test.markdown-diagnostic",
|
"id": "verstak.platform-test.markdown-diagnostic",
|
||||||
"title": "Platform Test Markdown Diagnostic",
|
"title": "Platform Test Markdown Diagnostic",
|
||||||
"priority": 100,
|
"priority": 10,
|
||||||
"component": "MarkdownDiagnosticProvider",
|
"component": "MarkdownDiagnosticProvider",
|
||||||
"supports": [
|
"supports": [
|
||||||
{
|
{
|
||||||
"kind": "vault-file",
|
"kind": "vault-file",
|
||||||
"extensions": [".md", ".markdown"],
|
"extensions": [".md", ".markdown"],
|
||||||
"contexts": ["generic-markdown", "notes-markdown"]
|
"contexts": ["generic-markdown", "notes-markdown"]
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "vault-file",
|
|
||||||
"mime": ["text/plain"],
|
|
||||||
"extensions": [".txt", ".log"],
|
|
||||||
"contexts": ["generic-text"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,57 @@ else
|
||||||
echo " ⚠️ node not available — skipping frontend smoke"
|
echo " ⚠️ node not available — skipping frontend smoke"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[frontend bundle execution]"
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
BUNDLE_FAILED=0
|
||||||
|
for plugin_dir in "$ROOT"/plugins/*/; do
|
||||||
|
plugin_id=$(basename "$plugin_dir")
|
||||||
|
manifest="$plugin_dir/plugin.json"
|
||||||
|
if [ ! -f "$manifest" ]; then continue; fi
|
||||||
|
# Check if plugin has frontend entry
|
||||||
|
entry=$(node -e "const m=require('$manifest');console.log(m.frontend&&m.frontend.entry||'')" 2>/dev/null)
|
||||||
|
if [ -z "$entry" ]; then continue; fi
|
||||||
|
bundle="$plugin_dir$entry"
|
||||||
|
if [ ! -f "$bundle" ]; then
|
||||||
|
echo " ❌ $plugin_id: bundle not found at $entry"
|
||||||
|
BUNDLE_FAILED=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Execute bundle via new Function() and verify registration
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const content = fs.readFileSync('$bundle', 'utf8');
|
||||||
|
// Provide minimal globals
|
||||||
|
global.window = { VerstakPluginRegister: function(id, def) { global.__registered = id; } };
|
||||||
|
global.document = { getElementById: function() { return null; }, createElement: function() { return { style: {}, setAttribute: function(){}, appendChild: function(){} }; }, head: { appendChild: function(){} } };
|
||||||
|
try {
|
||||||
|
new Function(content)();
|
||||||
|
if (!global.__registered) {
|
||||||
|
console.log('ERROR: plugin did not call VerstakPluginRegister');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('OK: ' + global.__registered);
|
||||||
|
} catch(e) {
|
||||||
|
console.log('ERROR: ' + e.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
" 2>&1 | while read -r line; do
|
||||||
|
if [[ "$line" == ERROR:* ]]; then
|
||||||
|
echo " ❌ $plugin_id: ${line#ERROR: }"
|
||||||
|
BUNDLE_FAILED=1
|
||||||
|
elif [[ "$line" == OK:* ]]; then
|
||||||
|
echo " ✅ $plugin_id: ${line#OK: }"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
if [ "$BUNDLE_FAILED" -ne 0 ]; then
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ⚠️ node not available — skipping bundle execution"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if [ "$FAILED" -eq 0 ]; then
|
if [ "$FAILED" -eq 0 ]; then
|
||||||
echo "✅ all checks passed"
|
echo "✅ all checks passed"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue