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

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:

  • next-pwa: 5.6.0
  • next: 13

Steps to reproduce the behavior: Migrate to NextJS v13.

Expected Behaviors: Service worker should be succesfully registered.

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 38
  • Comments: 26

Most upvoted comments

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.

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

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

  • Migrated to TS (examples are also in TS).
  • Added support for appDir (app/_offline/page.tsx now also automatically enables offline fallback for document).
  • Migrated from babel to swc (the original package used next/babel to build fallback/custom worker). You should have swc-loader and @swc/core in your devDependencies should you use any of these features (no longer the case in 8.1.0).
  • Moved workbox’s options from general plugin options to PluginOptions.workboxOptions (this change is why the package is at its 7.x right now, and if you have // @ts-check at the top of your next.config.js you should also be informed if you pass invalid options, for example having both GenerateSW-specfic and InjectManifest-specific options).
  • Fixed grammar.
  • For CJS, you will have to write const withPWA = require("@ducanh2912/next-pwa").default(), rather than const withPWA = require("next-pwa")().
  • You can add app/~offline/page.js to automatically enable the fallback worker.

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

In v8, I’ve also added these:

  • Support tsconfig.json path aliases in custom workers. (8.0.0)
  • Removed subdomainPrefix and workboxOptions.exclude. (8.0.0)
  • Added extendDefaultRuntimeCaching, which allows you to extend the default runtime caching array, rather than overriding it. (8.5.0)
  • Added a documentation website. (8.6.0)
  • Added aggressiveFrontEndNavCaching - when combined with cacheOnFrontEndNav, it will cache <link rel="stylesheet" href="" and <script src="" /> on page navigation. (8.7.0)

These are the changes in v9:

  • Bumped minimum supported Next.js version to 11.0.0. (older Next versions didn’t work prior to this anyways, 9.0.0)
  • Features that need to be built with Webpack now try to resolve next/dist/build/swc first before @swc/core to save disk space. (9.0.0)

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 (
    <></>
  )
}

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

@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 ☹️

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

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

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

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

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

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

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

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

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
  1. Uncomment appDir
  2. npm run build, npm run dev
  3. 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
  },
})