181 lines
5.7 KiB
JavaScript
181 lines
5.7 KiB
JavaScript
import {
|
|
createReadStream,
|
|
existsSync,
|
|
mkdtempSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { createServer } from "node:http";
|
|
import { homedir, tmpdir } from "node:os";
|
|
import { basename, extname, join, normalize, resolve } from "node:path";
|
|
import { extractFile } from "@electron/asar";
|
|
import esbuild from "esbuild";
|
|
import sveltePlugin from "esbuild-svelte";
|
|
|
|
const root = new URL("..", import.meta.url).pathname;
|
|
const port = Number(process.env.PORT ?? 4173);
|
|
|
|
const ASAR_CANDIDATES = [
|
|
process.env.OBSIDIAN_ASAR,
|
|
"/Applications/Obsidian.app/Contents/Resources/obsidian.asar",
|
|
join(homedir(), "Applications/Obsidian.app/Contents/Resources/obsidian.asar"),
|
|
"/opt/Obsidian/resources/obsidian.asar",
|
|
"/usr/lib/obsidian/resources/obsidian.asar",
|
|
].filter(Boolean);
|
|
|
|
function findAppCss() {
|
|
for (const candidate of ASAR_CANDIDATES) {
|
|
if (!existsSync(candidate)) continue;
|
|
try {
|
|
const path = join(mkdtempSync(join(tmpdir(), "bcal-app-")), "app.css");
|
|
writeFileSync(path, extractFile(candidate, "app.css"));
|
|
return path;
|
|
} catch {
|
|
/* try the next candidate */
|
|
}
|
|
}
|
|
console.warn(
|
|
"preview: could not read app.css from an Obsidian install. Set " +
|
|
"OBSIDIAN_ASAR to point at obsidian.asar. Without it, button and input " +
|
|
"chrome will not match the real app.",
|
|
);
|
|
return null;
|
|
}
|
|
|
|
function findTheme() {
|
|
if (process.env.THEME) return process.env.THEME;
|
|
const vault = process.env.VAULT;
|
|
if (!vault) return null;
|
|
const themes = join(vault, ".obsidian/themes");
|
|
if (!existsSync(themes)) return null;
|
|
for (const name of readdirSync(themes)) {
|
|
const css = join(themes, name, "theme.css");
|
|
if (existsSync(css)) return css;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const appCss = findAppCss();
|
|
const theme = findTheme();
|
|
if (!theme) {
|
|
console.warn(
|
|
"preview: no community theme loaded. Set VAULT to a vault path, or " +
|
|
"THEME to a theme.css, to preview against one.",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Other plugins' stylesheets, loaded after ours the way Obsidian would load a
|
|
* plugin enabled later: EXTRA_CSS=path[,path…]. This is how a cascade fight
|
|
* with another Tailwind build is reproduced outside Obsidian.
|
|
*/
|
|
const extraCss = (process.env.EXTRA_CSS ?? "")
|
|
.split(",")
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
.map((item) => resolve(item));
|
|
for (const file of extraCss) {
|
|
if (!existsSync(file)) console.warn(`preview: EXTRA_CSS not found: ${file}`);
|
|
}
|
|
|
|
/** Real boards instead of the fixtures: BOARDS=<folder of .md boards>. */
|
|
const boards = process.env.BOARDS ? resolve(process.env.BOARDS) : null;
|
|
if (boards && !existsSync(boards)) {
|
|
console.warn(`preview: BOARDS folder not found: ${boards}`);
|
|
}
|
|
|
|
const ctx = await esbuild.context({
|
|
entryPoints: [join(root, "preview/entry.ts")],
|
|
bundle: true,
|
|
format: "iife",
|
|
target: "es2022",
|
|
outfile: join(root, "preview/bundle.js"),
|
|
logLevel: "info",
|
|
sourcemap: "inline",
|
|
conditions: ["svelte", "browser"],
|
|
mainFields: ["svelte", "browser", "module", "main"],
|
|
alias: { obsidian: join(root, "tests/obsidian-stub.ts") },
|
|
plugins: [sveltePlugin({ compilerOptions: { css: "injected", runes: true } })],
|
|
});
|
|
await ctx.rebuild();
|
|
await ctx.watch();
|
|
|
|
const TYPES = {
|
|
".css": "text/css; charset=utf-8",
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".json": "application/json; charset=utf-8",
|
|
".md": "text/plain; charset=utf-8",
|
|
};
|
|
|
|
function boardNames() {
|
|
if (!boards || !existsSync(boards)) return null;
|
|
return readdirSync(boards)
|
|
.filter((name) => name.endsWith(".md"))
|
|
.sort();
|
|
}
|
|
|
|
createServer((request, response) => {
|
|
const url = decodeURIComponent((request.url ?? "/").split("?")[0]);
|
|
const external = { "/app.css": appCss, "/theme.css": theme };
|
|
|
|
if (url in external) {
|
|
const file = external[url];
|
|
if (!file) {
|
|
response.writeHead(200, { "content-type": TYPES[".css"] }).end("");
|
|
return;
|
|
}
|
|
response.writeHead(200, { "content-type": TYPES[".css"], "cache-control": "no-store" });
|
|
createReadStream(file).pipe(response);
|
|
return;
|
|
}
|
|
|
|
if (url === "/extra.css") {
|
|
const body = extraCss
|
|
.filter((file) => existsSync(file))
|
|
.map((file) => `/* ${file} */\n${readFileSync(file, "utf8")}`)
|
|
.join("\n");
|
|
response
|
|
.writeHead(200, { "content-type": TYPES[".css"], "cache-control": "no-store" })
|
|
.end(body);
|
|
return;
|
|
}
|
|
|
|
if (url === "/boards.json") {
|
|
const names = boardNames();
|
|
const body = names ? { folder: basename(boards), names } : null;
|
|
response
|
|
.writeHead(200, { "content-type": TYPES[".json"], "cache-control": "no-store" })
|
|
.end(JSON.stringify(body));
|
|
return;
|
|
}
|
|
|
|
if (url.startsWith("/boards/") && boards) {
|
|
const file = join(boards, normalize(url.slice("/boards/".length)));
|
|
if (!file.startsWith(boards) || !existsSync(file) || statSync(file).isDirectory()) {
|
|
response.writeHead(404).end("not found");
|
|
return;
|
|
}
|
|
response.writeHead(200, { "content-type": TYPES[".md"], "cache-control": "no-store" });
|
|
createReadStream(file).pipe(response);
|
|
return;
|
|
}
|
|
|
|
const file = join(root, normalize(url === "/" ? "/preview/index.html" : url));
|
|
if (!file.startsWith(root) || !existsSync(file) || statSync(file).isDirectory()) {
|
|
response.writeHead(404).end("not found");
|
|
return;
|
|
}
|
|
response.writeHead(200, {
|
|
"content-type": TYPES[extname(file)] ?? "application/octet-stream",
|
|
"cache-control": "no-store",
|
|
});
|
|
createReadStream(file).pipe(response);
|
|
}).listen(port, () => {
|
|
console.log(`preview: http://localhost:${port}/`);
|
|
if (boards) console.log(`preview: boards from ${boards}`);
|
|
if (extraCss.length > 0) console.log(`preview: extra css ${extraCss.join(", ")}`);
|
|
});
|