Compare commits

..
Author SHA1 Message Date
EthanHealy01andGitHub fb28de4d5e Merge branch 'main' into dev/ChangeBrowserLabelToMatchWorktree 2026-07-09 21:03:23 +01:00
EthanHealy01 eb754d12c3 feat(dev): prefix browser tab title with worktree name in dev
When running task dev / dev:all / dev:saas / dev:portal from a worktree,
the frontend dev server injects the worktree folder basename (e.g. wt1)
as a build-time constant, and the app prefixes the browser tab title with
it so concurrent worktrees are distinguishable instead of all reading
"Stirling PDF".

Only the folder basename is exposed (never path/host/user), and only at
vite dev-serve time — production builds inject an empty string and the
feature compiles to a no-op. Desktop (tauri dev) is unaffected.
2026-07-09 14:19:33 +01:00
10 changed files with 93 additions and 225 deletions
+20 -43
View File
@@ -26,14 +26,6 @@ vars:
esac
fi
env:
# Desktop builds default to the smallest useful backend surface unless a task
# explicitly opts into extra features or login.
DISABLE_ADDITIONAL_FEATURES: '{{.DISABLE_ADDITIONAL_FEATURES | default "true"}}'
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN | default "false"}}'
# Reuse the previously built backend JAR unless a clean rebuild is requested.
JLINK_REUSE_CACHE: '{{.JLINK_REUSE_CACHE | default "true"}}'
tasks:
prepare:
desc: "Prepare desktop build dependencies"
@@ -58,19 +50,6 @@ tasks:
cmds:
- npx tauri dev --no-watch
dev:login:
desc: "Start Tauri desktop dev mode with login enabled and cache reuse"
deps:
- task: prepare
vars:
SECURITY_ENABLELOGIN: "true"
ignore_error: true
dir: editor
env:
SECURITY_ENABLELOGIN: "true"
cmds:
- npx tauri dev --no-watch
build:
desc: "Build Tauri desktop app (production)"
deps: [prepare]
@@ -127,7 +106,7 @@ tasks:
jlink:
desc: "Build backend JAR and create JLink runtime for Tauri"
deps: [jlink:cacheremove, jlink:jar, jlink:runtime]
deps: [jlink:jar, jlink:runtime]
# Runs after the runtime is in place. Lives here (not in jlink:runtime's
# cmds) so it still fires when jlink:runtime short-circuits on its `status:`
# check and reuses an existing runtime/jre - that reuse path is exactly how
@@ -136,40 +115,27 @@ tasks:
- task: jlink:verify
jlink:verify:
desc: "Verify the bundled JRE exists and matches the app JAR's required Java version"
desc: "Fail the build if the bundled JRE is older than the app JAR requires"
dir: editor
env:
REQUIRED_JAVA: "{{.REQUIRED_JAVA}}"
cmds:
- node scripts/verify-bundled-jre.mjs
jlink:cacheremove:
desc: "Remove cached backend JARs before a clean desktop rebuild"
dir: ..
if: '{{eq .JLINK_REUSE_CACHE "false"}}'
cmds:
- cmd: rm -rf app/core/build/libs
- 'echo "rm -rf app/core/build/libs"'
- task: jlink:clean
- node scripts/verify-bundled-jre.mjs src-tauri/runtime/jre/release
jlink:jar:
desc: "Build backend JAR for Tauri bundling (host-OS natives only by default)"
run: once
dir: ..
env:
DISABLE_ADDITIONAL_FEATURES: '{{.DISABLE_ADDITIONAL_FEATURES}}'
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
DISABLE_ADDITIONAL_FEATURES: "true"
cmds:
- 'echo "Building bootJar with JPDFium natives for {{.JPDFIUM_PLATFORMS}}"'
- cmd: 'cmd /c "gradlew.bat bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}"'
- echo "Building bootJar with JPDFium natives for {{.JPDFIUM_PLATFORMS}}"
- cmd: cmd /c gradlew.bat bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
platforms: [windows]
- cmd: './gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}'
- cmd: ./gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
platforms: [linux, darwin]
- mkdir -p frontend/editor/src-tauri/libs
- cmd: cp app/core/build/libs/stirling-pdf-*.jar frontend/editor/src-tauri/libs/
platforms: [linux, darwin]
- cmd: powershell -NoProfile -ExecutionPolicy Bypass -Command "New-Item -ItemType Directory -Force -Path 'frontend/editor/src-tauri/libs' | Out-Null; Copy-Item -Force (Get-ChildItem 'app/core/build/libs/stirling-pdf-*.jar' | Select-Object -First 1) 'frontend/editor/src-tauri/libs'"
platforms: [windows]
- cp app/core/build/libs/stirling-pdf-*.jar frontend/editor/src-tauri/libs/
status:
- test -f frontend/editor/src-tauri/libs/stirling-pdf-*.jar
@@ -187,7 +153,16 @@ tasks:
# jdk.crypto.mscapi (the Windows certificate store / SunMSCAPI provider, used by
# hardware-backed cert signing) is a Windows-only module - it only exists in a Windows
# JDK's jmods, so it is added on Windows only or jlink fails to resolve it elsewhere.
- cmd: powershell -NoProfile -ExecutionPolicy Bypass -File ../../../scripts/build-jlink-runtime.ps1 -Modules "{{.JLINK_MODULES}}"
- cmd: |
JLINK="${JAVA_HOME:+$JAVA_HOME/bin/}jlink"
JLINK_COMPRESS="$("$JLINK" --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)"
"$JLINK" \
--add-modules {{.JLINK_MODULES}},jdk.crypto.mscapi \
--strip-debug \
--compress="$JLINK_COMPRESS" \
--no-header-files \
--no-man-pages \
--output runtime/jre
platforms: [windows]
- cmd: |
JLINK="${JAVA_HOME:+$JAVA_HOME/bin/}jlink"
@@ -219,6 +194,8 @@ tasks:
# `desktop:build` run `jlink:clean` first to force a fresh build.
- cmd: chmod -R u+w runtime/jre
platforms: [linux, darwin]
- cmd: powershell -NoProfile -Command "Get-ChildItem -Recurse runtime/jre | ForEach-Object { $_.IsReadOnly = $false }"
platforms: [windows]
status:
- test -f runtime/jre/release
+6
View File
@@ -80,6 +80,12 @@ tasks:
OPEN: '{{.OPEN | default ""}}'
env:
BACKEND_URL: '{{.BACKEND_URL}}'
# Dev-only browser-tab label so concurrent worktrees are distinguishable.
# Only the worktree folder basename (e.g. "wt1") is exposed — never the
# full path, hostname, or user. Consumed at dev-serve time by vite.config
# and dropped from production builds.
STIRLING_DEV_LABEL:
sh: basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cmds:
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
-24
View File
@@ -50,26 +50,6 @@ task desktop:dev
This ensures the JLink runtime and backend JAR exist (skipping if already built), then starts Tauri in dev mode.
For local login testing, use the login-enabled variant:
```bash
task desktop:dev:login
```
That task reuses the same desktop launcher, but it keeps the backend cache and enables the login flow through the desktop taskfile defaults.
You can override the desktop task defaults inline:
- `JLINK_REUSE_CACHE=false` - force a clean backend JAR/JRE rebuild before launch
- `DISABLE_ADDITIONAL_FEATURES=false` - keep the full backend feature set for the bundled desktop run
- `SECURITY_ENABLELOGIN=true` - enable the normal login flow in desktop mode
Example:
```bash
task desktop:dev:login JLINK_REUSE_CACHE=false DISABLE_ADDITIONAL_FEATURES=false SECURITY_ENABLELOGIN=true
```
### Build
```bash
@@ -98,10 +78,6 @@ task desktop:jlink:runtime # Create JLink custom JRE only
task desktop:jlink:clean # Remove JLink artifacts
```
`task desktop:jlink` verifies the bundled runtime after it is built. If the
runtime release file is missing or too old, the verification script will
rebuild `desktop:jlink:runtime` automatically and re-check the version.
### Clean
```bash
+7 -70
View File
@@ -9,87 +9,24 @@
//
// Required major comes from REQUIRED_JAVA (wired from .taskfiles/desktop.yml,
// which mirrors build.gradle `modernJavaVersion`). Keep them in sync.
import { spawnSync } from "node:child_process";
import { resolve } from "node:path";
import { readFileSync } from "node:fs";
const required = Number(process.env.REQUIRED_JAVA ?? "25");
const releasePath = resolve(process.cwd(), "src-tauri/runtime/jre/release");
function rebuildRuntime(reason) {
console.warn(
`${reason} Rebuilding runtime with 'task desktop:jlink:runtime'.`,
);
const isWindows = process.platform === "win32";
// Node does not reliably spawn the Task .cmd shim directly on Windows in
// this repo environment, so use cmd.exe as the stable trampoline.
const command = isWindows ? "cmd.exe" : "task";
const args = isWindows
? ["/d", "/s", "/c", "task desktop:jlink:runtime"]
: ["desktop:jlink:runtime"];
const result = spawnSync(command, args, {
stdio: "inherit",
});
if (result.error) {
console.error(
`FATAL: failed to launch 'task desktop:jlink:runtime': ${result.error.message}`,
);
process.exit(1);
}
if (result.status !== 0) {
console.error(
`FATAL: 'task desktop:jlink:runtime' exited with status ${result.status}.`,
);
process.exit(result.status ?? 1);
}
}
function readReleaseFile() {
return readFileSync(releasePath, "utf8");
}
function readReleaseOrFail() {
try {
return readReleaseFile();
} catch (err) {
console.error(
`FATAL: cannot read bundled JRE release file at "${releasePath}" after rebuilding: ${err.message}.`,
);
process.exit(1);
}
}
function parseMajor(raw) {
// jlink writes release metadata as plain text, so we can validate the
// embedded runtime without starting Java.
const match = raw.match(/JAVA_VERSION="?(\d+)/);
return match ? Number(match[1]) : 0;
}
const releasePath = process.argv[2] ?? "runtime/jre/release";
let raw;
try {
raw = readReleaseFile();
raw = readFileSync(releasePath, "utf8");
} catch (err) {
console.error(
`WARN: cannot read bundled JRE release file at "${releasePath}": ${err.message}.`,
`FATAL: cannot read bundled JRE release file at "${releasePath}": ${err.message}. ` +
`Is the runtime built? Run 'task desktop:jlink'.`,
);
rebuildRuntime("Bundled runtime is missing.");
raw = readReleaseOrFail();
process.exit(1);
}
let major = parseMajor(raw);
if (!major || major < required) {
// Rebuild stale or too-old runtimes automatically so desktop launches can
// recover from a cached JRE instead of failing at app start.
rebuildRuntime(
`Bundled runtime/jre is Java ${major || "unknown"} but the app JAR requires Java ${required}.`,
);
raw = readReleaseOrFail();
major = parseMajor(raw);
}
const match = raw.match(/JAVA_VERSION="?(\d+)/);
const major = match ? Number(match[1]) : 0;
console.log(
`Bundled JRE major: ${major || "unknown"} (required >= ${required})`,
@@ -54,18 +54,6 @@ fn check_backend_status() -> Result<(), String> {
Ok(())
}
/// Allow desktop launches to opt into the normal login flow when either the
/// new desktop-specific flag or the existing SECURITY_ENABLELOGIN flag is set.
fn desktop_login_enabled_from_env() -> bool {
matches!(
std::env::var("STIRLING_PDF_DESKTOP_ENABLE_LOGIN")
.or_else(|_| std::env::var("SECURITY_ENABLELOGIN"))
.ok()
.as_deref(),
Some("true" | "TRUE" | "True" | "1" | "yes" | "YES" | "on" | "ON")
)
}
// Find the bundled JRE and return the java executable path
fn find_bundled_jre(resource_dir: &PathBuf) -> Result<PathBuf, String> {
let jre_dir = resource_dir.join("runtime").join("jre");
@@ -225,11 +213,7 @@ fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &
// No reverse proxy in front of the local sidecar, so don't trust forwarded headers.
// Stops a LAN caller spoofing X-Forwarded-For to defeat the desktop-only signing gate.
"-Dserver.forward-headers-strategy=none",
if desktop_login_enabled_from_env() {
"-Dsecurity.enableLogin=true"
} else {
"-Dsecurity.enableLogin=false"
},
"-Dsecurity.enableLogin=false", // Disable login for desktop mode
"-Dsecurity.csrfDisabled=true", // Disable CSRF for desktop mode
];
@@ -0,0 +1,39 @@
/**
* Prefixes the browser tab title with the current worktree name during local
* development so multiple concurrently-running worktrees (e.g. wt1, wt2, spdf1)
* are distinguishable at a glance instead of all showing "Stirling PDF".
*
* The label is injected as a build-time constant by vite.config, sourced from
* the top-level dev tasks. It is an empty string in production builds, so this
* whole feature compiles away to a no-op outside `vite` dev-serve.
*/
const LABEL =
typeof __DEV_WORKTREE_LABEL__ === "string" ? __DEV_WORKTREE_LABEL__ : "";
export function applyDevWorktreeLabel(): void {
if (!LABEL || typeof document === "undefined") {
return;
}
const prefix = `[${LABEL}] `;
const ensurePrefixed = () => {
if (!document.title.startsWith(prefix)) {
// The app rewrites document.title on route/tool changes; re-apply the
// prefix on top of whatever the app just set.
document.title = prefix + document.title;
}
};
ensurePrefixed();
const titleEl = document.querySelector("title");
if (titleEl) {
new MutationObserver(ensurePrefixed).observe(titleEl, {
childList: true,
characterData: true,
subtree: true,
});
}
}
+3
View File
@@ -14,9 +14,12 @@ import { BrowserRouter } from "react-router-dom";
import App from "@app/App";
import "@app/i18n"; // Initialize i18next
import { BASE_PATH } from "@app/constants/app";
import { applyDevWorktreeLabel } from "@app/utils/applyDevWorktreeLabel";
import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler";
applyDevWorktreeLabel();
if (typeof window !== "undefined") {
const scheduleCompilation = () => {
if (typeof requestIdleCallback === "function") {
+7
View File
@@ -29,3 +29,10 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
/**
* Dev-only worktree folder basename injected by vite.config at dev-serve time
* (empty string in production builds). Used to prefix the browser tab title so
* concurrent worktrees are distinguishable.
*/
declare const __DEV_WORKTREE_LABEL__: string;
+10 -1
View File
@@ -182,7 +182,13 @@ const TSCONFIG_MAP: Record<BuildMode, string> = {
prototypes: "./tsconfig.prototypes.vite.json",
};
export default defineConfig(async ({ mode }) => {
export default defineConfig(async ({ mode, command }) => {
// Dev-only browser-tab label (worktree folder basename) surfaced by the
// top-level dev tasks so concurrent worktrees have distinguishable tabs.
// Only injected during `vite` (dev serve) — never baked into a production
// build — and carries only the folder name, no path/host/user info.
const devWorktreeLabel =
command === "serve" ? (process.env.STIRLING_DEV_LABEL ?? "") : "";
// Load env files relative to this config (frontend/editor/), regardless of
// where the build was invoked from. The previous `process.cwd()` worked when
// this file lived at frontend/, but after the editor was moved under
@@ -250,6 +256,9 @@ export default defineConfig(async ({ mode }) => {
};
return {
define: {
__DEV_WORKTREE_LABEL__: JSON.stringify(devWorktreeLabel),
},
plugins: [
react(),
...(runSubpath ? [subpathBareRedirectPlugin(runSubpath)] : []),
-70
View File
@@ -1,70 +0,0 @@
param(
[Parameter(Mandatory = $true)]
[string]$Modules
)
$ErrorActionPreference = "Stop"
$AllowedModules = @(
"java.base",
"java.compiler",
"java.desktop",
"java.instrument",
"java.logging",
"java.management",
"java.naming",
"java.net.http",
"java.prefs",
"java.rmi",
"java.scripting",
"java.security.jgss",
"java.security.sasl",
"java.sql",
"java.transaction.xa",
"java.xml",
"java.xml.crypto",
"jdk.crypto.ec",
"jdk.crypto.cryptoki",
"jdk.unsupported",
"jdk.dynalink"
)
$RequestedModules = @($Modules -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if ($RequestedModules.Count -eq 0) {
throw "At least one jlink module is required."
}
$InvalidModules = $RequestedModules | Where-Object {
($_ -notmatch '^[A-Za-z0-9_.]+$') -or ($_ -notin $AllowedModules)
}
if ($InvalidModules) {
throw "Unsupported jlink module(s): $($InvalidModules -join ', ')"
}
$ModulesArg = $RequestedModules -join ","
$Jlink = if ($env:JAVA_HOME) {
Join-Path $env:JAVA_HOME "bin/jlink.exe"
} else {
"jlink.exe"
}
$HelpText = & $Jlink --help 2>&1 | Out-String
# Older JDKs do not support the newer zip compressor selector, so fall back to
# the numeric compression level they do understand.
$Compress = if ($HelpText -match "zip-\[0-9\]") { "zip-6" } else { "2" }
& $Jlink `
--add-modules "$ModulesArg,jdk.crypto.mscapi" `
--strip-debug `
--compress="$Compress" `
--no-header-files `
--no-man-pages `
--output runtime/jre
# Tauri's resource staging preserves source permissions, so the bundled runtime
# must be writable here or the next incremental build can fail on read-only JRE
# files copied into the target directory.
Get-ChildItem -Recurse runtime/jre -Force -File | ForEach-Object {
$_.Attributes = $_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly)
}