Build edit-docs as standalone package using makeApp

Changed edit-docs from a simple wrapper script to a properly built Nix
package using makeApp, similar to how desktop and server are built.

Changes:
- Added build script to apps/edit-docs/package.json
- Created apps/edit-docs/scripts/build.ts based on desktop's build script
- Added edit-docs:build task to root package.json
- Changed flake.nix to use makeApp which:
  - Builds edit-docs with all dependencies bundled
  - Creates a standalone trilium-edit-docs executable
  - Can be installed with 'nix profile install' and run from any directory

This makes edit-docs truly reusable - it can now be installed and run
from any project without requiring the Trilium source tree.
This commit is contained in:
Wael Nasreddine 2026-01-10 12:04:22 -08:00
parent fb4d63b049
commit 0273c64bbf
9 changed files with 181 additions and 20 deletions

View File

@ -165,6 +165,17 @@ pnpm install
pnpm edit-docs:edit-docs
```
Alternatively, if you have Nix installed:
```shell
# Run directly
nix run .#edit-docs
# Or install to your profile
nix profile install .#edit-docs
trilium-edit-docs
```
### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
```shell

View File

@ -16,6 +16,8 @@
"fs-extra": "11.3.3"
},
"scripts": {
"build": "tsx scripts/build.ts",
"test-build": "vitest --config vitest.build.config.mts",
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store tsx ../../scripts/electron-start.mts src/edit-docs.ts",
"edit-demo": "cross-env TRILIUM_PORT=37744 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
}

View File

@ -0,0 +1,40 @@
import { writeFileSync } from "fs";
import { join } from "path";
import BuildHelper from "../../../scripts/build-utils";
import originalPackageJson from "../package.json" with { type: "json" };
const build = new BuildHelper("apps/edit-docs");
async function main() {
await build.buildBackend(["src/edit-docs.ts", "src/utils.ts"]);
// Copy assets from server (needed for DB initialization)
build.copy("/apps/server/src/assets", "assets/");
build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
build.buildFrontend();
// Copy node modules dependencies
build.copyNodeModules(["better-sqlite3", "bindings", "file-uri-to-path", "@electron/remote"]);
generatePackageJson();
}
function generatePackageJson() {
const { version, author, license, description, dependencies, devDependencies } = originalPackageJson;
const packageJson = {
name: "trilium-edit-docs",
main: "edit-docs.cjs",
version,
author,
license,
description,
dependencies: {"better-sqlite3": dependencies["better-sqlite3"]},
devDependencies: {electron: devDependencies.electron},
};
writeFileSync(join(build.outDir, "package.json"), JSON.stringify(packageJson, null, "\t"), "utf-8");
}
main();

View File

@ -0,0 +1,48 @@
import { globSync } from "fs";
import { join } from "path";
import { it, describe, expect } from "vitest";
describe("Check artifacts are present", () => {
const distPath = join(__dirname, "../../dist");
it("has the necessary node modules", async () => {
const paths = [
"node_modules/better-sqlite3",
"node_modules/bindings",
"node_modules/file-uri-to-path",
"node_modules/@electron/remote"
];
ensurePathsExist(paths);
});
it("includes the client", async () => {
const paths = [
"public/assets",
"public/fonts",
"public/node_modules",
"public/src",
"public/stylesheets",
"public/translations"
];
ensurePathsExist(paths);
});
it("includes necessary assets", async () => {
const paths = [
"assets",
"share-theme",
"ckeditor5-content.css"
];
ensurePathsExist(paths);
});
function ensurePathsExist(paths: string[]) {
for (const path of paths) {
const result = globSync(join(distPath, path, "**"));
expect(result, path).not.toHaveLength(0);
}
}
});

View File

