vite-plugin-singlefile-compression 1.4.0 → 2.0.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
@@ -37,58 +37,66 @@ const router = createRouter({
37
37
 
38
38
  ## Options
39
39
 
40
- See [src/options.ts](src/options.ts)
40
+ Example:
41
+
42
+ ```ts
43
+ singleFileCompression({
44
+ rename: 'example.html'
45
+ }),
46
+ ```
47
+
48
+ More info see [src/options.ts](src/options.ts)
41
49
 
42
50
  ```ts
43
51
  export interface Options {
44
- /**
45
- * Rename index.html
46
- */
47
- rename?: string
48
-
49
- /**
50
- * https://github.com/terser/html-minifier-terser?tab=readme-ov-file#options-quick-reference
51
- * @default defaultHtmlMinifierTerserOptions
52
- */
53
- htmlMinifierTerser?: htmlMinifierOptions | boolean
54
-
55
- /**
56
- * Try inline html used assets, if inlined or not used in JS.
57
- * @default true
58
- */
59
- tryInlineHtmlAssets?: boolean
60
-
61
- /**
62
- * Remove inlined asset files.
63
- * @default true
64
- */
65
- removeInlinedAssetFiles?: boolean
66
-
67
- /**
68
- * Try inline html icon, if icon is in public dir.
69
- * @default true
70
- */
71
- tryInlineHtmlPublicIcon?: boolean
72
-
73
- /**
74
- * Remove inlined html icon files.
75
- * @default true
76
- */
77
- removeInlinedPublicIconFiles?: boolean
78
-
79
- /**
80
- * Use Base128 to encode gzipped script.
81
- * If false, use Base64.
82
- * https://www.npmjs.com/package/base128-ascii
83
- * @default true
84
- */
85
- useBase128?: boolean
86
-
87
- /**
88
- * Compress format.
89
- * @default "deflate-raw"
90
- */
91
- compressFormat?: compressFormat
52
+ /**
53
+ * Rename index.html
54
+ */
55
+ rename?: string;
56
+
57
+ /**
58
+ * https://github.com/terser/html-minifier-terser?tab=readme-ov-file#options-quick-reference
59
+ * @default defaultHtmlMinifierTerserOptions
60
+ */
61
+ htmlMinifierTerser?: htmlMinifierOptions | boolean;
62
+
63
+ /**
64
+ * Try inline html used assets, if inlined or not used in JS.
65
+ * @default true
66
+ */
67
+ tryInlineHtmlAssets?: boolean;
68
+
69
+ /**
70
+ * Remove inlined asset files.
71
+ * @default true
72
+ */
73
+ removeInlinedAssetFiles?: boolean;
74
+
75
+ /**
76
+ * Try inline html icon, if icon is in public dir.
77
+ * @default true
78
+ */
79
+ tryInlineHtmlPublicIcon?: boolean;
80
+
81
+ /**
82
+ * Remove inlined html icon files.
83
+ * @default true
84
+ */
85
+ removeInlinedPublicIconFiles?: boolean;
86
+
87
+ /**
88
+ * Use Base128 to encode gzipped script.
89
+ * If false, use Base64.
90
+ * https://www.npmjs.com/package/base128-ascii
91
+ * @default true
92
+ */
93
+ useBase128?: boolean;
94
+
95
+ /**
96
+ * Compress format.
97
+ * @default "deflate-raw"
98
+ */
99
+ compressFormat?: compressFormat;
92
100
  }
93
101
  ```
94
102
 
@@ -97,19 +105,19 @@ export interface Options {
97
105
  https://bddjr.github.io/vite-plugin-singlefile-compression/
98
106
 
99
107
  ```
100
- vite v6.0.7 building for production...
108
+ vite v6.0.11 building for production...
101
109
  ✓ 45 modules transformed.
102
110
  rendering chunks (1)...
103
111
 
104
- vite-plugin-singlefile-compression 1.4.0 building...
112
+ vite-plugin-singlefile-compression 2.0.0 building...
105
113
 
106
114
  file:///D:/bddjr/Desktop/code/js/vite-plugin-singlefile-compression/test/dist/index.html
107
- 101.56 KiB -> 46.32 KiB
115
+ 101.6 KiB -> 46.39 KiB
108
116
 
109
117
  Finish.
110
118
 
111
- dist/index.html 47.42 kB
112
- ✓ built in 698ms
119
+ dist/index.html 47.50 kB
120
+ ✓ built in 732ms
113
121
  ```
114
122
 
115
123
  ![](effect.jpg)
@@ -1,4 +1,5 @@
1
1
  export declare const template: {
2
2
  base(script: string, format: string, useBase128: boolean): string;
3
3
  assets(assetsJSON: string): string;
4
+ resolve: string;
4
5
  };
@@ -4,6 +4,7 @@ const files = {
4
4
  base64: fs.readFileSync(fileURLToPath(import.meta.resolve("./template/base64.js"))).toString(),
5
5
  base128: fs.readFileSync(fileURLToPath(import.meta.resolve("./template/base128.js"))).toString(),
6
6
  assets: fs.readFileSync(fileURLToPath(import.meta.resolve("./template/assets.js"))).toString().split('{"":""}', 2),
7
+ resolve: fs.readFileSync(fileURLToPath(import.meta.resolve("./template/resolve.js"))).toString(),
7
8
  };
8
9
  export const template = {
9
10
  base(script, format, useBase128) {
@@ -19,4 +20,5 @@ export const template = {
19
20
  assets(assetsJSON) {
20
21
  return files.assets.join(assetsJSON);
21
22
  },
23
+ resolve: files.resolve,
22
24
  };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import pc from "picocolors";
2
2
  import { minify as htmlMinify } from 'html-minifier-terser';
3
+ import { JSDOM } from 'jsdom';
3
4
  import path from 'path';
4
5
  import fs from 'fs';
5
6
  import { pathToFileURL } from "url";
@@ -23,7 +24,7 @@ export default singleFileCompression;
23
24
  function setConfig(config) {
24
25
  config.base = './';
25
26
  config.build ??= {};
26
- config.build.assetsDir = 'assets';
27
+ // config.build.assetsDir = 'assets'
27
28
  config.build.cssCodeSplit = false;
28
29
  config.build.assetsInlineLimit ??= () => true;
29
30
  config.build.chunkSizeWarningLimit ??= Infinity;
@@ -34,9 +35,9 @@ function setConfig(config) {
34
35
  config.build.rollupOptions.output ??= {};
35
36
  function setRollupOutput(output) {
36
37
  output.inlineDynamicImports = true;
37
- delete output.assetFileNames;
38
- delete output.chunkFileNames;
39
- delete output.entryFileNames;
38
+ // delete output.assetFileNames
39
+ // delete output.chunkFileNames
40
+ // delete output.entryFileNames
40
41
  }
41
42
  if (Array.isArray(config.build.rollupOptions.output)) {
42
43
  for (const output of config.build.rollupOptions.output) {
@@ -48,7 +49,7 @@ function setConfig(config) {
48
49
  }
49
50
  }
50
51
  async function generateBundle(bundle, config, options) {
51
- console.log(pc.reset('') + pc.cyan('\n\nvite-plugin-singlefile-compression ' + version) + pc.green(' building...'));
52
+ console.log(pc.reset('\n\n') + pc.cyan('vite-plugin-singlefile-compression ' + version) + pc.green(' building...'));
52
53
  // rename
53
54
  if (options.rename
54
55
  && options.rename !== "index.html"
@@ -59,6 +60,13 @@ async function generateBundle(bundle, config, options) {
59
60
  delete bundle["index.html"];
60
61
  }
61
62
  const distURL = pathToFileURL(config.build.outDir).href + '/';
63
+ /** "assets/" */
64
+ const assetsDir = path.posix.join(config.build.assetsDir, '/');
65
+ /** '[href^="./assets/"]' */
66
+ const assetsHrefSelector = `[href^="./${assetsDir}"]`;
67
+ /** '[src^="./assets/"]' */
68
+ const assetsSrcSelector = `[src^="./${assetsDir}"]`;
69
+ const fakeScript = `_vitePluginSinglefileCompression(${Date.now()})`;
62
70
  const globalDelete = new Set();
63
71
  const globalDoNotDelete = new Set();
64
72
  const globalRemoveDistFileNames = new Set();
@@ -69,7 +77,7 @@ async function generateBundle(bundle, config, options) {
69
77
  /** format: ["index.html"] */
70
78
  const bundleHTMLNames = [];
71
79
  for (const name of Object.keys(bundle)) {
72
- if (name.startsWith('assets/'))
80
+ if (name.startsWith(assetsDir))
73
81
  bundleAssetsNames.push(name);
74
82
  else if (name.endsWith('.html'))
75
83
  bundleHTMLNames.push(name);
@@ -77,69 +85,80 @@ async function generateBundle(bundle, config, options) {
77
85
  for (const htmlFileName of bundleHTMLNames) {
78
86
  // init
79
87
  const htmlChunk = bundle[htmlFileName];
80
- let newHtml = htmlChunk.source;
81
- let oldSize = newHtml.length;
88
+ const oldHTML = htmlChunk.source;
89
+ let oldSize = oldHTML.length;
90
+ const dom = new JSDOM(oldHTML);
91
+ const document = dom.window.document;
92
+ const scriptElement = document.querySelector(`script[type=module]${assetsSrcSelector}`);
93
+ if (!scriptElement) {
94
+ console.error(`Error: Can not find script tag from ${htmlFileName}`);
95
+ continue;
96
+ }
82
97
  const thisDel = new Set();
83
98
  // Fix async import
84
99
  const newJSCode = ["self.__VITE_PRELOAD__=void 0"];
85
- // remove html comments
86
- newHtml = newHtml.replaceAll(/<!--[\d\D]*?-->/g, '');
87
100
  // get css tag
88
- newHtml = newHtml.replace(/\s*<link rel="stylesheet"[^>]* href="\.\/(assets\/[^"]+)"[^>]*>/, (match, name) => {
89
- thisDel.add(name);
90
- const css = bundle[name];
91
- const cssSource = css.source;
92
- if (cssSource) {
93
- oldSize += cssSource.length;
94
- // do not delete not inlined asset
95
- for (const name of bundleAssetsNames) {
96
- if (cssSource.includes(name.slice('assets/'.length)))
97
- globalDoNotDelete.add(name);
101
+ {
102
+ const element = document.querySelector(`link[rel=stylesheet]${assetsHrefSelector}`);
103
+ if (element) {
104
+ const name = element.href.slice("./".length);
105
+ thisDel.add(name);
106
+ const css = bundle[name];
107
+ const cssSource = css.source;
108
+ if (cssSource) {
109
+ oldSize += cssSource.length;
110
+ // do not delete not inlined asset
111
+ for (const name of bundleAssetsNames) {
112
+ if (cssSource.includes(name.slice(assetsDir.length)))
113
+ globalDoNotDelete.add(name);
114
+ }
115
+ // add script for load css
116
+ newJSCode.push(
117
+ // 'document.querySelector("script[type=module]").insertAdjacentElement("afterend",document.createElement("style")).innerHTML='
118
+ 'document.head.appendChild(document.createElement("style")).innerHTML='
119
+ + JSON.stringify(cssSource.replace(/\n$/, '')));
98
120
  }
99
- // add script for load css
100
- newJSCode.push('document.head.appendChild(document.createElement("style")).innerHTML='
101
- + JSON.stringify(cssSource.replace(/\s+$/, '')));
121
+ // delete tag
122
+ element.remove();
102
123
  }
103
- // delete tag
104
- return '';
105
- });
124
+ }
106
125
  // inline html assets
107
126
  const assetsDataURL = {};
108
127
  if (options.tryInlineHtmlAssets) {
109
- newHtml = newHtml.replaceAll(/(?:[\s"])(?:src|href)="\.\/assets\/([^"]+)"/g, (match, name) => {
128
+ for (const element of document.querySelectorAll(assetsSrcSelector)) {
129
+ const name = element.src.slice(`./${assetsDir}`.length);
110
130
  if (name.endsWith('.js'))
111
- return match;
131
+ continue;
112
132
  if (!Object.hasOwn(assetsDataURL, name)) {
113
- const bundleName = "assets/" + name;
133
+ const bundleName = assetsDir + name;
114
134
  const a = bundle[bundleName];
115
135
  if (!a)
116
- return match;
136
+ continue;
117
137
  thisDel.add(bundleName);
118
138
  oldSize += a.source.length;
119
139
  if (!Object.hasOwn(globalAssetsDataURL, name))
120
140
  globalAssetsDataURL[name] = bufferToDataURL(name, Buffer.from(a.source));
121
141
  assetsDataURL[name] = globalAssetsDataURL[name];
122
142
  }
123
- return `="data:${name}"`;
124
- });
143
+ element.src = `data:${name}`;
144
+ }
125
145
  }
126
146
  // inline html icon
127
147
  if (options.tryInlineHtmlPublicIcon) {
128
148
  let needInline = true;
129
- let hasTag = false;
130
149
  let iconName = 'favicon.ico';
131
150
  // replace tag
132
- newHtml = newHtml.replace(/<link\s([^>]*\s)?rel="(?:shortcut )?icon"([^>]*\s)?href="\.\/([^"]+)"([^>]*)>/, (match, p1, p2, name, after) => {
133
- hasTag = true;
134
- iconName = name;
135
- if (bundleAssetsNames.includes(name)) {
151
+ const element = document.querySelector('link[rel=icon][href^="./"], link[rel="shortcut icon"][href^="./"]');
152
+ if (element) {
153
+ iconName = element.href.slice("./".length);
154
+ if (bundleAssetsNames.includes(iconName)) {
136
155
  needInline = false;
137
- return match;
138
156
  }
139
- p1 ||= '';
140
- p2 ||= '';
141
- return `<link ${p1}rel="icon"${p2}href="data:"${after}>`;
142
- });
157
+ else {
158
+ element.rel = 'icon';
159
+ element.href = 'data:';
160
+ }
161
+ }
143
162
  if (needInline) {
144
163
  // inline
145
164
  try {
@@ -163,31 +182,30 @@ async function generateBundle(bundle, config, options) {
163
182
  const { dataURL, size } = globalPublicFilesCache[iconName];
164
183
  oldSize += size;
165
184
  newJSCode.push('document.querySelector("link[rel=icon]").href=' + JSON.stringify(dataURL));
166
- if (!hasTag) {
185
+ if (!element) {
167
186
  // add link icon tag
168
- const l = '<link rel="icon" href="data:">';
169
- oldSize += l.length;
170
- newHtml = newHtml.replace(/(?=<script )/, l);
187
+ document.head.insertAdjacentHTML('beforeend', '<link rel="icon" href="data:">');
171
188
  }
172
189
  }
173
190
  catch (e) {
174
- if (hasTag)
191
+ if (element)
175
192
  console.error(e);
176
193
  }
177
194
  }
178
195
  }
179
196
  // script
180
- let ok = false;
181
- newHtml = newHtml.replace(/<script type="module"[^>]* src="\.\/(assets\/[^"]+)"[^>]*><\/script>/, (match, name) => {
182
- ok = true;
197
+ {
198
+ const name = scriptElement.src.slice("./".length);
183
199
  thisDel.add(name);
184
200
  const js = bundle[name];
185
201
  oldSize += js.code.length;
186
202
  // fix new URL
187
203
  newJSCode.push(`import.meta.url=new URL(${JSON.stringify(name)},location).href`);
204
+ // fix import.meta.resolve
205
+ newJSCode.push(template.resolve);
188
206
  // do not delete not inlined asset
189
207
  for (const name of bundleAssetsNames) {
190
- const assetName = name.slice('assets/'.length);
208
+ const assetName = name.slice(assetsDir.length);
191
209
  if (js.code.includes(assetName)) {
192
210
  globalDoNotDelete.add(name);
193
211
  delete assetsDataURL[assetName];
@@ -201,22 +219,22 @@ async function generateBundle(bundle, config, options) {
201
219
  }
202
220
  // add script
203
221
  newJSCode.push(js.code.replace(/;?\n?$/, ''));
204
- // fill fake script
205
- return '<script type="module">self.__vitePluginSinglefileCompression=1</script>';
206
- });
207
- if (!ok)
208
- continue;
222
+ }
223
+ // fill fake script
224
+ scriptElement.removeAttribute('src');
225
+ scriptElement.removeAttribute('crossorigin');
226
+ scriptElement.innerHTML = fakeScript;
227
+ // generate html
228
+ htmlChunk.source = dom.serialize();
209
229
  // minify html
210
230
  if (options.htmlMinifierTerser)
211
- newHtml = await htmlMinify(newHtml, options.htmlMinifierTerser);
231
+ htmlChunk.source = await htmlMinify(htmlChunk.source, options.htmlMinifierTerser);
212
232
  // fill script
213
- const compressedScript = compress(options.compressFormat, newJSCode.join(';'), options.useBase128);
214
- newHtml = newHtml.split('self.__vitePluginSinglefileCompression=1', 2).join(template.base(compressedScript, options.compressFormat, options.useBase128));
215
- // finish
216
- htmlChunk.source = newHtml;
233
+ htmlChunk.source = htmlChunk.source.split(fakeScript, 2).join(template.base(compress(options.compressFormat, newJSCode.join(';'), options.useBase128), options.compressFormat, options.useBase128));
234
+ // log
217
235
  console.log("\n"
218
236
  + " " + pc.underline(pc.cyan(distURL) + pc.greenBright(bundle[htmlFileName].fileName)) + '\n'
219
- + " " + pc.gray(KiB(oldSize) + " -> ") + pc.cyanBright(KiB(newHtml.length)) + '\n');
237
+ + " " + pc.gray(KiB(oldSize) + " -> ") + pc.cyanBright(KiB(htmlChunk.source.length)) + '\n');
220
238
  // delete assets
221
239
  for (const name of thisDel) {
222
240
  globalDelete.add(name);
@@ -0,0 +1 @@
1
+ {let t=import.meta.resolve;import.meta.resolve=e=>/^\.{0,2}\//.test(e)?new URL(e,import.meta.url).href:t(e)}
package/package.json CHANGED
@@ -1,14 +1,10 @@
1
1
  {
2
2
  "name": "vite-plugin-singlefile-compression",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
6
6
  "files": [
7
- "dist",
8
- "package.json",
9
- "package-lock.json",
10
- "LICENSE.txt",
11
- "README.md"
7
+ "dist"
12
8
  ],
13
9
  "type": "module",
14
10
  "scripts": {
@@ -40,16 +36,20 @@
40
36
  "css"
41
37
  ],
42
38
  "dependencies": {
43
- "@types/html-minifier-terser": "^7.0.2",
44
- "@types/node": "^22.9.3",
45
39
  "base128-ascii": "^2.1.0",
46
- "esbuild": "^0.24.0",
47
40
  "html-minifier-terser": "^7.2.0",
41
+ "jsdom": "^26.0.0",
48
42
  "mime": "^4.0.4",
49
43
  "mini-svg-data-uri": "^1.4.4",
50
44
  "picocolors": "^1.1.1",
51
45
  "rimraf": "^6.0.1",
52
- "typescript": "^5.7.2",
53
- "vite": "^6.0.7"
46
+ "vite": "^6.0.11",
47
+ "@types/html-minifier-terser": "^7.0.2",
48
+ "@types/node": "^22.10.7",
49
+ "@types/jsdom": "^21.1.7"
50
+ },
51
+ "devDependencies": {
52
+ "esbuild": "^0.24.0",
53
+ "typescript": "^5.7.2"
54
54
  }
55
55
  }