photoprism/frontend
Cathie Integra 16eb3fb708 Frontend: Revert dep bump that broke options.js in prod bundle
Reverts 0182c46bb. The webpack 5.107.2->5.108.0 bump changed prod-bundle
scope-hoisting so option-builders in src/options/options.js (Languages,
TimeZones, StartPages, MapsAnimate) threw "Cannot read properties of
undefined", breaking the login and Settings pages on all editions.
Re-apply the photo-sphere-viewer/tar patch bumps separately.
2026-06-25 20:11:57 +02:00
..
scripts Frontend: Pre-compress static bundle assets at build time #5552 2026-05-03 13:43:44 +00:00
src Users: Centralize Add Account auth field gating in the user model 2026-06-25 16:24:55 +00:00
tests Users: Centralize Add Account auth field gating in the user model 2026-06-25 16:24:55 +00:00
.babelrc Frontend: Update deps in package.json and package-lock.json 2024-01-19 15:16:01 +01:00
.prettierignore Frontend: Restrict Prettier to CSS and JSON via .prettierignore 2026-05-16 10:34:38 +02:00
.prettierrc.json UX: Change order of feature flag toggles in Settings > General 2025-11-26 13:05:29 +01:00
.report.json CI: Generate NOTICE file with go-licenses and npm license-report 2024-05-15 10:11:15 +02:00
.testcaferc.cjs Tests: Enable elapsed time logging for each acceptance test 2026-06-15 08:53:33 +02:00
AGENTS.md Frontend: Upgrade axios from 1.16.1 to 1.17.0 2026-06-10 16:55:39 +00:00
CODEMAP.md Lightbox: Add support for 360° photos and videos #352 #4718 #5623 2026-06-17 03:26:25 +02:00
eslint.config.mjs Frontend: Tune lightbox theme for face markers, retire ESLint-Prettier glue #4966 2026-05-19 09:56:09 +00:00
gettext.config.js Frontend: Disable msgmerge fuzzy matching with vue3-gettext 2025-10-22 09:51:13 +02:00
Makefile Build: Centralize npm targets in root Makefile and align flags 2026-05-29 04:40:23 +00:00
NOTICE Frontend: Remove unused babel-plugin-istanbul and dead Karma config #5659 2026-06-25 16:29:40 +00:00
package.json Frontend: Revert dep bump that broke options.js in prod bundle 2026-06-25 20:11:57 +02:00
postcss.config.js Frontend: Integrate Vitest test framework #4990 2025-05-06 16:18:39 +02:00
README.md Frontend: Document webpack to Vite migration plan #5679 2026-06-22 20:17:34 +00:00
vitest.config.js Frontend: Upgrade Vitest to v4 and Vite to v8 to drop esbuild #5659 2026-06-14 16:15:51 +00:00
vitest.config.portal.js Frontend: Point the portal vitest alias at the cluster-instance model 2026-06-09 03:18:15 +00:00
vitest.config.pro.js Frontend: Reformat .js, .mjs, and .vue files 2026-03-06 13:48:22 +01:00
webpack.config.js Links: Use canonical trailing-slash form for website URLs 2026-06-24 17:20:30 +02:00

PhotoPrism Frontend

The Vue 3 + Vuetify 3 web UI for PhotoPrism. Built with webpack, tested with Vitest, and packaged into the Go binary as static assets.

Other frontend documentation lives next to this file:

  • frontend/AGENTS.md — agent quickstart
  • frontend/CODEMAP.md — module layout and responsibilities
  • frontend/src/common/README.md — dialog/focus patterns and shared helpers
  • frontend/tests/README.md — test layout

Common Commands

Task Command
Production build make build-js
Watch (development) make watch-js
Vitest unit tests make test-js (sets TZ=UTC and BABEL_ENV=test)
Vitest watch make vitest-watch
Coverage make vitest-coverage
Lint and format make fmt-js
Audit dependencies make audit
List outdated deps cd frontend && make dep-list
Refresh NOTICE make notice

Always invoke Vitest through make test-js or npm run test. Bare npx vitest run skips the cross-env wrapper that sets TZ=UTC BUILD_ENV=development NODE_ENV=development BABEL_ENV=test. Without those, ~50 component and TZ-sensitive tests fail spuriously.

Dependency Pinning Policy

Pins are intentional. When a version is locked without a caret (e.g., "axios": "1.18.1"), it is intentional. Before adjusting any pin, check the table below, the inline // comments at the top of package.json, and the git log (git log -p -- frontend/package.json | grep -B2 -A4 "<pkg>").

Currently Pinned Packages

