Files
Trilium/packages/share-theme/scripts/build.ts
T
Elian DoranandClaude Opus 5.5 4e211b2cee perf(share): render-block only the tree's restore, not the whole theme script
Making `scripts.js` render-blocking kept the tree from showing at the
top before its position was restored, but held the first paint until
the whole theme had loaded and run: search, table of contents, theme
buttons, footer and the rest, none of which has to run first.

`setupTreeState()` and its helpers move from `navigation.ts`, which
imports CSS, to `page/tree_state.ts`, and a new `tree.ts` entry runs it
through `whenActivated()`, so a prerendered page restores once it is
shown. `build.ts` builds it as `dist/tree.js` in a second esbuild pass
without code splitting: `whenActivated()` is inlined instead of moved
into a chunk shared with `scripts.js`, so the file loads alone, 4.4KB
with no imports, and `scripts.js` drops from 30KB to 25.5KB.

The share server and the static export put `assets/tree.js` first in
`jsToLoad`, so `page.ejs`'s first-entry rule makes it the only
render-blocking script; `scripts.js` and the page's `~shareJs` scripts
load as before. The server, the export and standalone each serve or
copy the whole `dist` folder, so the file reaches all three. In
Chromium the first frame still shows the pane at its restored position.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-09 18:55:43 +03:00

120 lines
4.3 KiB
TypeScript

import path from "node:path";
import * as esbuild from "esbuild";
import { rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import {
buildVsCodeThemeCss, VS_CODE_DARK, VS_CODE_LIGHT
} from "@triliumnext/highlightjs/src/vs_code_theme.js";
import * as sass from "sass";
const packageJson = process.env.npm_package_json;
if (!packageJson) {
throw new Error("Run the build through pnpm, which sets npm_package_json.");
}
const rootDir = path.dirname(packageJson);
// Sass resolves relative paths on its own; bare specifiers such as
// "katex/src/styles/katex.scss" go through Node.
const nodeModulesImporter: sass.FileImporter<"sync"> = {
findFileUrl(url) {
if (url.startsWith(".") || url.startsWith("/")) {
return null;
}
return pathToFileURL(createRequire(path.join(rootDir, "package.json")).resolve(url));
}
};
// esbuild has no Sass support of its own. katex.scss needs it to build KaTeX's
// stylesheet without the woff and ttf faces.
const sassPlugin: esbuild.Plugin = {
name: "sass",
setup(build) {
build.onLoad({ filter: /\.scss$/ }, (args) => ({
contents: sass.compile(args.path, { importers: [nodeModulesImporter] }).css,
loader: "css",
resolveDir: path.dirname(args.path)
}));
}
};
// Serves `virtual:code-themes.css`: the VS Code highlighting themes code notes also use, one per
// share theme mode. `:where()` keeps the block rules as weak as a stock highlight.js theme's, so the
// share theme's own `.ck-content code` colors still win inside text notes.
const codeThemesPlugin: esbuild.Plugin = {
name: "code-themes",
setup(build) {
build.onResolve({ filter: /^virtual:code-themes\.css$/ }, (args) => ({
path: args.path,
namespace: "code-themes"
}));
build.onLoad({ filter: /.*/, namespace: "code-themes" }, () => ({
contents: [
buildVsCodeThemeCss(VS_CODE_LIGHT, ":where(html.theme-light)"),
buildVsCodeThemeCss(VS_CODE_DARK, ":where(html.theme-dark)")
].join("\n"),
loader: "css"
}));
}
};
const outDir = path.join(rootDir, "dist");
async function runBuild(watch: boolean) {
const before = performance.now();
// esbuild leaves its outdir as it found it, and every `pnpm install` writes an unminified
// build there, so a minified release build would land beside those files and both sets would
// be copied into the app.
rmSync(outDir, { recursive: true, force: true });
const opts: esbuild.BuildOptions = {
entryPoints: [ { in: path.join(rootDir, "src", "index.ts"), out: "scripts" } ],
bundle: true,
splitting: true,
outdir: outDir,
format: "esm",
target: ["chrome96"],
loader: {
".png": "dataurl",
".gif": "dataurl",
".woff": "file",
".woff2": "file",
".ttf": "file",
".eot": "empty",
".svg": "empty",
".html": "text",
".css": "css"
},
plugins: [ sassPlugin, codeThemesPlugin ],
logLevel: "info",
metafile: true,
minify: process.argv.includes("--minify")
};
// `tree.js` runs before the page is first drawn, so it is built without splitting: the code it
// shares with `scripts.js` is inlined rather than moved into a chunk it would wait for.
const treeOpts: esbuild.BuildOptions = {
entryPoints: [ { in: path.join(rootDir, "src", "tree.ts"), out: "tree" } ],
bundle: true,
outdir: outDir,
format: "esm",
target: opts.target,
logLevel: "info",
minify: opts.minify
};
if (watch) {
const contexts = await Promise.all([ esbuild.context(opts), esbuild.context(treeOpts) ]);
await Promise.all(contexts.map((context) => context.watch()));
} else {
const [ result ] = await Promise.all([ esbuild.build(opts), esbuild.build(treeOpts) ]);
const after = performance.now();
writeFileSync("meta.json", JSON.stringify(result.metafile, null, 2));
console.log(`Build actually took ${(after - before).toFixed(2)}ms`);
}
}
const watch = process.argv.includes("--watch");
runBuild(watch).catch(console.error);