Files
Trilium/flake.nix
T
Elian DoranandClaude Opus 5.5 fb8518b549 build(share-theme): bundle the share theme with the client's Vite build
The share theme had a build of its own (esbuild, `packages/share-theme/scripts/build.ts`) whose
`dist/` the server, desktop, edit-docs, build-docs and standalone builds copied. It could share
nothing with the app: the previous commit had to load the client's mermaid through a dedicated
`share_mermaid` entry and a manifest to avoid shipping it twice. Any further library the share
pages want from the app (FullCalendar for collections) would need the same machinery.

`shareTheme()` in `apps/client/vite-plugins.mts` now builds the theme as part of the app, in both
the client and the standalone build:

- `scripts.js` is emitted as a chunk of the app's build, into `src/` beside the app's chunks, so
  the two share what both import. Mermaid is a plain `import("mermaid")` again; the
  `share_mermaid` entry, its manifest and the `/share/assets/client/` route are gone.
- `scripts.css` comes from a build of its own. In the app's build, the stylesheets the theme
  shares with the app (tabs, multicolumn, collapsible blocks, to-do lists) land in shared chunks,
  which put them before `ck-content.css` and changed the cascade. Built alone with
  `cssCodeSplit: false`, the stylesheet keeps the order `index.ts` imports in: a rule-by-rule
  comparison with the esbuild output shows the same rules in the same order, apart from
  lightningcss rewrites and six heading rules it drops as overridden.
- `tree.js` also builds alone, so it stays one file: the first paint waits for it, and the app's
  build would move what it shares with `scripts.js` into a chunk.
- Both separate builds target `chrome96`, as the esbuild build did, so `scripts.css` keeps
  `max-width` media queries rather than range syntax. `scripts.js` and its chunks use the
  client's `esnext`, as the shared mermaid already did.
- `src/share_theme.json` lists the files a page can load and, apart, the files only mermaid
  loads (130 files, 5 MB). The static export copies the first set always and the second only
  for a subtree with a diagram (`getShareThemeExportFiles()`). The stylesheets of the chunks
  `scripts.js` imports statically are in `scripts.css`, so they are listed only when a chunk
  loaded on demand imports them, since Vite's preload helper requests them then.

Serving:

- The server maps `/share/assets/` to the client build's `src/`. In development, the client's Vite
  middleware answers `scripts.js` and `tree.js` from source and `scripts.css` empty, since Vite
  injects the styles from the modules; a script or style change no longer needs
  `pnpm --filter share-theme dev`.
- A static host cannot map `/share/assets/` to `src/`, so the standalone build also writes the
  three files to `share/assets/`, `scripts.js` and `scripts.css` as one-line stubs importing their
  namesakes in `src/`.
- A share-theme export needs the client build: a development server without one now fails with
  a message instead of exporting without mermaid, and standalone's development server, which
  builds no share theme, says it needs a production build.

The eager load of a page is 19 KB of JavaScript (14 KB before), as the shared chunks bring
Vite's preload helper; the stylesheet is 94 KB (91 KB).

The share theme's own scripts, `esbuild` and `sass` go, along with `share-theme` in `prepare`,
the `triggerBuildAndCopyTo` calls and their `script` parameter, and the copy of `dist/` in the
Nix build-docs package.

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

438 lines
17 KiB
Nix

