vite-plugin-singlefile-compression 1.0.5 → 1.1.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
@@ -64,30 +64,56 @@ export interface Options {
64
64
  * https://github.com/terser/html-minifier-terser?tab=readme-ov-file#options-quick-reference
65
65
  * @default defaultHtmlMinifierTerserOptions
66
66
  */
67
- htmlMinifierTerser?: htmlMinifierOptions | true | false
67
+ htmlMinifierTerser?: htmlMinifierOptions | boolean
68
+
69
+ /**
70
+ * Try inline html used assets, if inlined or not used in JS.
71
+ * @default true
72
+ */
73
+ tryInlineHtmlAssets?: boolean
74
+
75
+ /**
76
+ * Remove inlined asset files.
77
+ * @default true
78
+ */
79
+ removeInlinedAssetFiles?: boolean
80
+
81
+ /**
82
+ * Try inline html icon, if icon is in public dir.
83
+ * @default true
84
+ */
85
+ tryInlineHtmlPublicIcon?: boolean
86
+
87
+ /**
88
+ * Remove inlined html icon files.
89
+ * @default true
90
+ */
91
+ removeInlinedPublicIconFiles?: boolean
68
92
  }
69
93
  ```
70
94
 
71
95
  ## Effect
72
96
 
97
+ https://bddjr.github.io/vite-plugin-singlefile-compression/
98
+
73
99
  ```
74
100
  vite v5.4.11 building for production...
75
101
  ✓ 45 modules transformed.
76
102
  rendering chunks (1)...
77
103
 
78
- vite-plugin-singlefile-compression building...
104
+ vite-plugin-singlefile-compression 1.1.0 building...
79
105
 
80
106
  file:///D:/bddjr/Desktop/code/js/vite-plugin-singlefile-compression/test/dist/index.html
81
- 97.52 KiB -> 50.98 KiB
107
+ 101.43 KiB -> 52.35 KiB
82
108
 
83
109
  Finish.
84
110
 
85
- dist/index.html 52.19 kB
86
- ✓ built in 685ms
111
+ dist/index.html 53.60 kB
112
+ ✓ built in 678ms
87
113
  ```
88
114
 
89
115
  ```html
