vite-plugin-singlefile-compression 1.0.4 → 1.1.0

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,7 +64,31 @@ 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
 
@@ -75,19 +99,19 @@ vite v5.4.11 building for production...
75
99
  ✓ 45 modules transformed.
76
100
  rendering chunks (1)...
77
101
 
78
- vite-plugin-singlefile-compression building...
102
+ vite-plugin-singlefile-compression 1.1.0 building...
79
103
 
80
104
  file:///D:/bddjr/Desktop/code/js/vite-plugin-singlefile-compression/test/dist/index.html
81
- 97.52 KiB -> 50.98 KiB
105
+ 101.43 KiB -> 52.35 KiB
82
106
 
83
107
  Finish.
84
108
 
85
- dist/index.html 52.19 kB
86
- ✓ built in 685ms
109
+ dist/index.html 53.60 kB
110
+ ✓ built in 678ms
87
111
  ```
88
112
 
89
113
  ```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>
114
+ <!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
115
  ```
92
116
 
93
117
  ## 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,25 +72,39 @@ 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();
57
- const bundleNames = Object.keys(bundle);
58
- for (const htmlFileName of bundleNames) {
59
- // key format:
60
- // index.html
61
- // assets/index-ZZZZZZZZ.js
62
- // skip other file
63
- if (!htmlFileName.endsWith('.html'))
64
- continue;
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 = {};
87
+ /** fotmat: ["assets/index-XXXXXXXX.js"] */
88
+ const bundleAssetsNames = [];
89
+ /** format: ["index.html"] */
90
+ const bundleHTMLNames = [];
91
+ for (const name of Object.keys(bundle)) {
92
+ if (name.startsWith('assets/'))
93
+ bundleAssetsNames.push(name);
94
+ else if (name.endsWith('.html'))
95
+ bundleHTMLNames.push(name);
96
+ }
97
+ for (const htmlFileName of bundleHTMLNames) {
65
98
  // init
66
99
  const htmlChunk = bundle[htmlFileName];
67
100
  let newHtml = htmlChunk.source;
68
101
  let oldSize = newHtml.length;
69
102
  const thisDel = new Set();
70
- // Fix async import, fix new URL
103
+ // Fix async import
71
104
  const newJSCode = ["self.__VITE_PRELOAD__=void 0"];
105
+ newJSCode.toString = () => newJSCode.join(';');
106
+ // remove html comments
107
+ newHtml = newHtml.replaceAll(/<!--[\d\D]*?-->/g, '');
72
108
  // get css tag
73
109
  newHtml = newHtml.replace(/\s*<link rel="stylesheet"[^>]* href="\.\/(assets\/[^"]+)"[^>]*>/, (match, name) => {
74
110
  thisDel.add(name);
@@ -76,6 +112,11 @@ async function generateBundle(bundle, htmlMinifierOptions) {
76
112
  const cssSource = css.source;
77
113
  if (cssSource) {
78
114
  oldSize += cssSource.length;
115
+ // do not delete not inlined asset
116
+ for (const name of bundleAssetsNames) {
117
+ if (cssSource.includes(name.slice('assets/'.length)))
118
+ globalDoNotDelete.add(name);
119
+ }
79
120
  // add script for load css
80
121
  newJSCode.push('document.head.appendChild(document.createElement("style")).innerHTML='
81
122
  + JSON.stringify(cssSource.replace(/\s+$/, '')));
@@ -83,30 +124,80 @@ async function generateBundle(bundle, htmlMinifierOptions) {
83
124
  // delete tag
84
125
  return '';
85
126
  });
86
- // get html assets
87
- const assets = {};
88
- newHtml = newHtml.replace(/(?<=[\s"])(src|href)="\.\/assets\/([^"]+)"/g, (match, attrName, name) => {
89
- if (name.endsWith('.js'))
90
- return match;
91
- if (!Object.hasOwn(assets, name)) {
92
- const bundleName = "assets/" + name;
93
- const a = bundle[bundleName];
94
- 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'))
95
132
  return match;
96
- thisDel.add(bundleName);
97
- const b = Buffer.from(a.source);
98
- oldSize += b.length;
99
- assets[name] =
100
- name.endsWith('.svg')
101
- ? svgToTinyDataUri(b.toString())
102
- : `data:${mime.getType(a.fileName)};base64,${b.toString('base64')}`;
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;
158
+ return match;
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
+ }
103
198
  }
104
- return `${attrName}="data:${name}"`;
105
- });
106
- // add script for load html assets
107
- const assetsJSON = JSON.stringify(assets);
108
- if (assetsJSON != '{}')
109
- newJSCode.push(templateAssets.replace('{"":""}', assetsJSON));
199
+ }
200
+ // script
110
201
  let ok = false;
111
202
  newHtml = newHtml.replace(/<script type="module"[^>]* src="\.\/(assets\/[^"]+)"[^>]*><\/script>/, (match, name) => {
112
203
  ok = true;
@@ -114,22 +205,33 @@ async function generateBundle(bundle, htmlMinifierOptions) {
114
205
  const js = bundle[name];
115
206
  oldSize += js.code.length;
116
207
  // fix new URL
117
- newJSCode.push(`import.meta.url=location.origin+location.pathname.replace(/[^/]*$/,"${name}")`);
118
- for (const name of bundleNames) {
119
- if (name.startsWith('assets/') && js.code.includes(name.slice('assets/'.length)))
120
- globalDoNotDel.add(name);
208
+ newJSCode.push(`import.meta.url=location.origin+location.pathname.replace(/[^/]*$/,${JSON.stringify(name)})`);
209
+ // do not delete not inlined asset
210
+ for (const name of bundleAssetsNames) {
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));
121
222
  }
122
223
  // add script
123
224
  newJSCode.push(js.code.replace(/;?\n?$/, ''));
124
225
  // gzip
125
226
  return '<script type="module">'
126
- + template.replace('{<script>}', gzipToBase64(newJSCode.join(';')))
227
+ + template.join(gzipToBase64(newJSCode.toString()))
127
228
  + '</script>';
128
229
  });
129
230
  if (!ok)
130
231
  continue;
131
- if (htmlMinifierOptions)
132
- newHtml = await htmlMinify(newHtml, htmlMinifierOptions);
232
+ // minify html
233
+ if (options.htmlMinifierTerser)
234
+ newHtml = await htmlMinify(newHtml, options.htmlMinifierTerser);
133
235
  // finish
134
236
  htmlChunk.source = newHtml;
135
237
  console.log("\n"
@@ -137,13 +239,27 @@ async function generateBundle(bundle, htmlMinifierOptions) {
137
239
  + " " + pc.gray(KiB(oldSize) + " -> ") + pc.cyanBright(KiB(newHtml.length)) + '\n');
138
240
  // delete assets
139
241
  for (const name of thisDel) {
140
- globalDel.add(name);
242
+ globalDelete.add(name);
243
+ }
244
+ }
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];
141
251
  }
142
252
  }
143
- // delete inlined assets
144
- for (const name of globalDel) {
145
- if (!globalDoNotDel.has(name))
146
- delete bundle[name];
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
+ }
147
263
  }
148
264
  console.log(pc.green('Finish.\n'));
149
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-singlefile-compression",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
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",