vite-pwa/vite-plugin-pwa

Issue on Vercel after each new deployement

jb-thery opened this issue · 8 comments

Hello,

I'm experiencing a recurring issue with my React application using Vite PWA after each new deployment. The following error appears:

Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.

This error surfaces in the Google Chrome Developer Tools. If I click on "Skip Waiting" or "Unregister" for the service worker and then refresh the page, everything starts working again.

Could this be a bug, or am I missing something in my configuration?

my package.json

"dependencies": {
    "@crossfox/react-animated-number": "^1.0.18",
    "@emotion/react": "^11.11.4",
    "@emotion/styled": "^11.11.5",
    "@fontsource/hanken-grotesk": "^5.0.20",
    "@fontsource/material-icons": "^5.0.18",
    "@fontsource/reenie-beanie": "^5.0.19",
    "@fontsource/roboto": "^5.0.13",
    "@hookform/resolvers": "^3.3.4",
    "@loadable/component": "^5.16.4",
    "@mui/icons-material": "^5.15.16",
    "@mui/lab": "5.0.0-alpha.170",
    "@mui/material": "^5.15.16",
    "@mui/x-data-grid": "^7.3.1",
    "@reduxjs/toolkit": "^2.2.3",
    "@rtk-query/codegen-openapi": "^1.2.0",
    "@sentry/integrations": "^7.112.2",
    "@sentry/react": "^7.112.2",
    "@sentry/vite-plugin": "^2.16.1",
    "ahooks": "^3.7.11",
    "clsx": "^2.1.1",
    "date-fns": "^3.6.0",
    "deep-equal": "^2.2.3",
    "esbuild": "^0.20.2",
    "esbuild-runner": "^2.2.2",
    "framer-motion": "^11.1.7",
    "js-confetti": "^0.12.0",
    "localforage": "^1.10.0",
    "lottie-react": "^2.4.0",
    "material-ui-popup-state": "^5.1.0",
    "openapi-typescript-codegen": "^0.29.0",
    "papaparse": "^5.4.1",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-hook-form": "^7.51.3",
    "react-redux": "^9.1.1",
    "react-router-dom": "^6.23.0",
    "react-toastify": "^10.0.5",
    "ts-node": "^10.9.2",
    "vite-plugin-pwa": "^0.20.0",
    "vite-tsconfig-paths": "^4.3.2",
    "workbox-window": "^7.1.0",
    "yup": "^1.4.0"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.3.0",
    "@commitlint/config-conventional": "^19.2.2",
    "@jest/globals": "^29.7.0",
    "@testing-library/jest-dom": "^6.4.2",
    "@testing-library/react": "^15.0.6",
    "@testing-library/user-event": "^14.5.2",
    "@types/deep-equal": "^1.0.4",
    "@types/jest": "^29.5.12",
    "@types/loadable__component": "^5.13.9",
    "@types/node": "^20.12.8",
    "@types/papaparse": "^5.3.14",
    "@types/react": "^18.3.1",
    "@types/react-dom": "^18.3.0",
    "@typescript-eslint/eslint-plugin": "^7.8.0",
    "@vite-pwa/assets-generator": "^0.2.4",
    "@vitejs/plugin-react-swc": "^3.6.0",
    "cypress": "^13.8.1",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-config-standard-with-typescript": "^43.0.1",
    "eslint-plugin-import": "^2.29.1",
    "eslint-plugin-n": "^17.3.0",
    "eslint-plugin-promise": "^6.1.1",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-storybook": "^0.8.0",
    "gh-pages": "^6.1.1",
    "globals": "^15.1.0",
    "husky": "^9.0.11",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "jsdom": "^24.0.0",
    "path-browserify": "^1.0.1",
    "prettier": "3.2.5",
    "ts-jest": "^29.1.2",
    "tsx": "^4.8.2",
    "typescript": "^5.4.5",
    "vite": "^5.2.10",
    "vitest": "^1.5.3"
  }

Here is the configuration of my plugin in Vite:

VitePWA({
        registerType: "prompt",
        includeAssets: ["favicon.ico", "apple-touch-icon.png", "mask-icon.svg"],
        manifest: {
          name: "Lorem",
          short_name: "Lorem",
          description:
            "Lorem ipsum",
          theme_color: "#ffffff",
          icons: [
            {
              src: "pwa-64x64.png",
              sizes: "64x64",
              type: "image/png",
            },
            {
              src: "pwa-192x192.png",
              sizes: "192x192",
              type: "image/png",
            },
            {
              src: "pwa-512x512.png",
              sizes: "512x512",
              type: "image/png",
              purpose: "any",
            },
            {
              src: "maskable-icon-512x512.png",
              sizes: "512x512",
              type: "image/png",
              purpose: "maskable",
            },
          ],
        },
      })

My usage