{
description = "Trilium Notes (experimental flake)";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
pnpm2nix = {
url = "github:FliegendeWurst/pnpm2nix-nzbr/main";
inputs = {
flake-utils.follows = "flake-utils";
nixpkgs.follows = "nixpkgs";
};
};
};
outputs =
{
self,
nixpkgs,
flake-utils,
pnpm2nix,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs {
inherit system;
config = {
allowUnfreePredicate = pkg:
system == "aarch64-darwin" && nixpkgs.lib.getName pkg == "google-chrome";
};
};
electronVersion = packageJsonDesktop.devDependencies.electron;
# `or null` because a major bump lands in apps/desktop/package.json long before
# nixpkgs has the matching electron_<major> attribute; without it the flake dies
# with an attribute error instead of falling through to the pinned binary below.
electronFromNixpkgs = pkgs."electron_${lib.versions.major electronVersion}" or null;
# nixpkgs lags behind the Electron version pinned in apps/desktop/package.json —
# often by a whole major — and its source build cannot be bumped without
# upstream's Chromium dependency hashes. Build the exact pinned version from
# Electron's official binary release instead, reusing the nixpkgs builder.
#
# Don't refresh these by hand — `pnpm chore:update-flake-electron` rewrites both
# bindings from the release's SHASUMS256.txt, and the update-nix-flake workflow
# opens a PR whenever apps/desktop/package.json moves ahead of the pin.
pinnedElectronVersion = "44.5.1";
pinnedElectronHashes = {
x86_64-linux = "5bcd217611d6843ececd6c9e9c1fcd1da3ab066c43d8b1a9e4b44689a1fba6f5";
aarch64-linux = "ee1790d743af1abd6a7e3971589dcd8635b58dab51ca1359123ba5216ce3a453";
aarch64-darwin = "1d75703019bb16461ae65f3081d7e6f5c0b11e901d0ccb5c343bcf7bcdd6435c";
headers = "07qjxn071d21rcadjsawdvq3dj8ypsbkkq0nhr8bj0k4vzxigya0";
};
mkElectronBin = pkgs.callPackage (
pkgs.path + "/pkgs/development/tools/electron/binary/generic.nix"
) { };
# The nixpkgs Linux builder rewrites the rpath of Electron's ANGLE libraries with
# an unguarded `patchelf ... lib*GL*`. Electron 44 links ANGLE into the main binary
# and ships no libEGL.so/libGLESv2.so, so the glob expands to nothing and patchelf
# exits with "missing filename". Let that one command tolerate an empty match; it
# still patches the libraries on releases that do ship them.
#
# NixOS/nixpkgs@b3041dc18a skips that patchelf for Electron >= 44, so against a
# newer nixpkgs (e.g. via `inputs.nixpkgs.follows`) the replacement is a no-op.
# Drop this override once flake.lock's nixpkgs includes that commit.
angleLibGlob = "$out/libexec/electron/lib*GL*";
tolerateMissingAngleLibs =
drv:
drv.overrideAttrs (prev: {
postFixup = builtins.replaceStrings [ angleLibGlob ] [ "${angleLibGlob} || true" ] prev.postFixup;
});
# Guarded on Linux because only that branch of the builder defines postFixup.
pinnedElectron =
let
bin = mkElectronBin pinnedElectronVersion pinnedElectronHashes;
in
if stdenv.hostPlatform.isLinux then tolerateMissingAngleLibs bin else bin;
electron =
if electronFromNixpkgs != null && electronFromNixpkgs.version == electronVersion then
electronFromNixpkgs
else
lib.throwIf (pinnedElectronVersion != electronVersion) ''
flake.nix pins Electron ${pinnedElectronVersion}, but apps/desktop/package.json wants ${electronVersion}.
Refresh pinnedElectronVersion/pinnedElectronHashes in flake.nix, or drop the override if nixpkgs ships ${electronVersion}.
'' pinnedElectron;
nodejs = pkgs.nodejs_24;
# pnpm creates an overly long PATH env variable for child processes.
# This patch deduplicates entries in PATH, which results in an equivalent but shorter entry.
# https://github.com/pnpm/pnpm/issues/6106
# https://github.com/pnpm/pnpm/issues/8552
pnpm = (pkgs.pnpm_11.overrideAttrs (prev: {
postInstall = prev.postInstall + ''
patch $out/libexec/pnpm/dist/pnpm.mjs ${./patches/pnpm-PATH-reduction.patch}
'';
# pnpm sometimes fails with ERR_PNPM_ENOENT
# https://github.com/pnpm/pnpm/issues/12880
# "fix" from https://github.com/dniku/selfhostblocks/commit/ee6fc4f04fe34714fb8676704048d8d76aba85b7
postFixup = (prev.postFixup or "") + (if pkgs.stdenv.hostPlatform.isLinux then ''
mv "$out/bin/pnpm" "$out/bin/.pnpm-unwrapped"
cat > "$out/bin/pnpm" <<EOF
#!${stdenv.shell}
if [ "\''${1-}" = install ]; then
cpuMask="\$(${lib.getExe' pkgs.util-linux "taskset"} -cp "\$\$")"
# Keep only the cpuset reported after the colon.
cpuMask="\''${cpuMask##*: }"
# Pick the first comma-separated segment from the cpuset.
firstCpu="\''${cpuMask%%,*}"
# If that segment is a range, pick its first CPU.
firstCpu="\''${firstCpu%%-*}"
exec ${lib.getExe' pkgs.util-linux "taskset"} -c "\$firstCpu" "$out/bin/.pnpm-unwrapped" "\$@"
else
exec "$out/bin/.pnpm-unwrapped" "\$@"
fi
EOF
chmod +x "$out/bin/pnpm"
'' else "");
}));
inherit (pkgs)
copyDesktopItems
darwin
lib
makeBinaryWrapper
makeDesktopItem
makeShellWrapper
removeReferencesTo
stdenv
wrapGAppsHook3
xcodebuild
which
;
fullCleanSourceFilter =
name: type:
(lib.cleanSourceFilter name type)
&& (
let
baseName = baseNameOf (toString name);
in
# No need to copy the flake.
# No need to copy local copy of node_modules.
baseName != "flake.nix" && baseName != "flake.lock" && baseName != "node_modules"
);
fullCleanSource =
src:
lib.cleanSourceWith {
filter = fullCleanSourceFilter;
src = src;
};
# Minimal source used for pnpm2nix's dependency-fetching derivation.
# Only the files pnpm actually needs to resolve and fetch dependencies
# are included, so unrelated source changes don't bust the deps cache.
workspaceSourceFilter =
name: type:
let
baseName = baseNameOf (toString name);
rootStr = toString ./.;
relPath = lib.removePrefix "${rootStr}/" (toString name);
inPatches = relPath == "patches" || lib.hasPrefix "patches/" relPath;
in
(lib.cleanSourceFilter name type)
&& baseName != "node_modules"
&& (
type == "directory"
|| baseName == "package.json"
|| baseName == "pnpm-workspace.yaml"
|| baseName == "pnpm-lock.yaml"
|| inPatches
);
workspaceSource = lib.cleanSourceWith {
filter = workspaceSourceFilter;
src = ./.;
};
packageJson = builtins.fromJSON (builtins.readFile ./package.json);
packageJsonDesktop = builtins.fromJSON (builtins.readFile ./apps/desktop/package.json);
makeApp =
{
app,
buildTask,
mainProgram,
installCommands,
preBuildCommands ? "",
}:
pnpm2nix.packages.${system}.mkPnpmPackage rec {
pname = "trilium-${app}";
version = packageJson.version + (lib.optionalString (self ? shortRev) "-${self.shortRev}");
src = fullCleanSource ./.;
packageJSON = ./package.json;
pnpmLockYaml = ./pnpm-lock.yaml;
workspace = workspaceSource;
pnpmWorkspaceYaml = ./pnpm-workspace.yaml;
inherit nodejs pnpm;
extraNodeModuleSources = [
rec {
name = "patches";
value = ./patches;
}
];
# avoid ERR_PNPM_RESOLUTION_SHAPE_MISMATCH errors
env.PNPM_CONFIG_TRUST_LOCKFILE = "true";
# remove pnpm version override
preConfigure = ''
node -e "const p = require('./package.json'); delete p.packageManager; require('fs').writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n')"
'';
postConfigure =
''
chmod +x node_modules/electron/install.js
patchShebangs --build node_modules
'';
extraNativeBuildInputs =
[
nodejs.python
removeReferencesTo
]
++ lib.optionals (app == "desktop" || app == "edit-docs") [
copyDesktopItems
# required for NIXOS_OZONE_WL expansion
# https://github.com/NixOS/nixpkgs/issues/172583
makeShellWrapper
wrapGAppsHook3
# For the launcher wrapper generated in installCommands:
which
electron
]
++ lib.optionals (app == "server" || app == "build-docs") [
makeBinaryWrapper
]
++ lib.optionals stdenv.hostPlatform.isDarwin [
xcodebuild
darwin.cctools
];
dontWrapGApps = true;
preBuild = ''
${preBuildCommands}
'';
scriptFull = "pnpm run ${buildTask}";
installPhase = ''
runHook preInstall
${installCommands}
runHook postInstall
'';
# Symlinks pointing to /build directory are not allowed in the Nix store.
# This removes all dangling symlinks that point to the temporary build directory.
postFixup = ''
find $out/opt -type l -lname '/build/*' -delete || true
'';
components = [
"packages/ckeditor5"
"packages/codemirror"
"packages/commons"
"packages/highlightjs"
"packages/turndown-plugin-gfm"
"apps/build-docs"
"apps/client"
"apps/db-compare"
"apps/desktop"
"apps/dump-db"
"apps/edit-docs"
"apps/server"
"packages/trilium-e2e"
];
desktopItems = lib.optionals (app == "desktop") [
(makeDesktopItem {
name = meta.mainProgram;
exec = meta.mainProgram;
icon = "trilium";
comment = meta.description;
desktopName = "Trilium Notes";
categories = [ "Office" ];
startupWMClass = meta.mainProgram;
})
];
meta = {
description = "Trilium: ${app}";
inherit mainProgram;
};
};
desktop = makeApp {
app = "desktop";
# better-sqlite3 v13 is N-API based and ships prebuilt binaries that
# load unchanged under Electron, so there is no native rebuild step
# (and no need for ELECTRON_NODEDIR) any more.
preBuildCommands = ''
pnpm postinstall
'';
buildTask = "desktop:build";
mainProgram = "trilium";
installCommands = ''
mkdir -p $out/{bin,share/icons/hicolor/512x512/apps,opt/trilium}
cp --archive apps/desktop/dist/* $out/opt/trilium
cp apps/client/src/assets/icon.png $out/share/icons/hicolor/512x512/apps/trilium.png
makeShellWrapper ${lib.getExe electron} $out/bin/trilium \
"''${gappsWrapperArgs[@]}" \
--add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" \
--set-default ELECTRON_IS_DEV 0 \
--set TRILIUM_RESOURCE_DIR $out/opt/trilium \
--add-flags $out/opt/trilium/main.mjs
'';
};
server = makeApp {
app = "server";
# Important note: if pnpm throws an error similar to the follow at the end of `pnpm rebuild`,
# you should ensure the version of tsx (and other dependencies mentioned in the error) is identical project-wide.
# ERR_PNPM_MISSING_HOISTED_LOCATIONS
# [email protected](@types/[email protected])([email protected])([email protected])([email protected])
# ([email protected])([email protected])([email protected])([email protected])([email protected])
# is not found in hoistedLocations inside node_modules/.modules.yaml
preBuildCommands = ''
pushd apps/server
pnpm rebuild
popd
'';
buildTask = "server:build";
mainProgram = "trilium-server";
installCommands = ''
# No better-sqlite3 cleanup needed here any more: the server build
# trims deps/, src/ and build/ (which held the store paths that used
# to need remove-references-to) out of dist itself.
mkdir -p $out/{bin,opt/trilium-server}
cp --archive apps/server/dist/* $out/opt/trilium-server
makeWrapper ${lib.getExe nodejs} $out/bin/trilium-server \
--add-flags $out/opt/trilium-server/main.mjs
'';
};
edit-docs = makeApp {
app = "edit-docs";
preBuildCommands = ''
pnpm postinstall
'';
buildTask = "edit-docs:build";
mainProgram = "trilium-edit-docs";
installCommands = ''
mkdir -p $out/{bin,opt/trilium-edit-docs}
cp --archive apps/edit-docs/dist/* $out/opt/trilium-edit-docs
makeShellWrapper ${lib.getExe electron} $out/bin/trilium-edit-docs \
--set-default ELECTRON_IS_DEV 0 \
--set TRILIUM_RESOURCE_DIR $out/opt/trilium-edit-docs \
--add-flags $out/opt/trilium-edit-docs/edit-docs.cjs
'';
};
build-docs = makeApp {
app = "build-docs";
preBuildCommands = ''
pushd apps/server
pnpm rebuild || true
popd
'';
buildTask = "client:build && pnpm run server:build && pnpm run --filter build-docs build";
mainProgram = "trilium-build-docs";
installCommands = ''
mkdir -p $out/{bin,opt/trilium-build-docs}
# Copy build-docs dist
cp --archive apps/build-docs/dist/* $out/opt/trilium-build-docs
# Copy server dist (needed for runtime)
mkdir -p $out/opt/trilium-build-docs/server
cp --archive apps/server/dist/* $out/opt/trilium-build-docs/server/
# Copy client dist (needed for runtime)
mkdir -p $out/opt/trilium-build-docs/client
cp --archive apps/client/dist/* $out/opt/trilium-build-docs/client/
# Create wrapper script
makeWrapper ${lib.getExe nodejs} $out/bin/trilium-build-docs \
--add-flags $out/opt/trilium-build-docs/cli.cjs \
--set TRILIUM_RESOURCE_DIR $out/opt/trilium-build-docs/server
'';
};
chrome =
if stdenv.hostPlatform.system == "aarch64-darwin" then
pkgs.google-chrome
else
pkgs.chromium;
in
{
packages.desktop = desktop;
packages.server = server;
packages.edit-docs = edit-docs;
packages.build-docs = build-docs;
packages.default = desktop;
# Not something to install — it is here so the pinned Electron binary can be
# built (and therefore its hashes verified) on its own, without going through
# a full desktop build. The update-nix-flake workflow does exactly that.
packages.electron = electron;
devShells.default = pkgs.mkShell {
buildInputs = [
nodejs
pnpm
electron
nodejs.python
# For the browser-mode tests (packages/ckeditor5). The Chromium Playwright downloads
# for itself is dynamically linked against libraries no NixOS system provides, so it
# dies on a missing libxcb.so.1. Chromium is unsupported on aarch64-darwin, so we use
chrome
];
# Read by packages/ckeditor5/vitest.config.ts and passed to Playwright as
# `launchOptions.executablePath`. Without it Playwright launches its own Chromium and the
# suite cannot start.
CHROME_BIN = lib.getExe chrome;
};
}
);
}