PWA
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m24s

This commit is contained in:
eewing
2026-02-19 07:41:49 -06:00
parent 5776a1fdb1
commit c0fae761da
10919 changed files with 874329 additions and 13511 deletions
+36
View File
@@ -0,0 +1,36 @@
import { relative } from 'node:path';
import { cyan, magenta, green, dim, yellow } from 'kolorist';
const version = "1.1.0";
function logWorkboxResult(strategy, viteOptions, buildResult) {
const { root, logLevel = "info" } = viteOptions;
if (logLevel === "silent")
return;
if (!buildResult) {
console.info([
"",
`${cyan(`SvelteKit VitePWA v${version}`)}`,
`mode ${magenta(strategy)}`
].join("\n"));
return;
}
const { count, size, filePaths, warnings } = buildResult;
if (logLevel === "info") {
console.info([
"",
`${cyan(`SvelteKit VitePWA v${version}`)}`,
`mode ${magenta(strategy)}`,
`precache ${green(`${count} entries`)} ${dim(`(${(size / 1024).toFixed(2)} KiB)`)}`,
"files generated",
...filePaths.map((p) => ` ${dim(relative(root, p))}`)
].join("\n"));
}
warnings && warnings.length > 0 && console.warn(yellow([
"warnings",
...warnings.map((w) => ` ${w}`),
""
].join("\n")));
}
export { logWorkboxResult };
+77
View File
@@ -0,0 +1,77 @@
import { Plugin } from 'vite';
import { VitePWAOptions } from 'vite-plugin-pwa';
interface KitOptions {
/**
* The base path for your application: by default will use the Vite base.
*
* @deprecated since ^0.1.0 version, the plugin has SvelteKit ^1.0.0 as peer dependency, Vite's base is now properly configured.
* @default '/'
* @see https://kit.svelte.dev/docs/configuration#paths
*/
base?: string;
/**
* The static folder for your application.
*
* @default 'static'
* @see https://kit.svelte.dev/docs/configuration#files
*/
assets?: string;
/**
* @default '.svelte-kit'
* @see https://kit.svelte.dev/docs/configuration#outdir
*/
outDir?: string;
/**
* @see https://kit.svelte.dev/docs/adapter-static#options-fallback
*/
adapterFallback?: string;
/**
* Check your SvelteKit version, `trailingSlash` should be used in `+page[jt]s` files or `+layout.[jt]s.
* @default 'never'
*/
trailingSlash?: 'never' | 'always' | 'ignore';
/**
* @default `_app`
* @see https://kit.svelte.dev/docs/configuration#appdir
*/
appDir?: string;
/**
* Include `${appDir}/version.json` in the service worker precache manifest?
*
* @default false
*/
includeVersionFile?: boolean;
/**
* Enable SPA mode for the application.
*
* By default, the plugin will use `adapterFallback` to include the entry in the service worker
* precache manifest.
*
* If you are using a logical name for the fallback, you can use the object syntax with the
* `fallbackMapping`.
*
* For example, if you're using `fallback: 'app.html'` in your static adapter and your server
* is redirecting to `/app`, you can configure `fallbackMapping: '/app'`.
*
* Since the static adapter will run after the PWA plugin generates the service worker,
* the PWA plugin doesn't have access to the adapter fallback page to include the revision in the
* service worker precache manifest.
* To generate the revision for the fallback page, the PWA plugin will use the
* `.svelte-kit/output/client/_app/version.json` file.
* You can configure the `fallbackRevision` to generate a custom revision.
*
* @see https://svelte.dev/docs/kit/single-page-apps
*/
spa?: true | {
fallbackMapping?: string;
fallbackRevision?: () => Promise<string>;
};
}
interface SvelteKitPWAOptions extends Partial<VitePWAOptions> {
kit?: KitOptions;
}
declare function SvelteKitPWA(userOptions?: Partial<SvelteKitPWAOptions>): Plugin[];
export { type KitOptions, SvelteKitPWA, type SvelteKitPWAOptions };
+77
View File
@@ -0,0 +1,77 @@
import { Plugin } from 'vite';
import { VitePWAOptions } from 'vite-plugin-pwa';
interface KitOptions {
/**
* The base path for your application: by default will use the Vite base.
*
* @deprecated since ^0.1.0 version, the plugin has SvelteKit ^1.0.0 as peer dependency, Vite's base is now properly configured.
* @default '/'
* @see https://kit.svelte.dev/docs/configuration#paths
*/
base?: string;
/**
* The static folder for your application.
*
* @default 'static'
* @see https://kit.svelte.dev/docs/configuration#files
*/
assets?: string;
/**
* @default '.svelte-kit'
* @see https://kit.svelte.dev/docs/configuration#outdir
*/
outDir?: string;
/**
* @see https://kit.svelte.dev/docs/adapter-static#options-fallback
*/
adapterFallback?: string;
/**
* Check your SvelteKit version, `trailingSlash` should be used in `+page[jt]s` files or `+layout.[jt]s.
* @default 'never'
*/
trailingSlash?: 'never' | 'always' | 'ignore';
/**
* @default `_app`
* @see https://kit.svelte.dev/docs/configuration#appdir
*/
appDir?: string;
/**
* Include `${appDir}/version.json` in the service worker precache manifest?
*
* @default false
*/
includeVersionFile?: boolean;
/**
* Enable SPA mode for the application.
*
* By default, the plugin will use `adapterFallback` to include the entry in the service worker
* precache manifest.
*
* If you are using a logical name for the fallback, you can use the object syntax with the
* `fallbackMapping`.
*
* For example, if you're using `fallback: 'app.html'` in your static adapter and your server
* is redirecting to `/app`, you can configure `fallbackMapping: '/app'`.
*
* Since the static adapter will run after the PWA plugin generates the service worker,
* the PWA plugin doesn't have access to the adapter fallback page to include the revision in the
* service worker precache manifest.
* To generate the revision for the fallback page, the PWA plugin will use the
* `.svelte-kit/output/client/_app/version.json` file.
* You can configure the `fallbackRevision` to generate a custom revision.
*
* @see https://svelte.dev/docs/kit/single-page-apps
*/
spa?: true | {
fallbackMapping?: string;
fallbackRevision?: () => Promise<string>;
};
}
interface SvelteKitPWAOptions extends Partial<VitePWAOptions> {
kit?: KitOptions;
}
declare function SvelteKitPWA(userOptions?: Partial<SvelteKitPWAOptions>): Plugin[];
export { type KitOptions, SvelteKitPWA, type SvelteKitPWAOptions };
+319
View File
@@ -0,0 +1,319 @@
import { VitePWA } from 'vite-plugin-pwa';
import { resolve, join } from 'node:path';
import { mkdir, rm, readFile, writeFile, rename, lstat } from 'node:fs/promises';
import { glob } from 'tinyglobby';
function configureSvelteKitOptions(kit, viteOptions, options) {
const {
base = viteOptions.base ?? "/",
adapterFallback,
outDir = `${viteOptions.root}/.svelte-kit`,
assets = "static"
} = kit;
if (typeof options.includeManifestIcons === "undefined")
options.includeManifestIcons = false;
let config;
if (options.strategies === "injectManifest") {
if (!options.srcDir)
options.srcDir = "src";
if (!options.filename)
options.filename = "service-worker.js";
options.injectManifest = options.injectManifest ?? {};
config = options.injectManifest;
} else {
options.workbox = options.workbox ?? {};
if (!("navigateFallback" in options.workbox))
options.workbox.navigateFallback = adapterFallback ?? base;
config = options.workbox;
}
if (!config.globDirectory)
config.globDirectory = `${outDir}/output`;
let buildAssetsDir = kit.appDir ?? "_app/";
if (buildAssetsDir[0] === "/")
buildAssetsDir = buildAssetsDir.slice(1);
if (buildAssetsDir[buildAssetsDir.length - 1] !== "/")
buildAssetsDir += "/";
if (!config.modifyURLPrefix) {
config.globPatterns = buildGlobPatterns(config.globPatterns);
if (kit.includeVersionFile)
config.globPatterns.push(`client/${buildAssetsDir}version.json`);
}
config.globIgnores = buildGlobIgnores(config.globIgnores);
if (!("dontCacheBustURLsMatching" in config))
config.dontCacheBustURLsMatching = new RegExp(`${buildAssetsDir}immutable/`);
if (!config.manifestTransforms) {
config.manifestTransforms = [createManifestTransform(
base,
config.globDirectory,
options.strategies === "injectManifest" ? void 0 : options.manifestFilename ?? "manifest.webmanifest",
kit
)];
}
if (options.pwaAssets) {
options.pwaAssets.integration = {
baseUrl: base,
publicDir: resolve(viteOptions.root, assets),
outDir: resolve(outDir, "output/client")
};
}
}
function createManifestTransform(base, outDir, webManifestName, options) {
return async (entries) => {
const defaultAdapterFallback = "prerendered/fallback.html";
const suffix = options?.trailingSlash === "always" ? "/" : "";
let adapterFallback = options?.adapterFallback;
let excludeFallback = false;
if (!adapterFallback) {
adapterFallback = defaultAdapterFallback;
excludeFallback = true;
}
const manifest = entries.filter(({ url }) => !(excludeFallback && url === defaultAdapterFallback)).map((e) => {
let url = e.url;
if (url.startsWith("client/"))
url = url.slice(7);
else if (url.startsWith("prerendered/dependencies/"))
url = url.slice(25);
else if (url.startsWith("prerendered/pages/"))
url = url.slice(18);
else if (url === defaultAdapterFallback)
url = adapterFallback;
if (url.endsWith(".html")) {
if (url.startsWith("/"))
url = url.slice(1);
if (url === "index.html") {
url = base;
} else {
const idx = url.lastIndexOf("/");
if (idx > -1) {
if (url.endsWith("/index.html"))
url = `${url.slice(0, idx)}${suffix}`;
else
url = `${url.substring(0, url.lastIndexOf("."))}${suffix}`;
} else {
url = `${url.substring(0, url.lastIndexOf("."))}${suffix}`;
}
}
}
e.url = url;
return e;
});
if (options?.spa && options?.adapterFallback) {
const name = typeof options.spa === "object" && options.spa.fallbackMapping ? options.spa.fallbackMapping : options.adapterFallback;
if (typeof options.spa === "object" && typeof options.spa.fallbackRevision === "function") {
manifest.push({
url: name,
revision: await options.spa.fallbackRevision(),
size: 0
});
} else {
manifest.push(await buildManifestEntry(
name,
resolve(outDir, "client/_app/version.json")
));
}
}
if (!webManifestName)
return { manifest };
return { manifest: manifest.filter((e) => e.url !== webManifestName) };
};
}
function buildGlobPatterns(globPatterns) {
if (globPatterns) {
if (!globPatterns.some((g) => g.startsWith("prerendered/")))
globPatterns.push("prerendered/**/*.{html,json}");
if (!globPatterns.some((g) => g.startsWith("client/")))
globPatterns.push("client/**/*.{js,css,ico,png,svg,webp,webmanifest}");
if (!globPatterns.some((g) => g.includes("webmanifest")))
globPatterns.push("client/*.webmanifest");
return globPatterns;
}
return ["client/**/*.{js,css,ico,png,svg,webp,webmanifest}", "prerendered/**/*.{html,json}"];
}
function buildGlobIgnores(globIgnores) {
if (globIgnores) {
if (!globIgnores.some((g) => g.startsWith("server/")))
globIgnores.push("server/**");
return globIgnores;
}
return ["server/**"];
}
async function buildManifestEntry(url, path) {
const [crypto, createReadStream] = await Promise.all([
import('node:crypto').then((m) => m.default),
import('node:fs').then((m) => m.createReadStream)
]);
return new Promise((resolve2, reject) => {
const cHash = crypto.createHash("MD5");
const stream = createReadStream(path);
stream.on("error", (err) => {
reject(err);
});
stream.on("data", (chunk) => {
cHash.update(chunk);
});
stream.on("end", () => {
return resolve2({
url,
size: 0,
revision: `${cHash.digest("hex")}`
});
});
});
}
function SvelteKitPlugin(options, apiResolver) {
let viteConfig;
return {
name: "vite-plugin-pwa:sveltekit:build",
apply: "build",
enforce: "pre",
configResolved(config) {
viteConfig = config;
},
async generateBundle(_, bundle) {
if (viteConfig.build.ssr)
return;
const api = apiResolver();
if (!api)
return;
const assetsGenerator = await api.pwaAssetsGenerator();
if (assetsGenerator)
assetsGenerator.injectManifestIcons();
api.generateBundle(bundle, this);
},
writeBundle: {
sequential: true,
enforce: "pre",
async handler() {
const api = apiResolver();
if (!api || viteConfig.build.ssr)
return;
const assetsGenerator = await api.pwaAssetsGenerator();
if (assetsGenerator)
await assetsGenerator.generate();
}
},
closeBundle: {
sequential: true,
enforce: "pre",
async handler() {
const api = apiResolver();
if (api && !api.disabled && viteConfig.build.ssr) {
const webManifest = options.manifestFilename ?? "manifest.webmanifest";
let swName = options.filename ?? "sw.js";
const outDir = options.outDir ?? `${viteConfig.root}/.svelte-kit/output`;
const clientOutputDir = join(outDir, "client");
await mkdir(clientOutputDir, { recursive: true });
if (!options.strategies || options.strategies === "generateSW" || options.selfDestroying) {
let path;
let existsFile;
if (options.selfDestroying && options.strategies === "injectManifest") {
if (swName.endsWith(".ts"))
swName = swName.replace(/\.ts$/, ".js");
path = join(clientOutputDir, "service-worker.js").replace("\\/g", "/");
existsFile = await isFile(path);
if (existsFile)
await rm(path);
}
await api.generateSW();
const serverOutputDir = join(outDir, "server");
path = join(serverOutputDir, swName).replace(/\\/g, "/");
existsFile = await isFile(path);
if (existsFile) {
const sw = await readFile(path, "utf-8");
await writeFile(
join(clientOutputDir, swName).replace("\\/g", "/"),
sw,
"utf-8"
);
await rm(path);
}
const result = await glob({
patterns: ["workbox-*.js"],
cwd: serverOutputDir,
onlyFiles: true,
expandDirectories: false
});
if (result && result.length > 0) {
path = join(serverOutputDir, result[0]).replace(/\\/g, "/");
await writeFile(
join(clientOutputDir, result[0]).replace("\\/g", "/"),
await readFile(path, "utf-8"),
"utf-8"
);
await rm(path);
}
path = join(serverOutputDir, webManifest).replace(/\\/g, "/");
existsFile = await isFile(path);
if (existsFile)
await rm(path);
return;
}
if (swName.endsWith(".ts"))
swName = swName.replace(/\.ts$/, ".js");
const injectionPoint = !options.injectManifest || !("injectionPoint" in options.injectManifest) || !!options.injectManifest.injectionPoint;
if (injectionPoint) {
const injectManifestOptions = {
globDirectory: outDir.replace(/\\/g, "/"),
...options.injectManifest ?? {},
swSrc: join(clientOutputDir, "service-worker.js").replace(/\\/g, "/"),
swDest: join(clientOutputDir, "service-worker.js").replace(/\\/g, "/")
};
const [injectManifest, logWorkboxResult] = await Promise.all([
import('workbox-build').then((m) => m.injectManifest),
import('./chunks/log.mjs').then((m) => m.logWorkboxResult)
]);
const buildResult = await injectManifest(injectManifestOptions);
logWorkboxResult("injectManifest", viteConfig, buildResult);
if (swName !== "service-worker.js") {
await rename(
join(clientOutputDir, "service-worker.js").replace("\\/g", "/"),
join(clientOutputDir, swName).replace("\\/g", "/")
);
}
} else {
const { logWorkboxResult } = await import('./chunks/log.mjs');
logWorkboxResult("injectManifest", viteConfig);
if (swName !== "service-worker.js") {
await rename(
join(clientOutputDir, "service-worker.js").replace("\\/g", "/"),
join(clientOutputDir, swName).replace("\\/g", "/")
);
}
}
}
}
}
};
}
async function isFile(path) {
try {
const stats = await lstat(path);
return stats.isFile();
} catch {
return false;
}
}
function SvelteKitPWA(userOptions = {}) {
if (!userOptions.integration)
userOptions.integration = {};
userOptions.integration.closeBundleOrder = "pre";
userOptions.integration.configureOptions = (viteConfig, options) => configureSvelteKitOptions(
userOptions.kit ?? {},
viteConfig,
options
);
const plugins = VitePWA(userOptions);
const plugin = plugins.find((p) => p && typeof p === "object" && "name" in p && p.name === "vite-plugin-pwa");
const resolveVitePluginPWAAPI = () => {
return plugin?.api;
};
return [
// remove the build plugin: we're using a custom one
...plugins.filter((p) => p && typeof p === "object" && "name" in p && p.name !== "vite-plugin-pwa:build"),
SvelteKitPlugin(userOptions, resolveVitePluginPWAAPI)
];
}
export { SvelteKitPWA };