shadowwalker / next-pwa

Zero config PWA plugin for Next.js, with workbox 🧰
MIT License
3.75k stars 311 forks source link

Next v13 `app-build-manifest.json` - Does not register a service worker that controls page and start_url #424

Open jpnws opened 1 year ago

jpnws commented 1 year ago

Summary

I was running into "Does not register a service worker that controls page and start_url" after migrating to NextJS v13.

image

After some investigation, I noticed that I was running into a bad precaching runtime error due to http://localhost:3000/_next/app-build-manifest.json.

image

Does this have something to do with the fact that Next13 uses app directory?

Versions:

Steps to reproduce the behavior: Migrate to NextJS v13.

Expected Behaviors: Service worker should be succesfully registered.

Oussamaosman02 commented 1 year ago

NextJs 13 still don´t support some other libraries like this one because they have to update it to the app folder. If you want the pwa you have to do it with pages folder

Rishab1207 commented 1 year ago

@jpnws are you adding your pages into the app directory?

BluThaitanium commented 1 year ago

Not a fix, but a behavior I found that gets PWA working briefly on apps not using /pages/:

  1. Comment out appDir on next.config.js (breaks website)

  2. npm run build, npm run dev

  3. Open localhost:3000

    • You should see 404 not found
  4. Uncomment appDir

  5. npm run build, npm run dev

  6. Refresh webpage, and the PWA should work

    • I did this on Chrome. PWA shows up working in console and applications tab in dev tools. Once I ran Lighthouse, things stop working again.
/** @type {import('next').NextConfig} */

const withPWA = require('next-pwa')({
  dest: 'public',
})

module.exports = withPWA({
  experimental: {
    appDir: true,             // <---- Comment and Uncomment this
  },
})
jpnws commented 1 year ago

@jpnws are you adding your pages into the app directory?

Yes I'm using the new app directory on NextJS 13.

DuCanhGH commented 1 year ago

I ended up forking this repo: https://github.com/DuCanhGH/next-pwa (it works with App Router and has built-in TypeScript, JSDoc support - all examples are also written in TypeScript).

This happens because this line doesn't check for app-build-manifest.json alongside with build-manifest.json although it is supposed to as there hasn't been any activity on this repo since App Router was released.

One alternative is changing your next-pwa config:

// next.config.js
const withPWAInit = require("next-pwa");

const isDev = process.env.NODE_ENV !== "production";

const withPWA = withPWAInit({
    // your other config...
    exclude: [
        // add buildExcludes here
        ({ asset, compilation }) => {
            if (
                asset.name.startsWith("server/") ||
                asset.name.match(/^((app-|^)build-manifest\.json|react-loadable-manifest\.json)$/)
            ) {
                return true;
            }
            if (isDev && !asset.name.startsWith("static/runtime/")) {
                return true;
            }
            return false;
        }
    ],
});

/** @type {import("next").NextConfig} */
const nextConfig = {
    // your other config...
    webpack(config) {
        const registerJs = path.join(path.dirname(require.resolve("next-pwa")), "register.js");
        const entry = config.entry;

        config.entry = () =>
            entry().then((entries) => {
                // Automatically registers the SW and enables certain `next-pwa` features in 
                // App Router (https://github.com/shadowwalker/next-pwa/pull/427)
                if (entries["main-app"] && !entries["main-app"].includes(registerJs)) {
                    if (Array.isArray(entries["main-app"])) {
                        entries["main-app"].unshift(registerJs);
                    } else if (typeof entries["main-app"] === "string") {
                        entries["main-app"] = [registerJs, entries["main-app"]];
                    }
                }
                return entries;
            });

        return config;
    },
}

module.exports = nextConfig;

Edit: updated the solution based on @Schular's comment.

CombeeMike commented 1 year ago

Does anyone know if there are any plans to also add this fix to this repo anytime soon?

evolify commented 1 year ago

Any update, or any alternative?

AlainYRS commented 1 year ago

Done! Create a head.tsx in app root directory as client component, then with a use effect hook register the service worker.

AaronLayton commented 1 year ago

@AlainYRS you got an example you could paste in?

I can get it to register in the header but get the following error

Uncaught (in promise) bad-precaching-response: bad-precaching-response :: [{"url":"http://localhost:3000/_next/app-build-manifest.json","status":404}]
DuCanhGH commented 1 year ago

@AaronLayton I don't think his solution would work. If you want to use next-pwa, you can try adding this to your next-pwa config:

