Files
penpot-nix/packages/frontend.nix
T
kurogeek 08b5496980 Package Penpot 2.18.3 for Nix with NixOS module; afk devShell
Vendored from debling/nix-penpot (MIT). Fixes on top:
- pnpm fetchers: drop --config.fetch-* flags (UND_ERR_INVALID_ARG on pnpm 11.27)
  and pin real store hashes (frontend x3, exporter, mcp)
- deps-lock.json: add transitive funcool/beicon2 v2.0 so fake-git resolves
  potok2's pin during the shadow-cljs classpath build
- expose frontend pnpm stores via passthru for rehashing
- x86_64-linux only; devShell ships afk, jj, git
2026-10-20 17:45:02 +00:00

248 lines
8.6 KiB
Nix

# Penpot frontend: compiles the upstream ClojureScript/JS monorepo into the
# static site served by nginx ($out/dist, mirroring upstream `bundles/frontend`).
#
# Mirrors upstream `frontend/scripts/build`:
# 1. render-wasm artifacts are placed where the wasm cljs glue expects them
# (instead of building the wasm in-tree).
# 2. MCP plugin is built (its own pnpm workspace).
# 3. Official plugins are built (their own pnpm workspace); the plugins
# runtime library is built first since the cljs build links it.
# 4. shadow-cljs release build of the `main` and `worker` builds.
# 5. esbuild bundling of the JS libs.
# 6. Asset compilation (styles, sprites, translations, templates, ...).
# 7. Finalization: worker render.js cache-busting, rsync into dist,
# plugins, version.txt.
{
lib,
callPackage,
penpot,
version,
subSrc,
stdenv,
stdenvNoCC,
runCommand,
nodejs_24,
pnpm_11,
clojure,
jdk25_headless,
rsync,
patchelf,
fetchPnpmDeps,
pnpmConfigHook,
# Named renderWasm (not `wasm`) so pkgs.callPackage cannot auto-fill it
# with nixpkgs' unrelated `pkgs.wasm` (the OCaml interpreter).
renderWasm ? callPackage ./render-wasm.nix { inherit penpot version; },
}:
let
src = subSrc penpot [
"frontend"
"common"
"mcp"
"plugins"
];
# The frontend workspace links `../plugins/libs/plugins-runtime` during
# install, so plugins/ must be present in its fetch source too.
frontendSrc = subSrc penpot [
"frontend"
"plugins"
];
pluginsSrc = subSrc penpot [ "plugins" ];
mcpSrc = subSrc penpot [ "mcp" ];
# One pnpm store per workspace (fetched once, reused for the offline
# installs in the main build via pnpmConfigHook).
#
# 2.18 adds a shared `storeDir: ../.pnpm-store` to every workspace's
# pnpm-workspace.yaml; our fetches and consumer installs are per-workspace,
# so strip that line before the fixed-output fetch (the main build removes
# it again in postPatch).
patchedPnpmSrc =
name: wsrc:
# Named "source" (see the note on `mcpSrcP` in mcp.nix): the deps FOD
# relies on the dir-source unpack landing at /build/source so that
# sourceRoot = "source/<workspace>" resolves.
runCommand "source" { } ''
mkdir "$out"
cp -r "${wsrc}"/. "$out"/
chmod -R u+w "$out"
find "$out" -name pnpm-workspace.yaml -exec sed -i '/^storeDir:/d' {} +
'';
fetchWorkspaceDeps =
pname: sourceRoot: wsrc: hash:
fetchPnpmDeps {
inherit pname sourceRoot hash;
pnpm = pnpm_11;
fetcherVersion = 4;
src = patchedPnpmSrc pname wsrc;
};
frontendPnpmDeps =
fetchWorkspaceDeps "penpot-frontend-frontend" "source/frontend" frontendSrc
"sha256-zZxUFuyWLMxIse5AjPwEAmq/fJ9tbGYkcG2kG68HV4U=";
pluginsPnpmDeps =
fetchWorkspaceDeps "penpot-frontend-plugins" "source/plugins" pluginsSrc
"sha256-Tq6aSL0lHFCfQo5H94d+XNGeY2oMNYG2J8YOQhYl3T8=";
mcpPnpmDeps =
fetchWorkspaceDeps "penpot-frontend-mcp" "source/mcp" mcpSrc
"sha256-99ef/Qk3tx+etEqepQQVqAW3XL7T1Kr6qQJ7Uxf1z0Y=";
# Upstream's deps.edn passes --sun-misc-unsafe-memory-access=allow, which
# needs a newer JDK than the one the clojure CLI wrapper ships with.
clojureJdk25 = clojure.override { jdk = jdk25_headless; };
# Upstream leaves a few dev deps on "RELEASE"; pin them so the lockfile
# generation and the (offline) shadow-cljs build resolve the same
# deterministic, purely local maven versions.
pinDepsEdn = ''
sed -i frontend/deps.edn \
-e 's|binaryage/devtools {:mvn/version "RELEASE"}|binaryage/devtools {:mvn/version "1.0.7"}|' \
-e 's|org.clojure/tools.namespace {:mvn/version "RELEASE"}|org.clojure/tools.namespace {:mvn/version "1.5.1"}|' \
-e 's|com.bhauman/rebel-readline {:mvn/version "RELEASE"}|com.bhauman/rebel-readline {:mvn/version "0.1.11"}|'
'';
# Offline Clojure dependencies: eval-time cache from deps-lock.json
# (no fixed-output derivation) + fake-git shim + short-SHA expansion.
# Resolution runs offline from the cache on every build; no cpcache needed.
cljOffline = callPackage ./clj-offline.nix { };
in
stdenvNoCC.mkDerivation {
pname = "penpot-frontend";
inherit version;
inherit src;
# Local carries of upstream plugin API fixes (see the patches for the
# upstream issue references). Remove once the `penpot` input contains them.
patches = [
./patches/plugin-library-name-path.patch
./patches/plugin-typography-lineheight.patch
];
nativeBuildInputs = [
nodejs_24
pnpm_11
pnpmConfigHook
clojureJdk25
cljOffline.fake-git
cljOffline.clj-builder
rsync
patchelf
];
# pnpmConfigHook is invoked once per workspace in preBuild instead
# (dontPnpmConfigure disables the implicit single-workspace run).
dontPnpmConfigure = true;
postPatch = ''
${pinDepsEdn}
# 2.18's shared pnpm store config would redirect the offline installs
# away from the fetched per-workspace stores.
find . -name pnpm-workspace.yaml -exec sed -i '/^storeDir:/d' {} +
'';
env = {
NODE_ENV = "production";
# Baked into the cljs output (app.config/compiled-version-tag).
VERSION = version;
# Upstream appends the wall-clock build time (VERSION_TAG =
# "$VERSION-$BUILD_TS"); a plain version keeps the build deterministic
# while still busting caches. TEMPORARY: the -fix suffix forces clients
# to fetch the patched bundle while the local patches are carried.
VERSION_TAG = "${version}-fix2";
# _helpers.js embeds these in the bundle when set; pin them for
# reproducibility (upstream exports the wall clock).
BUILD_DATE = "Thu Jan 01 00:00:00 UTC 1970";
BUILD_TS = "0";
};
preBuild = ''
# pnpmConfigHook honors two variables: $pnpmRoot (where the workspace
# lives; the hook pushd's into it) and $pnpmDeps (the fetched store,
# visible to the hook via bash dynamic scoping from the local).
installWorkspace() {
local pnpmRoot="$1" pnpmDeps="$2"
pnpmConfigHook
}
installWorkspace frontend ${frontendPnpmDeps}
installWorkspace plugins ${pluginsPnpmDeps}
installWorkspace mcp ${mcpPnpmDeps}
# sass-embedded ships a prebuilt dart VM expecting the host loader.
sassPkg=${
if stdenvNoCC.hostPlatform.isx86_64 then "sass-embedded-linux-x64" else "sass-embedded-linux-arm64"
}
patchelf --set-interpreter "$(cat ${stdenv.cc.bintools}/nix-support/dynamic-linker)" \
frontend/node_modules/.pnpm/$sassPkg@*/node_modules/$sassPkg/dart-sass/src/dart
'';
buildPhase = ''
runHook preBuild
# 1. render-wasm artifacts.
mkdir -p frontend/src/app/render_wasm/api frontend/resources/public/js/worker
install -m644 ${renderWasm}/render-wasm.js ${renderWasm}/render-wasm.wasm frontend/resources/public/js/
install -m644 ${renderWasm}/worker/render.js frontend/resources/public/js/worker/render.js
install -m644 ${renderWasm}/shared.js frontend/src/app/render_wasm/api/shared.js
# 2. MCP plugin.
(cd mcp && WS_URI=/mcp/ws pnpm run --filter mcp-plugin build)
# 3. Official plugins.
(cd plugins/libs/plugins-runtime && pnpm run build)
(cd plugins && pnpm run build:plugins)
# 4. Main cljs build, resolving offline from the eval-time clj-nix cache.
${cljOffline.setup}
(cd frontend && pnpm run build:app:main)
(cd frontend && pnpm run build:app:libs)
(cd frontend && pnpm run build:app:assets)
# 5. Finalize the compiled assets (the dist assembly itself happens in
# installPhase).
sed -i "s|\./render.js|./render.js?version=$version|g" frontend/resources/public/js/worker/main*.js
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/dist
rsync -a frontend/resources/public/ $out/dist/
rsync -a plugins/dist/apps/ $out/dist/plugins/
mkdir -p $out/dist/plugins/mcp
rsync -a mcp/packages/plugin/dist/ $out/dist/plugins/mcp/
echo "$version" > $out/dist/version.txt
install -m644 ${penpot}/LICENSE $out/dist/LICENSE
# The NixOS module re-renders flags into the config template at system
# build time; upstream also serves it from dist/js/config.js.
install -m644 ${penpot}/docker/images/files/config.js $out/config.js
install -m644 ${penpot}/docker/images/files/config.js $out/dist/js/config.js
runHook postInstall
'';
# The pnpm stores are exposed so a version bump can rehash them directly
# (`nix build .#penpot-frontend.<name>` with hash = lib.fakeHash).
passthru = {
inherit
version
frontendPnpmDeps
pluginsPnpmDeps
mcpPnpmDeps
;
};
meta = {
description = "Penpot frontend static bundle";
homepage = "https://penpot.app";
license = lib.licenses.mpl20;
platforms = lib.platforms.linux;
};
}