2025-08-25 11:41:08 +00:00
|
|
|
import esbuild from "esbuild";
|
|
|
|
|
import process from "process";
|
2026-05-17 12:13:19 +00:00
|
|
|
import { builtinModules as builtins } from "node:module";
|
2025-12-04 23:04:20 +00:00
|
|
|
import { copyFileSync, mkdirSync, existsSync, readdirSync, statSync, readFileSync, writeFileSync, unlinkSync, watch as fsWatch } from "fs";
|
2025-11-24 21:29:54 +00:00
|
|
|
import { join } from "path";
|
|
|
|
|
import esbuildSvelte from "esbuild-svelte";
|
|
|
|
|
import { sveltePreprocess } from "svelte-preprocess";
|
2025-09-08 14:50:06 +00:00
|
|
|
|
2025-08-25 11:41:08 +00:00
|
|
|
const banner =
|
2025-12-04 23:04:20 +00:00
|
|
|
`/*
|
2025-08-25 11:41:08 +00:00
|
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
|
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
|
|
|
*/
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const prod = (process.argv[2] === "production");
|
|
|
|
|
|
2025-11-24 21:29:54 +00:00
|
|
|
// Clean up build artifacts that aren"t needed (main.css, main.js, font files)
|
|
|
|
|
const CLEANUP_BUILD_ARTIFACTS = true; // Set to false if you need to debug these files
|
|
|
|
|
|
2025-09-16 16:14:37 +00:00
|
|
|
// Function to copy directory recursively
|
|
|
|
|
function copyDir(src, dest) {
|
|
|
|
|
if (!existsSync(dest)) {
|
|
|
|
|
mkdirSync(dest, { recursive: true });
|
|
|
|
|
}
|
2025-11-24 21:29:54 +00:00
|
|
|
|
2025-09-16 16:14:37 +00:00
|
|
|
const files = readdirSync(src);
|
2025-11-24 21:29:54 +00:00
|
|
|
|
2025-09-16 16:14:37 +00:00
|
|
|
for (const file of files) {
|
|
|
|
|
const srcPath = join(src, file);
|
|
|
|
|
const destPath = join(dest, file);
|
2025-11-24 21:29:54 +00:00
|
|
|
|
2025-09-16 16:14:37 +00:00
|
|
|
if (statSync(srcPath).isDirectory()) {
|
|
|
|
|
copyDir(srcPath, destPath);
|
|
|
|
|
} else {
|
|
|
|
|
copyFileSync(srcPath, destPath);
|
|
|
|
|
console.log(`📁 Copied: ${srcPath} → ${destPath}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 16:58:23 +00:00
|
|
|
// Neutralises `new Function(...)` literals in the bundled output.
|
|
|
|
|
//
|
2026-06-28 13:06:56 +00:00
|
|
|
// All such literals come from dependencies, never from our own source, and every
|
|
|
|
|
// one is dead code in our usage:
|
2026-05-31 16:58:23 +00:00
|
|
|
// - diff2html/@profoundlogic/hogan: 2x template compilers reachable only via
|
|
|
|
|
// Hogan.compile(), which diff2html invokes only for runtime `rawTemplates`.
|
|
|
|
|
// We use diff2html's precompiled defaults, so these never run.
|
|
|
|
|
//
|
2026-06-28 13:06:56 +00:00
|
|
|
// We rewrite them to a throwing stub so the bundle contains no `new Function(`
|
|
|
|
|
// literal, which clears the Obsidian scanner's "Dynamic Code Execution" warning. The
|
|
|
|
|
// build FAILS if the count drifts from EXPECTED_NEW_FUNCTION_COUNT, so a dependency
|
|
|
|
|
// change can never silently reintroduce a live (or un-neutralised) dynamic-eval path.
|
|
|
|
|
//
|
2026-06-28 14:08:50 +00:00
|
|
|
// 2 = Hogan.
|
|
|
|
|
const EXPECTED_NEW_FUNCTION_COUNT = 2;
|
2026-05-31 16:58:23 +00:00
|
|
|
const NEW_FUNCTION_STUB = "VKBlockedDynamicFn";
|
|
|
|
|
function neutraliseDynamicEval(outfile) {
|
|
|
|
|
if (!existsSync(outfile)) return;
|
|
|
|
|
let contents = readFileSync(outfile, "utf-8");
|
|
|
|
|
const matches = contents.match(/new Function\s*\(/g) || [];
|
|
|
|
|
if (matches.length !== EXPECTED_NEW_FUNCTION_COUNT) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`neutraliseDynamicEval: expected ${EXPECTED_NEW_FUNCTION_COUNT} ` +
|
|
|
|
|
`\`new Function(\` literals in ${outfile} but found ${matches.length}. ` +
|
|
|
|
|
`A dependency changed — re-audit which library introduced/removed the ` +
|
|
|
|
|
`call and confirm it is unreachable before updating EXPECTED_NEW_FUNCTION_COUNT.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
// Throwing stub: callable as `new VKBlockedDynamicFn(...)` with any args.
|
|
|
|
|
const stubDecl =
|
|
|
|
|
`function ${NEW_FUNCTION_STUB}(){throw new Error("Dynamic code execution is disabled in this build.")}`;
|
|
|
|
|
contents = contents.replace(/new Function\s*\(/g, `new ${NEW_FUNCTION_STUB}(`);
|
|
|
|
|
// Insert the stub right after the generated banner comment (a hoisted function
|
|
|
|
|
// declaration is in scope for the whole module regardless of position).
|
|
|
|
|
const bannerEnd = contents.indexOf("*/");
|
|
|
|
|
if (bannerEnd !== -1) {
|
|
|
|
|
const insertAt = contents.indexOf("\n", bannerEnd) + 1;
|
|
|
|
|
contents = contents.slice(0, insertAt) + stubDecl + "\n" + contents.slice(insertAt);
|
|
|
|
|
} else {
|
|
|
|
|
contents = `${stubDecl}\n${contents}`;
|
|
|
|
|
}
|
|
|
|
|
writeFileSync(outfile, contents);
|
|
|
|
|
console.log(
|
|
|
|
|
`🛡️ Neutralised ${matches.length} \`new Function(\` literal(s) in ${outfile}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 13:06:56 +00:00
|
|
|
// Runs the dynamic-eval neutralisation on every build and rebuild, for both dev and
|
|
|
|
|
// production, so the dev bundle matches what ships and the post-build step is
|
|
|
|
|
// exercised continuously rather than only at release time. Registered last so it
|
|
|
|
|
// transforms the finalised main.js.
|
2026-05-31 16:58:23 +00:00
|
|
|
const dynamicEvalPlugin = {
|
|
|
|
|
name: "neutralise-dynamic-eval",
|
|
|
|
|
setup(build) {
|
|
|
|
|
build.onEnd(() => {
|
|
|
|
|
neutraliseDynamicEval(build.initialOptions.outfile);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-24 21:29:54 +00:00
|
|
|
// Plugin to merge CSS files into styles.css and cleanup build artifacts
|
|
|
|
|
const cssMergerPlugin = {
|
|
|
|
|
name: "css-merger",
|
|
|
|
|
setup(build) {
|
|
|
|
|
build.onEnd(() => {
|
|
|
|
|
// esbuild outputs CSS as main.css (based on outfile: main.js)
|
|
|
|
|
const generatedCss = "main.css";
|
|
|
|
|
const customCssDir = "Styles";
|
|
|
|
|
const outputCss = "styles.css";
|
|
|
|
|
|
|
|
|
|
let mergedCss = "";
|
|
|
|
|
|
|
|
|
|
// Read generated CSS from dependencies if it exists
|
|
|
|
|
if (existsSync(generatedCss)) {
|
|
|
|
|
mergedCss += readFileSync(generatedCss, "utf-8");
|
|
|
|
|
mergedCss += "\n\n/* Custom Styles */\n\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Append custom CSS files from styles directory
|
|
|
|
|
if (existsSync(customCssDir)) {
|
|
|
|
|
const cssFiles = readdirSync(customCssDir)
|
|
|
|
|
.filter(file => file.endsWith(".css"))
|
|
|
|
|
.sort();
|
|
|
|
|
|
|
|
|
|
for (const cssFile of cssFiles) {
|
|
|
|
|
const cssPath = join(customCssDir, cssFile);
|
|
|
|
|
mergedCss += readFileSync(cssPath, "utf-8");
|
|
|
|
|
mergedCss += "\n\n";
|
|
|
|
|
console.log(`📦 Merged: ${cssPath}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write merged CSS to styles.css
|
|
|
|
|
writeFileSync(outputCss, mergedCss);
|
|
|
|
|
console.log(`✅ Generated: ${outputCss}`);
|
|
|
|
|
|
|
|
|
|
// Clean up build artifacts if enabled
|
|
|
|
|
if (CLEANUP_BUILD_ARTIFACTS) {
|
|
|
|
|
// Remove main.css (intermediate CSS file)
|
|
|
|
|
if (existsSync(generatedCss)) {
|
|
|
|
|
unlinkSync(generatedCss);
|
|
|
|
|
console.log(`🗑️ Removed: ${generatedCss}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove KaTeX font files
|
2025-12-04 23:04:20 +00:00
|
|
|
let anyRemoved = false;
|
2025-11-24 21:29:54 +00:00
|
|
|
const fontExtensions = [".woff", ".woff2", ".ttf"];
|
|
|
|
|
const files = readdirSync(".");
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
const ext = file.substring(file.lastIndexOf("."));
|
|
|
|
|
if (fontExtensions.includes(ext)) {
|
|
|
|
|
unlinkSync(file);
|
2025-12-04 23:04:20 +00:00
|
|
|
anyRemoved = true;
|
2025-11-24 21:29:54 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-04 23:04:20 +00:00
|
|
|
if (anyRemoved) {
|
|
|
|
|
console.log("🗑️ Removed KaTeX Font Files");
|
|
|
|
|
}
|
2025-11-24 21:29:54 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-21 17:20:31 +00:00
|
|
|
const buildOptions = {
|
2025-12-04 23:04:20 +00:00
|
|
|
plugins: [
|
|
|
|
|
esbuildSvelte({
|
|
|
|
|
compilerOptions: { css: "injected" },
|
|
|
|
|
preprocess: sveltePreprocess(),
|
|
|
|
|
}),
|
|
|
|
|
cssMergerPlugin,
|
2026-05-31 16:58:23 +00:00
|
|
|
dynamicEvalPlugin,
|
2025-12-04 23:04:20 +00:00
|
|
|
],
|
|
|
|
|
banner: {
|
|
|
|
|
js: banner,
|
|
|
|
|
},
|
|
|
|
|
entryPoints: ["main.ts"],
|
|
|
|
|
bundle: true,
|
|
|
|
|
define: {
|
|
|
|
|
"process.env.NODE_ENV": JSON.stringify(prod ? "production" : "development"),
|
|
|
|
|
},
|
|
|
|
|
external: [
|
|
|
|
|
"obsidian",
|
|
|
|
|
"electron",
|
|
|
|
|
"@codemirror/autocomplete",
|
|
|
|
|
"@codemirror/collab",
|
|
|
|
|
"@codemirror/commands",
|
|
|
|
|
"@codemirror/language",
|
|
|
|
|
"@codemirror/lint",
|
|
|
|
|
"@codemirror/search",
|
|
|
|
|
"@codemirror/state",
|
|
|
|
|
"@codemirror/view",
|
|
|
|
|
"@lezer/common",
|
|
|
|
|
"@lezer/highlight",
|
|
|
|
|
"@lezer/lr",
|
|
|
|
|
...builtins],
|
|
|
|
|
format: "cjs",
|
2026-05-05 19:54:34 +00:00
|
|
|
target: "es2022",
|
2025-12-04 23:04:20 +00:00
|
|
|
logLevel: "info",
|
|
|
|
|
sourcemap: prod ? false : "inline",
|
|
|
|
|
treeShaking: true,
|
|
|
|
|
outfile: "main.js",
|
|
|
|
|
minify: prod,
|
|
|
|
|
loader: {
|
|
|
|
|
".css": "css",
|
2026-05-31 23:04:39 +00:00
|
|
|
".svg": "text",
|
|
|
|
|
".png": "dataurl",
|
2025-12-04 23:04:20 +00:00
|
|
|
".ttf": "file",
|
|
|
|
|
".woff": "file",
|
|
|
|
|
".woff2": "file",
|
|
|
|
|
},
|
2025-09-26 19:48:05 +00:00
|
|
|
};
|
2025-08-25 11:41:08 +00:00
|
|
|
|
|
|
|
|
if (prod) {
|
2025-12-04 23:04:20 +00:00
|
|
|
await esbuild.build(buildOptions);
|
|
|
|
|
console.log("✅ Production build complete!");
|
2025-08-25 11:41:08 +00:00
|
|
|
} else {
|
2025-12-04 23:04:20 +00:00
|
|
|
const ctx = await esbuild.context(buildOptions);
|
|
|
|
|
await ctx.watch();
|
|
|
|
|
|
|
|
|
|
// Watch Styles directory for CSS changes
|
|
|
|
|
if (existsSync("Styles")) {
|
|
|
|
|
fsWatch("Styles", { recursive: true }, (_eventType, filename) => {
|
|
|
|
|
if (filename && filename.endsWith(".css")) {
|
|
|
|
|
console.log(`🔄 CSS file changed: ${filename} - Rebuilding...`);
|
|
|
|
|
ctx.rebuild();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-16 16:14:37 +00:00
|
|
|
}
|