Reverts
|
||
|---|---|---|
| .. | ||
| scripts | ||
| src | ||
| tests | ||
| .babelrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .report.json | ||
| .testcaferc.cjs | ||
| AGENTS.md | ||
| CODEMAP.md | ||
| eslint.config.mjs | ||
| gettext.config.js | ||
| Makefile | ||
| NOTICE | ||
| package.json | ||
| postcss.config.js | ||
| README.md | ||
| vitest.config.js | ||
| vitest.config.portal.js | ||
| vitest.config.pro.js | ||
| webpack.config.js | ||
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 quickstartfrontend/CODEMAP.md— module layout and responsibilitiesfrontend/src/common/README.md— dialog/focus patterns and shared helpersfrontend/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-jsornpm run test. Barenpx vitest runskips thecross-envwrapper that setsTZ=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-terser → serialize-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.js → hls.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 --watch — not 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., easygettext → vue3-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
- Confirm the package has an active maintainer, scoped name, and a 2FA-protected publisher.
- Avoid packages that require
postinstall/installscripts. Installs default to--ignore-scripts. - Add to
frontend/package.json. From the repo root runnpm install --ignore-scripts --no-audit --no-fund --no-update-notifierso the workspace lockfile updates. make auditmust report zero advisories.- Run
make build-jsandmake test-js. make noticeto refreshNOTICEandfrontend/NOTICE.
Removing a Dependency
- Confirm no source imports anywhere (use the
rgcommand in Known Unused or Legacy Dependencies). - Drop the line from
frontend/package.json. - Run
npm installfrom the repo root (refreshes the workspace lockfile). make audit,make build-js,make test-js,make notice.
Bumping a Dependency
- Check the table in Currently Pinned Packages; pinned packages need extra care.
- Check the table in Major-Version Upgrades — Known Blockers for ESM-only majors that would require a config rewrite.
- Edit the version in
frontend/package.json, thennpm installfrom repo root. - 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-analyzeforwebpack-bundle-analyzer). - Refresh
make noticeif the package count or licenses changed. - Update Currently Pinned Packages or this document if the rationale for an existing pin no longer applies.
Sources of Truth
Makefileandfrontend/Makefilefor build, test, and audit targets.frontend/package.jsonfor 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.