@ -36,6 +36,10 @@ function parseArgs() {
for (let i = 0; i < args.length; i++) {
if (args[i] === '--config' || args[i] === '-c') {
configPath = args[i + 1];
if (!configPath) {
console.error("Error: --config/-c requires a path argument");
process.exit(1);
}
i++; // Skip the next argument as it's the value
} else if (args[i] === '--help' || args[i] === '-h') {
showHelp = true;
@ -86,9 +90,15 @@ let NOTE_MAPPINGS: NoteMapping[];
// Load configuration from edit-docs-config.yaml
async function loadConfig() {
const CONFIG_PATH = configPath
let CONFIG_PATH = configPath
? path.resolve(configPath)
: path.join(__dirname, "../../../edit-docs-config.yaml");
: path.join(process.cwd(), "edit-docs-config.yaml");
const exists = await fs.access(CONFIG_PATH).then(() => true).catch(() => false);
if (!exists && !configPath) {
// Fallback to project root if running from within a subproject
CONFIG_PATH = path.join(__dirname, "../../../edit-docs-config.yaml");
}
const configContent = await fs.readFile(CONFIG_PATH, "utf-8");
const config = yaml.load(configContent) as Config;
@ -106,12 +116,18 @@ async function main() {
await loadConfig();
const initializedPromise = startElectron(() => {
// Wait for the import to be finished and the application to be loaded before we listen to changes.
setTimeout(() => registerHandlers(), 10_000);
setTimeout(() => {
registerHandlers();
}, 10_000);
});
await initializeTranslations();
await initializeDatabase(true);
// Wait for becca to be loaded before importing data
const beccaLoader = await import("@triliumnext/server/src/becca/becca_loader.js");
await beccaLoader.beccaLoaded;
cls.init(async () => {
for (const mapping of NOTE_MAPPINGS) {
if (!mapping.exportOnly) {
@ -248,7 +264,6 @@ async function registerHandlers() {
return;
}
console.log("Got entity changed", e.entityName, e.entity.title);
debouncer();
});
}

View File

@ -7,18 +7,21 @@ import type { WriteStream } from "fs";
import fs from "fs/promises";
import fsExtra from "fs-extra";
import path from "path";
import { resolve } from "path";
import { deferred, DeferredPromise } from "../../../packages/commons/src";
import { deferred, type DeferredPromise } from "../../../packages/commons/src/index.js";
export function initializeDatabase(skipDemoDb: boolean) {
return new Promise<void>(async (resolve) => {
const sqlInit = (await import("@triliumnext/server/src/services/sql_init.js")).default;
cls.init(async () => {
if (!sqlInit.isDbInitialized()) {
await sqlInit.createInitialDatabase(skipDemoDb);
}
resolve();
export function initializeDatabase(skipDemoDb: boolean): Promise<void> {
return new Promise<void>((resolve) => {
import("@triliumnext/server/src/services/sql_init.js").then((m) => {
const sqlInit = m.default;
cls.init(async () => {
if (!sqlInit.isDbInitialized()) {
sqlInit.createInitialDatabase(skipDemoDb).then(() => resolve());
} else {
sqlInit.dbReady.resolve();
resolve();
}
});
});
});
}
@ -78,7 +81,6 @@ async function createImportZip(path: string) {
zlib: { level: 0 }
});
console.log("Archive path is ", resolve(path));
archive.directory(path, "/");
const outputStream = fsExtra.createWriteStream(inputFile);
@ -93,9 +95,11 @@ async function createImportZip(path: string) {
}
function waitForEnd(archive: Archiver, stream: WriteStream) {
return new Promise<void>(async (res, rej) => {
stream.on("finish", () => res());
await archive.finalize();
return new Promise<void>((res, rej) => {
stream.on("finish", res);
stream.on("error", rej);
archive.on("error", rej);
archive.finalize().catch(rej);
});
}

View File

@ -0,0 +1,17 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/edit-docs',
plugins: [],
test: {
watch: false,
globals: true,
environment: "node",
include: ['spec/build-checks/**'],
reporters: [
"verbose"
]
},
}));

View File

@ -112,7 +112,7 @@
nodejs.python
removeReferencesTo
]
++ lib.optionals (app == "desktop") [
++ lib.optionals (app == "desktop" || app == "edit-docs") [
copyDesktopItems
# required for NIXOS_OZONE_WL expansion
# https://github.com/NixOS/nixpkgs/issues/172583
@ -252,10 +252,33 @@
--add-flags $out/opt/trilium-server/main.cjs
'';
};
edit-docs = makeApp {
app = "edit-docs";
preBuildCommands = ''
export npm_config_nodedir=${electron.headers}
pnpm postinstall
'';
buildTask = "edit-docs:build";
mainProgram = "trilium-edit-docs";
installCommands = ''
#remove-references-to -t ${electron.headers} apps/edit-docs/dist/node_modules/better-sqlite3/build/config.gypi
#remove-references-to -t ${nodejs.python} apps/edit-docs/dist/node_modules/better-sqlite3/build/config.gypi
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
'';
};
in
{
packages.desktop = desktop;
packages.server = server;
packages.edit-docs = edit-docs;
packages.default = desktop;

View File

@ -17,6 +17,8 @@
"desktop:start": "pnpm run --filter desktop dev",
"desktop:build": "pnpm run --filter desktop build",
"desktop:start-prod": "pnpm run --filter desktop start-prod",
"edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs",
"edit-docs:build": "pnpm run --filter edit-docs build",
"website:start": "pnpm run --filter website dev",
"website:build": "pnpm run --filter website build",
"electron:build": "pnpm desktop:build",
@ -28,7 +30,6 @@
"chore:update-version": "tsx ./scripts/update-version.ts",
"docs:build": "pnpm run --filter build-docs start",
"docs:preview": "pnpm http-server site -p 9000",
"edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs",
"edit-docs:edit-demo": "pnpm run --filter edit-docs edit-demo",
"test:all": "pnpm test:parallel && pnpm test:sequential",
"test:parallel": "pnpm --filter=!server --filter=!ckeditor5-mermaid --filter=!ckeditor5-math --parallel test",