90
- <!DOCTYPE html><meta charset=UTF-8><link rel=icon href=data:logo-_cUAdIX-.svg><meta name=viewport content="width=device-width,initial-scale=1"><title>Vite App</title><script type=module>fetch("data:application/gzip;base64,H4sI********hAEA").then(r=>r.blob()).then(b=>new Response(b.stream().pipeThrough(new DecompressionStream("gzip")),{headers:{"Content-Type":"text/javascript"}}).blob()).then(b=>import(b=URL.createObjectURL(b)).finally(()=>URL.revokeObjectURL(b)))</script><div id=app></div>
116
+ <!DOCTYPE html><meta charset=UTF-8><link rel=icon href=data:><meta name=viewport content="width=device-width,initial-scale=1"><title>Vite App</title><script type=module>fetch("data:application/gzip;base64,********").then(r=>r.blob()).then(b=>new Response(b.stream().pipeThrough(new DecompressionStream("gzip")),{headers:{"Content-Type":"text/javascript"}}).blob()).then(b=>import(b=URL.createObjectURL(b)).finally(()=>URL.revokeObjectURL(b)))</script><div id=app></div>
91
117
  ```
92
118
 
93
119
  ## Clone
package/dist/index.d.ts CHANGED
@@ -6,7 +6,27 @@ export interface Options {
6
6
  * @default defaultHtmlMinifierTerserOptions
7
7
  */
8
8
  htmlMinifierTerser?: htmlMinifierOptions | boolean;
9
+ /**
10
+ * Try inline html used assets, if inlined or not used in JS.
11
+ * @default true
12
+ */
13
+ tryInlineHtmlAssets?: boolean;
14
+ /**
15
+ * Remove inlined asset files.
16
+ * @default true
17
+ */
18
+ removeInlinedAssetFiles?: boolean;
19
+ /**
20
+ * Try inline html icon, if icon is in public dir.
21
+ * @default true
22
+ */
23
+ tryInlineHtmlPublicIcon?: boolean;
24
+ /**
25
+ * Remove inlined html icon files.
26
+ * @default true
27
+ */
28
+ removeInlinedPublicIconFiles?: boolean;
9
29
  }
10
30
  export declare const defaultHtmlMinifierTerserOptions: htmlMinifierOptions;
11
- export declare function singleFileCompression(options?: Options): PluginOption;
31
+ export declare function singleFileCompression(opt?: Options): PluginOption;
12
32
  export default singleFileCompression;
package/dist/index.js CHANGED
@@ -14,21 +14,43 @@ export const defaultHtmlMinifierTerserOptions = {
14
14
  removeRedundantAttributes: true,
15
15
  minifyJS: false,
16
16
  };
17
- export function singleFileCompression(options) {
18
- const htmlMinifierOptions = options?.htmlMinifierTerser == null || options.htmlMinifierTerser === true
19
- ? defaultHtmlMinifierTerserOptions
20
- : options.htmlMinifierTerser;
17
+ export function singleFileCompression(opt) {
18
+ opt ||= {};
19
+ const innerOpt = {
20
+ htmlMinifierTerser: opt.htmlMinifierTerser == null || opt.htmlMinifierTerser === true
21
+ ? defaultHtmlMinifierTerserOptions
22
+ : opt.htmlMinifierTerser,
23
+ tryInlineHtmlAssets: opt.tryInlineHtmlAssets == null
24
+ ? true
25
+ : opt.tryInlineHtmlAssets,
26
+ removeInlinedAssetFiles: opt.removeInlinedAssetFiles == null
27
+ ? true
28
+ : opt.removeInlinedAssetFiles,
29
+ tryInlineHtmlPublicIcon: opt.tryInlineHtmlPublicIcon == null
30
+ ? true
31
+ : opt.tryInlineHtmlPublicIcon,
32
+ removeInlinedPublicIconFiles: opt.removeInlinedPublicIconFiles == null
33
+ ? true
34
+ : opt.removeInlinedPublicIconFiles
35
+ };
36
+ let conf;
21
37
  return {
22
38
  name: "singleFileCompression",
23
39
  enforce: "post",
24
40
  config: setConfig,
25
- generateBundle: (_, bundle) => generateBundle(bundle, htmlMinifierOptions),
41
+ configResolved(c) { conf = c; },
42
+ generateBundle: (_, bundle) => generateBundle(bundle, conf, innerOpt),
26
43
  };
27
44
  }
28
45
  export default singleFileCompression;
29
- const template = fs.readFileSync(path.join(import.meta.dirname, "template.js")).toString();
30
- const templateAssets = fs.readFileSync(path.join(import.meta.dirname, "template-assets.js")).toString();
31
- const distURL = pathToFileURL(path.resolve("dist")) + "/";
46
+ const template = fs.readFileSync(path.join(import.meta.dirname, "template.js")).toString().split('{<script>}', 2);
47
+ const templateAssets = fs.readFileSync(path.join(import.meta.dirname, "template-assets.js")).toString().split('{"":""}', 2);
48
+ const { version } = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "../package.json")).toString());
49
+ function bufferToDataURL(name, b) {
50
+ return name.endsWith('.svg')
51
+ ? svgToTinyDataUri(b.toString())
52
+ : `data:${mime.getType(name)};base64,${b.toString('base64')}`;
53
+ }
32
54
  function gzipToBase64(buf) {
33
55
  return zlib.gzipSync(buf, {
34
56
  level: zlib.constants.Z_BEST_COMPRESSION,
@@ -50,10 +72,18 @@ function setConfig(config) {
50
72
  config.build.rollupOptions = {};
51
73
  config.build.rollupOptions.output = { inlineDynamicImports: true };
52
74
  }
53
- async function generateBundle(bundle, htmlMinifierOptions) {
54
- console.log(pc.cyan('\n\nvite-plugin-singlefile-compression ') + pc.green('building...'));
55
- const globalDel = new Set();
56
- const globalDoNotDel = new Set();
75
+ async function generateBundle(bundle, config, options) {
76
+ console.log(pc.cyan('\n\nvite-plugin-singlefile-compression ' + version) + pc.green(' building...'));
77
+ if (config.base !== './')
78
+ return console.error("error: config.base has been changed!");
79
+ if (config.build.assetsDir !== 'assets')
80
+ return console.error("error: config.build.assetsDir has been changed!");
81
+ const distURL = (u => u.endsWith('/') ? u : u + '/')(pathToFileURL(path.resolve(config.build.outDir)).href);
82
+ const globalDelete = new Set();
83
+ const globalDoNotDelete = new Set();
84
+ const globalRemoveDistFileNames = new Set();
85
+ const globalAssetsDataURL = {};
86
+ const globalPublicFilesCache = {};
57
87
  /** fotmat: ["assets/index-XXXXXXXX.js"] */
58
88
  const bundleAssetsNames = [];
59
89
  /** format: ["index.html"] */
@@ -70,8 +100,11 @@ async function generateBundle(bundle, htmlMinifierOptions) {
70
100
  let newHtml = htmlChunk.source;
71
101
  let oldSize = newHtml.length;
72
102
  const thisDel = new Set();
73
- // Fix async import, fix new URL
103
+ // Fix async import
74
104
  const newJSCode = ["self.__VITE_PRELOAD__=void 0"];
105
+ newJSCode.toString = () => newJSCode.join(';');
106
+ // remove html comments
107
+ newHtml = newHtml.replaceAll(/<!--[\d\D]*?-->/g, '');
75
108
  // get css tag
76
109
  newHtml = newHtml.replace(/\s*<link rel="stylesheet"[^>]* href="\.\/(assets\/[^"]+)"[^>]*>/, (match, name) => {
77
110
  thisDel.add(name);
@@ -82,7 +115,7 @@ async function generateBundle(bundle, htmlMinifierOptions) {
82
115
  // do not delete not inlined asset
83
116
  for (const name of bundleAssetsNames) {
84
117
  if (cssSource.includes(name.slice('assets/'.length)))
85
- globalDoNotDel.add(name);
118
+ globalDoNotDelete.add(name);
86
119
  }
87
120
  // add script for load css
88
121
  newJSCode.push('document.head.appendChild(document.createElement("style")).innerHTML='
@@ -91,30 +124,80 @@ async function generateBundle(bundle, htmlMinifierOptions) {
91
124
  // delete tag
92
125
  return '';
93
126
  });
94
- // get html assets
95
- const assets = {};
96
- newHtml = newHtml.replace(/(?<=[\s"])(src|href)="\.\/assets\/([^"]+)"/g, (match, attrName, name) => {
97
- if (name.endsWith('.js'))
98
- return match;
99
- if (!Object.hasOwn(assets, name)) {
100
- const bundleName = "assets/" + name;
101
- const a = bundle[bundleName];
102
- if (!a)
127
+ // inline html assets
128
+ const assetsDataURL = {};
129
+ if (options.tryInlineHtmlAssets) {
130
+ newHtml = newHtml.replaceAll(/(?:[\s"])(?:src|href)="\.\/assets\/([^"]+)"/g, (match, name) => {
131
+ if (name.endsWith('.js'))
132
+ return match;
133
+ if (!Object.hasOwn(assetsDataURL, name)) {
134
+ const bundleName = "assets/" + name;
135
+ const a = bundle[bundleName];
136
+ if (!a)
137
+ return match;
138
+ thisDel.add(bundleName);
139
+ oldSize += a.source.length;
140
+ if (!Object.hasOwn(globalAssetsDataURL, name))
141
+ globalAssetsDataURL[name] = bufferToDataURL(name, Buffer.from(a.source));
142
+ assetsDataURL[name] = globalAssetsDataURL[name];
143
+ }
144
+ return `="data:${name}"`;
145
+ });
146
+ }
147
+ // inline html icon
148
+ if (options.tryInlineHtmlPublicIcon) {
149
+ let needInline = true;
150
+ let hasTag = false;
151
+ let iconName = 'favicon.ico';
152
+ // replace tag
153
+ newHtml = newHtml.replace(/<link\s([^>]*\s)?rel="(?:shortcut )?icon"([^>]*\s)?href="\.\/([^"]+)"([^>]*)>/, (match, p1, p2, name, after) => {
154
+ hasTag = true;
155
+ iconName = name;
156
+ if (bundleAssetsNames.includes(name)) {
157
+ needInline = false;
103
158
  return match;
104
- thisDel.add(bundleName);
105
- const b = Buffer.from(a.source);
106
- oldSize += b.length;
107
- assets[name] =
108
- name.endsWith('.svg')
109
- ? svgToTinyDataUri(b.toString())
110
- : `data:${mime.getType(a.fileName)};base64,${b.toString('base64')}`;
159
+ }
160
+ p1 ||= '';
161
+ p2 ||= '';
162
+ return `<link ${p1}rel="icon"${p2}href="data:"${after}>`;
163
+ });
164
+ if (needInline) {
165
+ // inline
166
+ try {
167
+ if (!Object.hasOwn(globalPublicFilesCache, iconName)) {
168
+ // dist/favicon.ico
169
+ let Path = path.join(config.build.outDir, iconName);
170
+ if (fs.existsSync(Path)) {
171
+ globalRemoveDistFileNames.add(iconName);
172
+ }
173
+ else {
174
+ // public/favicon.ico
175
+ Path = path.join(config.publicDir, iconName);
176
+ }
177
+ // read
178
+ const b = fs.readFileSync(Path);
179
+ globalPublicFilesCache[iconName] = {
180
+ dataURL: bufferToDataURL(iconName, b),
181
+ size: b.length
182
+ };
183
+ }
184
+ const { dataURL, size } = globalPublicFilesCache[iconName];
185
+ oldSize += size;
186
+ newJSCode.push('document.querySelector("link[rel=icon]").href=' + JSON.stringify(dataURL));
187
+ if (!hasTag) {
188
+ // add link icon tag
189
+ const l = '<link rel="icon" href="data:">';
190
+ oldSize += l.length;
191
+ newHtml = newHtml.replace(/(?=<script )/, l);
192
+ }
193
+ }
194
+ catch (e) {
195
+ if (hasTag)
196
+ console.error(e);
197
+ }
111
198
  }
112
- return `${attrName}="data:${name}"`;
113
- });
114
- // add script for load html assets
115
- const assetsJSON = JSON.stringify(assets);
116
- if (assetsJSON != '{}')
117
- newJSCode.push(templateAssets.replace('{"":""}', assetsJSON));
199
+ }
200
+ // script
118
201
  let ok = false;
119
202
  newHtml = newHtml.replace(/<script type="module"[^>]* src="\.\/(assets\/[^"]+)"[^>]*><\/script>/, (match, name) => {
120
203
  ok = true;
@@ -122,23 +205,33 @@ async function generateBundle(bundle, htmlMinifierOptions) {
122
205
  const js = bundle[name];
123
206
  oldSize += js.code.length;
124
207
  // fix new URL
125
- newJSCode.push(`import.meta.url=location.origin+location.pathname.replace(/[^/]*$/,"${name}")`);
208
+ newJSCode.push(`import.meta.url=location.origin+location.pathname.replace(/[^/]*$/,${JSON.stringify(name)})`);
126
209
  // do not delete not inlined asset
127
210
  for (const name of bundleAssetsNames) {
128
- if (js.code.includes(name.slice('assets/'.length)))
129
- globalDoNotDel.add(name);
211
+ const assetName = name.slice('assets/'.length);
212
+ if (js.code.includes(assetName)) {
213
+ globalDoNotDelete.add(name);
214
+ delete assetsDataURL[assetName];
215
+ }
216
+ }
217
+ if (options.tryInlineHtmlAssets) {
218
+ // add script for load html assets
219
+ const assetsJSON = JSON.stringify(assetsDataURL);
220
+ if (assetsJSON != '{}')
221
+ newJSCode.push(templateAssets.join(assetsJSON));
130
222
  }
131
223
  // add script
132
224
  newJSCode.push(js.code.replace(/;?\n?$/, ''));
133
225
  // gzip
134
226
  return '<script type="module">'
135
- + template.replace('{<script>}', gzipToBase64(newJSCode.join(';')))
227
+ + template.join(gzipToBase64(newJSCode.toString()))
136
228
  + '</script>';
137
229
  });
138
230
  if (!ok)
139
231
  continue;
140
- if (htmlMinifierOptions)
141
- newHtml = await htmlMinify(newHtml, htmlMinifierOptions);
232
+ // minify html
233
+ if (options.htmlMinifierTerser)
234
+ newHtml = await htmlMinify(newHtml, options.htmlMinifierTerser);
142
235
  // finish
143
236
  htmlChunk.source = newHtml;
144
237
  console.log("\n"
@@ -146,14 +239,27 @@ async function generateBundle(bundle, htmlMinifierOptions) {
146
239
  + " " + pc.gray(KiB(oldSize) + " -> ") + pc.cyanBright(KiB(newHtml.length)) + '\n');
147
240
  // delete assets
148
241
  for (const name of thisDel) {
149
- globalDel.add(name);
242
+ globalDelete.add(name);
150
243
  }
151
244
  }
152
- // delete inlined assets
153
- for (const name of globalDel) {
154
- // do not delete not inlined asset
155
- if (!globalDoNotDel.has(name))
156
- delete bundle[name];
245
+ if (options.removeInlinedAssetFiles) {
246
+ // delete inlined assets
247
+ for (const name of globalDelete) {
248
+ // do not delete not inlined asset
249
+ if (!globalDoNotDelete.has(name))
250
+ delete bundle[name];
251
+ }
252
+ }
253
+ if (options.removeInlinedPublicIconFiles) {
254
+ // delete inlined public files
255
+ for (const name of globalRemoveDistFileNames) {
256
+ try {
257
+ fs.unlinkSync(path.join(config.build.outDir, name));
258
+ }
259
+ catch (e) {
260
+ console.error(e);
261
+ }
262
+ }
157
263
  }
158
264
  console.log(pc.green('Finish.\n'));
159
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-singlefile-compression",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
6
6
  "files": [
@@ -23,7 +23,9 @@
23
23
  "url": "https://github.com/bddjr/vite-plugin-singlefile-compression"
24
24
  },
25
25
  "keywords": [
26
+ "vite-plugin",
26
27
  "vite",
28
+ "plugin",
27
29
  "SFA",
28
30
  "single-file",
29
31
  "singlefile",