vite-plugin-singlefile-compression 1.4.1 → 2.0.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
@@ -109,15 +109,15 @@ vite v6.0.11 building for production...
109
109
  ✓ 45 modules transformed.
110
110
  rendering chunks (1)...
111
111
 
112
- vite-plugin-singlefile-compression 1.4.1 building...
112
+ vite-plugin-singlefile-compression 2.0.1 building...
113
113
 
114
114
  file:///D:/bddjr/Desktop/code/js/vite-plugin-singlefile-compression/test/dist/index.html
115
- 101.56 KiB -> 46.32 KiB
115
+ 101.6 KiB -> 46.39 KiB
116
116
 
117
117
  Finish.
118
118
 
119
- dist/index.html 47.42 kB
120
- ✓ built in 680ms
119
+ dist/index.html 47.50 kB
120
+ ✓ built in 732ms
121
121
  ```
122
122
 
123
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) {
@@ -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);
@@ -1 +1 @@
1
- {let e={"":""};for(let o in e){for(let r of document.querySelectorAll(`[src="data:${o}"]`))r.src=e[o];for(let r of document.querySelectorAll(`[href="data:${o}"]`))r.href=e[o]}}
1
+ {let e={"":""};for(let o in e)for(let s of document.querySelectorAll(`[src="data:${o}"]`))s.src=e[o]}
@@ -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.1",
3
+ "version": "2.0.1",
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.10.7",
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.11"
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
  }