vite-pwa / vite-plugin-pwa

Zero-config PWA for Vite
https://vite-pwa-org.netlify.app/
MIT License
2.8k stars 189 forks source link

Issue on Vercel after each new deployement #708

Closed jb-thery closed 2 weeks ago

jb-thery commented 2 weeks ago

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 />
}
jb-thery commented 2 weeks ago

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.

userquin commented 2 weeks ago

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

jb-thery commented 2 weeks ago

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'.

userquin commented 2 weeks ago

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

userquin commented 2 weeks ago

check also if you have Disabled cache checked in dev tools

jb-thery commented 2 weeks ago

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

userquin commented 2 weeks ago

@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.

jb-thery commented 2 weeks ago

@userquin of course