portapack 0.3.1 → 0.3.3

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.
Files changed (74) hide show
  1. package/.eslintrc.json +67 -8
  2. package/.releaserc.js +25 -27
  3. package/CHANGELOG.md +14 -22
  4. package/LICENSE.md +21 -0
  5. package/README.md +22 -53
  6. package/commitlint.config.js +30 -34
  7. package/dist/cli/cli-entry.cjs +183 -98
  8. package/dist/cli/cli-entry.cjs.map +1 -1
  9. package/dist/index.d.ts +0 -3
  10. package/dist/index.js +178 -97
  11. package/dist/index.js.map +1 -1
  12. package/docs/.vitepress/config.ts +38 -33
  13. package/docs/.vitepress/sidebar-generator.ts +89 -38
  14. package/docs/architecture.md +186 -0
  15. package/docs/cli.md +23 -23
  16. package/docs/code-of-conduct.md +7 -1
  17. package/docs/configuration.md +12 -11
  18. package/docs/contributing.md +6 -2
  19. package/docs/deployment.md +10 -5
  20. package/docs/development.md +8 -5
  21. package/docs/getting-started.md +13 -13
  22. package/docs/index.md +1 -1
  23. package/docs/public/android-chrome-192x192.png +0 -0
  24. package/docs/public/android-chrome-512x512.png +0 -0
  25. package/docs/public/apple-touch-icon.png +0 -0
  26. package/docs/public/favicon-16x16.png +0 -0
  27. package/docs/public/favicon-32x32.png +0 -0
  28. package/docs/public/favicon.ico +0 -0
  29. package/docs/roadmap.md +233 -0
  30. package/docs/site.webmanifest +1 -0
  31. package/docs/troubleshooting.md +12 -1
  32. package/examples/main.ts +5 -30
  33. package/examples/sample-project/script.js +1 -1
  34. package/jest.config.ts +8 -13
  35. package/nodemon.json +5 -10
  36. package/package.json +2 -5
  37. package/src/cli/cli-entry.ts +2 -2
  38. package/src/cli/cli.ts +21 -16
  39. package/src/cli/options.ts +127 -113
  40. package/src/core/bundler.ts +253 -222
  41. package/src/core/extractor.ts +632 -565
  42. package/src/core/minifier.ts +173 -162
  43. package/src/core/packer.ts +141 -137
  44. package/src/core/parser.ts +74 -73
  45. package/src/core/web-fetcher.ts +270 -258
  46. package/src/index.ts +18 -17
  47. package/src/types.ts +9 -11
  48. package/src/utils/font.ts +12 -6
  49. package/src/utils/logger.ts +110 -105
  50. package/src/utils/meta.ts +75 -76
  51. package/src/utils/mime.ts +50 -50
  52. package/src/utils/slugify.ts +33 -34
  53. package/tests/unit/cli/cli-entry.test.ts +72 -70
  54. package/tests/unit/cli/cli.test.ts +314 -278
  55. package/tests/unit/cli/options.test.ts +294 -301
  56. package/tests/unit/core/bundler.test.ts +426 -329
  57. package/tests/unit/core/extractor.test.ts +793 -549
  58. package/tests/unit/core/minifier.test.ts +374 -274
  59. package/tests/unit/core/packer.test.ts +298 -264
  60. package/tests/unit/core/parser.test.ts +538 -150
  61. package/tests/unit/core/web-fetcher.test.ts +389 -359
  62. package/tests/unit/index.test.ts +238 -197
  63. package/tests/unit/utils/font.test.ts +26 -21
  64. package/tests/unit/utils/logger.test.ts +267 -260
  65. package/tests/unit/utils/meta.test.ts +29 -28
  66. package/tests/unit/utils/mime.test.ts +73 -74
  67. package/tests/unit/utils/slugify.test.ts +14 -12
  68. package/tsconfig.build.json +9 -10
  69. package/tsconfig.jest.json +1 -1
  70. package/tsconfig.json +2 -2
  71. package/tsup.config.ts +8 -9
  72. package/typedoc.json +5 -9
  73. /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
  74. /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
@@ -17,307 +17,407 @@ import { jest, describe, it, expect, beforeEach } from '@jest/globals';
17
17
  // =================== MOCK SETUP ===================
18
18
 
19
19
  // --- Define TOP-LEVEL mock functions WITH EXPLICIT TYPES ---
