From fdd5f9ce655ba3abcbd65a61fa137d56cd9ab9ba Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:32:09 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20fix:=20Precache=20PWA?= =?UTF-8?q?=20Icons=20and=20Let=20Them=20Refresh=20(#15654)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🖼️ fix: Precache PWA Icons and Let Them Refresh Production builds set `publicDir: false`, so `client/scripts/post-build.cjs` copied `public/assets` into `dist/assets` only after `vite build` had finished. vite-plugin-pwa globs `dist/` from its `closeBundle` hook, inside that build, so the icon patterns in `workbox.globPatterns` matched zero files and the generated `sw.js` shipped without a single icon, silently: workbox treats an empty glob as a no-op. `build:ci`, used by the Playwright lanes, never ran the post-build step at all and produced a `dist` with no icons or `robots.txt`. Move the copy into a `copyPublicAssets()` build plugin that runs in `writeBundle`, which completes for every plugin before `closeBundle`, and drop the post-build script with its lone `fs-extra` dependency. Ordering alone was not enough. The plugin defaults `dontCacheBustURLsMatching` to `/^assets\//`, true only for hashed bundle output. The icons live in `assets/` under stable filenames, so they entered the manifest with `revision: null`, permanently immutable, meaning an installed PWA would never pick up a rebranded icon: the other half of the report. Match Vite's `[name].[hash].[ext]` shape instead so hashed chunks stay revision-free while the icons carry content revisions. Also drop `manifest.webmanifest` from `globPatterns`, since the plugin always appends it as an additional manifest entry and globbing it duplicated the precache entry. A clean `npm run build` now yields 55 precache entries with all five icons revisioned, against 0 icon entries before. * 📝 docs: Describe the Full Copy Set in copyPublicAssets * 🔒 chore: Drop fs-extra From bun.lock --- .github/workflows/codegraph-e2e-votes.yml | 2 +- .github/workflows/playwright-bombadil.yml | 2 +- .github/workflows/playwright-mock.yml | 2 +- bun.lock | 3 -- client/package.json | 3 +- client/scripts/post-build.cjs | 14 ------- client/vite.config.ts | 47 ++++++++++++++++++++++- package-lock.json | 16 -------- 8 files changed, 50 insertions(+), 39 deletions(-) delete mode 100644 client/scripts/post-build.cjs diff --git a/.github/workflows/codegraph-e2e-votes.yml b/.github/workflows/codegraph-e2e-votes.yml index b3783e2658..251c7a9a71 100644 --- a/.github/workflows/codegraph-e2e-votes.yml +++ b/.github/workflows/codegraph-e2e-votes.yml @@ -125,7 +125,7 @@ jobs: uses: actions/cache@v5 with: path: client/dist - key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'client/src/**', 'client/public/**', 'client/scripts/post-build.cjs', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', 'packages/client/src/**', 'packages/client/tailwind.preset.cjs', 'packages/client/tsconfig*.json', 'packages/client/tsdown.config.mjs', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }} + key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'client/src/**', 'client/public/**', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', 'packages/client/src/**', 'packages/client/tailwind.preset.cjs', 'packages/client/tsconfig*.json', 'packages/client/tsdown.config.mjs', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }} - name: Build client app if: steps.cache-client-app.outputs.cache-hit != 'true' diff --git a/.github/workflows/playwright-bombadil.yml b/.github/workflows/playwright-bombadil.yml index 064a3141dc..0f37ff1b11 100644 --- a/.github/workflows/playwright-bombadil.yml +++ b/.github/workflows/playwright-bombadil.yml @@ -113,7 +113,7 @@ jobs: uses: actions/cache@v4 with: path: client/dist - key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'client/src/**', 'client/public/**', 'client/scripts/post-build.cjs', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', 'packages/client/src/**', 'packages/client/tailwind.preset.cjs', 'packages/client/tsconfig*.json', 'packages/client/tsdown.config.mjs', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }} + key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'client/src/**', 'client/public/**', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', 'packages/client/src/**', 'packages/client/tailwind.preset.cjs', 'packages/client/tsconfig*.json', 'packages/client/tsdown.config.mjs', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }} - name: Build client app if: steps.cache-client-app.outputs.cache-hit != 'true' diff --git a/.github/workflows/playwright-mock.yml b/.github/workflows/playwright-mock.yml index 2d6de6b197..acb62732b1 100644 --- a/.github/workflows/playwright-mock.yml +++ b/.github/workflows/playwright-mock.yml @@ -260,7 +260,7 @@ jobs: uses: actions/cache@v5 with: path: client/dist - key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'client/src/**', 'client/public/**', 'client/scripts/post-build.cjs', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', 'packages/client/src/**', 'packages/client/tailwind.preset.cjs', 'packages/client/tsconfig*.json', 'packages/client/tsdown.config.mjs', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }} + key: build-client-app-e2e-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json', 'client/src/**', 'client/public/**', 'client/index.html', 'client/package.json', 'client/vite.config.*', 'client/tsconfig*.json', 'client/tailwind.config.*', 'client/postcss.config.*', 'packages/client/src/**', 'packages/client/tailwind.preset.cjs', 'packages/client/tsconfig*.json', 'packages/client/tsdown.config.mjs', 'packages/client/package.json', 'packages/data-provider/src/**', 'packages/data-provider/tsconfig*.json', 'packages/data-provider/tsdown.config.mjs', 'packages/data-provider/package.json') }} - name: Build client app if: steps.cache-client-app.outputs.cache-hit != 'true' diff --git a/bun.lock b/bun.lock index 3950b99681..c024ae7812 100644 --- a/bun.lock +++ b/bun.lock @@ -276,7 +276,6 @@ "babel-plugin-transform-import-meta": "^2.3.2", "babel-plugin-transform-vite-meta-env": "^1.0.3", "eslint-plugin-jest": "^29.1.0", - "fs-extra": "^11.3.2", "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", @@ -3296,8 +3295,6 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], diff --git a/client/package.json b/client/package.json index 0e73dbb04c..347c432552 100644 --- a/client/package.json +++ b/client/package.json @@ -7,7 +7,7 @@ "typecheck": "tsc --noEmit", "data-provider": "cd .. && npm run build:data-provider", "build:file": "cross-env NODE_ENV=production vite build --debug > vite-output.log 2>&1", - "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 vite build && node ./scripts/post-build.cjs", + "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 vite build", "build:ci": "cross-env NODE_ENV=development vite build --mode ci", "dev": "cross-env NODE_ENV=development vite", "preview-prod": "cross-env NODE_ENV=development vite preview", @@ -156,7 +156,6 @@ "babel-plugin-transform-import-meta": "^2.3.2", "babel-plugin-transform-vite-meta-env": "^1.0.3", "eslint-plugin-jest": "^29.1.0", - "fs-extra": "^11.3.2", "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", diff --git a/client/scripts/post-build.cjs b/client/scripts/post-build.cjs deleted file mode 100644 index 0c0f00dc14..0000000000 --- a/client/scripts/post-build.cjs +++ /dev/null @@ -1,14 +0,0 @@ -const fs = require('fs-extra'); - -async function postBuild() { - try { - await fs.copy('public/assets', 'dist/assets'); - await fs.copy('public/robots.txt', 'dist/robots.txt'); - console.log('✅ PWA icons and robots.txt copied successfully. Glob pattern warnings resolved.'); - } catch (err) { - console.error('❌ Error copying files:', err); - process.exit(1); - } -} - -postBuild(); diff --git a/client/vite.config.ts b/client/vite.config.ts index 4f39f2653c..5a279a92ca 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -89,6 +89,7 @@ export default defineConfig(({ command }) => ({ }); }, }, + copyPublicAssets(), VitePWA({ injectRegister: 'auto', // 'auto' | 'manual' | 'disabled' registerType: 'autoUpdate', // 'prompt' | 'autoUpdate' @@ -103,8 +104,9 @@ export default defineConfig(({ command }) => ({ 'assets/favicon*.png', 'assets/icon-*.png', 'assets/apple-touch-icon*.png', + /** `manifest.webmanifest` is not listed: vite-plugin-pwa always appends it as an + * additional manifest entry, so globbing it too duplicates the precache entry. */ 'assets/maskable-icon.png', - 'manifest.webmanifest', ], globIgnores: [ 'images/**/*', @@ -116,6 +118,14 @@ export default defineConfig(({ command }) => ({ 'assets/query-devtools*.js', ], maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, + /** + * vite-plugin-pwa defaults this to `/^assets\//`, which is only true for the + * hashed bundle output: the icons live in `assets/` under stable filenames, so the + * default marks them immutable (`revision: null`) and an installed PWA would keep + * a rebranded icon forever. Match Vite's `[name].[hash].[ext]` shape instead so + * hashed chunks stay revision-free while the icons get content revisions. + */ + dontCacheBustURLsMatching: /\.[\w-]{8}\.(?:js|css)$/, /** LibreChat mutates index.html per request for subpath and language support. */ navigateFallback: null, /** Reloads window clients that cannot answer a ping after activation — @@ -447,3 +457,38 @@ export function sourcemapExclude(opts?: SourcemapExclude): Plugin { }, }; } + +/** + * Production builds set `publicDir: false`, so nothing under public/ reaches dist on its + * own. This copies what the server actually has to serve: all of public/assets (the PWA + * icons plus the endpoint, tool and language logos referenced at runtime) and robots.txt. + * public/fonts is deliberately left out, since fonts are emitted as bundle assets through + * the `$fonts` alias. + * + * The copy MUST happen inside the build. vite-plugin-pwa globs dist/ for + * `workbox.globPatterns` from its `closeBundle` hook, which runs after every plugin's + * `writeBundle`, so copying here is what lets the `assets/*.png` icon patterns match. An + * `npm run build && node scripts/post-build.cjs` chain cannot: it runs after the service + * worker has already been generated, so the icons were silently absent from the precache + * manifest. + */ +export function copyPublicAssets(): Plugin { + const publicDir = path.resolve(import.meta.dirname, 'public'); + let outDir = path.resolve(import.meta.dirname, 'dist'); + return { + name: 'copy-public-assets', + apply: 'build', + configResolved(config) { + outDir = path.resolve(config.root, config.build.outDir); + }, + async writeBundle() { + await fs.promises.cp(path.join(publicDir, 'assets'), path.join(outDir, 'assets'), { + recursive: true, + }); + await fs.promises.copyFile( + path.join(publicDir, 'robots.txt'), + path.join(outDir, 'robots.txt'), + ); + }, + }; +} diff --git a/package-lock.json b/package-lock.json index 465f677018..5ebcebfa50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1054,7 +1054,6 @@ "babel-plugin-transform-import-meta": "^2.3.2", "babel-plugin-transform-vite-meta-env": "^1.0.3", "eslint-plugin-jest": "^29.1.0", - "fs-extra": "^11.3.2", "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-canvas-mock": "^2.5.2", @@ -26547,21 +26546,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",