const withPWAInit = require("next-pwa");

const isDev = process.env.NODE_ENV !== "production";

const withPWA = withPWAInit({
  // your other config
  exclude: [
    // add buildExcludes here
    ({ asset, compilation }) => {
      if (
        asset.name.startsWith("server/") ||
        asset.name.match(/^((app-|^)build-manifest\.json|react-loadable-manifest\.json)$/)
      ) {
        return true;
      }
      if (isDev && !asset.name.startsWith("static/runtime/")) {
        return true;
      }
      return false;
    }
  ],
});

// your Next config

and you still have to register the SW yourself.

AaronLayton commented 1 year ago

Thanks @DuCanhGH that works for me. I did try and exclude the app-build-manifest but think I was using buildExcludes instead of exclude.

Temporary fix
For anyone coming here for the same reason and before this PR gets merged - https://github.com/shadowwalker/next-pwa/pull/427 - the below works for me in production with the following caveat.

This excludes the nextjs build manifest which includes all the page URLs that get generated. In my testing, the below will pre cache static files but it won't cache any URL until you first visit the page after the service-worker has been installed on first visit.

next-pwa": "^5.6.0

next.config.js

const withPWAInit = require("next-pwa");

const isDev = process.env.NODE_ENV !== "production";

const withPWA = withPWAInit({
  dest: 'public',
  disable: isDev,

  exclude: [
    // add buildExcludes here
    ({ asset, compilation }) => {
      if (
        asset.name.startsWith("server/") ||
        asset.name.match(/^((app-|^)build-manifest\.json|react-loadable-manifest\.json)$/)
      ) {
        return true;
      }
      if (isDev && !asset.name.startsWith("static/runtime/")) {
        return true;
      }
      return false;
    }
  ],
});

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
}

module.exports = withPWA(nextConfig);

app/layout.tsx

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html 
      lang="en" 
    >
      {/*
        <head /> will contain the components returned by the nearest parent
        head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
      */}
      <head />
      <body
        className="min-h-[100svh]"
      >
        <Providers>
            {children}
        </Providers>
        <Pwa />
      </body>
    </html>
  )
}

Custom PWA Header component that runs in the client

app/Pwa.tsx

"use client"

import { useEffect } from "react";

export default function Pwa() {
  let sw: ServiceWorkerContainer | undefined;

  if (typeof window !== "undefined") {
    sw = window?.navigator?.serviceWorker;
  }

  useEffect(() => {
    if (sw) {
      sw.register("/sw.js", { scope: "/" }).then((registration) => {
        console.log("Service Worker registration successful with scope: ", registration.scope);
      }).catch((err) => {
        console.log("Service Worker registration failed: ", err);
      });
    }
  }, [sw]);

  return (
    <></>
  )
}
DuCanhGH commented 1 year ago

@AaronLayton I think <HeadPwa /> should be put in your app/layout.tsx instead.

And that PR won't completely fix the problem, it will only make auto registration work in appDir, it won't fix that bad-precaching-response issue. Not that I think the maintainers will merge it, I think this repo seems dead (despite the author saying otherwise).

Also, disable: process.env.NODE_ENV === 'development' can be changed to disable: isDev. Just saying :)

AaronLayton commented 1 year ago

@DuCanhGH fully agree - yeah the HeadPwa only ended up being there due to iterations to get to this point. Adding it to the layout.tsx will be just as good. I have updated the snippet above.

By all means this is not a fix but it should get people by for the time being.

I did try to contact @shadowwalker on Twitter to see if they were getting notifications or if there is any way I can help to get the PR merged in, but no luck yet ☹️

DuCanhGH commented 1 year ago

@AaronLayton I'd also suggest changing your app/Pwa.tsx a bit:

import type { Workbox } from "workbox-window";

declare global {
  interface Window {
    workbox: Workbox;
  }
}

export default function Pwa() {
  useEffect(() => {
    if (
      "serviceWorker" in navigator &&
      window.workbox !== undefined
    ) {
      const wb = window.workbox;
      wb.register();
    }
  }, []);
  return <></>;
}

I think this looks best :) I also think it's kinda hard to contact the author, let alone have him accept the PR as he hasn't been working on this project for months...

Murkrage commented 1 year ago

@DuCanhGH I see you've made lots of changes since you forked the repo. I'm inclined to go with your fork instead but unsure what kind of impact your changes have on the package.

DuCanhGH commented 1 year ago

@Murkrage so far I've done these changes to the package:

