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
@@ -1,5 +1,3 @@
|
|
1
|
-
// tests/unit/cli/cli.test.ts
|
2
|
-
|
3
1
|
import { jest, describe, it, beforeEach, afterEach, expect } from '@jest/globals'; // Assuming beforeAll/afterAll aren't strictly needed for these tests unless used by setup
|
4
2
|
|
5
3
|
// Import types
|
@@ -19,12 +17,12 @@ const mockPackFn = jest.fn<() => Promise<BuildResult>>();
|
|
19
17
|
|
20
18
|
// Tell Jest to replace the actual modules with our mocks *before* cli.ts is imported
|
21
19
|
jest.mock('../../../src/cli/options', () => ({
|
22
|
-
|
23
|
-
|
20
|
+
__esModule: true,
|
21
|
+
parseOptions: mockParseOptions,
|
24
22
|
}));
|
25
23
|
jest.mock('../../../src/index', () => ({
|
26
|
-
|
27
|
-
|
24
|
+
__esModule: true,
|
25
|
+
pack: mockPackFn,
|
28
26
|
}));
|
29
27
|
|
30
28
|
// --- Import the module under test AFTER mocks are defined ---
|
@@ -33,296 +31,334 @@ import { runCli } from '../../../src/cli/cli';
|
|
33
31
|
|
34
32
|
// --- Test Suite ---
|
35
33
|
describe('CLI Runner Logic', () => {
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
34
|
+
// --- Declare Spies for FS module (used by cli.ts) ---
|
35
|
+
let writeFileSyncSpy: jest.SpiedFunction<typeof fs.writeFileSync>;
|
36
|
+
let existsSyncSpy: jest.SpiedFunction<typeof fs.existsSync>;
|
37
|
+
let readFileSyncSpy: jest.SpiedFunction<typeof fs.readFileSync>;
|
38
|
+
|
39
|
+
// Helper to create mock metadata
|
40
|
+
const createMockMetadata = (
|
41
|
+
input: string,
|
42
|
+
overrides: Partial<BundleMetadata> = {}
|
43
|
+
): BundleMetadata => ({
|
44
|
+
input: input,
|
45
|
+
assetCount: 5,
|
46
|
+
outputSize: 1024,
|
47
|
+
buildTimeMs: 100,
|
48
|
+
pagesBundled: undefined, // Default to undefined
|
49
|
+
errors: [], // Default to empty errors/warnings array
|
50
|
+
...overrides,
|
51
|
+
});
|
52
|
+
|
53
|
+
// Default options structure returned by the mock parseOptions
|
54
|
+
const defaultCliOptions: CLIOptions = {
|
55
|
+
input: 'default.html',
|
56
|
+
output: undefined,
|
57
|
+
recursive: false,
|
58
|
+
dryRun: false,
|
59
|
+
logLevel: LogLevel.INFO,
|
60
|
+
verbose: false,
|
61
|
+
embedAssets: true,
|
62
|
+
minifyHtml: true,
|
63
|
+
minifyCss: true,
|
64
|
+
minifyJs: true,
|
65
|
+
};
|
66
|
+
|
67
|
+
beforeEach(() => {
|
68
|
+
// --- Reset ALL Mocks and Spies ---
|
69
|
+
// Clears call history etc. between tests
|
70
|
+
jest.clearAllMocks();
|
71
|
+
|
72
|
+
// --- Setup Spies on FS module ---
|
73
|
+
// Mock implementations for fs functions called by cli.ts
|
74
|
+
writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); // Don't actually write files
|
75
|
+
existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true); // Assume files exist by default
|
76
|
+
|
77
|
+
// Mock readFileSync to handle the package.json read required by getPackageJson (internal to cli.ts)
|
78
|
+
readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockImplementation((pathInput, options) => {
|
79
|
+
const pathStr = String(pathInput); // Ensure path is treated as a string
|
80
|
+
// Check if it's trying to read package.json (adjust check if needed)
|
81
|
+
if (pathStr.includes('portapack') && pathStr.endsWith('package.json')) {
|
82
|
+
return JSON.stringify({ version: '1.0.0-test' }); // Return mock version
|
83
|
+
}
|
84
|
+
// If cli.ts needed to read other files, mock them here too
|
85
|
+
// if (pathStr.endsWith('some-other-expected-file.txt')) { return 'file content'; }
|
86
|
+
|
87
|
+
// Throw an error for any unexpected file reads during tests
|
88
|
+
throw new Error(`Unexpected readFileSync call in test: ${pathStr}`);
|
51
89
|
});
|
52
90
|
|
53
|
-
// Default
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
91
|
+
// --- Default mock implementations for dependencies ---
|
92
|
+
// Ensure parseOptions returns a valid structure by default
|
93
|
+
mockParseOptions.mockReturnValue(defaultCliOptions);
|
94
|
+
// Ensure pack resolves successfully by default
|
95
|
+
mockPackFn.mockResolvedValue({
|
96
|
+
html: '<html>Default Pack Result</html>',
|
97
|
+
metadata: createMockMetadata(defaultCliOptions.input ?? 'default.html'), // Use input from default options
|
98
|
+
});
|
99
|
+
});
|
100
|
+
|
101
|
+
afterEach(() => {
|
102
|
+
// Restore original implementations spied on by jest.spyOn
|
103
|
+
// Important to avoid mocks leaking between test files
|
104
|
+
jest.restoreAllMocks();
|
105
|
+
});
|
106
|
+
|
107
|
+
// --- Tests ---
|
108
|
+
|
109
|
+
it('calls parseOptions with process arguments', async () => {
|
110
|
+
const testArgs = ['node', 'cli.js', 'test-input.html', '-o', 'test-output.html'];
|
111
|
+
const specificOptions = {
|
112
|
+
...defaultCliOptions,
|
113
|
+
input: 'test-input.html',
|
114
|
+
output: 'test-output.html',
|
65
115
|
};
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
// --- Setup Spies on FS module ---
|
73
|
-
// Mock implementations for fs functions called by cli.ts
|
74
|
-
writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { }); // Don't actually write files
|
75
|
-
existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true); // Assume files exist by default
|
76
|
-
|
77
|
-
// Mock readFileSync to handle the package.json read required by getPackageJson (internal to cli.ts)
|
78
|
-
readFileSyncSpy = jest.spyOn(fs, 'readFileSync')
|
79
|
-
.mockImplementation((pathInput, options) => {
|
80
|
-
const pathStr = String(pathInput); // Ensure path is treated as a string
|
81
|
-
// Check if it's trying to read package.json (adjust check if needed)
|
82
|
-
if (pathStr.includes('portapack') && pathStr.endsWith('package.json')) {
|
83
|
-
return JSON.stringify({ version: "1.0.0-test" }); // Return mock version
|
84
|
-
}
|
85
|
-
// If cli.ts needed to read other files, mock them here too
|
86
|
-
// if (pathStr.endsWith('some-other-expected-file.txt')) { return 'file content'; }
|
87
|
-
|
88
|
-
// Throw an error for any unexpected file reads during tests
|
89
|
-
throw new Error(`Unexpected readFileSync call in test: ${pathStr}`);
|
90
|
-
});
|
91
|
-
|
92
|
-
// --- Default mock implementations for dependencies ---
|
93
|
-
// Ensure parseOptions returns a valid structure by default
|
94
|
-
mockParseOptions.mockReturnValue(defaultCliOptions);
|
95
|
-
// Ensure pack resolves successfully by default
|
96
|
-
mockPackFn.mockResolvedValue({
|
97
|
-
html: '<html>Default Pack Result</html>',
|
98
|
-
metadata: createMockMetadata(defaultCliOptions.input ?? 'default.html') // Use input from default options
|
99
|
-
});
|
116
|
+
mockParseOptions.mockReturnValueOnce(specificOptions); // Override default for this test
|
117
|
+
// Configure pack mock for this specific input if its result matters
|
118
|
+
mockPackFn.mockResolvedValueOnce({
|
119
|
+
html: '<html>Specific Test Result</html>',
|
120
|
+
metadata: createMockMetadata('test-input.html'),
|
100
121
|
});
|
101
122
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
123
|
+
await runCli(testArgs);
|
124
|
+
|
125
|
+
expect(mockParseOptions).toHaveBeenCalledTimes(1);
|
126
|
+
expect(mockParseOptions).toHaveBeenCalledWith(testArgs);
|
127
|
+
// Verify pack was called because parseOptions returned valid input and not dry-run
|
128
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
129
|
+
// Check if pack was called with the correct arguments derived from options
|
130
|
+
expect(mockPackFn).toHaveBeenCalledWith(specificOptions.input, specificOptions);
|
131
|
+
});
|
132
|
+
|
133
|
+
it('calls pack for non-recursive input and writes file', async () => {
|
134
|
+
const args = ['node', 'cli.js', 'index.html', '-o', 'output.html'];
|
135
|
+
const mockCliOptions: CLIOptions = {
|
136
|
+
...defaultCliOptions,
|
137
|
+
input: 'index.html',
|
138
|
+
output: 'output.html', // Explicit output path
|
139
|
+
recursive: false,
|
140
|
+
};
|
141
|
+
const specificMetadata = createMockMetadata('index.html', {
|
142
|
+
assetCount: 3,
|
143
|
+
outputSize: 2048,
|
144
|
+
buildTimeMs: 55,
|
106
145
|
});
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
const specificOptions = { ...defaultCliOptions, input: 'test-input.html', output: 'test-output.html' };
|
113
|
-
mockParseOptions.mockReturnValueOnce(specificOptions); // Override default for this test
|
114
|
-
// Configure pack mock for this specific input if its result matters
|
115
|
-
mockPackFn.mockResolvedValueOnce({ html: '<html>Specific Test Result</html>', metadata: createMockMetadata('test-input.html') });
|
116
|
-
|
117
|
-
await runCli(testArgs);
|
118
|
-
|
119
|
-
expect(mockParseOptions).toHaveBeenCalledTimes(1);
|
120
|
-
expect(mockParseOptions).toHaveBeenCalledWith(testArgs);
|
121
|
-
// Verify pack was called because parseOptions returned valid input and not dry-run
|
122
|
-
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
123
|
-
// Check if pack was called with the correct arguments derived from options
|
124
|
-
expect(mockPackFn).toHaveBeenCalledWith(specificOptions.input, specificOptions);
|
146
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions); // Use specific options for this test
|
147
|
+
mockPackFn.mockResolvedValueOnce({
|
148
|
+
// Use specific pack result for this test
|
149
|
+
html: '<html>Packed Non-Recursive</html>',
|
150
|
+
metadata: specificMetadata,
|
125
151
|
});
|
126
152
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
153
|
+
const result = await runCli(args);
|
154
|
+
|
155
|
+
// Check that parseOptions was called
|
156
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
157
|
+
// Check pack was called correctly
|
158
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
159
|
+
expect(mockPackFn).toHaveBeenCalledWith(mockCliOptions.input, mockCliOptions); // Called with input and options object
|
160
|
+
// Check file write happened
|
161
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
162
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
163
|
+
'output.html',
|
164
|
+
'<html>Packed Non-Recursive</html>',
|
165
|
+
'utf-8'
|
166
|
+
);
|
167
|
+
// Check result object
|
168
|
+
expect(result.exitCode).toBe(0);
|
169
|
+
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
170
|
+
expect(result.stdout).toContain('📦 Size: 2.00 KB'); // Check stats based on mock metadata
|
171
|
+
});
|
172
|
+
|
173
|
+
it('calls pack for recursive input (boolean) and writes file', async () => {
|
174
|
+
const args = ['node', 'cli.js', 'https://site.com', '-r', '-o', 'site-packed.html'];
|
175
|
+
const mockCliOptions: CLIOptions = {
|
176
|
+
...defaultCliOptions,
|
177
|
+
input: 'https://site.com',
|
178
|
+
output: 'site-packed.html',
|
179
|
+
recursive: true, // Recursive flag is true
|
180
|
+
};
|
181
|
+
const specificMetadata = createMockMetadata('https://site.com', {
|
182
|
+
pagesBundled: 3,
|
183
|
+
assetCount: 10,
|
156
184
|
});
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
...defaultCliOptions,
|
162
|
-
input: 'https://site.com',
|
163
|
-
output: 'site-packed.html',
|
164
|
-
recursive: true, // Recursive flag is true
|
165
|
-
};
|
166
|
-
const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 3, assetCount: 10 });
|
167
|
-
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
168
|
-
mockPackFn.mockResolvedValueOnce({
|
169
|
-
html: '<html>Packed Recursive Boolean</html>',
|
170
|
-
metadata: specificMetadata
|
171
|
-
});
|
172
|
-
|
173
|
-
const result = await runCli(args);
|
174
|
-
|
175
|
-
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
176
|
-
// Check pack was called
|
177
|
-
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
178
|
-
expect(mockPackFn).toHaveBeenCalledWith(mockCliOptions.input, mockCliOptions);
|
179
|
-
// Check file write
|
180
|
-
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
181
|
-
expect(writeFileSyncSpy).toHaveBeenCalledWith('site-packed.html', '<html>Packed Recursive Boolean</html>', 'utf-8');
|
182
|
-
// Check result object and specific recursive output
|
183
|
-
expect(result.exitCode).toBe(0);
|
184
|
-
expect(result.stdout).toContain('✅ Packed: https://site.com → site-packed.html');
|
185
|
-
expect(result.stdout).toContain('🧩 Pages: 3'); // Check pages bundled output
|
185
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
186
|
+
mockPackFn.mockResolvedValueOnce({
|
187
|
+
html: '<html>Packed Recursive Boolean</html>',
|
188
|
+
metadata: specificMetadata,
|
186
189
|
});
|
187
190
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
191
|
+
const result = await runCli(args);
|
192
|
+
|
193
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
194
|
+
// Check pack was called
|
195
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
196
|
+
expect(mockPackFn).toHaveBeenCalledWith(mockCliOptions.input, mockCliOptions);
|
197
|
+
// Check file write
|
198
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
199
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
200
|
+
'site-packed.html',
|
201
|
+
'<html>Packed Recursive Boolean</html>',
|
202
|
+
'utf-8'
|
203
|
+
);
|
204
|
+
// Check result object and specific recursive output
|
205
|
+
expect(result.exitCode).toBe(0);
|
206
|
+
expect(result.stdout).toContain('✅ Packed: https://site.com → site-packed.html');
|
207
|
+
expect(result.stdout).toContain('🧩 Pages: 3'); // Check pages bundled output
|
208
|
+
});
|
209
|
+
|
210
|
+
it('calls pack for recursive input (number) and writes file', async () => {
|
211
|
+
const args = ['node', 'cli.js', 'https://site.com', '-r', '2', '-o', 'site-packed.html'];
|
212
|
+
const mockCliOptions: CLIOptions = {
|
213
|
+
...defaultCliOptions,
|
214
|
+
input: 'https://site.com',
|
215
|
+
output: 'site-packed.html',
|
216
|
+
recursive: 2, // Recursive flag is number
|
217
|
+
};
|
218
|
+
const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 5 });
|
219
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
220
|
+
mockPackFn.mockResolvedValueOnce({
|
221
|
+
html: '<html>Packed Recursive Number</html>',
|
222
|
+
metadata: specificMetadata,
|
216
223
|
});
|
217
224
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
225
|
+
const result = await runCli(args);
|
226
|
+
|
227
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
228
|
+
// Check pack was called
|
229
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
230
|
+
expect(mockPackFn).toHaveBeenCalledWith(mockCliOptions.input, mockCliOptions);
|
231
|
+
// Check file write
|
232
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
233
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
234
|
+
'site-packed.html',
|
235
|
+
'<html>Packed Recursive Number</html>',
|
236
|
+
'utf-8'
|
237
|
+
);
|
238
|
+
// Check result object and specific recursive output
|
239
|
+
expect(result.exitCode).toBe(0);
|
240
|
+
expect(result.stdout).toContain('✅ Packed: https://site.com → site-packed.html');
|
241
|
+
expect(result.stdout).toContain('🧩 Pages: 5');
|
242
|
+
});
|
243
|
+
|
244
|
+
it('returns error if input is missing', async () => {
|
245
|
+
const args = ['node', 'cli.js']; // No input provided
|
246
|
+
// Simulate parseOptions returning options without an input
|
247
|
+
mockParseOptions.mockReturnValueOnce({ ...defaultCliOptions, input: undefined });
|
248
|
+
|
249
|
+
const result = await runCli(args);
|
250
|
+
|
251
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
252
|
+
// Check for early exit
|
253
|
+
expect(result.exitCode).toBe(1);
|
254
|
+
expect(result.stderr).toContain('❌ Missing input file or URL');
|
255
|
+
// Ensure pack and writeFile were not called
|
256
|
+
expect(mockPackFn).not.toHaveBeenCalled();
|
257
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
258
|
+
});
|
259
|
+
|
260
|
+
it('skips processing and writing in dry-run mode', async () => {
|
261
|
+
const args = ['node', 'cli.js', 'index.html', '--dry-run'];
|
262
|
+
// Simulate parseOptions returning dryRun: true
|
263
|
+
mockParseOptions.mockReturnValueOnce({
|
264
|
+
...defaultCliOptions,
|
265
|
+
input: 'index.html',
|
266
|
+
dryRun: true,
|
232
267
|
});
|
233
268
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
269
|
+
const result = await runCli(args);
|
270
|
+
|
271
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
272
|
+
// Ensure pack and writeFile were not called due to dry run
|
273
|
+
expect(mockPackFn).not.toHaveBeenCalled();
|
274
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
275
|
+
// Check for successful exit code and dry run message
|
276
|
+
expect(result.exitCode).toBe(0);
|
277
|
+
expect(result.stdout).toContain('💡 Dry run mode');
|
278
|
+
});
|
279
|
+
|
280
|
+
it('handles unexpected errors from pack() gracefully', async () => {
|
281
|
+
const args = ['node', 'cli.js', './valid-input.html'];
|
282
|
+
const errorMessage = 'Something broke during processing';
|
283
|
+
// Use valid options, but make pack() reject
|
284
|
+
mockParseOptions.mockReturnValueOnce({ ...defaultCliOptions, input: './valid-input.html' });
|
285
|
+
mockPackFn.mockRejectedValueOnce(new Error(errorMessage)); // Simulate error from pack
|
286
|
+
|
287
|
+
const result = await runCli(args);
|
288
|
+
|
289
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
290
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1); // Ensure pack was called
|
291
|
+
// Check for error exit code and message
|
292
|
+
expect(result.exitCode).toBe(1);
|
293
|
+
expect(result.stderr).toContain(`💥 Error: ${errorMessage}`);
|
294
|
+
// Ensure file was not written on error
|
295
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
296
|
+
});
|
297
|
+
|
298
|
+
it('displays warnings from metadata in stderr', async () => {
|
299
|
+
const args = ['node', 'cli.js', 'index.html', '-o', 'output.html']; // Ensure output path is set
|
300
|
+
const warningMessage = 'Asset not found: missing.css';
|
301
|
+
const metadataWithWarning = createMockMetadata('index.html', { errors: [warningMessage] }); // Add error/warning to metadata
|
302
|
+
mockParseOptions.mockReturnValueOnce({
|
303
|
+
...defaultCliOptions,
|
304
|
+
input: 'index.html',
|
305
|
+
output: 'output.html',
|
248
306
|
});
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
307
|
+
mockPackFn.mockResolvedValueOnce({
|
308
|
+
html: '<html>Warning</html>',
|
309
|
+
metadata: metadataWithWarning,
|
310
|
+
}); // Return metadata with warning
|
311
|
+
|
312
|
+
const result = await runCli(args);
|
313
|
+
|
314
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
315
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
316
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Verify write still happens
|
317
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>Warning</html>', 'utf-8');
|
318
|
+
// Should exit 0 even with warnings
|
319
|
+
expect(result.exitCode).toBe(0);
|
320
|
+
// Standard output should still show success
|
321
|
+
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
322
|
+
expect(result.stderr).toMatch(/⚠️\s+1\s+warning\(s\):/);
|
323
|
+
// Warnings should be logged to stderr
|
324
|
+
expect(result.stderr).toContain(`- ${warningMessage}`);
|
325
|
+
});
|
326
|
+
|
327
|
+
it('displays verbose startup logs when --verbose is used', async () => {
|
328
|
+
const args = ['node', 'cli.js', 'index.html', '--verbose', '-o', 'out.html']; // Add verbose flag
|
329
|
+
const verboseOptions: CLIOptions = {
|
330
|
+
...defaultCliOptions,
|
331
|
+
input: 'index.html',
|
332
|
+
output: 'out.html', // Explicit output needed for logs
|
333
|
+
verbose: true,
|
334
|
+
logLevel: LogLevel.DEBUG, // Verbose usually implies a lower log level from parseOptions
|
335
|
+
};
|
336
|
+
mockParseOptions.mockReturnValueOnce(verboseOptions);
|
337
|
+
// We still need pack to resolve successfully
|
338
|
+
mockPackFn.mockResolvedValue({
|
339
|
+
html: '<!DOCTYPE html><html><body>Mock HTML</body></html>',
|
340
|
+
metadata: createMockMetadata('index.html'), // Use simple metadata
|
266
341
|
});
|
267
342
|
|
268
|
-
|
269
|
-
const args = ['node', 'cli.js', 'index.html', '-o', 'output.html']; // Ensure output path is set
|
270
|
-
const warningMessage = 'Asset not found: missing.css';
|
271
|
-
const metadataWithWarning = createMockMetadata('index.html', { errors: [warningMessage] }); // Add error/warning to metadata
|
272
|
-
mockParseOptions.mockReturnValueOnce({ ...defaultCliOptions, input: 'index.html', output: 'output.html' });
|
273
|
-
mockPackFn.mockResolvedValueOnce({ html: '<html>Warning</html>', metadata: metadataWithWarning }); // Return metadata with warning
|
274
|
-
|
275
|
-
const result = await runCli(args);
|
276
|
-
|
277
|
-
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
278
|
-
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
279
|
-
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Verify write still happens
|
280
|
-
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>Warning</html>', 'utf-8');
|
281
|
-
// Should exit 0 even with warnings
|
282
|
-
expect(result.exitCode).toBe(0);
|
283
|
-
// Standard output should still show success
|
284
|
-
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
285
|
-
expect(result.stderr).toMatch(/⚠️\s+1\s+warning\(s\):/);
|
286
|
-
// Warnings should be logged to stderr
|
287
|
-
expect(result.stderr).toContain(`- ${warningMessage}`);
|
288
|
-
});
|
343
|
+
const result = await runCli(args);
|
289
344
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
html: '<!DOCTYPE html><html><body>Mock HTML</body></html>',
|
303
|
-
metadata: createMockMetadata('index.html') // Use simple metadata
|
304
|
-
});
|
305
|
-
|
306
|
-
const result = await runCli(args);
|
307
|
-
|
308
|
-
const expectedVersionString = `📦 PortaPack v${packageJson.version}`; // Get version dynamically
|
309
|
-
|
310
|
-
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
311
|
-
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
312
|
-
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Write should still occur
|
313
|
-
|
314
|
-
// Check stdout for verbose logs
|
315
|
-
expect(result.stdout).toContain(expectedVersionString);
|
316
|
-
expect(result.stdout).toContain('📥 Input: index.html');
|
317
|
-
expect(result.stdout).toContain('📤 Output: out.html'); // Checks resolved output path
|
318
|
-
expect(result.stdout).toContain('Recursive: false'); // Check other options logged
|
319
|
-
expect(result.stdout).toContain(`Log Level: ${verboseOptions.logLevel}`); // Check logged log level
|
320
|
-
|
321
|
-
// Check for standard success logs as well
|
322
|
-
expect(result.stdout).toContain('✅ Packed: index.html → out.html');
|
323
|
-
expect(result.exitCode).toBe(0);
|
324
|
-
});
|
345
|
+
const expectedVersionString = `📦 PortaPack v${packageJson.version}`; // Get version dynamically
|
346
|
+
|
347
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
348
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
349
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Write should still occur
|
350
|
+
|
351
|
+
// Check stdout for verbose logs
|
352
|
+
expect(result.stdout).toContain(expectedVersionString);
|
353
|
+
expect(result.stdout).toContain('📥 Input: index.html');
|
354
|
+
expect(result.stdout).toContain('📤 Output: out.html'); // Checks resolved output path
|
355
|
+
expect(result.stdout).toContain('Recursive: false'); // Check other options logged
|
356
|
+
expect(result.stdout).toContain(`Log Level: ${verboseOptions.logLevel}`); // Check logged log level
|
325
357
|
|
326
|
-
//
|
358
|
+
// Check for standard success logs as well
|
359
|
+
expect(result.stdout).toContain('✅ Packed: index.html → out.html');
|
360
|
+
expect(result.exitCode).toBe(0);
|
361
|
+
});
|
327
362
|
|
328
|
-
|
363
|
+
// Add more tests for other options, edge cases, different log levels etc.
|
364
|
+
});
|