vite-sw-cacher-plugin 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,7 @@ export default defineConfig({
35
35
  ttl: 24 * 60 * 60 * 1000,
36
36
  maxItemsCount: 200,
37
37
  cacheName: "vite-sw-cacher-plugin",
38
+ inlineSw: false,
38
39
  }),
39
40
  ],
40
41
  });
@@ -47,3 +48,4 @@ export default defineConfig({
47
48
  - `ttl?`: время жизни в мс, по умолчанию 24 часа.
48
49
  - `maxItemsCount?`: максимум кэша. По умолчанию `кол-во статичных файлов из Vite * 2`.
49
50
  - `cacheName?`: имя группы кешей, по умолчанию `vite-sw-cacher-plugin`.
51
+ - `inlineSw?`: вшивает SW в инлайн-скрипт через Blob URL (может быть ограничено CSP/браузером).
package/dist/index.cjs CHANGED
@@ -36,12 +36,13 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
  var import_ejs = __toESM(require("ejs"), 1);
38
38
  var import_terser = require("terser");
39
+ var import_mime_types = require("mime-types");
39
40
 
40
41
  // src/templates/sw.ejs
41
- var sw_default = 'const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\n\nself.addEventListener("install", () => self.skipWaiting());\n\nself.addEventListener("activate", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener("fetch", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== "GET") return false;\n const url = request.url;\n return matchesPattern(url) && matchesExtension(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n await putInCache(request, response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n return cached || response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set("sw-cache-time", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get("sw-cache-time");\n const dateHeader = response.headers.get("date");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n';
42
+ var sw_default = 'const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst ALLOWED_CONTENT_TYPES = <%- allowedContentTypesJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\nconst HTML_FALLBACK_KEY = "__sw-cacher-html-fallback__";\n\nself.addEventListener("install", () => self.skipWaiting());\n\nself.addEventListener("activate", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener("fetch", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== "GET") return false;\n const url = request.url;\n return matchesPattern(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst matchesContentType = (response) => {\n if (!ALLOWED_CONTENT_TYPES.length) return false;\n const header = response.headers.get("content-type");\n if (!header) return false;\n const contentType = header.split(";")[0]?.trim().toLowerCase();\n if (!contentType) return false;\n return ALLOWED_CONTENT_TYPES.includes(contentType);\n};\n\nconst isNavigationRequest = (request) => {\n if (request.mode === "navigate") return true;\n const accept = request.headers.get("accept") || "";\n return accept.includes("text/html");\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n const shouldCache =\n matchesExtension(request.url) || matchesContentType(response);\n if (shouldCache) {\n await putInCache(request, response);\n }\n await maybeStoreHtmlFallback(response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n if (cached) return cached;\n if (isNavigationRequest(request)) {\n const fallback = await getHtmlFallback();\n if (fallback) return fallback;\n }\n return response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set("sw-cache-time", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst maybeStoreHtmlFallback = async (response) => {\n if (!matchesContentType(response)) return;\n const header = response.headers.get("content-type") || "";\n const contentType = header.split(";")[0]?.trim().toLowerCase();\n if (contentType !== "text/html") return;\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set("sw-cache-time", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n await cache.put(HTML_FALLBACK_KEY, responseToCache);\n};\n\nconst getHtmlFallback = async () => {\n const cache = await caches.open(CACHE_NAME);\n return cache.match(HTML_FALLBACK_KEY);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get("sw-cache-time");\n const dateHeader = response.headers.get("date");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n';
42
43
 
43
44
  // src/templates/inline-script.ejs
44
- var inline_script_default = '(() => {\n if (!("serviceWorker" in navigator)) return;\n window.addEventListener("load", () => {\n navigator.serviceWorker.register(<%- swUrlJson %>).catch((error) => {\n console.warn("[vite-sw-cacher-plugin] SW registration failed", error);\n });\n });\n})();\n';
45
+ var inline_script_default = '(() => {\n if (!("serviceWorker" in navigator)) return;\n window.addEventListener("load", () => {\n <% if (inlineSw) { %>\n const swCode = <%- swCodeJson %>;\n const blob = new Blob([swCode], { type: "text/javascript" });\n const swUrl = URL.createObjectURL(blob);\n navigator.serviceWorker\n .register(swUrl)\n .catch((error) => {\n console.warn("[vite-sw-cacher-plugin] SW registration failed", error);\n })\n .finally(() => {\n URL.revokeObjectURL(swUrl);\n });\n <% } else { %>\n navigator.serviceWorker\n .register(<%- swUrlJson %>)\n .catch((error) => {\n console.warn("[vite-sw-cacher-plugin] SW registration failed", error);\n });\n <% } %>\n });\n})();\n';
45
46
 
46
47
  // src/index.ts
47
48
  var DEFAULT_EXTENSIONS = [
@@ -89,6 +90,17 @@ var joinBase = (base, fileName) => {
89
90
  const normalizedBase = base.endsWith("/") ? base : `${base}/`;
90
91
  return `${normalizedBase}${fileName}`;
91
92
  };
93
+ var injectScriptIntoHtml = (html, script) => {
94
+ const tag = `<script>${script}</script>`;
95
+ if (/<\/head>/i.test(html)) {
96
+ return html.replace(/<\/head>/i, `${tag}</head>`);
97
+ }
98
+ return `${html}${tag}`;
99
+ };
100
+ var toHtmlString = (source) => {
101
+ if (typeof source === "string") return source;
102
+ return new TextDecoder().decode(source);
103
+ };
92
104
  var renderTemplate = (template, data) => import_ejs.default.render(template, data);
93
105
  var minifyScript = async (code) => {
94
106
  const result = await (0, import_terser.minify)(code, {
@@ -101,11 +113,17 @@ var minifyScript = async (code) => {
101
113
  var buildServiceWorkerSource = async (options) => {
102
114
  const patternSource = options.pattern ? wildcardToRegexSource(options.pattern) : null;
103
115
  const urlPatternSource = patternSource ? `new RegExp(${JSON.stringify(patternSource)})` : "null";
116
+ const allowedContentTypes = Array.from(
117
+ new Set(
118
+ options.extensions.map((ext) => (0, import_mime_types.lookup)(ext)).filter((value) => Boolean(value)).map((value) => value.toLowerCase())
119
+ )
120
+ );
104
121
  const source = renderTemplate(sw_default, {
105
122
  cacheNameJson: JSON.stringify(options.cacheName),
106
123
  ttlMs: Math.max(0, options.ttlMs),
107
124
  maxItems: options.maxItems,
108
125
  extensionsJson: JSON.stringify(options.extensions),
126
+ allowedContentTypesJson: JSON.stringify(allowedContentTypes),
109
127
  urlPatternSource
110
128
  });
111
129
  return minifyScript(source);
@@ -120,9 +138,11 @@ var viteSwCacherPlugin = (options = {}) => {
120
138
  resolvedConfig = config;
121
139
  },
122
140
  async transformIndexHtml(html) {
141
+ if (options.inlineSw) return html;
123
142
  const base = resolvedConfig?.base ?? "/";
124
143
  const swUrl = joinBase(base, swFileName);
125
144
  const inlineScript = renderTemplate(inline_script_default, {
145
+ inlineSw: false,
126
146
  swUrlJson: JSON.stringify(swUrl)
127
147
  });
128
148
  const minifiedInlineScript = await minifyScript(inlineScript);
@@ -143,6 +163,7 @@ var viteSwCacherPlugin = (options = {}) => {
143
163
  const ttlMs = options.ttl ?? DEFAULT_TTL_MS;
144
164
  const maxItems = options.maxItemsCount ?? staticCount * 2;
145
165
  const cacheName = options.cacheName ?? DEFAULT_CACHE_NAME;
166
+ const inlineSw = options.inlineSw ?? false;
146
167
  const swSource = await buildServiceWorkerSource({
147
168
  cacheName,
148
169
  ttlMs,
@@ -150,6 +171,20 @@ var viteSwCacherPlugin = (options = {}) => {
150
171
  extensions,
151
172
  pattern: options.pattern
152
173
  });
174
+ if (inlineSw) {
175
+ const inlineScript = renderTemplate(inline_script_default, {
176
+ inlineSw: true,
177
+ swCodeJson: JSON.stringify(swSource)
178
+ });
179
+ const minifiedInlineScript = await minifyScript(inlineScript);
180
+ for (const item of Object.values(bundle)) {
181
+ if (item.type !== "asset") continue;
182
+ if (!item.fileName.toLowerCase().endsWith(".html")) continue;
183
+ const html = toHtmlString(item.source);
184
+ item.source = injectScriptIntoHtml(html, minifiedInlineScript);
185
+ }
186
+ return;
187
+ }
153
188
  this.emitFile({
154
189
  type: "asset",
155
190
  fileName: swFileName,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/templates/sw.ejs","../src/templates/inline-script.ejs"],"sourcesContent":["import type { OutputAsset, OutputBundle, OutputChunk } from \"rollup\";\nimport type { Plugin, ResolvedConfig } from \"vite\";\nimport ejs from \"ejs\";\nimport { minify } from \"terser\";\nimport swTemplate from \"./templates/sw.ejs\";\nimport inlineScriptTemplate from \"./templates/inline-script.ejs\";\n\nexport interface ViteSwCacherPluginOptions {\n extensions?: string[];\n pattern?: string;\n ttl?: number;\n maxItemsCount?: number;\n cacheName?: string;\n}\n\nconst DEFAULT_EXTENSIONS = [\n \".html\",\n \".css\",\n \".js\",\n \".svg\",\n \".png\",\n \".jpeg\",\n \".jpg\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".bmp\",\n \".ico\",\n \".tif\",\n \".tiff\",\n];\n\nconst DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_CACHE_NAME = \"vite-sw-cacher-plugin\";\nconst DEFAULT_SW_FILE_NAME = \"sw-cacher.js\";\n\nconst normalizeExtensions = (extensions?: string[]): string[] => {\n const list = extensions?.length ? extensions : DEFAULT_EXTENSIONS;\n const normalized = list.map((ext) => {\n const trimmed = ext.trim();\n if (!trimmed) return \"\";\n return trimmed.startsWith(\".\") ? trimmed.toLowerCase() : `.${trimmed.toLowerCase()}`;\n });\n\n return Array.from(new Set(normalized.filter(Boolean)));\n};\n\nconst wildcardToRegexSource = (pattern: string): string => {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return `^${escaped.replace(/\\*/g, \".*\")}$`;\n};\n\nconst countStaticOutputs = (bundle: OutputBundle, extensions: string[]): number => {\n if (!extensions.length) return 0;\n const normalized = extensions.map((ext) => ext.toLowerCase());\n const items = Object.values(bundle) as Array<OutputAsset | OutputChunk>;\n return items.filter((item) => {\n const name = item.fileName.toLowerCase();\n return normalized.some((ext) => name.endsWith(ext));\n }).length;\n};\n\nconst joinBase = (base: string, fileName: string): string => {\n const normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n return `${normalizedBase}${fileName}`;\n};\n\nconst renderTemplate = (template: string, data: Record<string, unknown>): string =>\n ejs.render(template, data);\n\nconst minifyScript = async (code: string): Promise<string> => {\n const result = await minify(code, {\n compress: true,\n mangle: true,\n format: { comments: false },\n });\n return result.code ?? code;\n};\n\nconst buildServiceWorkerSource = async (options: {\n cacheName: string;\n ttlMs: number;\n maxItems: number;\n extensions: string[];\n pattern?: string;\n}): Promise<string> => {\n const patternSource = options.pattern ? wildcardToRegexSource(options.pattern) : null;\n const urlPatternSource = patternSource\n ? `new RegExp(${JSON.stringify(patternSource)})`\n : \"null\";\n\n const source = renderTemplate(swTemplate, {\n cacheNameJson: JSON.stringify(options.cacheName),\n ttlMs: Math.max(0, options.ttlMs),\n maxItems: options.maxItems,\n extensionsJson: JSON.stringify(options.extensions),\n urlPatternSource,\n });\n\n return minifyScript(source);\n};\n\nexport const viteSwCacherPlugin = (\n options: ViteSwCacherPluginOptions = {},\n): Plugin => {\n let resolvedConfig: ResolvedConfig | null = null;\n const swFileName = DEFAULT_SW_FILE_NAME;\n\n return {\n name: \"vite-sw-cacher-plugin\",\n apply: \"build\",\n configResolved(config) {\n resolvedConfig = config;\n },\n async transformIndexHtml(html) {\n const base = resolvedConfig?.base ?? \"/\";\n const swUrl = joinBase(base, swFileName);\n const inlineScript = renderTemplate(inlineScriptTemplate, {\n swUrlJson: JSON.stringify(swUrl),\n });\n const minifiedInlineScript = await minifyScript(inlineScript);\n\n return {\n html,\n tags: [\n {\n tag: \"script\",\n injectTo: \"head\",\n children: minifiedInlineScript,\n },\n ],\n };\n },\n async generateBundle(_, bundle) {\n const extensions = normalizeExtensions(options.extensions);\n const staticCount = countStaticOutputs(bundle, extensions);\n const ttlMs = options.ttl ?? DEFAULT_TTL_MS;\n const maxItems =\n options.maxItemsCount ?? staticCount * 2;\n const cacheName = options.cacheName ?? DEFAULT_CACHE_NAME;\n\n const swSource = await buildServiceWorkerSource({\n cacheName,\n ttlMs,\n maxItems,\n extensions,\n pattern: options.pattern,\n });\n\n this.emitFile({\n type: \"asset\",\n fileName: swFileName,\n source: swSource,\n });\n },\n };\n};\n\nexport default viteSwCacherPlugin;\n","const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\n\nself.addEventListener(\"install\", () => self.skipWaiting());\n\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener(\"fetch\", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== \"GET\") return false;\n const url = request.url;\n return matchesPattern(url) && matchesExtension(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n await putInCache(request, response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n return cached || response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set(\"sw-cache-time\", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get(\"sw-cache-time\");\n const dateHeader = response.headers.get(\"date\");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n","(() => {\n if (!(\"serviceWorker\" in navigator)) return;\n window.addEventListener(\"load\", () => {\n navigator.serviceWorker.register(<%- swUrlJson %>).catch((error) => {\n console.warn(\"[vite-sw-cacher-plugin] SW registration failed\", error);\n });\n });\n})();\n"],"mappings":"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","names":["ejs"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/templates/sw.ejs","../src/templates/inline-script.ejs"],"sourcesContent":["import type { OutputAsset, OutputBundle, OutputChunk } from \"rollup\";\nimport type { Plugin, ResolvedConfig } from \"vite\";\nimport ejs from \"ejs\";\nimport { minify } from \"terser\";\nimport { lookup as lookupMimeType } from \"mime-types\";\nimport swTemplate from \"./templates/sw.ejs\";\nimport inlineScriptTemplate from \"./templates/inline-script.ejs\";\n\nexport interface ViteSwCacherPluginOptions {\n extensions?: string[];\n pattern?: string;\n ttl?: number;\n maxItemsCount?: number;\n cacheName?: string;\n inlineSw?: boolean;\n}\n\nconst DEFAULT_EXTENSIONS = [\n \".html\",\n \".css\",\n \".js\",\n \".svg\",\n \".png\",\n \".jpeg\",\n \".jpg\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".bmp\",\n \".ico\",\n \".tif\",\n \".tiff\",\n];\n\nconst DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_CACHE_NAME = \"vite-sw-cacher-plugin\";\nconst DEFAULT_SW_FILE_NAME = \"sw-cacher.js\";\n\nconst normalizeExtensions = (extensions?: string[]): string[] => {\n const list = extensions?.length ? extensions : DEFAULT_EXTENSIONS;\n const normalized = list.map((ext) => {\n const trimmed = ext.trim();\n if (!trimmed) return \"\";\n return trimmed.startsWith(\".\") ? trimmed.toLowerCase() : `.${trimmed.toLowerCase()}`;\n });\n\n return Array.from(new Set(normalized.filter(Boolean)));\n};\n\nconst wildcardToRegexSource = (pattern: string): string => {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return `^${escaped.replace(/\\*/g, \".*\")}$`;\n};\n\nconst countStaticOutputs = (bundle: OutputBundle, extensions: string[]): number => {\n if (!extensions.length) return 0;\n const normalized = extensions.map((ext) => ext.toLowerCase());\n const items = Object.values(bundle) as Array<OutputAsset | OutputChunk>;\n return items.filter((item) => {\n const name = item.fileName.toLowerCase();\n return normalized.some((ext) => name.endsWith(ext));\n }).length;\n};\n\nconst joinBase = (base: string, fileName: string): string => {\n const normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n return `${normalizedBase}${fileName}`;\n};\n\nconst injectScriptIntoHtml = (html: string, script: string): string => {\n const tag = `<script>${script}</script>`;\n if (/<\\/head>/i.test(html)) {\n return html.replace(/<\\/head>/i, `${tag}</head>`);\n }\n return `${html}${tag}`;\n};\n\nconst toHtmlString = (source: OutputAsset[\"source\"]): string => {\n if (typeof source === \"string\") return source;\n return new TextDecoder().decode(source);\n};\n\nconst renderTemplate = (template: string, data: Record<string, unknown>): string =>\n ejs.render(template, data);\n\nconst minifyScript = async (code: string): Promise<string> => {\n const result = await minify(code, {\n compress: true,\n mangle: true,\n format: { comments: false },\n });\n return result.code ?? code;\n};\n\nconst buildServiceWorkerSource = async (options: {\n cacheName: string;\n ttlMs: number;\n maxItems: number;\n extensions: string[];\n pattern?: string;\n}): Promise<string> => {\n const patternSource = options.pattern ? wildcardToRegexSource(options.pattern) : null;\n const urlPatternSource = patternSource\n ? `new RegExp(${JSON.stringify(patternSource)})`\n : \"null\";\n const allowedContentTypes = Array.from(\n new Set(\n options.extensions\n .map((ext) => lookupMimeType(ext))\n .filter((value): value is string => Boolean(value))\n .map((value) => value.toLowerCase()),\n ),\n );\n\n const source = renderTemplate(swTemplate, {\n cacheNameJson: JSON.stringify(options.cacheName),\n ttlMs: Math.max(0, options.ttlMs),\n maxItems: options.maxItems,\n extensionsJson: JSON.stringify(options.extensions),\n allowedContentTypesJson: JSON.stringify(allowedContentTypes),\n urlPatternSource,\n });\n\n return minifyScript(source);\n};\n\nexport const viteSwCacherPlugin = (\n options: ViteSwCacherPluginOptions = {},\n): Plugin => {\n let resolvedConfig: ResolvedConfig | null = null;\n const swFileName = DEFAULT_SW_FILE_NAME;\n\n return {\n name: \"vite-sw-cacher-plugin\",\n apply: \"build\",\n configResolved(config) {\n resolvedConfig = config;\n },\n async transformIndexHtml(html) {\n if (options.inlineSw) return html;\n const base = resolvedConfig?.base ?? \"/\";\n const swUrl = joinBase(base, swFileName);\n const inlineScript = renderTemplate(inlineScriptTemplate, {\n inlineSw: false,\n swUrlJson: JSON.stringify(swUrl),\n });\n const minifiedInlineScript = await minifyScript(inlineScript);\n\n return {\n html,\n tags: [\n {\n tag: \"script\",\n injectTo: \"head\",\n children: minifiedInlineScript,\n },\n ],\n };\n },\n async generateBundle(_, bundle) {\n const extensions = normalizeExtensions(options.extensions);\n const staticCount = countStaticOutputs(bundle, extensions);\n const ttlMs = options.ttl ?? DEFAULT_TTL_MS;\n const maxItems =\n options.maxItemsCount ?? staticCount * 2;\n const cacheName = options.cacheName ?? DEFAULT_CACHE_NAME;\n const inlineSw = options.inlineSw ?? false;\n\n const swSource = await buildServiceWorkerSource({\n cacheName,\n ttlMs,\n maxItems,\n extensions,\n pattern: options.pattern,\n });\n\n if (inlineSw) {\n const inlineScript = renderTemplate(inlineScriptTemplate, {\n inlineSw: true,\n swCodeJson: JSON.stringify(swSource),\n });\n const minifiedInlineScript = await minifyScript(inlineScript);\n\n for (const item of Object.values(bundle)) {\n if (item.type !== \"asset\") continue;\n if (!item.fileName.toLowerCase().endsWith(\".html\")) continue;\n const html = toHtmlString(item.source);\n item.source = injectScriptIntoHtml(html, minifiedInlineScript);\n }\n return;\n }\n\n this.emitFile({\n type: \"asset\",\n fileName: swFileName,\n source: swSource,\n });\n },\n };\n};\n\nexport default viteSwCacherPlugin;\n","const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst ALLOWED_CONTENT_TYPES = <%- allowedContentTypesJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\nconst HTML_FALLBACK_KEY = \"__sw-cacher-html-fallback__\";\n\nself.addEventListener(\"install\", () => self.skipWaiting());\n\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener(\"fetch\", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== \"GET\") return false;\n const url = request.url;\n return matchesPattern(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst matchesContentType = (response) => {\n if (!ALLOWED_CONTENT_TYPES.length) return false;\n const header = response.headers.get(\"content-type\");\n if (!header) return false;\n const contentType = header.split(\";\")[0]?.trim().toLowerCase();\n if (!contentType) return false;\n return ALLOWED_CONTENT_TYPES.includes(contentType);\n};\n\nconst isNavigationRequest = (request) => {\n if (request.mode === \"navigate\") return true;\n const accept = request.headers.get(\"accept\") || \"\";\n return accept.includes(\"text/html\");\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n const shouldCache =\n matchesExtension(request.url) || matchesContentType(response);\n if (shouldCache) {\n await putInCache(request, response);\n }\n await maybeStoreHtmlFallback(response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n if (cached) return cached;\n if (isNavigationRequest(request)) {\n const fallback = await getHtmlFallback();\n if (fallback) return fallback;\n }\n return response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set(\"sw-cache-time\", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst maybeStoreHtmlFallback = async (response) => {\n if (!matchesContentType(response)) return;\n const header = response.headers.get(\"content-type\") || \"\";\n const contentType = header.split(\";\")[0]?.trim().toLowerCase();\n if (contentType !== \"text/html\") return;\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set(\"sw-cache-time\", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n await cache.put(HTML_FALLBACK_KEY, responseToCache);\n};\n\nconst getHtmlFallback = async () => {\n const cache = await caches.open(CACHE_NAME);\n return cache.match(HTML_FALLBACK_KEY);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get(\"sw-cache-time\");\n const dateHeader = response.headers.get(\"date\");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n","(() => {\n if (!(\"serviceWorker\" in navigator)) return;\n window.addEventListener(\"load\", () => {\n <% if (inlineSw) { %>\n const swCode = <%- swCodeJson %>;\n const blob = new Blob([swCode], { type: \"text/javascript\" });\n const swUrl = URL.createObjectURL(blob);\n navigator.serviceWorker\n .register(swUrl)\n .catch((error) => {\n console.warn(\"[vite-sw-cacher-plugin] SW registration failed\", error);\n })\n .finally(() => {\n URL.revokeObjectURL(swUrl);\n });\n <% } else { %>\n navigator.serviceWorker\n .register(<%- swUrlJson %>)\n .catch((error) => {\n console.warn(\"[vite-sw-cacher-plugin] SW registration failed\", error);\n });\n <% } %>\n });\n})();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,iBAAgB;AAChB,oBAAuB;AACvB,wBAAyC;;;ACJzC;;;ACAA;;;AFiBA,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,iBAAiB,KAAK,KAAK,KAAK;AACtC,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;AAE7B,IAAM,sBAAsB,CAAC,eAAoC;AAC/D,QAAM,OAAO,YAAY,SAAS,aAAa;AAC/C,QAAM,aAAa,KAAK,IAAI,CAAC,QAAQ;AACnC,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO,QAAQ,WAAW,GAAG,IAAI,QAAQ,YAAY,IAAI,IAAI,QAAQ,YAAY,CAAC;AAAA,EACpF,CAAC;AAED,SAAO,MAAM,KAAK,IAAI,IAAI,WAAW,OAAO,OAAO,CAAC,CAAC;AACvD;AAEA,IAAM,wBAAwB,CAAC,YAA4B;AACzD,QAAM,UAAU,QAAQ,QAAQ,sBAAsB,MAAM;AAC5D,SAAO,IAAI,QAAQ,QAAQ,OAAO,IAAI,CAAC;AACzC;AAEA,IAAM,qBAAqB,CAAC,QAAsB,eAAiC;AACjF,MAAI,CAAC,WAAW,OAAQ,QAAO;AAC/B,QAAM,aAAa,WAAW,IAAI,CAAC,QAAQ,IAAI,YAAY,CAAC;AAC5D,QAAM,QAAQ,OAAO,OAAO,MAAM;AAClC,SAAO,MAAM,OAAO,CAAC,SAAS;AAC5B,UAAM,OAAO,KAAK,SAAS,YAAY;AACvC,WAAO,WAAW,KAAK,CAAC,QAAQ,KAAK,SAAS,GAAG,CAAC;AAAA,EACpD,CAAC,EAAE;AACL;AAEA,IAAM,WAAW,CAAC,MAAc,aAA6B;AAC3D,QAAM,iBAAiB,KAAK,SAAS,GAAG,IAAI,OAAO,GAAG,IAAI;AAC1D,SAAO,GAAG,cAAc,GAAG,QAAQ;AACrC;AAEA,IAAM,uBAAuB,CAAC,MAAc,WAA2B;AACrE,QAAM,MAAM,WAAW,MAAM;AAC7B,MAAI,YAAY,KAAK,IAAI,GAAG;AAC1B,WAAO,KAAK,QAAQ,aAAa,GAAG,GAAG,SAAS;AAAA,EAClD;AACA,SAAO,GAAG,IAAI,GAAG,GAAG;AACtB;AAEA,IAAM,eAAe,CAAC,WAA0C;AAC9D,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,SAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AACxC;AAEA,IAAM,iBAAiB,CAAC,UAAkB,SACxC,WAAAA,QAAI,OAAO,UAAU,IAAI;AAE3B,IAAM,eAAe,OAAO,SAAkC;AAC5D,QAAM,SAAS,UAAM,sBAAO,MAAM;AAAA,IAChC,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,QAAQ,EAAE,UAAU,MAAM;AAAA,EAC5B,CAAC;AACD,SAAO,OAAO,QAAQ;AACxB;AAEA,IAAM,2BAA2B,OAAO,YAMjB;AACrB,QAAM,gBAAgB,QAAQ,UAAU,sBAAsB,QAAQ,OAAO,IAAI;AACjF,QAAM,mBAAmB,gBACrB,cAAc,KAAK,UAAU,aAAa,CAAC,MAC3C;AACJ,QAAM,sBAAsB,MAAM;AAAA,IAChC,IAAI;AAAA,MACF,QAAQ,WACL,IAAI,CAAC,YAAQ,kBAAAC,QAAe,GAAG,CAAC,EAChC,OAAO,CAAC,UAA2B,QAAQ,KAAK,CAAC,EACjD,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC;AAAA,IACvC;AAAA,EACF;AAEA,QAAM,SAAS,eAAe,YAAY;AAAA,IACxC,eAAe,KAAK,UAAU,QAAQ,SAAS;AAAA,IAC/C,OAAO,KAAK,IAAI,GAAG,QAAQ,KAAK;AAAA,IAChC,UAAU,QAAQ;AAAA,IAClB,gBAAgB,KAAK,UAAU,QAAQ,UAAU;AAAA,IACjD,yBAAyB,KAAK,UAAU,mBAAmB;AAAA,IAC3D;AAAA,EACF,CAAC;AAED,SAAO,aAAa,MAAM;AAC5B;AAEO,IAAM,qBAAqB,CAChC,UAAqC,CAAC,MAC3B;AACX,MAAI,iBAAwC;AAC5C,QAAM,aAAa;AAEnB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,eAAe,QAAQ;AACrB,uBAAiB;AAAA,IACnB;AAAA,IACA,MAAM,mBAAmB,MAAM;AAC7B,UAAI,QAAQ,SAAU,QAAO;AAC7B,YAAM,OAAO,gBAAgB,QAAQ;AACrC,YAAM,QAAQ,SAAS,MAAM,UAAU;AACvC,YAAM,eAAe,eAAe,uBAAsB;AAAA,QACxD,UAAU;AAAA,QACV,WAAW,KAAK,UAAU,KAAK;AAAA,MACjC,CAAC;AACD,YAAM,uBAAuB,MAAM,aAAa,YAAY;AAE5D,aAAO;AAAA,QACL;AAAA,QACA,MAAM;AAAA,UACJ;AAAA,YACE,KAAK;AAAA,YACL,UAAU;AAAA,YACV,UAAU;AAAA,UACZ;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,eAAe,GAAG,QAAQ;AAC9B,YAAM,aAAa,oBAAoB,QAAQ,UAAU;AACzD,YAAM,cAAc,mBAAmB,QAAQ,UAAU;AACzD,YAAM,QAAQ,QAAQ,OAAO;AAC7B,YAAM,WACJ,QAAQ,iBAAiB,cAAc;AACzC,YAAM,YAAY,QAAQ,aAAa;AACvC,YAAM,WAAW,QAAQ,YAAY;AAErC,YAAM,WAAW,MAAM,yBAAyB;AAAA,QAC9C;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,QAAQ;AAAA,MACnB,CAAC;AAED,UAAI,UAAU;AACZ,cAAM,eAAe,eAAe,uBAAsB;AAAA,UACxD,UAAU;AAAA,UACV,YAAY,KAAK,UAAU,QAAQ;AAAA,QACrC,CAAC;AACD,cAAM,uBAAuB,MAAM,aAAa,YAAY;AAE5D,mBAAW,QAAQ,OAAO,OAAO,MAAM,GAAG;AACxC,cAAI,KAAK,SAAS,QAAS;AAC3B,cAAI,CAAC,KAAK,SAAS,YAAY,EAAE,SAAS,OAAO,EAAG;AACpD,gBAAM,OAAO,aAAa,KAAK,MAAM;AACrC,eAAK,SAAS,qBAAqB,MAAM,oBAAoB;AAAA,QAC/D;AACA;AAAA,MACF;AAEA,WAAK,SAAS;AAAA,QACZ,MAAM;AAAA,QACN,UAAU;AAAA,QACV,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;","names":["ejs","lookupMimeType"]}
package/dist/index.d.cts CHANGED
@@ -6,6 +6,7 @@ interface ViteSwCacherPluginOptions {
6
6
  ttl?: number;
7
7
  maxItemsCount?: number;
8
8
  cacheName?: string;
9
+ inlineSw?: boolean;
9
10
  }
10
11
  declare const viteSwCacherPlugin: (options?: ViteSwCacherPluginOptions) => Plugin;
11
12
 
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ interface ViteSwCacherPluginOptions {
6
6
  ttl?: number;
7
7
  maxItemsCount?: number;
8
8
  cacheName?: string;
9
+ inlineSw?: boolean;
9
10
  }
10
11
  declare const viteSwCacherPlugin: (options?: ViteSwCacherPluginOptions) => Plugin;
11
12
 
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  // src/index.ts
2
2
  import ejs from "ejs";
3
3
  import { minify } from "terser";
4
+ import { lookup as lookupMimeType } from "mime-types";
4
5
 
5
6
  // src/templates/sw.ejs
6
- var sw_default = 'const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\n\nself.addEventListener("install", () => self.skipWaiting());\n\nself.addEventListener("activate", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener("fetch", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== "GET") return false;\n const url = request.url;\n return matchesPattern(url) && matchesExtension(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n await putInCache(request, response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n return cached || response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set("sw-cache-time", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get("sw-cache-time");\n const dateHeader = response.headers.get("date");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n';
7
+ var sw_default = 'const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst ALLOWED_CONTENT_TYPES = <%- allowedContentTypesJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\nconst HTML_FALLBACK_KEY = "__sw-cacher-html-fallback__";\n\nself.addEventListener("install", () => self.skipWaiting());\n\nself.addEventListener("activate", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener("fetch", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== "GET") return false;\n const url = request.url;\n return matchesPattern(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst matchesContentType = (response) => {\n if (!ALLOWED_CONTENT_TYPES.length) return false;\n const header = response.headers.get("content-type");\n if (!header) return false;\n const contentType = header.split(";")[0]?.trim().toLowerCase();\n if (!contentType) return false;\n return ALLOWED_CONTENT_TYPES.includes(contentType);\n};\n\nconst isNavigationRequest = (request) => {\n if (request.mode === "navigate") return true;\n const accept = request.headers.get("accept") || "";\n return accept.includes("text/html");\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n const shouldCache =\n matchesExtension(request.url) || matchesContentType(response);\n if (shouldCache) {\n await putInCache(request, response);\n }\n await maybeStoreHtmlFallback(response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n if (cached) return cached;\n if (isNavigationRequest(request)) {\n const fallback = await getHtmlFallback();\n if (fallback) return fallback;\n }\n return response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set("sw-cache-time", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst maybeStoreHtmlFallback = async (response) => {\n if (!matchesContentType(response)) return;\n const header = response.headers.get("content-type") || "";\n const contentType = header.split(";")[0]?.trim().toLowerCase();\n if (contentType !== "text/html") return;\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set("sw-cache-time", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n await cache.put(HTML_FALLBACK_KEY, responseToCache);\n};\n\nconst getHtmlFallback = async () => {\n const cache = await caches.open(CACHE_NAME);\n return cache.match(HTML_FALLBACK_KEY);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get("sw-cache-time");\n const dateHeader = response.headers.get("date");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n';
7
8
 
8
9
  // src/templates/inline-script.ejs
9
- var inline_script_default = '(() => {\n if (!("serviceWorker" in navigator)) return;\n window.addEventListener("load", () => {\n navigator.serviceWorker.register(<%- swUrlJson %>).catch((error) => {\n console.warn("[vite-sw-cacher-plugin] SW registration failed", error);\n });\n });\n})();\n';
10
+ var inline_script_default = '(() => {\n if (!("serviceWorker" in navigator)) return;\n window.addEventListener("load", () => {\n <% if (inlineSw) { %>\n const swCode = <%- swCodeJson %>;\n const blob = new Blob([swCode], { type: "text/javascript" });\n const swUrl = URL.createObjectURL(blob);\n navigator.serviceWorker\n .register(swUrl)\n .catch((error) => {\n console.warn("[vite-sw-cacher-plugin] SW registration failed", error);\n })\n .finally(() => {\n URL.revokeObjectURL(swUrl);\n });\n <% } else { %>\n navigator.serviceWorker\n .register(<%- swUrlJson %>)\n .catch((error) => {\n console.warn("[vite-sw-cacher-plugin] SW registration failed", error);\n });\n <% } %>\n });\n})();\n';
10
11
 
11
12
  // src/index.ts
12
13
  var DEFAULT_EXTENSIONS = [
@@ -54,6 +55,17 @@ var joinBase = (base, fileName) => {
54
55
  const normalizedBase = base.endsWith("/") ? base : `${base}/`;
55
56
  return `${normalizedBase}${fileName}`;
56
57
  };
58
+ var injectScriptIntoHtml = (html, script) => {
59
+ const tag = `<script>${script}</script>`;
60
+ if (/<\/head>/i.test(html)) {
61
+ return html.replace(/<\/head>/i, `${tag}</head>`);
62
+ }
63
+ return `${html}${tag}`;
64
+ };
65
+ var toHtmlString = (source) => {
66
+ if (typeof source === "string") return source;
67
+ return new TextDecoder().decode(source);
68
+ };
57
69
  var renderTemplate = (template, data) => ejs.render(template, data);
58
70
  var minifyScript = async (code) => {
59
71
  const result = await minify(code, {
@@ -66,11 +78,17 @@ var minifyScript = async (code) => {
66
78
  var buildServiceWorkerSource = async (options) => {
67
79
  const patternSource = options.pattern ? wildcardToRegexSource(options.pattern) : null;
68
80
  const urlPatternSource = patternSource ? `new RegExp(${JSON.stringify(patternSource)})` : "null";
81
+ const allowedContentTypes = Array.from(
82
+ new Set(
83
+ options.extensions.map((ext) => lookupMimeType(ext)).filter((value) => Boolean(value)).map((value) => value.toLowerCase())
84
+ )
85
+ );
69
86
  const source = renderTemplate(sw_default, {
70
87
  cacheNameJson: JSON.stringify(options.cacheName),
71
88
  ttlMs: Math.max(0, options.ttlMs),
72
89
  maxItems: options.maxItems,
73
90
  extensionsJson: JSON.stringify(options.extensions),
91
+ allowedContentTypesJson: JSON.stringify(allowedContentTypes),
74
92
  urlPatternSource
75
93
  });
76
94
  return minifyScript(source);
@@ -85,9 +103,11 @@ var viteSwCacherPlugin = (options = {}) => {
85
103
  resolvedConfig = config;
86
104
  },
87
105
  async transformIndexHtml(html) {
106
+ if (options.inlineSw) return html;
88
107
  const base = resolvedConfig?.base ?? "/";
89
108
  const swUrl = joinBase(base, swFileName);
90
109
  const inlineScript = renderTemplate(inline_script_default, {
110
+ inlineSw: false,
91
111
  swUrlJson: JSON.stringify(swUrl)
92
112
  });
93
113
  const minifiedInlineScript = await minifyScript(inlineScript);
@@ -108,6 +128,7 @@ var viteSwCacherPlugin = (options = {}) => {
108
128
  const ttlMs = options.ttl ?? DEFAULT_TTL_MS;
109
129
  const maxItems = options.maxItemsCount ?? staticCount * 2;
110
130
  const cacheName = options.cacheName ?? DEFAULT_CACHE_NAME;
131
+ const inlineSw = options.inlineSw ?? false;
111
132
  const swSource = await buildServiceWorkerSource({
112
133
  cacheName,
113
134
  ttlMs,
@@ -115,6 +136,20 @@ var viteSwCacherPlugin = (options = {}) => {
115
136
  extensions,
116
137
  pattern: options.pattern
117
138
  });
139
+ if (inlineSw) {
140
+ const inlineScript = renderTemplate(inline_script_default, {
141
+ inlineSw: true,
142
+ swCodeJson: JSON.stringify(swSource)
143
+ });
144
+ const minifiedInlineScript = await minifyScript(inlineScript);
145
+ for (const item of Object.values(bundle)) {
146
+ if (item.type !== "asset") continue;
147
+ if (!item.fileName.toLowerCase().endsWith(".html")) continue;
148
+ const html = toHtmlString(item.source);
149
+ item.source = injectScriptIntoHtml(html, minifiedInlineScript);
150
+ }
151
+ return;
152
+ }
118
153
  this.emitFile({
119
154
  type: "asset",
120
155
  fileName: swFileName,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/templates/sw.ejs","../src/templates/inline-script.ejs"],"sourcesContent":["import type { OutputAsset, OutputBundle, OutputChunk } from \"rollup\";\nimport type { Plugin, ResolvedConfig } from \"vite\";\nimport ejs from \"ejs\";\nimport { minify } from \"terser\";\nimport swTemplate from \"./templates/sw.ejs\";\nimport inlineScriptTemplate from \"./templates/inline-script.ejs\";\n\nexport interface ViteSwCacherPluginOptions {\n extensions?: string[];\n pattern?: string;\n ttl?: number;\n maxItemsCount?: number;\n cacheName?: string;\n}\n\nconst DEFAULT_EXTENSIONS = [\n \".html\",\n \".css\",\n \".js\",\n \".svg\",\n \".png\",\n \".jpeg\",\n \".jpg\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".bmp\",\n \".ico\",\n \".tif\",\n \".tiff\",\n];\n\nconst DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_CACHE_NAME = \"vite-sw-cacher-plugin\";\nconst DEFAULT_SW_FILE_NAME = \"sw-cacher.js\";\n\nconst normalizeExtensions = (extensions?: string[]): string[] => {\n const list = extensions?.length ? extensions : DEFAULT_EXTENSIONS;\n const normalized = list.map((ext) => {\n const trimmed = ext.trim();\n if (!trimmed) return \"\";\n return trimmed.startsWith(\".\") ? trimmed.toLowerCase() : `.${trimmed.toLowerCase()}`;\n });\n\n return Array.from(new Set(normalized.filter(Boolean)));\n};\n\nconst wildcardToRegexSource = (pattern: string): string => {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return `^${escaped.replace(/\\*/g, \".*\")}$`;\n};\n\nconst countStaticOutputs = (bundle: OutputBundle, extensions: string[]): number => {\n if (!extensions.length) return 0;\n const normalized = extensions.map((ext) => ext.toLowerCase());\n const items = Object.values(bundle) as Array<OutputAsset | OutputChunk>;\n return items.filter((item) => {\n const name = item.fileName.toLowerCase();\n return normalized.some((ext) => name.endsWith(ext));\n }).length;\n};\n\nconst joinBase = (base: string, fileName: string): string => {\n const normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n return `${normalizedBase}${fileName}`;\n};\n\nconst renderTemplate = (template: string, data: Record<string, unknown>): string =>\n ejs.render(template, data);\n\nconst minifyScript = async (code: string): Promise<string> => {\n const result = await minify(code, {\n compress: true,\n mangle: true,\n format: { comments: false },\n });\n return result.code ?? code;\n};\n\nconst buildServiceWorkerSource = async (options: {\n cacheName: string;\n ttlMs: number;\n maxItems: number;\n extensions: string[];\n pattern?: string;\n}): Promise<string> => {\n const patternSource = options.pattern ? wildcardToRegexSource(options.pattern) : null;\n const urlPatternSource = patternSource\n ? `new RegExp(${JSON.stringify(patternSource)})`\n : \"null\";\n\n const source = renderTemplate(swTemplate, {\n cacheNameJson: JSON.stringify(options.cacheName),\n ttlMs: Math.max(0, options.ttlMs),\n maxItems: options.maxItems,\n extensionsJson: JSON.stringify(options.extensions),\n urlPatternSource,\n });\n\n return minifyScript(source);\n};\n\nexport const viteSwCacherPlugin = (\n options: ViteSwCacherPluginOptions = {},\n): Plugin => {\n let resolvedConfig: ResolvedConfig | null = null;\n const swFileName = DEFAULT_SW_FILE_NAME;\n\n return {\n name: \"vite-sw-cacher-plugin\",\n apply: \"build\",\n configResolved(config) {\n resolvedConfig = config;\n },\n async transformIndexHtml(html) {\n const base = resolvedConfig?.base ?? \"/\";\n const swUrl = joinBase(base, swFileName);\n const inlineScript = renderTemplate(inlineScriptTemplate, {\n swUrlJson: JSON.stringify(swUrl),\n });\n const minifiedInlineScript = await minifyScript(inlineScript);\n\n return {\n html,\n tags: [\n {\n tag: \"script\",\n injectTo: \"head\",\n children: minifiedInlineScript,\n },\n ],\n };\n },\n async generateBundle(_, bundle) {\n const extensions = normalizeExtensions(options.extensions);\n const staticCount = countStaticOutputs(bundle, extensions);\n const ttlMs = options.ttl ?? DEFAULT_TTL_MS;\n const maxItems =\n options.maxItemsCount ?? staticCount * 2;\n const cacheName = options.cacheName ?? DEFAULT_CACHE_NAME;\n\n const swSource = await buildServiceWorkerSource({\n cacheName,\n ttlMs,\n maxItems,\n extensions,\n pattern: options.pattern,\n });\n\n this.emitFile({\n type: \"asset\",\n fileName: swFileName,\n source: swSource,\n });\n },\n };\n};\n\nexport default viteSwCacherPlugin;\n","const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\n\nself.addEventListener(\"install\", () => self.skipWaiting());\n\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener(\"fetch\", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== \"GET\") return false;\n const url = request.url;\n return matchesPattern(url) && matchesExtension(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n await putInCache(request, response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n return cached || response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set(\"sw-cache-time\", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get(\"sw-cache-time\");\n const dateHeader = response.headers.get(\"date\");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n","(() => {\n if (!(\"serviceWorker\" in navigator)) return;\n window.addEventListener(\"load\", () => {\n navigator.serviceWorker.register(<%- swUrlJson %>).catch((error) => {\n console.warn(\"[vite-sw-cacher-plugin] SW registration failed\", error);\n });\n });\n})();\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/templates/sw.ejs","../src/templates/inline-script.ejs"],"sourcesContent":["import type { OutputAsset, OutputBundle, OutputChunk } from \"rollup\";\nimport type { Plugin, ResolvedConfig } from \"vite\";\nimport ejs from \"ejs\";\nimport { minify } from \"terser\";\nimport { lookup as lookupMimeType } from \"mime-types\";\nimport swTemplate from \"./templates/sw.ejs\";\nimport inlineScriptTemplate from \"./templates/inline-script.ejs\";\n\nexport interface ViteSwCacherPluginOptions {\n extensions?: string[];\n pattern?: string;\n ttl?: number;\n maxItemsCount?: number;\n cacheName?: string;\n inlineSw?: boolean;\n}\n\nconst DEFAULT_EXTENSIONS = [\n \".html\",\n \".css\",\n \".js\",\n \".svg\",\n \".png\",\n \".jpeg\",\n \".jpg\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".bmp\",\n \".ico\",\n \".tif\",\n \".tiff\",\n];\n\nconst DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_CACHE_NAME = \"vite-sw-cacher-plugin\";\nconst DEFAULT_SW_FILE_NAME = \"sw-cacher.js\";\n\nconst normalizeExtensions = (extensions?: string[]): string[] => {\n const list = extensions?.length ? extensions : DEFAULT_EXTENSIONS;\n const normalized = list.map((ext) => {\n const trimmed = ext.trim();\n if (!trimmed) return \"\";\n return trimmed.startsWith(\".\") ? trimmed.toLowerCase() : `.${trimmed.toLowerCase()}`;\n });\n\n return Array.from(new Set(normalized.filter(Boolean)));\n};\n\nconst wildcardToRegexSource = (pattern: string): string => {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return `^${escaped.replace(/\\*/g, \".*\")}$`;\n};\n\nconst countStaticOutputs = (bundle: OutputBundle, extensions: string[]): number => {\n if (!extensions.length) return 0;\n const normalized = extensions.map((ext) => ext.toLowerCase());\n const items = Object.values(bundle) as Array<OutputAsset | OutputChunk>;\n return items.filter((item) => {\n const name = item.fileName.toLowerCase();\n return normalized.some((ext) => name.endsWith(ext));\n }).length;\n};\n\nconst joinBase = (base: string, fileName: string): string => {\n const normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n return `${normalizedBase}${fileName}`;\n};\n\nconst injectScriptIntoHtml = (html: string, script: string): string => {\n const tag = `<script>${script}</script>`;\n if (/<\\/head>/i.test(html)) {\n return html.replace(/<\\/head>/i, `${tag}</head>`);\n }\n return `${html}${tag}`;\n};\n\nconst toHtmlString = (source: OutputAsset[\"source\"]): string => {\n if (typeof source === \"string\") return source;\n return new TextDecoder().decode(source);\n};\n\nconst renderTemplate = (template: string, data: Record<string, unknown>): string =>\n ejs.render(template, data);\n\nconst minifyScript = async (code: string): Promise<string> => {\n const result = await minify(code, {\n compress: true,\n mangle: true,\n format: { comments: false },\n });\n return result.code ?? code;\n};\n\nconst buildServiceWorkerSource = async (options: {\n cacheName: string;\n ttlMs: number;\n maxItems: number;\n extensions: string[];\n pattern?: string;\n}): Promise<string> => {\n const patternSource = options.pattern ? wildcardToRegexSource(options.pattern) : null;\n const urlPatternSource = patternSource\n ? `new RegExp(${JSON.stringify(patternSource)})`\n : \"null\";\n const allowedContentTypes = Array.from(\n new Set(\n options.extensions\n .map((ext) => lookupMimeType(ext))\n .filter((value): value is string => Boolean(value))\n .map((value) => value.toLowerCase()),\n ),\n );\n\n const source = renderTemplate(swTemplate, {\n cacheNameJson: JSON.stringify(options.cacheName),\n ttlMs: Math.max(0, options.ttlMs),\n maxItems: options.maxItems,\n extensionsJson: JSON.stringify(options.extensions),\n allowedContentTypesJson: JSON.stringify(allowedContentTypes),\n urlPatternSource,\n });\n\n return minifyScript(source);\n};\n\nexport const viteSwCacherPlugin = (\n options: ViteSwCacherPluginOptions = {},\n): Plugin => {\n let resolvedConfig: ResolvedConfig | null = null;\n const swFileName = DEFAULT_SW_FILE_NAME;\n\n return {\n name: \"vite-sw-cacher-plugin\",\n apply: \"build\",\n configResolved(config) {\n resolvedConfig = config;\n },\n async transformIndexHtml(html) {\n if (options.inlineSw) return html;\n const base = resolvedConfig?.base ?? \"/\";\n const swUrl = joinBase(base, swFileName);\n const inlineScript = renderTemplate(inlineScriptTemplate, {\n inlineSw: false,\n swUrlJson: JSON.stringify(swUrl),\n });\n const minifiedInlineScript = await minifyScript(inlineScript);\n\n return {\n html,\n tags: [\n {\n tag: \"script\",\n injectTo: \"head\",\n children: minifiedInlineScript,\n },\n ],\n };\n },\n async generateBundle(_, bundle) {\n const extensions = normalizeExtensions(options.extensions);\n const staticCount = countStaticOutputs(bundle, extensions);\n const ttlMs = options.ttl ?? DEFAULT_TTL_MS;\n const maxItems =\n options.maxItemsCount ?? staticCount * 2;\n const cacheName = options.cacheName ?? DEFAULT_CACHE_NAME;\n const inlineSw = options.inlineSw ?? false;\n\n const swSource = await buildServiceWorkerSource({\n cacheName,\n ttlMs,\n maxItems,\n extensions,\n pattern: options.pattern,\n });\n\n if (inlineSw) {\n const inlineScript = renderTemplate(inlineScriptTemplate, {\n inlineSw: true,\n swCodeJson: JSON.stringify(swSource),\n });\n const minifiedInlineScript = await minifyScript(inlineScript);\n\n for (const item of Object.values(bundle)) {\n if (item.type !== \"asset\") continue;\n if (!item.fileName.toLowerCase().endsWith(\".html\")) continue;\n const html = toHtmlString(item.source);\n item.source = injectScriptIntoHtml(html, minifiedInlineScript);\n }\n return;\n }\n\n this.emitFile({\n type: \"asset\",\n fileName: swFileName,\n source: swSource,\n });\n },\n };\n};\n\nexport default viteSwCacherPlugin;\n","const CACHE_NAME = <%- cacheNameJson %>;\nconst MAX_AGE_MS = <%- ttlMs %>;\nconst MAX_ITEMS = <%- maxItems %>;\nconst EXTENSIONS = <%- extensionsJson %>;\nconst ALLOWED_CONTENT_TYPES = <%- allowedContentTypesJson %>;\nconst URL_PATTERN = <%- urlPatternSource %>;\nconst HTML_FALLBACK_KEY = \"__sw-cacher-html-fallback__\";\n\nself.addEventListener(\"install\", () => self.skipWaiting());\n\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(cleanUpCache().then(() => self.clients.claim()));\n});\n\nself.addEventListener(\"fetch\", (event) => {\n const { request } = event;\n if (!shouldHandleRequest(request)) return;\n\n event.respondWith(handleRequest(request));\n});\n\nconst shouldHandleRequest = (request) => {\n if (request.method !== \"GET\") return false;\n const url = request.url;\n return matchesPattern(url);\n};\n\nconst matchesPattern = (url) => {\n if (!URL_PATTERN) return true;\n return URL_PATTERN.test(url);\n};\n\nconst matchesExtension = (url) => {\n if (!EXTENSIONS.length) return true;\n try {\n const pathname = new URL(url).pathname.toLowerCase();\n return EXTENSIONS.some((ext) => pathname.endsWith(ext));\n } catch {\n return false;\n }\n};\n\nconst matchesContentType = (response) => {\n if (!ALLOWED_CONTENT_TYPES.length) return false;\n const header = response.headers.get(\"content-type\");\n if (!header) return false;\n const contentType = header.split(\";\")[0]?.trim().toLowerCase();\n if (!contentType) return false;\n return ALLOWED_CONTENT_TYPES.includes(contentType);\n};\n\nconst isNavigationRequest = (request) => {\n if (request.mode === \"navigate\") return true;\n const accept = request.headers.get(\"accept\") || \"\";\n return accept.includes(\"text/html\");\n};\n\nconst handleRequest = async (request) => {\n try {\n const response = await fetch(request);\n\n if (response && response.ok) {\n const shouldCache =\n matchesExtension(request.url) || matchesContentType(response);\n if (shouldCache) {\n await putInCache(request, response);\n }\n await maybeStoreHtmlFallback(response);\n return response;\n }\n\n if (response && response.status === 404) {\n const cached = await caches.match(request);\n if (cached) return cached;\n if (isNavigationRequest(request)) {\n const fallback = await getHtmlFallback();\n if (fallback) return fallback;\n }\n return response;\n }\n\n return response;\n } catch (error) {\n const cached = await caches.match(request);\n if (cached) return cached;\n throw error;\n }\n};\n\nconst putInCache = async (request, response) => {\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set(\"sw-cache-time\", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n\n await cache.put(request, responseToCache);\n limitCacheSize().catch(() => undefined);\n};\n\nconst maybeStoreHtmlFallback = async (response) => {\n if (!matchesContentType(response)) return;\n const header = response.headers.get(\"content-type\") || \"\";\n const contentType = header.split(\";\")[0]?.trim().toLowerCase();\n if (contentType !== \"text/html\") return;\n const cache = await caches.open(CACHE_NAME);\n const headers = new Headers(response.headers);\n headers.set(\"sw-cache-time\", Date.now().toString());\n const body = await response.clone().arrayBuffer();\n const responseToCache = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n await cache.put(HTML_FALLBACK_KEY, responseToCache);\n};\n\nconst getHtmlFallback = async () => {\n const cache = await caches.open(CACHE_NAME);\n return cache.match(HTML_FALLBACK_KEY);\n};\n\nconst cleanUpCache = async () => {\n if (MAX_AGE_MS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n const now = Date.now();\n\n for (const request of keys) {\n const response = await cache.match(request);\n if (!response) continue;\n\n const storedTime = response.headers.get(\"sw-cache-time\");\n const dateHeader = response.headers.get(\"date\");\n const timestamp = storedTime ? Number(storedTime) : dateHeader ? new Date(dateHeader).getTime() : NaN;\n\n if (!Number.isFinite(timestamp)) continue;\n if (now - timestamp > MAX_AGE_MS) {\n await cache.delete(request);\n }\n }\n};\n\nconst limitCacheSize = async () => {\n if (MAX_ITEMS <= 0) return;\n const cache = await caches.open(CACHE_NAME);\n const keys = await cache.keys();\n\n if (keys.length <= MAX_ITEMS) return;\n const itemsToDelete = keys.slice(0, keys.length - MAX_ITEMS);\n for (const request of itemsToDelete) {\n await cache.delete(request);\n }\n};\n","(() => {\n if (!(\"serviceWorker\" in navigator)) return;\n window.addEventListener(\"load\", () => {\n <% if (inlineSw) { %>\n const swCode = <%- swCodeJson %>;\n const blob = new Blob([swCode], { type: \"text/javascript\" });\n const swUrl = URL.createObjectURL(blob);\n navigator.serviceWorker\n .register(swUrl)\n .catch((error) => {\n console.warn(\"[vite-sw-cacher-plugin] SW registration failed\", error);\n })\n .finally(() => {\n URL.revokeObjectURL(swUrl);\n });\n <% } else { %>\n navigator.serviceWorker\n .register(<%- swUrlJson %>)\n .catch((error) => {\n console.warn(\"[vite-sw-cacher-plugin] SW registration failed\", error);\n });\n <% } %>\n });\n})();\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-sw-cacher-plugin",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Vite 7 plugin that injects a SW cacher",
5
5
  "type": "module",
6
6
  "author": {
@@ -17,6 +17,7 @@
17
17
  "types": "dist/index.d.ts",
18
18
  "dependencies": {
19
19
  "ejs": "^3.1.10",
20
+ "mime-types": "^2.1.35",
20
21
  "terser": "^5.36.0"
21
22
  },
22
23
  "files": [
@@ -34,6 +35,7 @@
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/ejs": "^3.1.5",
38
+ "@types/mime-types": "^2.1.4",
37
39
  "@types/node": "^22.10.0",
38
40
  "rollup": "^4.0.0",
39
41
  "tsup": "^8.0.0",