20
- const mockHtmlMinifierMinifyFn = jest.fn<(text: string, options?: HtmlMinifyOptions) => Promise<string>>();
21
- const mockCleanCSSInstanceMinifyFn = jest.fn<(source: string | Record<string, string> | Array<string | Record<string, string>>) => CleanCSSOutput>();
22
- const mockCleanCSSConstructorFn = jest.fn<() => ({ minify: typeof mockCleanCSSInstanceMinifyFn})>() // Type the constructor mock
23
- .mockImplementation(() => ({ // Provide implementation immediately
24
- minify: mockCleanCSSInstanceMinifyFn
25
- }));
26
- const mockTerserMinifyFn = jest.fn<(code: string | Record<string, string> | string[], options?: MinifyOptions) => Promise<MinifyOutput>>();
27
-
20
+ const mockHtmlMinifierMinifyFn =
21
+ jest.fn<(text: string, options?: HtmlMinifyOptions) => Promise<string>>();
22
+ const mockCleanCSSInstanceMinifyFn =
23
+ jest.fn<
24
+ (
25
+ source: string | Record<string, string> | Array<string | Record<string, string>>
26
+ ) => CleanCSSOutput
27
+ >();
28
+ const mockCleanCSSConstructorFn = jest
29
+ .fn<() => { minify: typeof mockCleanCSSInstanceMinifyFn }>() // Type the constructor mock
30
+ .mockImplementation(() => ({
31
+ // Provide implementation immediately
32
+ minify: mockCleanCSSInstanceMinifyFn,
33
+ }));
34
+ const mockTerserMinifyFn =
35
+ jest.fn<
36
+ (
37
+ code: string | Record<string, string> | string[],
38
+ options?: MinifyOptions
39
+ ) => Promise<MinifyOutput>
40
+ >();
28
41
 
29
42
  // --- Mock the dependencies using standard jest.mock and factories ---
30
43
  jest.mock('html-minifier-terser', () => ({
31
- __esModule: true,
32
- minify: mockHtmlMinifierMinifyFn,
44
+ __esModule: true,
45
+ minify: mockHtmlMinifierMinifyFn,
33
46
  }));
34
47
  jest.mock('clean-css', () => ({
35
- __esModule: true,
36
- // Mock the default export which is the class constructor
37
- default: mockCleanCSSConstructorFn,
48
+ __esModule: true,
49
+ // Mock the default export which is the class constructor
50
+ default: mockCleanCSSConstructorFn,
38
51
  }));
39
52
  jest.mock('terser', () => ({
40
- __esModule: true,
41
- minify: mockTerserMinifyFn,
53
+ __esModule: true,
54
+ minify: mockTerserMinifyFn,
42
55
  }));
43
56
  // ====================================================
44
57
 
45
58
  // Import the module under test *after* mocks are set up
46
59
  import { minifyAssets } from '../../../src/core/minifier';
47
60
 
48
-
49
61
  // Helper function (keep as is)
50
62
  const simpleMockCssMinify = (css: string): string => {
51
- return css.replace(/\/\*.*?\*\//g, '').replace(/\s*([{}:;,])\s*/g, '$1').replace(/\s+/g, ' ').replace(/;}/g, '}').trim();
52
- }
63
+ return css
64
+ .replace(/\/\*.*?\*\//g, '')
65
+ .replace(/\s*([{}:;,])\s*/g, '$1')
66
+ .replace(/\s+/g, ' ')
67
+ .replace(/;}/g, '}')
68
+ .trim();
69
+ };
53
70
 
