Add CLAUDE.md and the test harness
CLAUDE.md records the Nova constraints this extension is built around — established by reading Nova 14.1's bundle and probing its preview server, and expensive to re-derive: no previewer extension point, generated pages must live under the preview root, dot-directories are served, and no API reaches into a preview WebView. It also flags the one unverified assumption, that a tab showing Preview still appears in nova.workspace.textEditors, with its symptom and fix. Tests/ is the harness that built the extension, kept rather than left in a scratchpad: a stubbed Nova runtime backed by the real filesystem, node suites for generation and the four close paths, and a headless-browser render test that skips itself without a browser. It caught the ./ doubling in rebased links and checkbox state not surviving serialization. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TueXpMi7eAtgWPuMr4ii52
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// A minimal stand-in for Nova's extension runtime, backed by the real fs, so the
|
||||
// extension's own Scripts/*.js can be exercised outside Nova.
|
||||
const fs = require("fs");
|
||||
const nodePath = require("path");
|
||||
|
||||
class Disposable {
|
||||
constructor(fn) { this.fn = fn; this.disposed = false; }
|
||||
dispose() { if (!this.disposed) { this.disposed = true; if (this.fn) this.fn(); } }
|
||||
}
|
||||
class CompositeDisposable {
|
||||
constructor() { this.items = []; }
|
||||
add(d) { this.items.push(d); }
|
||||
dispose() { for (const d of this.items) d.dispose(); this.items = []; }
|
||||
}
|
||||
class Range {
|
||||
constructor(start, end) { this.start = start; this.end = end; }
|
||||
}
|
||||
|
||||
class FakeEditor {
|
||||
constructor(path, text) {
|
||||
this.document = {
|
||||
path, uri: `file://${path}`, isUntitled: false, isRemote: false,
|
||||
syntax: path.endsWith(".md") ? "markdown" : "html",
|
||||
length: text.length,
|
||||
getTextInRange: () => text,
|
||||
};
|
||||
this.handlers = { save: [], stop: [], destroy: [] };
|
||||
}
|
||||
onDidSave(cb) { this.handlers.save.push(cb); return new Disposable(); }
|
||||
onDidStopChanging(cb) { this.handlers.stop.push(cb); return new Disposable(); }
|
||||
onDidDestroy(cb) { this.handlers.destroy.push(cb); return new Disposable(); }
|
||||
fireDestroy() { for (const cb of this.handlers.destroy.slice()) cb(this); }
|
||||
}
|
||||
|
||||
function build({ root, extensionPath }) {
|
||||
const config = new Map();
|
||||
const commands = new Map();
|
||||
const configApi = {
|
||||
get: (k) => (config.has(k) ? config.get(k) : null),
|
||||
set: (k, v) => config.set(k, v),
|
||||
onDidChange: () => new Disposable(),
|
||||
};
|
||||
|
||||
const nova = {
|
||||
extension: { path: extensionPath },
|
||||
config: configApi,
|
||||
commands: {
|
||||
register: (name, cb) => { commands.set(name, cb); return new Disposable(() => commands.delete(name)); },
|
||||
},
|
||||
path: {
|
||||
join: (...a) => nodePath.join(...a),
|
||||
dirname: (p) => nodePath.dirname(p),
|
||||
basename: (p) => nodePath.basename(p),
|
||||
extname: (p) => nodePath.extname(p),
|
||||
isAbsolute: (p) => nodePath.isAbsolute(p),
|
||||
relative: (a, b) => nodePath.relative(a, b),
|
||||
splitext: (p) => { const e = nodePath.extname(p); return [p.slice(0, p.length - e.length), e]; },
|
||||
},
|
||||
fs: {
|
||||
F_OK: fs.constants.F_OK,
|
||||
access: (p) => fs.existsSync(p),
|
||||
mkdir: (p) => fs.mkdirSync(p),
|
||||
copy: (a, b) => fs.copyFileSync(a, b),
|
||||
remove: (p) => fs.unlinkSync(p),
|
||||
rmdir: (p) => fs.rmdirSync(p),
|
||||
listdir: (p) => fs.readdirSync(p),
|
||||
stat: (p) => { const st = fs.statSync(p); return { isDirectory: () => st.isDirectory() }; },
|
||||
open: (p, mode) => {
|
||||
if (mode === "w") {
|
||||
const chunks = [];
|
||||
return { write: (s) => chunks.push(s), close: () => fs.writeFileSync(p, chunks.join("")) };
|
||||
}
|
||||
const text = fs.readFileSync(p, "utf8");
|
||||
return { read: () => text, close: () => {} };
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
path: root,
|
||||
previewRootPath: root,
|
||||
config: { get: () => null, set: () => {} },
|
||||
textEditors: [],
|
||||
activeTextEditor: null,
|
||||
openFile: async (p) => {
|
||||
let editor = nova.workspace.textEditors.find((e) => e.document.path === p);
|
||||
if (!editor) {
|
||||
editor = new FakeEditor(p, require('fs').readFileSync(p, 'utf8'));
|
||||
nova.workspace.textEditors.push(editor);
|
||||
}
|
||||
return editor;
|
||||
},
|
||||
showInformativeMessage: (m) => messages.push(["info", m]),
|
||||
showErrorMessage: (m) => messages.push(["error", m]),
|
||||
showActionPanel: (m, o, cb) => cb(0),
|
||||
},
|
||||
};
|
||||
|
||||
const messages = [];
|
||||
nova.__messages = messages;
|
||||
nova.__invoke = (name, ...args) => commands.get(name)(...args);
|
||||
nova.__close = (editor) => {
|
||||
nova.workspace.textEditors = nova.workspace.textEditors.filter((e) => e !== editor);
|
||||
editor.fireDestroy();
|
||||
};
|
||||
return nova;
|
||||
}
|
||||
|
||||
module.exports = { build, FakeEditor, Disposable, CompositeDisposable, Range };
|
||||
Reference in New Issue
Block a user