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 +32 -6
- package/dist/index.d.ts +21 -1
- package/dist/index.js +154 -48
- package/package.json +3 -1
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 |
|
|
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
|
-
|
|
107
|
+
101.43 KiB -> 52.35 KiB
|
|
82
108
|
|
|
83
109
|
Finish.
|
|
84
110
|
|
|
85
|
-
dist/index.html
|
|
86
|
-
✓ built in
|
|
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
|
|
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(
|
|
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(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
:
|
|
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
|
-
|
|
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
|
|
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,
|
|
54
|
-
console.log(pc.cyan('\n\nvite-plugin-singlefile-compression ') + pc.green('building...'));
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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(/[^/]
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
227
|
+
+ template.join(gzipToBase64(newJSCode.toString()))
|
|
136
228
|
+ '</script>';
|
|
137
229
|
});
|
|
138
230
|
if (!ok)
|
|
139
231
|
continue;
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
242
|
+
globalDelete.add(name);
|
|
150
243
|
}
|
|
151
244
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
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",
|