These are the ones I can pull off the top of my head :)

In v8, I've also added these:

These are the changes in v9:

hk86 commented 1 year ago

Had the same issue. @DuCanhGH You package solves the issue. Thank you!

ncinme commented 1 year ago

I was struggling to implement PWA in Next13 App directory. Thank you @DuCanhGH. Your solution worked like a charm. :)

Schular commented 1 year ago

For anyone that wants a working solution using only the next-pwa (keep in mind that this repo seems like is not maintained anymore), I created a sample repository with a workaround: next.config.js

The above repo is deployed on Vercel (app & pages dirs concomitantly) with the following pages:

This example is a simple Next.js starter project that should get ~100% on all Lighthouse categories. image image

Basically we add buildExcludes: ["app-build-manifest.json"] at next-pwa config, as the others are already excluded on the next-pwa implementation (https://github.com/shadowwalker/next-pwa/issues/424#issuecomment-1332258575) and we modify the webpack config with the custom main-app entry (https://github.com/shadowwalker/next-pwa/pull/427)

Thanks @DuCanhGH, floatingdino

DuCanhGH commented 1 year ago

@Schular nice, that sounds like a great solution! I've updated my own comment to match this repo as well.

flaviobzs commented 1 year ago

@Schular does this solution work with bashPath?

Schular commented 1 year ago

@Schular does this solution work with bashPath?

Yes, you should have no problems. For example adding basePath: "/test" then you would have to update the path to all public assets to have the /test prefix (manifest.json, favicon.ico, images, etc.), other than that should work out-of-the-box.

NikitaPoly commented 11 months ago

The above solutions worked for my next.js project using the /src with /app. My next issue was configuring the start_url. My start url is not / so the service worker would not get registered. After installing the app and running it a 404 error would appear. Lighthouse test showed that no service worker was registered on the start_url. The solution I found that lets you add a start_url that is something other than /:

  1. Use solutions from above to get the start_url:/ to work.
  2. Change manifest.json to point to correct start_url I also put my manifest and icons into a separate worker directory in /public (optional) If you did change the location of the manifest/icons then also make sure to update the routes of the icons in the manifest and the meta import for manifest.json
  3. Change next.config.js to include sw:"nameOfWorker.js" (Optional )
  4. Register the service worker in a client component on you desried start_url
    useEffect(() => {
    if ("serviceWorker" in navigator) {
      window.addEventListener("load", function () {
        navigator.serviceWorker.register("/nameofSW.js").then(
          function (registration) {
            console.log("Service Worker registration successful with scope: ", registration.scope);
          },
          function (err) {
            console.log("Service Worker registration failed: ", err);
          }
        );
      });
    }
    }, []);
    const [installPromptEvent, setInstallPromptEvent] = useState(null);

    !!!The service worker is still at root of website so you don't need to prefix it like the manifest.json!!!

  5. npm run build and start

Edit: Is it a bad idea to go about it this way? Edit1: make sure the scope in the manifest is also "/" or the service worker wont have access to your page

devotox commented 9 months ago

hey @Schular i followed your examples except for using a manifest.ts file instead of manifest.json but everything builds locally but as soon as it goes up on vercel i get an error


Failed to compile.
--
10:20:00.811 |  
10:20:00.811 | Worker is not a constructor
10:20:00.812 |  
10:20:00.812 |  
10:20:00.812 | > Build failed because of webpack errors

has anyone seen this before

booster085 commented 6 months ago

@AaronLayton I don't think his solution would work. If you want to use next-pwa, you can try adding this to your next-pwa config:

const withPWAInit = require("next-pwa");

const isDev = process.env.NODE_ENV !== "production";

const withPWA = withPWAInit({
  // your other config
  exclude: [
    // add buildExcludes here
    ({ asset, compilation }) => {
      if (
        asset.name.startsWith("server/") ||
        asset.name.match(/^((app-|^)build-manifest\.json|react-loadable-manifest\.json)$/)
      ) {
        return true;
      }
      if (isDev && !asset.name.startsWith("static/runtime/")) {
        return true;
      }
      return false;
    }
  ],
});

// your Next config

and you still have to register the SW yourself.

Thanks, this works for me too

JWesorick commented 5 months ago

After adding the workaround provided by @Schular I get the error unhandledRejection: Error: Entry app/page depends on main-app, but this entry was not found in the terminal.

using next@npm:14.1.0 [d369e] next-pwa@npm:5.6.0 [d369e]