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.
- package/.eslintrc.json +67 -8
- package/.releaserc.js +25 -27
- package/CHANGELOG.md +14 -22
- package/LICENSE.md +21 -0
- package/README.md +22 -53
- package/commitlint.config.js +30 -34
- package/dist/cli/cli-entry.cjs +183 -98
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +178 -97
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +38 -33
- package/docs/.vitepress/sidebar-generator.ts +89 -38
- package/docs/architecture.md +186 -0
- package/docs/cli.md +23 -23
- package/docs/code-of-conduct.md +7 -1
- package/docs/configuration.md +12 -11
- package/docs/contributing.md +6 -2
- package/docs/deployment.md +10 -5
- package/docs/development.md +8 -5
- package/docs/getting-started.md +13 -13
- package/docs/index.md +1 -1
- package/docs/public/android-chrome-192x192.png +0 -0
- package/docs/public/android-chrome-512x512.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/roadmap.md +233 -0
- package/docs/site.webmanifest +1 -0
- package/docs/troubleshooting.md +12 -1
- package/examples/main.ts +5 -30
- package/examples/sample-project/script.js +1 -1
- package/jest.config.ts +8 -13
- package/nodemon.json +5 -10
- package/package.json +2 -5
- package/src/cli/cli-entry.ts +2 -2
- package/src/cli/cli.ts +21 -16
- package/src/cli/options.ts +127 -113
- package/src/core/bundler.ts +253 -222
- package/src/core/extractor.ts +632 -565
- package/src/core/minifier.ts +173 -162
- package/src/core/packer.ts +141 -137
- package/src/core/parser.ts +74 -73
- package/src/core/web-fetcher.ts +270 -258
- package/src/index.ts +18 -17
- package/src/types.ts +9 -11
- package/src/utils/font.ts +12 -6
- package/src/utils/logger.ts +110 -105
- package/src/utils/meta.ts +75 -76
- package/src/utils/mime.ts +50 -50
- package/src/utils/slugify.ts +33 -34
- package/tests/unit/cli/cli-entry.test.ts +72 -70
- package/tests/unit/cli/cli.test.ts +314 -278
- package/tests/unit/cli/options.test.ts +294 -301
- package/tests/unit/core/bundler.test.ts +426 -329
- package/tests/unit/core/extractor.test.ts +793 -549
- package/tests/unit/core/minifier.test.ts +374 -274
- package/tests/unit/core/packer.test.ts +298 -264
- package/tests/unit/core/parser.test.ts +538 -150
- package/tests/unit/core/web-fetcher.test.ts +389 -359
- package/tests/unit/index.test.ts +238 -197
- package/tests/unit/utils/font.test.ts +26 -21
- package/tests/unit/utils/logger.test.ts +267 -260
- package/tests/unit/utils/meta.test.ts +29 -28
- package/tests/unit/utils/mime.test.ts +73 -74
- package/tests/unit/utils/slugify.test.ts +14 -12
- package/tsconfig.build.json +9 -10
- package/tsconfig.jest.json +1 -1
- package/tsconfig.json +2 -2
- package/tsup.config.ts +8 -9
- package/typedoc.json +5 -9
- /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
- /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 =
|
21
|
-
|
22
|
-
const
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
32
|
-
|
44
|
+
__esModule: true,
|
45
|
+
minify: mockHtmlMinifierMinifyFn,
|
33
46
|
}));
|
34
47
|
jest.mock('clean-css', () => ({
|
35
|
-
|
36
|
-
|
37
|
-
|
48
|
+
__esModule: true,
|
49
|
+
// Mock the default export which is the class constructor
|
50
|
+
default: mockCleanCSSConstructorFn,
|
38
51
|
}));
|
39
52
|
jest.mock('terser', () => ({
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
+
});
|