@mangadex-pub/vite-pwa-nuxt
Version:
Zero-config PWA for Nuxt 3
206 lines (201 loc) • 8.3 kB
JavaScript
import { defineNuxtModule, createResolver, addPluginTemplate, addComponent, extendWebpackConfig } from '@nuxt/kit';
import { VitePWA } from 'vite-plugin-pwa';
import { resolve } from 'pathe';
import { writeFile, lstat } from 'node:fs/promises';
function configurePWAOptions(options, nuxt) {
if (!options.outDir) {
const publicDir = nuxt.options.nitro?.output?.publicDir;
options.outDir = publicDir ? resolve(publicDir) : resolve(nuxt.options.buildDir, "../.output/public");
}
if (options.devOptions?.enabled)
options.devOptions.resolveTempFolder = () => resolve(nuxt.options.buildDir, "dev-sw-dist");
let config;
if (options.strategies === "injectManifest") {
options.injectManifest = options.injectManifest ?? {};
config = options.injectManifest;
} else {
options.workbox = options.workbox ?? {};
if (options.registerType === "autoUpdate" && (options.client?.registerPlugin || options.injectRegister === "script" || options.injectRegister === "inline")) {
options.workbox.clientsClaim = true;
options.workbox.skipWaiting = true;
}
if (nuxt.options.dev) {
options.workbox.navigateFallback = options.workbox.navigateFallback ?? nuxt.options.app.baseURL ?? "/";
if (options.devOptions?.enabled && !options.devOptions.navigateFallbackAllowlist)
options.devOptions.navigateFallbackAllowlist = [nuxt.options.app.baseURL ? new RegExp(nuxt.options.app.baseURL) : /\//];
}
config = options.workbox;
}
if (!nuxt.options.dev)
config.manifestTransforms = [createManifestTransform(nuxt.options.app.baseURL ?? "/")];
}
function createManifestTransform(base) {
return async (entries) => {
entries.filter((e) => e && e.url.endsWith(".html")).forEach((e) => {
const url = e.url.startsWith("/") ? e.url.slice(1) : e.url;
if (url === "index.html") {
e.url = base;
} else {
const parts = url.split("/");
parts[parts.length - 1] = parts[parts.length - 1].replace(/\.html$/, "");
e.url = parts.length > 1 ? parts.slice(0, parts.length - 1).join("/") : parts[0];
}
});
return { manifest: entries, warnings: [] };
};
}
async function regeneratePWA(dir, path, api) {
if (!api)
return;
await api.generateSW();
if (path)
await writeWebManifest(dir, path, api);
}
async function isFile(path) {
try {
const stats = await lstat(path);
return stats.isFile();
} catch {
return false;
}
}
async function writeWebManifest(dir, path, api) {
const exists = await isFile(path);
if (exists)
return;
const manifest = api.generateBundle({})?.[path];
if (manifest && "source" in manifest)
await writeFile(resolve(dir, path), manifest.source, "utf-8");
}
const module = defineNuxtModule({
meta: {
name: "pwa",
configKey: "pwa"
},
defaults: (nuxt) => ({
base: nuxt.options.app.baseURL,
scope: nuxt.options.app.baseURL,
injectRegister: false,
includeManifestIcons: false,
registerPlugin: true,
writePlugin: false,
client: {
registerPlugin: true,
installPrompt: false,
periodicSyncForUpdates: 0
}
}),
async setup(options, nuxt) {
const resolver = createResolver(import.meta.url);
let vitePwaClientPlugin;
const resolveVitePluginPWAAPI = () => {
return vitePwaClientPlugin?.api;
};
const client = options.client ?? { registerPlugin: true, installPrompt: false, periodicSyncForUpdates: 0 };
if (client.registerPlugin) {
addPluginTemplate({
src: resolver.resolve("../templates/pwa.client.ts"),
write: nuxt.options.dev || options.writePlugin,
options: {
periodicSyncForUpdates: typeof client.periodicSyncForUpdates === "number" ? client.periodicSyncForUpdates : 0,
installPrompt: typeof client.installPrompt === "undefined" || client.installPrompt === false ? void 0 : client.installPrompt === true || client.installPrompt.trim() === "" ? "vite-pwa:hide-install" : client.installPrompt.trim()
}
});
}
await addComponent({
name: "VitePwaManifest",
filePath: resolver.resolve("./runtime/VitePwaManifest")
});
nuxt.hook("prepare:types", ({ references }) => {
references.push({ types: "vite-plugin-pwa/client" });
});
nuxt.hook("nitro:init", (nitro) => {
options.outDir = nitro.options.output.publicDir;
options.injectManifest = options.injectManifest || {};
options.injectManifest.globDirectory = nitro.options.output.publicDir;
});
nuxt.hook("vite:extend", ({ config }) => {
const plugin = config.plugins?.find((p) => p && typeof p === "object" && "name" in p && p.name === "vite-plugin-pwa");
if (plugin)
throw new Error("Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!");
});
nuxt.hook("vite:extendConfig", async (viteInlineConfig, { isClient }) => {
viteInlineConfig.plugins = viteInlineConfig.plugins || [];
const plugin = viteInlineConfig.plugins.find((p) => p && typeof p === "object" && "name" in p && p.name === "vite-plugin-pwa");
if (plugin)
throw new Error("Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!");
configurePWAOptions(options, nuxt);
const plugins = VitePWA(options);
viteInlineConfig.plugins.push(plugins);
if (isClient)
vitePwaClientPlugin = plugins.find((p) => p.name === "vite-plugin-pwa");
});
extendWebpackConfig(() => {
throw new Error("Webpack is not supported: @vite-pwa/nuxt module can only be used with Vite!");
});
if (nuxt.options.dev) {
const webManifest = `${nuxt.options.app.baseURL}${options.devOptions?.webManifestUrl ?? options.manifestFilename ?? "manifest.webmanifest"}`;
const devSw = `${nuxt.options.app.baseURL}dev-sw.js?dev-sw`;
const workbox = `${nuxt.options.app.baseURL}workbox-`;
const emptyHandle = (_req, _res, next) => {
next();
};
nuxt.hook("vite:serverCreated", (viteServer, { isServer }) => {
if (isServer)
return;
viteServer.middlewares.stack.push({ route: webManifest, handle: emptyHandle });
viteServer.middlewares.stack.push({ route: devSw, handle: emptyHandle });
});
if (!options.strategies || options.strategies === "generateSW") {
nuxt.hook("vite:serverCreated", (viteServer, { isServer }) => {
if (isServer)
return;
viteServer.middlewares.stack.push({ route: workbox, handle: emptyHandle });
});
nuxt.hook("close", async () => {
});
}
} else {
if (!options.disable && options.registerWebManifestInRouteRules) {
nuxt.hook("nitro:config", async (nitroConfig) => {
nitroConfig.routeRules = nitroConfig.routeRules || {};
let swName = options.filename || "sw.js";
if (options.strategies === "injectManifest" && swName.endsWith(".ts"))
swName = swName.replace(/\.ts$/, ".js");
nitroConfig.routeRules[`${nuxt.options.app.baseURL}${swName}`] = {
headers: {
"Cache-Control": "public, max-age=0, must-revalidate"
}
};
if (options.manifest) {
nitroConfig.routeRules[`${nuxt.options.app.baseURL}${options.manifestFilename ?? "manifest.webmanifest"}`] = {
headers: {
"Content-Type": "application/manifest+json",
"Cache-Control": "public, max-age=0, must-revalidate"
}
};
}
});
}
nuxt.hook("nitro:init", (nitro) => {
nitro.hooks.hook("rollup:before", async () => {
await regeneratePWA(
options.outDir,
!options.disable && options.manifest ? options.manifestFilename || "manifest.webmanifest" : void 0,
resolveVitePluginPWAAPI()
);
});
});
if (nuxt.options._generate) {
nuxt.hook("close", async () => {
await regeneratePWA(
options.outDir,
!options.disable && options.manifest ? options.manifestFilename || "manifest.webmanifest" : void 0,
resolveVitePluginPWAAPI()
);
});
}
}
}
});
export { module as default };