Files
SRCmail/scripts/assemble-standalone.mjs
T
Bernd RodlerandClaude Sonnet 5 fbfaf528ab fix(build): copy next/dist/lib/metadata into standalone output
output: "standalone" + next build --webpack drops the whole metadata
directory despite a plain top-level require in router-utils/filesystem.js
("../../../lib/metadata/get-metadata-route") - every packaged build
(Electron dmg and the Docker image) crashed on its very first line with
Cannot find module. Verified: a fresh dist:mac build failed to boot at
all; copying the directory by hand (same pattern already used for the
sqlcipher prebuilds and plugin bundles) fixes it, confirmed by booting
.next/standalone directly and getting a real HTTP response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 12:50:33 +02:00

101 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
// `next build --webpack` (see next.config.ts's `output: "standalone"`)
// emits .next/standalone/server.js but - deliberately, per Next's own docs -
// leaves out public/ and .next/static/. The Dockerfile copies both in by
// hand for the container image; this does the same thing for local Electron
// dev and packaging, so every path boots the exact same artifact.
import { cpSync, existsSync, rmSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const rootDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const standaloneDir = path.join(rootDir, ".next", "standalone");
if (!existsSync(standaloneDir)) {
console.error(`Missing ${standaloneDir} - run "next build --webpack" first.`);
process.exit(1);
}
const publicSrc = path.join(rootDir, "public");
const publicDest = path.join(standaloneDir, "public");
rmSync(publicDest, { recursive: true, force: true });
cpSync(publicSrc, publicDest, { recursive: true });
const staticSrc = path.join(rootDir, ".next", "static");
const staticDest = path.join(standaloneDir, ".next", "static");
rmSync(staticDest, { recursive: true, force: true });
cpSync(staticSrc, staticDest, { recursive: true });
// The native SQLCipher prebuilds for the local search index (lib/mail-index/**).
//
// Next's output file tracing DOES pick up @signalapp/sqlcipher's JS
// (package.json + dist/index.cjs) and its node-gyp-build dependency, but NOT
// the prebuilds/ directory holding the actual .node binaries - node-gyp-build
// resolves those by scanning the directory at runtime, which no static tracer
// can follow. Verified by inspecting a real `build:standalone` output: the
// package was present, `prebuilds/` was absent, so `require()` would have
// failed at runtime in every packaged build.
//
// Copying the WHOLE prebuilds directory (all six platform/arch pairs, ~11 MB)
// rather than just this host's is deliberate: electron-builder cross-builds the
// x64 and arm64 macOS targets from one runner (electron-builder.config.js), so
// the artifact has to contain a prebuild for an arch this machine isn't.
//
// Skipped silently when absent - the package is an OPTIONAL dependency and is
// legitimately missing on musl/Alpine, where both Dockerfiles build.
const sqlcipherSrc = path.join(rootDir, "node_modules", "@signalapp", "sqlcipher", "prebuilds");
if (existsSync(sqlcipherSrc)) {
const sqlcipherDest = path.join(
standaloneDir, "node_modules", "@signalapp", "sqlcipher", "prebuilds",
);
rmSync(sqlcipherDest, { recursive: true, force: true });
cpSync(sqlcipherSrc, sqlcipherDest, { recursive: true });
console.log("Copied @signalapp/sqlcipher prebuilds into the standalone output");
} else {
console.log(
"@signalapp/sqlcipher not installed (optional dependency) - " +
"the encrypted local index will be disabled at runtime",
);
}
// The staged first-party plugin bundles (scripts/build-plugins.mjs). The
// server installs these into its plugin registry at startup
// (lib/admin/bundled-plugins.ts), reading them from
// `<cwd>/vnc/plugins/build` - and Next's generated server.js chdir's to its
// own directory, so "cwd" is this standalone dir in every packaged build.
//
// Output file tracing cannot find these: nothing imports them, they are read
// by path at runtime. Without this copy the Electron/standalone build boots
// with no S/MIME plugin at all while the policy toggle still says it is on -
// the same silent-drop failure mode as the sqlcipher prebuilds above.
const pluginsSrc = path.join(rootDir, "vnc", "plugins", "build");
if (existsSync(pluginsSrc)) {
const pluginsDest = path.join(standaloneDir, "vnc", "plugins", "build");
rmSync(pluginsDest, { recursive: true, force: true });
cpSync(pluginsSrc, pluginsDest, { recursive: true });
console.log("Copied bundled first-party plugins into the standalone output");
} else {
console.warn(
"No bundled plugins staged at vnc/plugins/build - " +
'run "npm run build:plugins" first, or the packaged app ships without S/MIME',
);
}
// next/dist/lib/metadata/** (get-metadata-route.js and its neighbours).
//
// router-utils/filesystem.js has a plain top-level `require("../../../lib/
// metadata/get-metadata-route")` - not dynamic, not conditional - yet Next's
// own output-file-tracing for `output: "standalone"` + `next build --webpack`
// drops the entire directory. Verified by inspecting a real build: the
// package was present, this one subfolder was missing, so the standalone
// server crashed on its very first line with "Cannot find module" - every
// packaged build (Electron and Docker) was affected. Same failure class as
// the sqlcipher prebuilds above: copy by hand what the tracer misses.
const metadataSrc = path.join(rootDir, "node_modules", "next", "dist", "lib", "metadata");
const metadataDest = path.join(standaloneDir, "node_modules", "next", "dist", "lib", "metadata");
rmSync(metadataDest, { recursive: true, force: true });
cpSync(metadataSrc, metadataDest, { recursive: true });
console.log("Copied next/dist/lib/metadata into the standalone output");
console.log("Assembled standalone server at", standaloneDir);