import { LbButton } from "@components/feedback/LbButton/LbButton"
import { useAppDispatch } from "@hooks/useAppDispatch"
import { useAppSelector } from "@hooks/useAppSelector"
import { useAuth } from "@hooks/useAuth"
import { Box, List, ListItem, ListItemText, Typography } from "@mui/material"
import { selectAuthState } from "@redux/authSlice"
import { setDeferredPrompt } from "@redux/commonsSlice"
import { AppRoutes } from "@routes/AppRoutes"
import { useAsyncEffect } from "ahooks"
import { useEffect, useLayoutEffect } from "react"
import { useNavigate } from "react-router-dom"
import { toast } from "react-toastify"
import { DashboardRoutes } from "routes/DashboardRoutes"
import { noIndexDev } from "utils/noIndexDev"
import { useRegisterSW } from "virtual:pwa-register/react"
import { ROOT_ROUTE } from "./constants"
import { type BeforeInstallPromptEvent, type ReleaseInfo } from "./typing"

export const App = () => {
  const dispatch = useAppDispatch()

  const { appLoading } = useAuth()

  const navigate = useNavigate()

  const { user } = useAppSelector(selectAuthState)

  const userCanLogin = user && user.verified

  const {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    offlineReady: [offlineReady, setOfflineReady],
    needRefresh: [needRefresh, setNeedRefresh],
    updateServiceWorker,
  } = useRegisterSW({
    onRegistered(r) {
      console.log("SW Registered: " + r)
    },
    onRegisterError(error) {
      console.log("SW registration error", error)
    },
  })

  useLayoutEffect(() => {
    window.addEventListener("beforeinstallprompt", (e: Event) => {
      e.preventDefault()

      dispatch(setDeferredPrompt(e as BeforeInstallPromptEvent))
    })
  }, [])

  const close = () => {
    setOfflineReady(false)
    setNeedRefresh(false)
  }

  const handleClick = async () => {
    if (needRefresh) await updateServiceWorker(true)

    close()
  }

  useEffect(() => {
    if (needRefresh) {
      const updateInfo = JSON.parse(
        import.meta.env.VITE_APP_RELEASE_NOTES,
      ) as ReleaseInfo

      toast(
        <Box sx={{ width: "100%", display: "flex", flexDirection: "column" }}>
          <Typography variant="body1" fontWeight="bold" sx={{ ml: 2 }}>
            New version {updateInfo.version} available !
          </Typography>

          <Typography variant="subtitle2" fontWeight="bold" sx={{ ml: 2 }}>
            {updateInfo.date}
          </Typography>

          {updateInfo.details.map((detail) => (
            <List sx={{ width: "100%", my: 1 }} key={detail.title}>
              <ListItem alignItems="flex-start" sx={{ my: 0, py: 0 }}>
                <ListItemText
                  primary={detail.title}
                  secondary={detail.description}
                  sx={{ my: 0, py: 0 }}
                />
              </ListItem>
            </List>
          ))}

          <LbButton
            size="small"
            variant="text"
            sx={{ color: "white", mx: "auto" }}
          >
            Launch software upgrade
          </LbButton>
        </Box>,
        {
          onClick: handleClick,
          autoClose: false,
          closeButton: false,
        },
      )
    }
  }, [needRefresh])

  useAsyncEffect(async () => {
    noIndexDev()

    if (userCanLogin) navigate(ROOT_ROUTE)
  }, [])

  return appLoading ? null : userCanLogin ? <DashboardRoutes /> : <AppRoutes />
}

I've noticed that each time we release a new version of our PWA, it initially tries to load the old index.html along with the addresses of the old scripts. When I manually click on "SkipWaiting" in Chrome DevTools, everything gets sorted out and the new content loads correctly.

Is this behavior a bug, or is there something I need to configure differently to ensure that the new index.html is loaded automatically after a release? Any guidance or advice on how to handle this would be greatly appreciated.

Review the cache headers, rebuilding the app will remove old assets: check https://vite-pwa-org.netlify.app/deployment/#cache-control

I also encounter this issue on localhost when I rebuild my PWA and run the vite preview script:

Failed to load resource: the server responded with a status of 404 (Not Found).

This occurs because the old index.html remains until I click on 'skipWaiting'.

do you have a link to the repo or deployed url (vercel)?

check also if you have Disabled cache checked in dev tools

This caching issue on Vercel has been resolved by implementing the following configuration in the vercel.json file :

{
  "headers": [
    {
      "source": "/(.*).html",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "no-store"
        }
      ]
    },
    {
      "source": "/sw.js",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=0, must-revalidate"
        }
      ]
    },
    {
      "source": "/manifest.webmanifest",
      "headers": [
        {
          "key": "Content-Type",
          "value": "application/manifest+json"
        }
      ]
    },
    {
      "source": "/assets/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "max-age=31536000, immutable"
        }
      ]
    },
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        }
      ]
    }
  ],
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ]
}

and click to purge data cache in Vercel dashboard

@jb-thery can you send a PR to the docs repo for Vercel entry in the deploy section? I have no idea about Vervel (I don't use it)

This is the file https://github.com/vite-pwa/docs/blob/main/deployment/vercel.md

You can fork docs repo to your GH account and then visit the page and click on edit this page at the bottom.

@userquin of course