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 +2 -0
- package/dist/index.cjs +37 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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)
|
|
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
|
|
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,
|
package/dist/index.cjs.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":["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":"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","names":["ejs","lookupMimeType"]}
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
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)
|
|
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
|
|
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":";AAEA,OAAO,SAAS;AAChB,SAAS,cAAc;AACvB,SAAS,UAAU,sBAAsB;;;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,IAAI,OAAO,UAAU,IAAI;AAE3B,IAAM,eAAe,OAAO,SAAkC;AAC5D,QAAM,SAAS,MAAM,OAAO,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,QAAQ,eAAe,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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-sw-cacher-plugin",
|
|
3
|
-
"version": "0.0.
|
|
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",
|