Package Pin Reason
vuetify 3.12.2 3.12.3+ added an onFocusout handler to VAutocomplete/VSelect/VCombobox that closes long autocomplete/select dropdowns on open (#5538). Still unfixed in 3.12.5; upstream development moved to v4. See the long //vuetify comment in package.json and frontend/CODEMAP.md for retest steps.
axios 1.18.1 High-risk package. Originally pinned to 1.14.0 after the March 2026 supply-chain compromise (malicious 1.14.1/0.30.4 from a hijacked maintainer account). Quarantine was unwound on 2026-04-27 once OSV-Scanner came back clean; bumped to 1.17.0 on 2026-06-10, then to 1.18.1 on 2026-06-22 (security-hardening minor: strips caller-supplied sensitive headers on cross-origin redirects, rejects malformed http/https URLs, tightens prototype-pollution defenses; no breaking changes; OSV-Scanner clean). Keep an exact pin (no caret) per industry guidance for high-risk packages.

Override Layer (Transitive Pins)

frontend/package.json and root package.json declare matching overrides. Mirroring them keeps the npm workspace lockfile resolution consistent.

Override Reason
"serialize-javascript": "^7.0.5" Closes the workbox-build@rollup/plugin-terserserialize-javascript RCE advisory (GHSA-5c6j-r48x-rmvq, GHSA-qj8w-gfj5-8c6v).

When an upstream advisory is fully resolved, retire the override and rerun make audit plus a focused build/test pass before committing the cleanup.

Major-Version Upgrades — Known Blockers

Some major upgrades are blocked by config-file module style (the configs referenced below are CommonJS today and need an ESM migration first) or by a bundler-level incompatibility in the package itself. Track each as its own change:

Package Latest Blocker
postcss-preset-env 11.x ESM frontend/postcss.config.js is CommonJS (module.exports = { plugins: [require("postcss-preset-env"), ...] }).
webpack-manifest-plugin 6.x ESM frontend/webpack.config.js is CommonJS (require("webpack-manifest-plugin")). Webpack accepts ESM configs, but the migration is non-trivial.
vue3-gettext 4.x ESM v4 is ESM-only and exports its extraction tooling from the same runtime entry: dist/index.js does an unconditional import PO from "pofile", and pofile calls require("fs"). The exports map has no runtime-only subpath and the package sets no sideEffects: false, so webpack cannot tree-shake it — make build-js fails with Can't resolve 'fs' in pofile/lib/po.js. The runtime API itself (createGettext({ translations, silent, defaultLanguage }), the $gettext/$ngettext/$pgettext/$npgettext globals, %{} interpolation) is compatible and the removed <translate> component / v-translate directive are unused here, so the only fix needed is a bundler workaround (e.g. resolve.fallback: { fs: false }, which ships dead extraction code), an upstream split of runtime vs. tooling exports, or a migration to Vite (Rollup externalizes fs instead of failing).
vuetify 4.x See the vuetify row in Currently Pinned Packages; also a separate v3 → v4 migration project.
vue-router 5.x Major release with breaking changes across frontend/src/app/routes.js and dynamic imports. Needs its own evaluation pass with TestCafe verification.

Migrating the Production Build From webpack to Vite

Status: not started — tracked in #5679. The production bundle is built with webpack (frontend/webpack.config.js, run via make build-js); Vitest already runs on Vite (frontend/vitest.config.js), so a production migration would converge the two toolchains. This is its own project (related: #5659 test-side Vite v8, and the historical #838). It is not a prerequisite for any single dependency bump; treat the ESM-only unblock (e.g. vue3-gettext 4.x) as a side benefit, not the reason.

Why it helps the ESM-only blockers. Vite/Rollup externalize Node built-ins for the browser — a fs import resolves to a stub that only throws if actually called at runtime — instead of failing the build the way webpack 5 does. Verified locally: a minimal Vite build of vue3-gettext 4's createGettext succeeds with a Module "fs" has been externalized warning. Caveat: Vite does not tree-shake the dead extraction tooling either (the package sets no sideEffects: false), so the runtime bundle still carries the pofile/parser code — migrate for the modernization, not as a bundle slimmer.

The contract that must not break. The Go server reads a flat asset manifest: internal/config/client_assets.go (ClientAssets) unmarshals assets/static/build/assets.json keyed by logical name — app.js, app.css, share.js, share.css, splash.js, splash.css, plus named font/image entries (MaterialIcons-Regular.*, default-skin.svg, preloader.gif, …). That shape is webpack-manifest-plugin's output. Vite's default .vite/manifest.json is keyed by source path with nested objects and is not interchangeable, so the migration must either emit the flat assets.json from a small post-build step or teach ClientAssets.Load the Vite shape. All editions (plus/pro/portal) read the same struct, so whatever shape is chosen must hold for every edition build.

Feature-by-feature mapping (from webpack.config.js):

webpack today Vite equivalent
3 entries app/share/splash build.rollupOptions.input (3 inputs)
output../assets/static/build, [name].[contenthash].js, clean build.outDir + emptyOutDir + entryFileNames/chunkFileNames/assetFileNames with [name].[hash]
flat assets.json via webpack-manifest-plugin build.manifest + post-build transform to the flat shape (see contract above)
resolve.modules: [src] + preferRelative reuse the resolve.alias map already in vitest.config.js
vue-loader, vue$ runtime alias, whitespace: "preserve" @vitejs/plugin-vue (already a dep) + alias + template.compilerOptions
webpack-plugin-vuetify (auto-import) vite-plugin-vuetify (new dep)
babel-loader + @babel/preset-env + core-js (browserslist Safari 13 / iOS 13) @vitejs/plugin-legacy (new dep; reads browserslist) — esbuild alone won't downlevel to the iOS 13 baseline
workbox-webpack-plugin GenerateSW (sw.js, importScripts sw-scope-cleanup.js, modifyURLPrefix: static/build/, exclusions, 5 MiB cap) vite-plugin-pwa (new dep; workbox under the hood) with the same options
CSS/PostCSS/Sass loader stack (mini-css-extract-plugin, css-loader, postcss-loader, resolve-url-loader, sass-loader) Vite native CSS (reads postcss.config.js; keep the sass dep)
DefinePlugin Vue flags (__VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__) Vite define
EmitStaticFilePlugin (copies sw-scope-cleanup.js) publicDir or a small emit plugin
hls.jshls.light.min.js alias same resolve.alias entry
png/jpg/svg/font asset/resource Vite asset pipeline — confirm the named manifest entries still land in assets.json
CUSTOM_SRC/CUSTOM_NAME edition overlay env-driven resolve.alias/input
webpack-bundle-analyzer (BUILD_ENV=analyze) rollup-plugin-visualizer
make watch-js (webpack --watch) vite build --watchnot the dev server; Go serves the embedded build output, so HMR isn't wired into the templates
postbuild scripts/precompress.js unchanged; it operates on the output dir, not the bundler

Dependency churn. Retire webpack, webpack-cli, webpack-manifest-plugin, webpack-plugin-vuetify, workbox-webpack-plugin, eslint-webpack-plugin, webpack-bundle-analyzer, vue-loader, and the loader stack (babel-loader, css-loader, postcss-loader, resolve-url-loader, mini-css-extract-plugin, vue-style-loader, file-loader, url-loader, svg-url-loader). Add vite-plugin-vuetify, @vitejs/plugin-legacy, vite-plugin-pwa, and rollup-plugin-visualizer (vite and @vitejs/plugin-vue are already deps). Then make audit and make notice.

Validation. make build-js emits assets/static/build/assets.json in the flat shape and the Go server renders the app/share/splash entrypoints without cannot read assets.json; sw.js is generated and the precompressed .gz/.zst siblings appear; smoke-test the Safari 13 / iOS 13 baseline; the plus/pro/portal edition builds still produce valid manifests; finally make test-js and the acceptance suite.

Auditing for Orphaned Dependencies

Past migrations (e.g., easygettextvue3-gettext, mocha → Vitest) have occasionally left top-level deps behind that no longer have any consumer. Before adding a new dep — and ideally as a periodic sweep — verify each candidate has a real consumer:

rg -nF "<pkg>" frontend \
  --glob '!node_modules/**' --glob '!package-lock.json' --glob '!NOTICE'
cd frontend && npm ls <pkg> --all

If neither command surfaces a real source-level import or transitive consumer, the dep is a removal candidate (recent precedents: postcss-url, @vitejs/plugin-react, cheerio, @testing-library/react, vite-tsconfig-paths).

Adding a New Dependency

  1. Confirm the package has an active maintainer, scoped name, and a 2FA-protected publisher.
  2. Avoid packages that require postinstall/install scripts. Installs default to --ignore-scripts.
  3. Add to frontend/package.json. From the repo root run npm install --ignore-scripts --no-audit --no-fund --no-update-notifier so the workspace lockfile updates.
  4. make audit must report zero advisories.
  5. Run make build-js and make test-js.
  6. make notice to refresh NOTICE and frontend/NOTICE.

Removing a Dependency

  1. Confirm no source imports anywhere (use the rg command in Known Unused or Legacy Dependencies).
  2. Drop the line from frontend/package.json.
  3. Run npm install from the repo root (refreshes the workspace lockfile).
  4. make audit, make build-js, make test-js, make notice.

Bumping a Dependency

  1. Check the table in Currently Pinned Packages; pinned packages need extra care.
  2. Check the table in Major-Version Upgrades — Known Blockers for ESM-only majors that would require a config rewrite.
  3. Edit the version in frontend/package.json, then npm install from repo root.
  4. Run make audit && make build-js && make test-js. For test runner or build tooling, also do an ad-hoc smoke test (e.g., npm run build-analyze for webpack-bundle-analyzer).
  5. Refresh make notice if the package count or licenses changed.
  6. Update Currently Pinned Packages or this document if the rationale for an existing pin no longer applies.

Sources of Truth

  • Makefile and frontend/Makefile for build, test, and audit targets.
  • frontend/package.json for dependency declarations, overrides, and pin rationale comments.
  • Git log for the why behind any specific pin or removal — search with git log -p -S "<pkg>" -- frontend/package.json.