Greptile flagged the fourteen-line block: the measurements and the rejected
--no-devel narrative belong in the commit body of the change that introduced
the filter, not at the call site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Update Nix flake workflow has been failing since the bump to Electron
44 landed on main, for two independent reasons.
parseShasums treated every entry of ELECTRON_PLATFORM_TAGS as mandatory,
and Electron 44 ships no linux-armv7l assets at all — 43.4.1 lists seven
armv7l entries in SHASUMS256.txt, 44.0.0 lists none:
Error: SHASUMS256.txt for Electron 44.0.0 is missing:
electron-v44.0.0-linux-armv7l.zip
Drop armv7l-linux from the table, and x86_64-darwin with it: the nixpkgs
rev in flake.lock (6d12004) lists only x86_64-linux, armv7l-linux,
aarch64-linux and aarch64-darwin in generic.nix's `tags`, so that hash
could never have been read. Keep the hard throw on a missing asset —
shrinking the platform set is a decision for a human, not something to
paper over by silently pinning fewer systems.
Refreshing the hashes then got the job as far as the verification step,
which failed on its own:
patchelf: missing filename
The nixpkgs Linux builder rewrites the rpath of Electron's ANGLE
libraries with an unguarded `patchelf ... lib*GL*`. Electron 43.4.1
shipped libEGL.so and libGLESv2.so; 44.0.0 links ANGLE into the main
binary and ships neither, leaving only libffmpeg.so, libvk_swiftshader.so
and libvulkan.so.1, so the glob expands to nothing. There is nothing
upstream to pull — nixpkgs master packages Electron 41-43 only and still
has the unguarded glob — so override that one command to tolerate an
empty match, guarded by a throwIf that fails loudly if the upstream text
changes rather than silently no-opping. Linux only, because the Darwin
branch of the builder defines no postFixup.
`nix build --no-link '.#electron' '.#electron.headers'` now succeeds and
the result reports v44.0.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`glob` is not a dependency of the repo, so the import resolved to a
phantom hoisted glob@7.2.3 pulled in by @electron/asar, archiver-utils
and rimraf. That version takes a single string pattern and a callback,
so the array of patterns threw `TypeError: invalid pattern` out of
minimatch, and an awaited call would have yielded undefined anyway.
Three fixed patterns of depth two do not need a glob library, and its
version would keep shifting with the lockfile. Enumerate the workspace
directories with readdir instead, matching scripts/build-utils.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`icnsutil compose` picks a chunk's OSType from the image size, so the 16px and
32px slots were stored as `icp4`/`icp5`. macOS IconServices decodes those as raw
pixel data rather than PNG, so every consumer that lands on a small
representation drew colored noise -- Raycast's result list, Finder at 16pt, and
Apple's own `iconutil -c iconset`. Spotlight draws the icon large enough to miss
it.
icnsutil also takes the OSType from the file name, so the intermediates are now
named by OSType instead of by size. That drops `icp4`/`icp5` and fills in the
slots the size-driven loop never produced: `ic08` (256) and the retina
`ic11`/`ic12`/`ic13`. Default and nightly get separate scratch directories --
they used to share `mac/` and be told apart by a `*-dev.png` glob, which cannot
survive OSType names.
The slot table avoids `declare -A`: macOS ships bash 3.2, and this is the
section a Mac user is most likely to re-run.
Also replaces `rm -r mac/*` with `rm -rf mac`. `app-icon/mac` is gitignored, so
on a fresh clone that line aborted the whole script under `set -e`.
Verified by building two throwaway `.app` bundles differing only in their icns
and drawing both through `NSWorkspace.icon(forFile:)`: before, 32pt is noise and
16pt is a cyan wash; after, both render the logo. `iconutil -c iconset` decodes
every slot of the regenerated files.
No 16pt slot is shipped. Its modern type, `ic04`, requires raw ARGB -- a PNG
payload under `ic04` is mis-decoded exactly like `icp4` -- and icnsutil needs
Pillow to produce ARGB. macOS synthesizes 16pt from `ic11` instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The main process now builds as main.mjs with dynamic-import boundaries
split into chunks/ that only load on first use; dist/package.json
points Electron at the new entry. The preload stays CJS (Electron's
sandboxed renderer cannot load ESM preloads) and image_worker.cjs
stays CJS (spawned by its path).
The ESM banner now resolves __dirname in a chunk to the bundle root
rather than chunks/: bundled code uses __dirname to find siblings of
the entry (preload.cjs, image_worker.cjs, getResourceDir), and which
chunk a module lands in must not change what the path means.
Verified by launching the built app: the renderer loads over
trilium-app:// and reaches the backend through the preload bridge.
Main-process RSS on identical boots: 348 MB (CJS) -> 287 MB (ESM).
The "Error executing query" lines at boot predate this change
(identical under CJS).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildBackend() gains format: "esm", emitting .mjs with dynamic-import
boundaries split into chunks/. The ESM banner defines require,
__filename and __dirname from import.meta.url (the mirror image of the
CJS import.meta.url shim); esbuild's interop helper picks up the
banner's require for external packages such as better-sqlite3.
Static imports of unpdf, officeparser and the Claude agent SDK become
dynamic imports inside the async methods that use them, so those
subsystems only load on first use. Under the current CJS output the
seams are inert (esbuild inlines the modules); under ESM they split.
Measured on the server bundle (Node 22.21, production build, real
146 MB database), CJS single bundle vs ESM split: settled RSS
221 -> 135 MB, JS heap post-GC 77 -> 54 MB, retained script source
28.2 -> 8.8 MB, V8 parser native allocation peak 77 -> 22 MB. Startup
loads ~9 MB of the 15 MB of emitted code; splitting also restores
seams the single bundle was flattening (core's exceljs/officeparser
dynamic imports, highlight.js per-language loading, migrations).
The server build still emits CJS: flipping it requires updating the
Dockerfiles, start-prod and the docker healthcheck, which all
reference main.cjs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`getLanguageStats()` returned the first response page, so anything past it was
invisible to both callers. The coverage gate would pass by never seeing the
languages it was meant to catch, and the README language bar would quietly drop
them.
Weblate serves 50 translations per page and each component is at 40, so the
current lists are complete and nothing was missed. That is ten languages of
headroom, and a language appears whenever someone starts translating one.
Follow `next` until it runs out, and report the count actually read. A non-OK
response now names the URL rather than surfacing as a JSON parse error on the
error body.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The website picked its languages from a hand-maintained `LOCALES` array that
nothing checked, so it had fallen behind the `website` Weblate component. Thirteen
languages above the 50% bar the client uses were bundled but unreachable, German,
Indonesian, Korean and Irish among them at 100%, while Romanian at 54.9% was
offered. Add them, reusing the endonyms already curated in `packages/commons`.
`mapLocale()` ended in `locale.split('-')[0]`, which cannot express a locale that
carries a region: `en-GB`, `pt-BR` and `nb-NO` would have collapsed to `en`, a 5.1%
`pt` and a nonexistent `nb`. Resolve against `LOCALES` instead, preferring an exact
match, then the region-less entry for the language, then any region sharing it. A
bare `nb` or `pt` now reaches `nb-NO` or `pt-BR`, and a language the website does
not offer returns `en` rather than a locale with no entry in the picker. The
region-less step matters: `LOCALES` is sorted by display name, so without it
`en-US` resolves to `en-GB`.
`check-translation-coverage.ts` now gates the `website` component alongside
`client`, so the next language to cross the threshold fails CI instead of sitting
unreachable. It reports every missing locale rather than the first, and names the
file to add it to.
`LOCALES` moves to `apps/website/src/locales.ts` because the gate runs under tsx,
which cannot evaluate the `import.meta.glob` at the top of `i18n.ts`.
`en` is renamed to "English (United States)", matching `packages/commons`, since
"English" alongside "English (United Kingdom)" names nothing in particular.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The freshness check compared the cache file mtime against a point one day
in the future, which is always true for a file already on disk. Once
`.language-stats-<project>.json` was written it was reused indefinitely, so
`check-translation-coverage.ts` and `manage-readme.ts` kept reading whatever
snapshot happened to be fetched first.
Extract the decision as `isCacheFresh()` and cover it. A future-stamped file
now counts as stale as well, so a skewed clock cannot reproduce the same
never-expiring cache.
CI is unaffected either way: the cache files are gitignored, so a fresh
checkout always queries Weblate.
The scripts project globbed `*.spec.ts`, matching only its own root, so no
spec under `scripts/translation/` would have run. Widen it to `**/*.spec.ts`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scripts project is referenced from the root solution, so `pnpm typecheck`
was building it on every run — over an empty program. `include` resolves against
the directory holding the tsconfig, but its globs were written as if they were
relative to the repository root, so `scripts/*.ts` looked for
scripts/scripts/*.ts and matched nothing. Nothing under scripts/ has been
typechecked for as long as the file has looked like this.
Widened to the whole tree, .mts included, which also covers translation/ and
repo-migration/. That found one bug: on anything other than NixOS,
`isNixOS() && getNixLdLibraryPath()` is `false` rather than `undefined`, and
Node stringifies it instead of dropping it — so `pnpm desktop:start` launched
Electron with LD_LIBRARY_PATH=false.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
swagger-jsdoc, the only thing the script ran on, was removed from the server in
eab353ca2e as an unnecessary dependency, and nothing has installed it since. Its
output, apps/server/src/assets/openapi.json, is not in the repository either,
and its `apis` globs still cover only apps/server/src/routes although most of
those routes now live in packages/trilium-core.
The @swagger blocks in the route files are left alone; they are ordinary
comments now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The notes under docs/Release Notes are exported from Trilium, so their images
are bare filenames sitting next to the Markdown. That renders in the repository,
but the publish step hands the same file to the release body, where GitHub
resolves a relative source against the releases URL — so every image would have
arrived broken. v0.105.0 is the first note to carry any.
The body is now composed by scripts/release-notes-body.mts, which rewrites those
sources to raw.githubusercontent.com URLs pinned to refs/tags/<tag>, and fails
the job when a referenced image is not in the repository. Pinning to the tag
rather than a branch keeps a published body correct if the images later move.
The script has no dependencies, so the publish job runs it off its sparse
checkout without installing the monorepo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The client config points `cacheDir` at a single repo-level directory, so
the server, desktop and edit-docs dev servers all shared one optimized-dep
cache. Vite discards that cache whenever its `configHash` changes, and the
hash differs between the Node-hosted dev server and the Electron-hosted
one — so alternating between `pnpm server:start` and `pnpm desktop:start`
re-scanned and re-bundled the whole dependency graph on every launch.
Measured on the desktop dev app, consecutive launches with nothing else
changed: 9055ms to full render with the re-optimization, 5796ms without.
The failed commits also leave orphaned `deps_temp_*` directories behind
(91 MB when measured), because on Windows the optimizer cannot delete a
`deps` directory another dev server still holds open.
Keying the cache directory on the app port removes the sharing. The perf
analyzer is taught the new layout, and the measure-startup-requests skill
picks up the same path change plus two corrections: the eager-load
offenders it listed are both fixed now (dialogs go through `LazyDialog`,
Inter ships as woff2), and a capture run without `TRILIUM_PASSWORD`
silently stops at the login page instead of failing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename the alias from `tsc-native` to the name Microsoft's TypeScript 7
announcement and Nx's guide both use, so the layout is recognisable to anyone
who has read either. No behavioural change -- same typescript@7.0.2, same
single consumer in scripts/filter-tsc-output.mts.
The rest of the documented layout is deliberately not adopted. Microsoft
recommends aliasing `typescript` to the @typescript/typescript6 compatibility
shim so the native compiler can own the `tsc` bin name; that assumes the only
consumer of the name is tooling. Here it is also a runtime dependency of
packages/codemirror, which runs the language service in the browser, and the
shim breaks it twice over:
- It ships five files and no lib.*.d.ts, so the 96 `typescript/lib/lib.*.d.ts?raw`
imports in ts_lib_files.ts fail to resolve and the client build dies.
- Giving codemirror its own real typescript to fix that splits resolution --
@typescript/vfs and @valtown/codemirror-ts are hoisted to the root and follow
the shim, codemirror's own source follows its nested copy -- so the 3.3 MB
compiler lands twice in the lazy script-note chunk. Client dist goes 69 M to
72 M.
Both were measured, not reasoned about; the CLAUDE.md note carries the numbers
so the next reader of the 7.0 announcement doesn't repeat the experiment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`tsc --build` over the full project graph took ~25s cold and ~12s after any
change to a widely-imported package such as commons. The native (Go) compiler
does the same build in ~3.5s with identical diagnostics.
It cannot simply replace the `typescript` dependency: TypeScript 7 no longer
exports the JS compiler API, so TypeDoc, typescript-eslint and the Vite plugins
would all break. Hence the `tsc-native` alias -- 6.x stays as the library that
tooling loads, 7 is the binary that typechecks.
While here, stop reporting a failed compiler run as a clean build. Anything tsc
printed that did not look like a `file(line,col): error TSxxxx` diagnostic --
a crash, an unknown option, a missing binary -- was filtered away and the script
exited 0. It now propagates the exit code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nixpkgs lags behind the Electron releases we track, so flake.nix builds the
exact pinned version from Electron's official binary release. Renovate bumps
the devDependency but knows nothing about that pin, so every Electron update
left the flake refusing to evaluate until someone refreshed six hashes by
hand.
scripts/update-flake-electron.ts does it instead: the per-platform checksums
come from the release's own SHASUMS256.txt and the headers hash from
nix-prefetch-url --unpack. It is a no-op when the pin already matches.
The workflow runs it on pushes to main that touch apps/desktop/package.json
(and on demand), verifies the written hashes by actually building .#electron
and .#electron.headers, and only then opens a PR.
Two supporting flake changes:
- packages.electron is exposed, so the pinned binary can be built — and its
hashes verified — without a full desktop build.
- electron_<major> is looked up with `or null`. A major bump lands in
package.json long before nixpkgs has the attribute, and without the
fallback the flake died with an attribute error rather than reaching the
throw that explains the mismatch.
scripts/ is not a workspace package, so its tests get their own vitest
project, a `pnpm scripts:test` entry point, and a step in dev.yml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`packages/ckeditor5-keyboard-marker` had no consumers outside
`@triliumnext/ckeditor5`, was never published, and carried a full
ckeditor5-package-generator scaffold (sample pages, its own ESLint /
Stylelint / vitest configs, typings) that no root script or CI job ever
invoked. Its GPL-3.0 license combines with this repository's
AGPL-3.0-only, so the split bought no license isolation either, and
upstream (mlewand/ckeditor5-keyboard-marker) targets a long-obsolete
CKEditor 5 version and is not tracked — the source was ported and has
since diverged.
Move the plugin to src/plugins/keyboard_marker/, renamed to match the
todo_list_multistate convention, and adapt it to the destination
package: repo code style, the augmentation folded into a `declare
module` block, and the spec moved onto the shared createTestEditor()
kit. The unused `icons` barrel export is dropped.
Attribution is preserved: the upstream LICENSE moves next to the code,
alongside a README recording the provenance and the divergence.
The plugin's tests land inside the aggregate's 100% coverage gate
without a carve-out; the suite stays at 100% (848 tests).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every premium plugin Trilium loaded now has an in-tree GPL replacement —
`SlashCommand` -> `TriliumSlashCommands` (#10711), `Template` ->
`TriliumSnippets` (#10727), `FormatPainter` -> `TriliumFormatPainter` — which
left `loadPremiumPlugins()` returning an empty array while the commercial
licence, its key plumbing and the dependency were all still carried around.
- `loadPremiumPlugins()` and its export are gone. `config.ts` always passes
`licenseKey: "GPL"`, so `getLicenseKey()`, `VITE_CKEDITOR_KEY` (both `.env`
files, `vite-env.d.ts`) and the `forceGplLicense` build option go with it,
along with the read-only-editor rebuild in `buildEditor()` — that fallback
existed only to recover from a licence CKEditor had rejected.
- `getCkLocale()` no longer loads the premium translation bundle. The GPL
plugins reuse premium's English source strings, so a handful of labels
("Insert template", "Copy formatting", the slash palette) now fall back to
English in non-English UI locales; everything from CKEditor's own core
translations is unaffected.
- Also removed: the dependency + lockfile entry, the `optimizeDeps` pre-bundle
entry, the renovate group member, and the premium category in
`analyze-perf.ts`.
Tests: `plugins.spec.ts` pins the three GPL replacements instead of asserting
the premium list; the licensing block in `config.spec.ts` collapses to one
test; `i18n.spec.ts` becomes table-driven (19 near-identical cases -> 2) and
expects a single translation bundle. Typecheck clean, 834 package tests and 169
client tests pass, and the client builds with the package pruned.
The "Premium features" user-guide note is now obsolete but is left alone — it
needs the docs editor, not a hand edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
trimBetterSqlite3 returned silently when the copied module had no
prebuilds/ directory, on the assumption that this only happened for
source builds. It also happens when a stale pre-v13 tree is left in
<app>/node_modules -- which copyNodeModules prefers over the hoisted
root -- and the silence let that module ship.
The result was a runtime failure far from the cause: a v12 module needs
the `bindings` package, which is no longer copied into dist, so Node
resolved it from a node_modules above the repository and loaded a
build/Release addon compiled for a different Electron ABI
("NODE_MODULE_VERSION 146 ... requires 148"), with `bindings` in the
stack trace long after it stopped being a dependency.
pnpm's `hoisted` linker never creates <app>/node_modules entries, so
anything found there is residue -- in practice from the electron-rebuild
script that used to copy the module in for its Electron build. Switching
between a branch that predates the v13 bump and one that follows it is
enough to leave one behind.
Throw instead, naming the version found and the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Since v13 the package bundles a prebuilt binary for all eight platforms
it supports (~17 MB) on top of the SQLite amalgamation needed to compile
from scratch (~10 MB). A packaged artifact loads exactly one binary and
never compiles, so all of that but ~2 MB was dead weight:
v0.104.1 (v12) 11.93 MB deps/ + a compiled build/Release binary
v13, untrimmed 27.0 MB deps/ + eight prebuilds
server, trimmed 4.5 MB lib/ + linux-x64 + linuxmusl-x64
desktop, trimmed 2.2 MB lib/ + linux-x64
That is ~6 MB off each compressed Linux artifact, and leaves us below
what v0.104.1 shipped rather than 15 MB above it. lib/ plus one prebuild
is the complete runtime set -- verified by loading a copy pruned to just
those files and running a query against it.
Two things the trim has to get right, both covered:
Linux keeps the musl build alongside the glibc one. The server's dist is
produced once on a glibc runner and then consumed by both the Debian and
the Alpine images, and better-sqlite3 resolves linuxmusl-* at runtime on
the latter, so dropping it would break the amd64 image at startup. The
Electron artifacts opt out, since Electron ships no musl builds.
The platform and architecture are the ones being built *for*, not the
host's: the macOS runners are arm64 and also package darwin-x64, so
trimming by host arch would strip the binary that build needs.
TARGET_ARCH / MATRIX_ARCH already carry the target in CI.
An unsupported platform/arch throws rather than silently producing an
artifact with no native addon.
Also drops the better-sqlite3 cleanup from flake.nix's server install
phase: it removed deps/sqlite3 and the build/ scaffolding that held
build-time store paths, all of which the build now trims itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
better-sqlite3 v13's bundled N-API prebuilds are linked against a much
newer toolchain than the assets prebuild-install used to fetch, which
would have raised the glibc floor of every native Linux artifact and
dropped distributions we still support:
v0.104.1 (v12 assets) GLIBC_2.29 / GLIBCXX_3.4.20
v13 prebuild, x64 GLIBC_2.34 / GLIBCXX_3.4.29
v13 prebuild, arm64 GLIBC_2.38 / GLIBCXX_3.4.31
The arm64 figure drops Debian 12 "bookworm" -- including Raspberry Pi OS
-- along with Ubuntu 22.04 and RHEL 9; x64 drops Ubuntu 20.04, Debian 11
and RHEL 8. Nothing in better-sqlite3 needs a newer glibc: the addon
picked up GLIBC_2.34 because it was linked where libpthread had merged
into libc, and GLIBC_2.38 because GCC 13+ redirects strtol to the
__isoc23_* symbols. The floor is an artifact of the build host, so build
on an older one and substitute the result for the bundled prebuild.
Compiling in node:22-bullseye restores GLIBC_2.29 / GLIBCXX_3.4.21 --
glibc parity with v0.104.1, and the one-step GLIBCXX move constrains
nothing, since glibc 2.29 already implies a 2019-or-later distro. No
platform is lost.
Notes on the implementation:
- force_build is required. binding.gyp resolves both targets to
`type: none` whenever a prebuild for the host exists, so an ordinary
node-gyp run emits a stamp file and no addon.
- The compiled addon replaces prebuilds/<target>.node rather than
relying on the build/Release fallback, leaving the loader's resolution
order untouched.
- nodeLinker is "hoisted", so every dependant gets its own copy and the
build scripts resolve <app>/node_modules before the root; all copies
are patched. Each is replaced by rename, since these files are
hardlinks into the pnpm store and writing in place would mutate it.
- python3 is apt-installed when absent: buildpack-deps supplies g++ and
make but not python3, which node-gyp needs. Sources fall back to
archive.debian.org so this keeps working once bullseye goes EOL on
2026-08-31 -- an old *build* container is deliberate here, the same
approach manylinux and Node's own release builds take.
Docker images are unaffected: they carry their own glibc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v13 moves the addon onto N-API and ships prebuilt binaries for every
supported platform/arch inside the package itself. Three parts of the
build assumed the old prebuild-install/node-gyp layout.
Dropped `bindings` and `file-uri-to-path` from copyNodeModules: v13 no
longer depends on either, so the copy aborted the build outright with
"Unable to find any of the paths: .../node_modules/bindings".
Removed the Electron native rebuild. v13's binding.gyp resolves both of
its targets to `type: none` whenever a prebuild for the host exists, so
electron-rebuild.mts produced a stamp file and no addon on every install,
leaving BETTERSQLITE3_NATIVE_PATH pointing at a build/Release binary that
is never created -- `pnpm desktop:start` died on MODULE_NOT_FOUND. The
prebuilds are ABI-stable and load unchanged under Electron 42 (Node-API
10), so the rebuild step, its @electron/rebuild and prebuild-install
devDependencies, and the ELECTRON_NODEDIR plumbing in flake.nix all go.
Rebuilt the Docker images around the same short-circuit: `pnpm rebuild`
in the builder stages was also a no-op, so the images would have shipped
the upstream prebuild, which needs glibc >= 2.34 (x64) / >= 2.38 (arm64)
against bullseye's 2.31. The glibc images move to trixie and lose their
builder stage; the Alpine pair lose theirs too, since the linuxmusl-*
prebuilds only need libstdc++, which node:*-alpine already provides.
Alpine stays pinned to 3.23 for the musl 1.2.5 reason in #10627.
Dockerfile.legacy must keep compiling from source -- 32-bit ARM has no
prebuild at all, and linux/arm/v8 normalizes to arm64, whose prebuild
outruns that image's bullseye glibc. npm_config_force_build=1 restores
the real compile and link rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `start-prod`/`start-prod-no-dir` scripts invoked the npm prebuilt
Electron binary directly (`electron dist`), which fails to load on NixOS
because it's dynamically linked against FHS system libraries that don't
exist there (libcups.so.2, libgtk-3, libnss3, ...). The dev launcher
already handles this via electron-start.mts, but prod never did.
Add scripts/electron-run-prod.mts, a build-free launcher that reuses the
same NixOS-aware getElectronPath() (preferring the Nix/nix-develop
Electron over the prebuilt binary) and sets LD_LIBRARY_PATH plus
--no-sandbox on NixOS. On all other platforms it's a transparent
`electron <args>` wrapper, so behaviour is unchanged.
Also extract the shared getNixLdLibraryPath() helper so the dev and prod
launchers stay DRY.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The import.meta.url shim added for the bundled Claude Agent SDK prepends a
`require("node:url")` banner to every buildBackend output. The desktop build
compiled main.ts and preload.ts together, so the banner landed atop
preload.cjs, which runs in Electron's sandboxed renderer where
`require("node:url")` throws. That aborted the preload before it could expose
`electronApi`, leaving window.electronApi undefined: the window rendered a
transparent background with no Mica material behind it and DevTools could not
be opened.
Gate the shim (define + banner) behind a buildBackend option and build the
preload without it; the preload never references import.meta.url.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The desktop build bundles everything into a CJS main.cjs. CJS has no
import.meta, so esbuild rewrites import.meta.url to {} (undefined).
@anthropic-ai/claude-agent-sdk calls createRequire(import.meta.url) at
module top level, so evaluating it threw ERR_INVALID_ARG_VALUE, which
surfaced as "[trilium-app] dispatch failed for GET trilium-app://app/".
Shim import.meta.url to the bundle's own file URL via esbuild define +
banner, so createRequire()/.resolve() anchor at dist/ and still resolve
sibling node_modules packages. Also ship the SDK's per-platform native
claude binary into dist/node_modules, since the JS is bundled but the
binary it spawns at query time cannot be.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The build copied pdfjs-dist assets from the package's own node_modules,
but under the hoisted nodeLinker pnpm installs pdfjs-dist into the root
node_modules, so the hardcoded relative path missed it entirely and the
build failed with ENOENT on build/pdf.mjs.
Resolve pdfjs-dist via require.resolve("pdfjs-dist/package.json") so we
pick up exactly the version declared in this package.json wherever pnpm
places it. Expose BuildHelper.rootDir so the script can pass a
root-relative source path to copy().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>