#!/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 // `/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);