54
71
  describe('🧼 Minifier', () => {
55
- let mockLogger: Logger;
56
- let mockLoggerWarnFn: jest.SpiedFunction<typeof Logger.prototype.warn>;
57
- let mockLoggerDebugFn: jest.SpiedFunction<typeof Logger.prototype.debug>;
58
-
59
- const sampleHtmlContent = '<html> <head> <title> Test </title> </head> <body> Test Content </body> </html>';
60
- const minifiedHtmlContent = '<html><head><title>Test</title></head><body>Test Content</body></html>';
61
- const sampleCssContent = ' body { color: blue; /* comment */ } ';
62
- const minifiedCssContent = 'body{color:blue}';
63
- const sampleJsContent = ' function hello ( name ) { console.log("hello", name ); alert ( 1 ) ; } ';
64
- const minifiedJsContent = 'function hello(o){console.log("hello",o),alert(1)}';
65
-
66
- const sampleParsedInput: ParsedHTML = {
72
+ let mockLogger: Logger;
73
+ let mockLoggerWarnFn: jest.SpiedFunction<typeof Logger.prototype.warn>;
74
+ let mockLoggerDebugFn: jest.SpiedFunction<typeof Logger.prototype.debug>;
75
+
76
+ const sampleHtmlContent =
77
+ '<html> <head> <title> Test </title> </head> <body> Test Content </body> </html>';
78
+ const minifiedHtmlContent =
79
+ '<html><head><title>Test</title></head><body>Test Content</body></html>';
80
+ const sampleCssContent = ' body { color: blue; /* comment */ } ';
81
+ const minifiedCssContent = 'body{color:blue}';
82
+ const sampleJsContent =
83
+ ' function hello ( name ) { console.log("hello", name ); alert ( 1 ) ; } ';
84
+ const minifiedJsContent = 'function hello(o){console.log("hello",o),alert(1)}';
85
+
86
+ const sampleParsedInput: ParsedHTML = {
87
+ htmlContent: sampleHtmlContent,
88
+ assets: [
89
+ { type: 'css', url: 'style.css', content: sampleCssContent },
90
+ { type: 'js', url: 'script.js', content: sampleJsContent },
91
+ { type: 'image', url: 'logo.png', content: 'data:image/png;base64,abc' },
92
+ ],
93
+ };
94
+
95
+ beforeEach(() => {
96
+ jest.clearAllMocks(); // Clear mocks between tests
97
+
98
+ // Set up logger spies
99
+ mockLogger = new Logger(LogLevel.WARN);
100
+ mockLoggerWarnFn = jest.spyOn(mockLogger, 'warn');
101
+ mockLoggerDebugFn = jest.spyOn(mockLogger, 'debug');
102
+
103
+ // --- Configure Mock Implementations using the *typed* mocks ---
104
+ // These should now type-check correctly
105
+ mockHtmlMinifierMinifyFn.mockImplementation(async (_html, _options) => minifiedHtmlContent);
106
+
107
+ mockCleanCSSInstanceMinifyFn.mockImplementation((css): CleanCSSOutput => {
108
+ if (typeof css !== 'string') css = '// Mocked non-string CSS input'; // Handle non-string input for mock
109
+ const minifiedStyles = simpleMockCssMinify(css);
110
+ const stats = {
111
+ originalSize: css.length,
112
+ minifiedSize: minifiedStyles.length,
113
+ efficiency: css.length > 0 ? (css.length - minifiedStyles.length) / css.length : 0,
114
+ timeSpent: 1,
115
+ };
116
+ return { styles: minifiedStyles, errors: [], warnings: [], stats: stats };
117
+ });
118
+
119
+ mockTerserMinifyFn.mockImplementation(async (_js, _options) => {
120
+ return Promise.resolve({ code: minifiedJsContent, error: undefined });
121
+ });
122
+ });
123
+
124
+ // --- Tests (Assertions use the explicitly typed mock functions) ---
125
+ describe('Basic functionality', () => {
126
+ it('✅ leaves content unchanged when minification is disabled', async () => {
127
+ const options: BundleOptions = { minifyHtml: false, minifyCss: false, minifyJs: false };
128
+ const result = await minifyAssets(sampleParsedInput, options, mockLogger);
129
+
130
+ expect(mockHtmlMinifierMinifyFn).not.toHaveBeenCalled();
131
+ expect(mockCleanCSSConstructorFn).not.toHaveBeenCalled(); // Check constructor
132
+ expect(mockCleanCSSInstanceMinifyFn).not.toHaveBeenCalled(); // Check instance method
133
+ expect(mockTerserMinifyFn).not.toHaveBeenCalled();
134
+
135
+ expect(result.htmlContent).toBe(sampleHtmlContent);
136
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
137
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
138
+ });
139
+
140
+ it('🔧 minifies HTML, CSS, and JS with all options enabled (default)', async () => {
141
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
142
+
143
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
144
+ expect(mockCleanCSSConstructorFn).toHaveBeenCalledTimes(1); // Constructor should be called once
145
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
146
+ expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
147
+
148
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
149
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
150
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
151
+ });
152
+ });
153
+
154
+ describe('Error handling', () => {
155
+ it('💥 handles broken HTML minification gracefully', async () => {
156
+ const htmlError = new Error('HTML parse error!');
157
+ // Use mockImplementationOnce to override for this test
158
+ mockHtmlMinifierMinifyFn.mockImplementationOnce(async () => {
159
+ throw htmlError;
160
+ });
161
+
162
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
163
+
164
+ expect(result.htmlContent).toBe(sampleHtmlContent);
165
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
166
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
167
+ expect(mockLoggerWarnFn).toHaveBeenCalledWith(
168
+ expect.stringContaining(`HTML minification failed: ${htmlError.message}`)
169
+ );
170
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
171
+ expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
172
+ });
173
+
174
+ it('💥 handles CSS minifier failure (returning errors array)', async () => {
175
+ const cssErrorMsg = 'Invalid CSS syntax';
176
+ // Use mockReturnValueOnce for the instance method
177
+ mockCleanCSSInstanceMinifyFn.mockReturnValueOnce({
178
+ errors: [cssErrorMsg],
179
+ warnings: [],
180
+ styles: '',
181
+ stats: {
182
+ originalSize: sampleCssContent.length,
183
+ minifiedSize: 0,
184
+ efficiency: 0,
185
+ timeSpent: 0,
186
+ },
187
+ });
188
+
189
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
190
+
191
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
192
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
193
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
194
+ expect(mockLoggerWarnFn).toHaveBeenCalledWith(
195
+ expect.stringContaining(`CleanCSS failed for style.css: ${cssErrorMsg}`)
196
+ );
197
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
198
+ expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
199
+ });
200
+
201
+ it('💥 handles CSS minifier failure (throwing exception)', async () => {
202
+ const cssError = new Error('CleanCSS crashed!');
203
+ mockCleanCSSInstanceMinifyFn.mockImplementationOnce(() => {
204
+ throw cssError;
205
+ });
206
+
207
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
208
+
209
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
210
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
211
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
212
+ expect(mockLoggerWarnFn).toHaveBeenCalledWith(
213
+ expect.stringContaining(`Failed to minify asset style.css (css): ${cssError.message}`)
214
+ );
215
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
216
+ expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
217
+ });
218
+
219
+ it('💥 handles JS minifier failure (returning error object)', async () => {
220
+ const jsError = new Error('Terser parse error!');
221
+ // Use mockImplementationOnce for the async function
222
+ mockTerserMinifyFn.mockImplementationOnce(async () => ({ code: undefined, error: jsError }));
223
+
224
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
225
+
226
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
227
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
228
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
229
+ expect(mockLoggerWarnFn).toHaveBeenCalledWith(
230
+ expect.stringContaining(`Terser failed for script.js: ${jsError.message}`)
231
+ );
232
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
233
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
234
+ });
235
+
236
+ it('💥 handles JS minifier failure (throwing exception)', async () => {
237
+ const jsError = new Error('Terser crashed!');
238
+ // Use mockImplementationOnce to reject the promise
239
+ mockTerserMinifyFn.mockImplementationOnce(async () => {
240
+ throw jsError;
241
+ });
242
+
243
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
244
+
245
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
246
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
247
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
248
+ expect(mockLoggerWarnFn).toHaveBeenCalledWith(
249
+ expect.stringContaining(`Failed to minify asset script.js (js): ${jsError.message}`)
250
+ );
251
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
252
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
253
+ });
254
+
255
+ it('🧼 skips minification for assets without content or empty content', async () => {
256
+ const inputWithMissingContent: ParsedHTML = {
67
257
  htmlContent: sampleHtmlContent,
68
258
  assets: [
69
- { type: 'css', url: 'style.css', content: sampleCssContent },
70
- { type: 'js', url: 'script.js', content: sampleJsContent },
71
- { type: 'image', url: 'logo.png', content: 'data:image/png;base64,abc' },
72
- ]
73
- };
74
-
75
- beforeEach(() => {
76
- jest.clearAllMocks(); // Clear mocks between tests
77
-
78
- // Set up logger spies
79
- mockLogger = new Logger(LogLevel.WARN);
80
- mockLoggerWarnFn = jest.spyOn(mockLogger, 'warn');
81
- mockLoggerDebugFn = jest.spyOn(mockLogger, 'debug');
82
-
83
- // --- Configure Mock Implementations using the *typed* mocks ---
84
- // These should now type-check correctly
85
- mockHtmlMinifierMinifyFn.mockImplementation(async (_html, _options) => minifiedHtmlContent);
86
-
87
- mockCleanCSSInstanceMinifyFn.mockImplementation((css): CleanCSSOutput => {
88
- if (typeof css !== 'string') css = '// Mocked non-string CSS input'; // Handle non-string input for mock
89
- const minifiedStyles = simpleMockCssMinify(css);
90
- const stats = {
91
- originalSize: css.length, minifiedSize: minifiedStyles.length,
92
- efficiency: css.length > 0 ? (css.length - minifiedStyles.length) / css.length : 0,
93
- timeSpent: 1,
94
- };
95
- return { styles: minifiedStyles, errors: [], warnings: [], stats: stats };
96
- });
97
-
98
- mockTerserMinifyFn.mockImplementation(async (_js, _options) => {
99
- return Promise.resolve({ code: minifiedJsContent, error: undefined });
100
- });
259
+ { type: 'css', url: 'style.css', content: sampleCssContent },
260
+ { type: 'js', url: 'missing.js' },
261
+ { type: 'css', url: 'empty.css', content: '' },
262
+ { type: 'js', url: 'script2.js', content: sampleJsContent },
263
+ ],
264
+ };
265
+ const result = await minifyAssets(inputWithMissingContent, {}, mockLogger);
266
+
267
+ expect(result.assets.find(a => a.url === 'style.css')?.content).toBe(minifiedCssContent);
268
+ expect(result.assets.find(a => a.url === 'missing.js')?.content).toBeUndefined();
269
+ expect(result.assets.find(a => a.url === 'empty.css')?.content).toBe('');
270
+ expect(result.assets.find(a => a.url === 'script2.js')?.content).toBe(minifiedJsContent);
271
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
272
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1); // Only for style.css
273
+ expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1); // Only for script2.js
274
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
275
+ });
276
+ });
277
+
278
+ describe('Selective minification', () => {
279
+ // These tests should remain the same, checking which mocks are called
280
+ it('🎛 only minifies CSS + HTML, leaves JS unchanged', async () => {
281
+ const options: BundleOptions = { minifyHtml: true, minifyCss: true, minifyJs: false };
282
+ const result = await minifyAssets(sampleParsedInput, options, mockLogger);
283
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
284
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
285
+ expect(mockTerserMinifyFn).not.toHaveBeenCalled();
286
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
287
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
288
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
101
289
  });
290
+ it('🎛 only minifies JS + CSS, leaves HTML unchanged', async () => {
291
+ const options: BundleOptions = { minifyHtml: false, minifyCss: true, minifyJs: true };
292
+ const result = await minifyAssets(sampleParsedInput, options, mockLogger);
293
+ expect(mockHtmlMinifierMinifyFn).not.toHaveBeenCalled();
294
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
295
+ expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
296
+ expect(result.htmlContent).toBe(sampleHtmlContent);
297
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
298
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
299
+ });
300
+ it('🎛 only minifies HTML, leaves CSS/JS unchanged', async () => {
301
+ const options: BundleOptions = { minifyHtml: true, minifyCss: false, minifyJs: false };
302
+ const result = await minifyAssets(sampleParsedInput, options, mockLogger);
303
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
304
+ expect(mockCleanCSSInstanceMinifyFn).not.toHaveBeenCalled();
305
+ expect(mockTerserMinifyFn).not.toHaveBeenCalled();
306
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
307
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
308
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
309
+ });
310
+ });
102
311
 
103
- // --- Tests (Assertions use the explicitly typed mock functions) ---
104
- describe('Basic functionality', () => {
105
- it('✅ leaves content unchanged when minification is disabled', async () => {
106
- const options: BundleOptions = { minifyHtml: false, minifyCss: false, minifyJs: false };
107
- const result = await minifyAssets(sampleParsedInput, options, mockLogger);
108
-
109
- expect(mockHtmlMinifierMinifyFn).not.toHaveBeenCalled();
110
- expect(mockCleanCSSConstructorFn).not.toHaveBeenCalled(); // Check constructor
111
- expect(mockCleanCSSInstanceMinifyFn).not.toHaveBeenCalled(); // Check instance method
112
- expect(mockTerserMinifyFn).not.toHaveBeenCalled();
113
-
114
- expect(result.htmlContent).toBe(sampleHtmlContent);
115
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
116
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
117
- });
118
-
119
- it('🔧 minifies HTML, CSS, and JS with all options enabled (default)', async () => {
120
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
121
-
122
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
123
- expect(mockCleanCSSConstructorFn).toHaveBeenCalledTimes(1); // Constructor should be called once
124
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
125
- expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
126
-
127
- expect(result.htmlContent).toBe(minifiedHtmlContent);
128
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
129
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
130
- });
312
+ describe('Content types', () => {
313
+ it('📦 only processes css/js types, skips image/font/other', async () => {
314
+ const inputWithVariousTypes: ParsedHTML = {
315
+ htmlContent: sampleHtmlContent,
316
+ assets: [
317
+ { type: 'css', url: 'style.css', content: sampleCssContent },
318
+ { type: 'js', url: 'script.js', content: sampleJsContent },
319
+ { type: 'image', url: 'logo.png', content: 'data:image/png;base64,abc' },
320
+ { type: 'font', url: 'font.woff2', content: 'data:font/woff2;base64,def' },
321
+ { type: 'other', url: 'data.json', content: '{"a":1}' },
322
+ ],
323
+ };
324
+ const result = await minifyAssets(inputWithVariousTypes, {}, mockLogger);
325
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
326
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
327
+ expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
328
+ expect(result.assets.find(a => a.type === 'image')?.content).toBe(
329
+ 'data:image/png;base64,abc'
330
+ );
331
+ expect(result.assets.find(a => a.type === 'font')?.content).toBe(
332
+ 'data:font/woff2;base64,def'
333
+ );
334
+ expect(result.assets.find(a => a.type === 'other')?.content).toBe('{"a":1}');
335
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
336
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
337
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
338
+ });
339
+ });
340
+
341
+ describe('Edge Cases', () => {
342
+ it('💨 handles empty input object gracefully', async () => {
343
+ const emptyInput: ParsedHTML = { htmlContent: '', assets: [] };
344
+ const result = await minifyAssets(emptyInput, {}, mockLogger);
345
+
346
+ expect(result.htmlContent).toBe('');
347
+ expect(result.assets).toEqual([]);
348
+ expect(mockHtmlMinifierMinifyFn).not.toHaveBeenCalled();
349
+ expect(mockCleanCSSConstructorFn).not.toHaveBeenCalled(); // Check constructor
350
+ expect(mockTerserMinifyFn).not.toHaveBeenCalled();
351
+ // Check debug log for skipping due to no content
352
+ expect(mockLoggerDebugFn).toHaveBeenCalledWith('Minification skipped: No content.');
131
353
  });
132
354
 
133
- describe('Error handling', () => {
134
- it('💥 handles broken HTML minification gracefully', async () => {
135
- const htmlError = new Error('HTML parse error!');
136
- // Use mockImplementationOnce to override for this test
137
- mockHtmlMinifierMinifyFn.mockImplementationOnce(async () => { throw htmlError; });
138
-
139
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
140
-
141
- expect(result.htmlContent).toBe(sampleHtmlContent);
142
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
143
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
144
- expect(mockLoggerWarnFn).toHaveBeenCalledWith(expect.stringContaining(`HTML minification failed: ${htmlError.message}`));
145
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
146
- expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
147
- });
148
-
149
- it('💥 handles CSS minifier failure (returning errors array)', async () => {
150
- const cssErrorMsg = 'Invalid CSS syntax';
151
- // Use mockReturnValueOnce for the instance method
152
- mockCleanCSSInstanceMinifyFn.mockReturnValueOnce({
153
- errors: [cssErrorMsg], warnings: [], styles: '',
154
- stats: { originalSize: sampleCssContent.length, minifiedSize: 0, efficiency: 0, timeSpent: 0 }
155
- });
156
-
157
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
158
-
159
- expect(result.htmlContent).toBe(minifiedHtmlContent);
160
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
161
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
162
- expect(mockLoggerWarnFn).toHaveBeenCalledWith(expect.stringContaining(`CleanCSS failed for style.css: ${cssErrorMsg}`));
163
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
164
- expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
165
- });
166
-
167
- it('💥 handles CSS minifier failure (throwing exception)', async () => {
168
- const cssError = new Error('CleanCSS crashed!');
169
- mockCleanCSSInstanceMinifyFn.mockImplementationOnce(() => { throw cssError; });
170
-
171
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
172
-
173
- expect(result.htmlContent).toBe(minifiedHtmlContent);
174
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
175
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
176
- expect(mockLoggerWarnFn).toHaveBeenCalledWith(expect.stringContaining(`Failed to minify asset style.css (css): ${cssError.message}`));
177
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
178
- expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
179
- });
180
-
181
- it('💥 handles JS minifier failure (returning error object)', async () => {
182
- const jsError = new Error('Terser parse error!');
183
- // Use mockImplementationOnce for the async function
184
- mockTerserMinifyFn.mockImplementationOnce(async () => ({ code: undefined, error: jsError }));
185
-
186
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
187
-
188
- expect(result.htmlContent).toBe(minifiedHtmlContent);
189
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
190
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
191
- expect(mockLoggerWarnFn).toHaveBeenCalledWith(expect.stringContaining(`Terser failed for script.js: ${jsError.message}`));
192
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
193
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
194
- });
195
-
196
- it('💥 handles JS minifier failure (throwing exception)', async () => {
197
- const jsError = new Error('Terser crashed!');
198
- // Use mockImplementationOnce to reject the promise
199
- mockTerserMinifyFn.mockImplementationOnce(async () => { throw jsError; });
200
-
201
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
202
-
203
- expect(result.htmlContent).toBe(minifiedHtmlContent);
204
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
205
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
206
- expect(mockLoggerWarnFn).toHaveBeenCalledWith(expect.stringContaining(`Failed to minify asset script.js (js): ${jsError.message}`));
207
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
208
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
209
- });
210
-
211
- it('🧼 skips minification for assets without content or empty content', async () => {
212
- const inputWithMissingContent: ParsedHTML = { /* ... as before ... */
213
- htmlContent: sampleHtmlContent,
214
- assets: [
215
- { type: 'css', url: 'style.css', content: sampleCssContent },
216
- { type: 'js', url: 'missing.js' },
217
- { type: 'css', url: 'empty.css', content: '' },
218
- { type: 'js', url: 'script2.js', content: sampleJsContent }
219
- ]
220
- };
221
- const result = await minifyAssets(inputWithMissingContent, {}, mockLogger);
222
-
223
- expect(result.assets.find(a => a.url === 'style.css')?.content).toBe(minifiedCssContent);
224
- expect(result.assets.find(a => a.url === 'missing.js')?.content).toBeUndefined();
225
- expect(result.assets.find(a => a.url === 'empty.css')?.content).toBe('');
226
- expect(result.assets.find(a => a.url === 'script2.js')?.content).toBe(minifiedJsContent);
227
- expect(result.htmlContent).toBe(minifiedHtmlContent);
228
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1); // Only for style.css
229
- expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1); // Only for script2.js
230
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
231
- });
355
+ it('💨 handles input with assets but empty HTML content string', async () => {
356
+ const input: ParsedHTML = {
357
+ htmlContent: '',
358
+ assets: [{ type: 'css', url: 'style.css', content: sampleCssContent }],
359
+ };
360
+ const result = await minifyAssets(input, {}, mockLogger);
361
+
362
+ expect(result.htmlContent).toBe('');
363
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
364
+ expect(mockHtmlMinifierMinifyFn).not.toHaveBeenCalled();
365
+ expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
366
+ expect(mockTerserMinifyFn).not.toHaveBeenCalled();
232
367
  });
233
368
 
234
- describe('Selective minification', () => {
235
- // These tests should remain the same, checking which mocks are called
236
- it('🎛 only minifies CSS + HTML, leaves JS unchanged', async () => { /* ... as before ... */
237
- const options: BundleOptions = { minifyHtml: true, minifyCss: true, minifyJs: false };
238
- const result = await minifyAssets(sampleParsedInput, options, mockLogger);
239
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
240
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
241
- expect(mockTerserMinifyFn).not.toHaveBeenCalled();
242
- expect(result.htmlContent).toBe(minifiedHtmlContent);
243
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
244
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
245
- });
246
- it('🎛 only minifies JS + CSS, leaves HTML unchanged', async () => { /* ... as before ... */
247
- const options: BundleOptions = { minifyHtml: false, minifyCss: true, minifyJs: true };
248
- const result = await minifyAssets(sampleParsedInput, options, mockLogger);
249
- expect(mockHtmlMinifierMinifyFn).not.toHaveBeenCalled();
250
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
251
- expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
252
- expect(result.htmlContent).toBe(sampleHtmlContent);
253
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
254
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
255
- });
256
- it('🎛 only minifies HTML, leaves CSS/JS unchanged', async () => { /* ... as before ... */
257
- const options: BundleOptions = { minifyHtml: true, minifyCss: false, minifyJs: false };
258
- const result = await minifyAssets(sampleParsedInput, options, mockLogger);
259
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
260
- expect(mockCleanCSSInstanceMinifyFn).not.toHaveBeenCalled();
261
- expect(mockTerserMinifyFn).not.toHaveBeenCalled();
262
- expect(result.htmlContent).toBe(minifiedHtmlContent);
263
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
264
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
265
- });
369
+ it('💨 handles input with HTML but empty assets array', async () => {
370
+ const input: ParsedHTML = { htmlContent: sampleHtmlContent, assets: [] };
371
+ const result = await minifyAssets(input, {}, mockLogger);
372
+
373
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
374
+ expect(result.assets).toEqual([]);
375
+ expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
376
+ expect(mockCleanCSSInstanceMinifyFn).not.toHaveBeenCalled();
377
+ expect(mockTerserMinifyFn).not.toHaveBeenCalled();
266
378
  });
267
379
 
268
- describe('Content types', () => {
269
- it('📦 only processes css/js types, skips image/font/other', async () => { /* ... as before ... */
270
- const inputWithVariousTypes: ParsedHTML = { /* ... as before ... */
271
- htmlContent: sampleHtmlContent,
272
- assets: [
273
- { type: 'css', url: 'style.css', content: sampleCssContent },
274
- { type: 'js', url: 'script.js', content: sampleJsContent },
275
- { type: 'image', url: 'logo.png', content: 'data:image/png;base64,abc' },
276
- { type: 'font', url: 'font.woff2', content: 'data:font/woff2;base64,def' },
277
- { type: 'other', url: 'data.json', content: '{"a":1}' }
278
- ]
279
- };
280
- const result = await minifyAssets(inputWithVariousTypes, {}, mockLogger);
281
- expect(mockHtmlMinifierMinifyFn).toHaveBeenCalledTimes(1);
282
- expect(mockCleanCSSInstanceMinifyFn).toHaveBeenCalledTimes(1);
283
- expect(mockTerserMinifyFn).toHaveBeenCalledTimes(1);
284
- expect(result.assets.find(a => a.type === 'image')?.content).toBe('data:image/png;base64,abc');
285
- expect(result.assets.find(a => a.type === 'font')?.content).toBe('data:font/woff2;base64,def');
286
- expect(result.assets.find(a => a.type === 'other')?.content).toBe('{"a":1}');
287
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
288
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
289
- expect(result.htmlContent).toBe(minifiedHtmlContent);
290
- });
380
+ it('⚠️ handles CleanCSS returning no styles without errors', async () => {
381
+ // Use mockReturnValueOnce for the instance method mock
382
+ mockCleanCSSInstanceMinifyFn.mockReturnValueOnce({
383
+ errors: [],
384
+ warnings: [],
385
+ styles: '',
386
+ stats: {
387
+ originalSize: sampleCssContent.length,
388
+ minifiedSize: 0,
389
+ efficiency: 0,
390
+ timeSpent: 0,
391
+ },
392
+ });
393
+
394
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
395
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
396
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
397
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
398
+ expect(mockLoggerWarnFn).toHaveBeenCalledWith(
399
+ expect.stringContaining(
400
+ 'CleanCSS produced no styles but reported no errors for style.css. Keeping original.'
401
+ )
402
+ );
291
403
  });
292
404
 
293
- describe('Edge Cases', () => {
294
- it('💨 handles empty input object gracefully', async () => { /* ... as before ... */ });
295
- it('💨 handles input with assets but empty HTML content string', async () => { /* ... as before ... */ });
296
- it('💨 handles input with HTML but empty assets array', async () => { /* ... as before ... */ });
297
-
298
- it('⚠️ handles CleanCSS returning no styles without errors', async () => {
299
- // Use mockReturnValueOnce for the instance method mock
300
- mockCleanCSSInstanceMinifyFn.mockReturnValueOnce({
301
- errors: [], warnings: [], styles: '',
302
- stats: { originalSize: sampleCssContent.length, minifiedSize: 0, efficiency: 0, timeSpent: 0 }
303
- });
304
-
305
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
306
- expect(result.htmlContent).toBe(minifiedHtmlContent);
307
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(sampleCssContent);
308
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(minifiedJsContent);
309
- expect(mockLoggerWarnFn).toHaveBeenCalledWith(expect.stringContaining('CleanCSS produced no styles but reported no errors for style.css. Keeping original.'));
310
- });
311
-
312
- it('⚠️ handles Terser returning no code without errors', async () => {
313
- // Use mockImplementationOnce for the async function
314
- mockTerserMinifyFn.mockImplementationOnce(async () => ({ code: undefined, error: undefined }));
315
-
316
- const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
317
- expect(result.htmlContent).toBe(minifiedHtmlContent);
318
- expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
319
- expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
320
- expect(mockLoggerWarnFn).toHaveBeenCalledWith(expect.stringContaining('Terser produced no code but reported no errors for script.js. Keeping original.'));
321
- });
405
+ it('⚠️ handles Terser returning no code without errors', async () => {
406
+ // Use mockImplementationOnce for the async function
407
+ mockTerserMinifyFn.mockImplementationOnce(async () => ({
408
+ code: undefined,
409
+ error: undefined,
410
+ }));
411
+
412
+ const result = await minifyAssets(sampleParsedInput, {}, mockLogger);
413
+ expect(result.htmlContent).toBe(minifiedHtmlContent);
414
+ expect(result.assets.find(a => a.type === 'css')?.content).toBe(minifiedCssContent);
415
+ expect(result.assets.find(a => a.type === 'js')?.content).toBe(sampleJsContent);
416
+ expect(mockLoggerWarnFn).toHaveBeenCalledWith(
417
+ expect.stringContaining(
418
+ 'Terser produced no code but reported no errors for script.js. Keeping original.'
419
+ )
420
+ );
322
421
  });
323
- });
422
+ });
423
+ });