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 +30 -6
- package/dist/index.d.ts +21 -1
- package/dist/index.js +171 -55
- package/package.json +3 -1
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 |
|
|
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
|
-
|
|
105
|
+
101.43 KiB -> 52.35 KiB
|
|
82
106
|
|
|
83
107
|
Finish.
|
|
84
108
|
|
|
85
|
-
dist/index.html
|
|
86
|
-
✓ built in
|
|
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
|
|
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(
|
|
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,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,
|
|
54
|
-
console.log(pc.cyan('\n\nvite-plugin-singlefile-compression ') + pc.green('building...'));
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
//
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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(/[^/]
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
227
|
+
+ template.join(gzipToBase64(newJSCode.toString()))
|
|
127
228
|
+ '</script>';
|
|
128
229
|
});
|
|
129
230
|
if (!ok)
|
|
130
231
|
continue;
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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",
|