vite-plugin-singlefile-compression 2.3.3 → 2.4.1

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
@@ -6,7 +6,7 @@ The recipient can open it directly in the browser without manually unzipping the
6
6
 
7
7
  Using [DecompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream) + [base128-ascii](https://www.npmjs.com/package/base128-ascii).
8
8
 
9
- Preview: https://bddjr.github.io/vite-plugin-singlefile-compression/
9
+ Preview: https://bddjr.github.io/vite-plugin-singlefile-compression/#/
10
10
 
11
11
  ## Setup
12
12
 
@@ -65,6 +65,8 @@ Use Base128 to encode compressed script.
65
65
  If false, use Base64.
66
66
  https://www.npmjs.com/package/base128-ascii
67
67
 
68
+ This option is only valid when the `enableCompress` option is set to true.
69
+
68
70
  default: `true`
69
71
 
70
72
  type: `boolean`
@@ -75,6 +77,8 @@ Compress format.
75
77
 
76
78
  https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream/DecompressionStream
77
79
 
80
+ This option is only valid when the `enableCompress` option is set to true.
81
+
78
82
  default: `"deflate-raw"`
79
83
 
80
84
  type:
@@ -94,6 +98,8 @@ type:
94
98
 
95
99
  Custom compressor.
96
100
 
101
+ This option is only valid when the `enableCompress` option is set to true.
102
+
97
103
  type: `(buf: zlib.InputType) => (Buffer | Uint8Array | Promise<Buffer | Uint8Array>)`
98
104
 
99
105
  ### htmlMinifierTerser
@@ -124,7 +130,7 @@ type: `boolean`
124
130
 
125
131
  ### tryInlineHtmlPublicIcon
126
132
 
127
- Try inline html icon, if icon is in public dir.
133
+ Try inline html favicon, if icon is in public dir.
128
134
 
129
135
  default: `true`
130
136
 
@@ -132,12 +138,24 @@ type: `boolean`
132
138
 
133
139
  ### removeInlinedPublicIconFiles
134
140
 
135
- Remove inlined html icon files.
141
+ Remove inlined html favicon files.
136
142
 
137
143
  default: `true`
138
144
 
139
145
  type: `boolean`
140
146
 
147
+ ### enableCompressInlinedIcon
148
+
149
+ Enable compress inlined html favicon.
150
+
151
+ This option is only valid when the `enableCompress` option is set to true.
152
+
153
+ ⚠️ Not works on Safari (See [#20](https://github.com/bddjr/vite-plugin-singlefile-compression/issues/20))
154
+
155
+ default: `false`
156
+
157
+ type: `boolean`
158
+
141
159
  ### useImportMetaPolyfill
142
160
 
143
161
  Use import.meta polyfill.
@@ -149,24 +167,24 @@ type: `boolean`
149
167
 
150
168
  ## Effect
151
169
 
152
- Preview: https://bddjr.github.io/vite-plugin-singlefile-compression/
170
+ Preview: https://bddjr.github.io/vite-plugin-singlefile-compression/#/
153
171
 
154
172
  ```
155
- vite v8.0.8 building client environment for production...
173
+ vite v8.0.10 building client environment for production...
156
174
  ✓ 43 modules transformed.
157
175
  rendering chunks (1)...
158
176
 
159
- vite-plugin-singlefile-compression 2.3.3 deflate-raw base128-ascii
177
+ vite-plugin-singlefile-compression 2.4.1 deflate-raw base128-ascii
160
178
 
161
179
  file:///D:/code/js/vite-plugin-singlefile-compression/test/dist/index.html
162
- 125.847 kB -> 51.312 kB
180
+ 126.509 kB -> 59.596 kB
163
181
 
164
182
  Finish.
165
183
 
166
184
  computing gzip size...
167
- dist/index.html 51.31 kB │ gzip: 44.73 kB
185
+ dist/index.html 59.59 kB │ gzip: 44.92 kB
168
186
 
169
- ✓ built in 285ms
187
+ ✓ built in 307ms
170
188
  ```
171
189
 
172
190
  ## Clone
package/dist/index.d.ts CHANGED
@@ -37,6 +37,9 @@ interface Options {
37
37
  * Use Base128 to encode compressed script.
38
38
  * If false, use Base64.
39
39
  * https://www.npmjs.com/package/base128-ascii
40
+ *
41
+ * This option is only valid when the `enableCompress` option is set to true.
42
+ *
40
43
  * @default true
41
44
  */
42
45
  useBase128?: boolean;
@@ -45,6 +48,8 @@ interface Options {
45
48
  *
46
49
  * https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream/DecompressionStream
47
50
  *
51
+ * This option is only valid when the `enableCompress` option is set to true.
52
+ *
48
53
  * @type {"deflate-raw" | "deflate" | "gzip" | "brotli" | "zstd" | "deflateRaw" | "gz" | "br" | "brotliCompress" | "zstandard" | "zst"}
49
54
  *
50
55
  * @default "deflate-raw"
@@ -52,6 +57,8 @@ interface Options {
52
57
  compressFormat?: CompressFormat | CompressFormatAlias;
53
58
  /**
54
59
  * Custom compressor.
60
+ *
61
+ * This option is only valid when the `enableCompress` option is set to true.
55
62
  */
56
63
  compressor?: Compressor;
57
64
  /**
@@ -70,15 +77,25 @@ interface Options {
70
77
  */
71
78
  removeInlinedAssetFiles?: boolean;
72
79
  /**
73
- * Try inline html icon, if icon is in public dir.
80
+ * Try inline html favicon, if icon is in public dir.
74
81
  * @default true
75
82
  */
76
83
  tryInlineHtmlPublicIcon?: boolean;
77
84
  /**
78
- * Remove inlined html icon files.
85
+ * Remove inlined html favicon files.
79
86
  * @default true
80
87
  */
81
88
  removeInlinedPublicIconFiles?: boolean;
89
+ /**
90
+ * Enable compress inlined html favicon.
91
+ *
92
+ * This option is only valid when the `enableCompress` option is set to true.
93
+ *
94
+ * ⚠️ Not works on Safari (See [#20](https://github.com/bddjr/vite-plugin-singlefile-compression/issues/20))
95
+ *
96
+ * @default false
97
+ */
98
+ enableCompressInlinedIcon?: boolean;
82
99
  /**
83
100
  * Use import.meta polyfill.
84
101
  * @default false
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import zlib from "zlib";
9
9
  import svgToTinyDataUri from "mini-svg-data-uri";
10
10
  import { lookup } from "mrmime";
11
11
  //#region package.json
12
- var version = "2.3.3";
12
+ var version = "2.4.1";
13
13
  //#endregion
14
14
  //#region src/compress.ts
15
15
  const compressors = {
@@ -77,8 +77,6 @@ const files = {
77
77
  "\").then(e=>new Response(e.body.pipeThrough(new DecompressionStream(",
78
78
  "))).text()).then(e=>{var t=document.createElement(\"script\");t.type=\"module\",t.innerHTML=e,document.head.appendChild(t)})"
79
79
  ],
80
- "css": "document.head.appendChild(document.createElement(\"style\")).innerHTML=",
81
- "icon": "document.querySelector(\"link[rel=icon]\").href=",
82
80
  "importmeta": ["{let e=import.meta,t=e.resolve,r=e.url=new URL(", ",location).href;e.resolve=function(e){return/^\\.{0,2}\\//.test(e)?new URL(e,r).href:t.apply(this,arguments)}}"]
83
81
  };
84
82
  //#endregion
@@ -93,12 +91,6 @@ const template = {
93
91
  const t = files.assets;
94
92
  return t[0].concat(JSON.stringify(assetsJSON), t[1]);
95
93
  },
96
- css(cssSource) {
97
- return files.css + JSON.stringify(cssSource);
98
- },
99
- icon(dataURL) {
100
- return files.icon + JSON.stringify(dataURL);
101
- },
102
94
  importmeta(p) {
103
95
  const t = files.importmeta;
104
96
  return t[0].concat(JSON.stringify(p), t[1]);
@@ -127,9 +119,10 @@ const defaultHtmlMinifierTerserOptions = {
127
119
  };
128
120
  function getInnerOptions(opt) {
129
121
  opt ||= {};
122
+ const enableCompress = opt.enableCompress ?? true;
130
123
  return {
131
124
  rename: opt.rename == null ? void 0 : String(opt.rename),
132
- enableCompress: opt.enableCompress ?? true,
125
+ enableCompress,
133
126
  useBase128: opt.useBase128 ?? true,
134
127
  compressFormat: opt.compressFormat ? compressFormatAlias.hasOwnProperty(opt.compressFormat) ? compressFormatAlias[opt.compressFormat] : String(opt.compressFormat) : "deflate-raw",
135
128
  compressor: typeof opt.compressor == "function" ? opt.compressor : void 0,
@@ -138,6 +131,7 @@ function getInnerOptions(opt) {
138
131
  removeInlinedAssetFiles: opt.removeInlinedAssetFiles ?? true,
139
132
  tryInlineHtmlPublicIcon: opt.tryInlineHtmlPublicIcon ?? true,
140
133
  removeInlinedPublicIconFiles: opt.removeInlinedPublicIconFiles ?? true,
134
+ enableCompressInlinedIcon: enableCompress && (opt.enableCompressInlinedIcon ?? false),
141
135
  useImportMetaPolyfill: opt.useImportMetaPolyfill ?? false
142
136
  };
143
137
  }
@@ -186,12 +180,12 @@ async function generateBundle(bundle, config, options) {
186
180
  bundle[options.rename].fileName = options.rename;
187
181
  delete bundle["index.html"];
188
182
  }
189
- const distURL = pathToFileURL(config.build.outDir).href + "/", assetsDir = path.posix.join(config.build.assetsDir, "/"), assetsDirWithBase = config.base + assetsDir, assetsHrefSelector = `[href^="${assetsDirWithBase}"]`, assetsSrcSelector = `[src^="${assetsDirWithBase}"]`, fakeScript = `_vitePluginSinglefileCompression(${Date.now()})`, globalDelete = /* @__PURE__ */ new Set(), globalDoNotDelete = /* @__PURE__ */ new Set(), globalRemoveDistFileNames = /* @__PURE__ */ new Set(), globalAssetsDataURL = {}, globalPublicFilesCache = {}, bundleAssetsNames = [], bundleHTMLNames = [];
183
+ const distURL = pathToFileURL(config.build.outDir).href + "/", assetsDir = path.posix.join(config.build.assetsDir, "/"), assetsDirWithBase = config.base + assetsDir, assetsHrefSelector = `[href^="${assetsDirWithBase}"]`, assetsSrcSelector = `[src^="${assetsDirWithBase}"]`, fakeScript = `_${Math.random().toString(36).slice(2, 10)}()`, globalDelete = /* @__PURE__ */ new Set(), globalDoNotDelete = /* @__PURE__ */ new Set(), globalRemoveDistFileNames = /* @__PURE__ */ new Set(), globalAssetsDataURL = {}, globalPublicFilesCache = {}, bundleAssetsNames = [], bundleHTMLNames = [];
190
184
  for (const name in bundle) if (name.startsWith(assetsDir)) bundleAssetsNames.push(name);
191
185
  else if (/\.html$/i.test(name)) bundleHTMLNames.push(name);
192
186
  for (const htmlFileName of bundleHTMLNames) {
193
187
  console.log("\n " + pc.underline(pc.cyan(distURL) + pc.greenBright(bundle[htmlFileName].fileName)));
194
- const htmlChunk = bundle[htmlFileName], oldHTML = htmlChunk.source, dom = new JSDOM(oldHTML), document = dom.window.document, thisDel = /* @__PURE__ */ new Set(), newJSCode = [], scriptElement = document.querySelector(`script[type=module]${assetsSrcSelector}`), scriptName = scriptElement ? cutPrefix(scriptElement.src, config.base) : "";
188
+ const htmlChunk = bundle[htmlFileName], oldHTML = htmlChunk.source, dom = new JSDOM(oldHTML), document = dom.window.document, thisDel = /* @__PURE__ */ new Set(), newJSCode = [], scriptElement = document.querySelector(`script[type=module]${assetsSrcSelector}`), scriptName = scriptElement ? cutPrefix(scriptElement.src, config.base) : "", compressHeadElements = [];
195
189
  let oldSize = Buffer.byteLength(oldHTML);
196
190
  if (scriptElement) {
197
191
  scriptElement.remove();
@@ -209,16 +203,18 @@ async function generateBundle(bundle, config, options) {
209
203
  if (cssSource) {
210
204
  oldSize += Buffer.byteLength(cssSource);
211
205
  for (const name of bundleAssetsNames) if (cssSource.includes(name.slice(assetsDir.length))) globalDoNotDelete.add(name);
212
- allCSS += cssSource.replace(/\s*(\/\*[^*]*\*\/)?\s*$/, "");
206
+ allCSS += cssSource.replace(/(\s*\/\*([^*]|\*(?!\/))*\*\/)*\s*$/, "");
213
207
  }
214
208
  if (options.enableCompress) element.remove();
215
209
  }
216
- if (allCSS) if (options.enableCompress) newJSCode.push(template.css(allCSS));
217
- else {
210
+ if (allCSS) {
218
211
  const e = document.createElement("style");
219
212
  e.innerHTML = allCSS;
220
- linkStylesheet[0].before(e);
221
- for (const e of linkStylesheet) e.remove();
213
+ if (options.enableCompress) compressHeadElements.push(e);
214
+ else {
215
+ linkStylesheet[0].before(e);
216
+ for (const e of linkStylesheet) e.remove();
217
+ }
222
218
  }
223
219
  const assetsDataURL = {};
224
220
  if (options.tryInlineHtmlAssets) for (const element of document.querySelectorAll(assetsSrcSelector)) {
@@ -243,44 +239,74 @@ async function generateBundle(bundle, config, options) {
243
239
  }
244
240
  if (options.enableCompress) element.src = `data:${name}`;
245
241
  }
246
- if (options.tryInlineHtmlPublicIcon) {
247
- let needInline = true;
248
- let iconName = "favicon.ico";
249
- const element = document.querySelector(`link[rel=icon][href^="${config.base}"], link[rel="shortcut icon"][href^="${config.base}"]`);
250
- if (element) {
251
- iconName = cutPrefix(element.href, config.base);
252
- if (bundleAssetsNames.includes(iconName)) needInline = false;
253
- else {
254
- element.rel = "icon";
255
- element.href = "data:";
242
+ const createIconElement = (href) => {
243
+ const e = document.createElement("link");
244
+ e.rel = "icon";
245
+ if (href != null) e.href = href;
246
+ return e;
247
+ };
248
+ const getPublicIcon = (faviconName) => {
249
+ if (Object.prototype.hasOwnProperty.call(globalPublicFilesCache, faviconName)) return globalPublicFilesCache[faviconName];
250
+ let _path = path.join(config.build.outDir, faviconName);
251
+ if (fs.existsSync(_path)) globalRemoveDistFileNames.add(faviconName);
252
+ else {
253
+ _path = path.join(config.publicDir, faviconName);
254
+ if (!fs.existsSync(_path)) return null;
255
+ }
256
+ const b = fs.readFileSync(_path);
257
+ return globalPublicFilesCache[faviconName] = {
258
+ buffer: b,
259
+ dataURL: bufferToDataURL(faviconName, b),
260
+ size: b.length
261
+ };
262
+ };
263
+ const linkFaviconAll = document.querySelectorAll(`link[rel=icon][href]:not([href=""]),link[rel="shortcut icon"][href]:not([href=""])`);
264
+ if (linkFaviconAll.length == 0) {
265
+ if (options.tryInlineHtmlPublicIcon) {
266
+ const fileCache = getPublicIcon("favicon.ico");
267
+ if (fileCache) {
268
+ oldSize += fileCache.size;
269
+ const e = createIconElement(fileCache.dataURL);
270
+ if (options.enableCompressInlinedIcon) compressHeadElements.push(e);
271
+ else document.head.appendChild(e);
256
272
  }
257
273
  }
258
- if (needInline) try {
259
- if (!Object.prototype.hasOwnProperty.call(globalPublicFilesCache, iconName)) {
260
- let Path = path.join(config.build.outDir, iconName);
261
- if (fs.existsSync(Path)) globalRemoveDistFileNames.add(iconName);
262
- else Path = path.join(config.publicDir, iconName);
263
- const b = fs.readFileSync(Path);
264
- globalPublicFilesCache[iconName] = {
265
- dataURL: bufferToDataURL(iconName, b),
266
- size: b.length
267
- };
274
+ } else for (const linkFavicon of linkFaviconAll) {
275
+ let faviconName = linkFavicon.href;
276
+ const faviconIsDataURL = /^data:/i.test(faviconName);
277
+ if (!faviconIsDataURL) faviconName = cutPrefix(faviconName, config.base);
278
+ const setFaviconDataURL = (dataURL) => {
279
+ if (options.enableCompressInlinedIcon) if (linkFavicon) {
280
+ linkFavicon.remove();
281
+ linkFavicon.href = dataURL;
282
+ compressHeadElements.push(linkFavicon);
283
+ } else compressHeadElements.push(createIconElement(dataURL));
284
+ else if (linkFavicon) linkFavicon.href = dataURL;
285
+ else document.head.appendChild(createIconElement(dataURL));
286
+ };
287
+ if (faviconIsDataURL) {
288
+ if (options.enableCompressInlinedIcon) {
289
+ linkFavicon.remove();
290
+ compressHeadElements.push(linkFavicon);
268
291
  }
269
- const { dataURL, size } = globalPublicFilesCache[iconName];
270
- oldSize += size;
271
- if (options.enableCompress) {
272
- newJSCode.push(template.icon(dataURL));
273
- if (!element) document.head.insertAdjacentHTML("beforeend", "<link rel=\"icon\" href=\"data:\">");
274
- } else if (element) element.href = dataURL;
275
- else {
276
- const e = document.head.appendChild(document.createElement("link"));
277
- e.rel = "icon";
278
- e.href = dataURL;
292
+ } else if (bundleAssetsNames.includes(faviconName)) {
293
+ const asset = bundle[faviconName];
294
+ if (asset) {
295
+ setFaviconDataURL(bufferToDataURL(faviconName, Buffer.from(asset.source)));
296
+ thisDel.add(faviconName);
297
+ }
298
+ } else if (options.tryInlineHtmlPublicIcon) {
299
+ const fileCache = getPublicIcon(faviconName);
300
+ if (fileCache) {
301
+ oldSize += fileCache.size;
302
+ setFaviconDataURL(fileCache.dataURL);
279
303
  }
280
- } catch (e) {
281
- if (element) console.error(e);
282
304
  }
283
305
  }
306
+ if (compressHeadElements.length) {
307
+ const html = compressHeadElements.map((v) => v.outerHTML).join("");
308
+ newJSCode.push(`document.head.insertAdjacentHTML("beforeend",${JSON.stringify(html)})`);
309
+ }
284
310
  htmlChunk.source = dom.serialize();
285
311
  if (options.htmlMinifierTerser) htmlChunk.source = await minify(htmlChunk.source, options.htmlMinifierTerser);
286
312
  function inlineHtmlAssets() {
@@ -293,13 +319,10 @@ async function generateBundle(bundle, config, options) {
293
319
  thisDel.add(scriptName);
294
320
  let { code } = bundle[scriptName];
295
321
  oldSize += Buffer.byteLength(code);
296
- code = code.replace(/;?\n?$/, "");
322
+ code = code.replace(/;?\s*$/, "");
297
323
  for (const name of bundleAssetsNames) {
298
324
  const assetName = name.slice(assetsDir.length);
299
- if (code.includes(assetName)) {
300
- globalDoNotDelete.add(name);
301
- delete assetsDataURL[assetName];
302
- }
325
+ if (code.includes(assetName)) globalDoNotDelete.add(name);
303
326
  }
304
327
  inlineHtmlAssets();
305
328
  if (options.useImportMetaPolyfill) newJSCode.push(template.importmeta(scriptName));
@@ -316,10 +339,15 @@ async function generateBundle(bundle, config, options) {
316
339
  if (options.removeInlinedAssetFiles) {
317
340
  for (const name of globalDelete) if (!globalDoNotDelete.has(name)) delete bundle[name];
318
341
  }
319
- if (options.removeInlinedPublicIconFiles) for (const name of globalRemoveDistFileNames) try {
320
- fs.rmSync(path.join(config.build.outDir, name), { force: true });
321
- } catch (e) {
322
- console.error(e);
342
+ if (options.removeInlinedPublicIconFiles) {
343
+ const { outDir } = config.build;
344
+ const mustStartsWith = path.resolve(outDir) + path.sep;
345
+ for (const name of globalRemoveDistFileNames) try {
346
+ const _path = path.resolve(outDir, name);
347
+ if (_path.startsWith(mustStartsWith)) fs.rmSync(_path, { force: true });
348
+ } catch (e) {
349
+ console.error(e);
350
+ }
323
351
  }
324
352
  console.log(pc.green("Finish.") + pc.reset("\n"));
325
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-singlefile-compression",
3
- "version": "2.3.3",
3
+ "version": "2.4.1",
4
4
  "author": "bddjr",
5
5
  "license": "MIT",
6
6
  "description": "Compress all assets and embeds them into dist/index.html, making it convenient to share as a single HTML file.",
@@ -14,7 +14,7 @@
14
14
  "build": "tsc && node build.js && tsc _dist/templateRaw.ts --ignoreConfig --noEmit && cd test && node --run build",
15
15
  "prepublishOnly": "node --run build"
16
16
  },
17
- "homepage": "https://bddjr.github.io/vite-plugin-singlefile-compression/",
17
+ "homepage": "https://bddjr.github.io/vite-plugin-singlefile-compression/#/",
18
18
  "repository": {
19
19
  "type": "git",
20
20
  "url": "git+https://github.com/bddjr/vite-plugin-singlefile-compression.git"
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "@types/html-minifier-terser": ">=7.0.2",
93
- "base128-ascii": ">=4.0.1",
93
+ "base128-ascii": ">=5.0.0",
94
94
  "html-minifier-terser": ">=7.2.0",
95
95
  "jsdom": ">=29.0.2",
96
96
  "mini-svg-data-uri": ">=1.4.4",
@@ -101,10 +101,10 @@
101
101
  "@bddjr/types-rollupoptions-4.43.0": "4.43.0",
102
102
  "@types/jsdom": ">=28.0.1",
103
103
  "@types/node": "^24.12.2",
104
- "rolldown": ">=1.0.0-rc.15",
104
+ "rolldown": ">=1.0.0-rc.17",
105
105
  "rolldown-plugin-dts": ">=0.23.2",
106
- "terser": ">=5.46.1",
107
- "typescript": ">=6.0.2",
108
- "vite": ">=8.0.8"
106
+ "terser": ">=5.46.2",
107
+ "typescript": ">=6.0.3",
108
+ "vite": ">=8.0.10"
109
109
  }
110
110
  }