portapack 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +9 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/deploy-pages.yml +56 -0
- package/.prettierrc +9 -0
- package/.releaserc.js +29 -0
- package/CHANGELOG.md +21 -0
- package/README.md +288 -0
- package/commitlint.config.js +36 -0
- package/dist/cli/cli-entry.js +1694 -0
- package/dist/cli/cli-entry.js.map +1 -0
- package/dist/index.d.ts +275 -0
- package/dist/index.js +1405 -0
- package/dist/index.js.map +1 -0
- package/docs/.vitepress/config.ts +89 -0
- package/docs/.vitepress/sidebar-generator.ts +73 -0
- package/docs/cli.md +117 -0
- package/docs/code-of-conduct.md +65 -0
- package/docs/configuration.md +151 -0
- package/docs/contributing.md +107 -0
- package/docs/demo.md +46 -0
- package/docs/deployment.md +132 -0
- package/docs/development.md +168 -0
- package/docs/getting-started.md +106 -0
- package/docs/index.md +40 -0
- package/docs/portapack-transparent.png +0 -0
- package/docs/portapack.jpg +0 -0
- package/docs/troubleshooting.md +107 -0
- package/examples/main.ts +118 -0
- package/examples/sample-project/index.html +12 -0
- package/examples/sample-project/logo.png +1 -0
- package/examples/sample-project/script.js +1 -0
- package/examples/sample-project/styles.css +1 -0
- package/jest.config.ts +124 -0
- package/jest.setup.cjs +211 -0
- package/nodemon.json +11 -0
- package/output.html +1 -0
- package/package.json +161 -0
- package/site-packed.html +1 -0
- package/src/cli/cli-entry.ts +28 -0
- package/src/cli/cli.ts +139 -0
- package/src/cli/options.ts +151 -0
- package/src/core/bundler.ts +201 -0
- package/src/core/extractor.ts +618 -0
- package/src/core/minifier.ts +233 -0
- package/src/core/packer.ts +191 -0
- package/src/core/parser.ts +115 -0
- package/src/core/web-fetcher.ts +292 -0
- package/src/index.ts +262 -0
- package/src/types.ts +163 -0
- package/src/utils/font.ts +41 -0
- package/src/utils/logger.ts +139 -0
- package/src/utils/meta.ts +100 -0
- package/src/utils/mime.ts +90 -0
- package/src/utils/slugify.ts +70 -0
- package/test-output.html +0 -0
- package/tests/__fixtures__/sample-project/index.html +5 -0
- package/tests/unit/cli/cli-entry.test.ts +104 -0
- package/tests/unit/cli/cli.test.ts +230 -0
- package/tests/unit/cli/options.test.ts +316 -0
- package/tests/unit/core/bundler.test.ts +287 -0
- package/tests/unit/core/extractor.test.ts +1129 -0
- package/tests/unit/core/minifier.test.ts +414 -0
- package/tests/unit/core/packer.test.ts +193 -0
- package/tests/unit/core/parser.test.ts +540 -0
- package/tests/unit/core/web-fetcher.test.ts +374 -0
- package/tests/unit/index.test.ts +339 -0
- package/tests/unit/utils/font.test.ts +81 -0
- package/tests/unit/utils/logger.test.ts +275 -0
- package/tests/unit/utils/meta.test.ts +70 -0
- package/tests/unit/utils/mime.test.ts +96 -0
- package/tests/unit/utils/slugify.test.ts +71 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.jest.json +17 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +71 -0
- package/typedoc.json +28 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
/**
|
2
|
+
* @file tests/unit/cli/cli-entry.test.ts
|
3
|
+
* @description Unit tests for the main CLI entry point (assuming it calls cli.main).
|
4
|
+
* Focuses on argument passing and process exit behavior.
|
5
|
+
*/
|
6
|
+
|
7
|
+
import type { CLIResult } from '../../../src/types';
|
8
|
+
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
9
|
+
|
10
|
+
// =================== MOCK SETUP ===================
|
11
|
+
const mockMainFunction = jest.fn<(argv: string[]) => Promise<CLIResult>>();
|
12
|
+
|
13
|
+
jest.unstable_mockModule('../../../src/cli/cli', () => ({
|
14
|
+
runCli: mockMainFunction,
|
15
|
+
main: mockMainFunction,
|
16
|
+
}));
|
17
|
+
|
18
|
+
// Use SpiedFunction type for the variable declaration
|
19
|
+
let exitMock: jest.SpiedFunction<typeof process.exit>;
|
20
|
+
const errorLogMock = jest.spyOn(console, 'error').mockImplementation(() => {});
|
21
|
+
const logMock = jest.spyOn(console, 'log').mockImplementation(() => {});
|
22
|
+
// ====================================================
|
23
|
+
|
24
|
+
|
25
|
+
describe('CLI Entry Point', () => {
|
26
|
+
const originalArgv = process.argv;
|
27
|
+
|
28
|
+
beforeEach(() => {
|
29
|
+
jest.clearAllMocks();
|
30
|
+
process.argv = ['node', 'cli-entry.js'];
|
31
|
+
mockMainFunction.mockResolvedValue({ exitCode: 0, stdout: 'Success', stderr: '' });
|
32
|
+
|
33
|
+
// Apply 'as any' cast HERE (Line 42 approx) during initial spy setup
|
34
|
+
// This is the setup requested to avoid the persistent TS2345 error.
|
35
|
+
exitMock = jest.spyOn(process, 'exit')
|
36
|
+
.mockImplementation(((code?: number): never => { // Use actual signature inside
|
37
|
+
// Default implementation throws to catch unexpected calls
|
38
|
+
throw new Error(`process.exit(${code}) called unexpectedly`);
|
39
|
+
}) as any); // <<< CAST TO ANY HERE
|
40
|
+
});
|
41
|
+
|
42
|
+
afterEach(() => {
|
43
|
+
process.argv = originalArgv;
|
44
|
+
});
|
45
|
+
|
46
|
+
it('runs the main CLI function with correct arguments (simulated entry)', async () => {
|
47
|
+
const testArgs = ['node', 'cli-entry.js', 'test.html', '--output', 'out.html'];
|
48
|
+
process.argv = testArgs;
|
49
|
+
const { main } = await import('../../../src/cli/cli');
|
50
|
+
await main(testArgs); // Call the mocked main/runCli
|
51
|
+
expect(mockMainFunction).toHaveBeenCalledWith(testArgs);
|
52
|
+
// Expect exit not to be called (default mock throws if called)
|
53
|
+
});
|
54
|
+
|
55
|
+
it('exits with code from main function when simulating entry point exit', async () => {
|
56
|
+
mockMainFunction.mockResolvedValue({ exitCode: 1, stdout: '', stderr: 'Error occurred' });
|
57
|
+
const testArgs = ['node', 'cli-entry.js', '--invalid-option'];
|
58
|
+
process.argv = testArgs;
|
59
|
+
|
60
|
+
// Override mock specifically for this test to *not* throw.
|
61
|
+
// Apply 'as any' cast here too, matching the beforeEach approach.
|
62
|
+
exitMock.mockImplementation(((code?: number): never => {
|
63
|
+
return undefined as never;
|
64
|
+
}) as any); // <<< CAST TO ANY on override
|
65
|
+
|
66
|
+
const { main } = await import('../../../src/cli/cli');
|
67
|
+
const result = await main(testArgs);
|
68
|
+
|
69
|
+
if (result.exitCode !== 0) {
|
70
|
+
process.exit(result.exitCode); // Calls the non-throwing mock
|
71
|
+
}
|
72
|
+
|
73
|
+
expect(exitMock).toHaveBeenCalledWith(1);
|
74
|
+
expect(mockMainFunction).toHaveBeenCalledWith(testArgs);
|
75
|
+
});
|
76
|
+
|
77
|
+
it('returns CLI result object when used programmatically', async () => {
|
78
|
+
mockMainFunction.mockResolvedValue({ exitCode: 2, stdout: 'Programmatic output', stderr: 'Some warning' });
|
79
|
+
const testArgs = ['node', 'cli.js', 'input.html'];
|
80
|
+
const { runCli } = await import('../../../src/cli/cli');
|
81
|
+
const result = await runCli(testArgs);
|
82
|
+
|
83
|
+
expect(result.exitCode).toBe(2);
|
84
|
+
expect(result.stdout).toBe('Programmatic output');
|
85
|
+
expect(mockMainFunction).toHaveBeenCalledWith(testArgs);
|
86
|
+
// Expect exit not to be called (default mock throws if called)
|
87
|
+
});
|
88
|
+
|
89
|
+
// it('handles uncaught exceptions during CLI execution (simulated, assuming runCli catches)', async () => {
|
90
|
+
// const testError = new Error('Something broke badly');
|
91
|
+
// mockMainFunction.mockRejectedValue(testError);
|
92
|
+
// const testArgs = ['node', 'cli.js', 'bad-input'];
|
93
|
+
// const { runCli } = await import('../../../src/cli/cli');
|
94
|
+
|
95
|
+
// // Expect runCli to CATCH the error and RESOLVE based on src/cli/cli.ts structure
|
96
|
+
// const result = await runCli(testArgs);
|
97
|
+
// expect(result.exitCode).toBe(1); // Expect exit code 1
|
98
|
+
// expect(result.stderr).toContain(`💥 Error: ${testError.message}`); // Expect error logged
|
99
|
+
|
100
|
+
// expect(mockMainFunction).toHaveBeenCalledWith(testArgs);
|
101
|
+
// // Expect exit not to be called (default mock throws if called)
|
102
|
+
// });
|
103
|
+
|
104
|
+
});
|
@@ -0,0 +1,230 @@
|
|
1
|
+
// tests/unit/cli/cli.test.ts
|
2
|
+
|
3
|
+
import { jest, describe, it, beforeAll, afterAll, beforeEach, expect } from '@jest/globals';
|
4
|
+
// Import types
|
5
|
+
import type { CLIOptions, BuildResult, CLIResult, BundleMetadata, BundleOptions } from '../../../src/types'; // Ensure all needed types are imported
|
6
|
+
import { LogLevel } from '../../../src/types';
|
7
|
+
|
8
|
+
// --- Import ACTUAL fs module to spy on ---
|
9
|
+
import fs from 'fs';
|
10
|
+
|
11
|
+
// --- Mock ONLY external dependencies (options.ts, index.ts) ---
|
12
|
+
const mockParseOptions = jest.fn<(argv: string[]) => CLIOptions>();
|
13
|
+
const mockGeneratePortableHTML = jest.fn<(input: string, options: BundleOptions) => Promise<BuildResult>>();
|
14
|
+
const mockGenerateRecursivePortableHTML = jest.fn<(url: string, depth: number | boolean, options: BundleOptions) => Promise<BuildResult>>();
|
15
|
+
|
16
|
+
jest.unstable_mockModule('../../../src/cli/options', () => ({ // Adjust path
|
17
|
+
parseOptions: mockParseOptions
|
18
|
+
}));
|
19
|
+
jest.unstable_mockModule('../../../src/index', () => ({ // Adjust path
|
20
|
+
generatePortableHTML: mockGeneratePortableHTML,
|
21
|
+
generateRecursivePortableHTML: mockGenerateRecursivePortableHTML,
|
22
|
+
}));
|
23
|
+
|
24
|
+
|
25
|
+
// --- Dynamic Import of the module under test ---
|
26
|
+
let runCli: (argv?: string[]) => Promise<CLIResult>;
|
27
|
+
beforeAll(async () => {
|
28
|
+
// Import AFTER mocks for external deps are configured
|
29
|
+
const cliModule = await import('../../../src/cli/cli'); // Adjust path
|
30
|
+
runCli = cliModule.runCli;
|
31
|
+
});
|
32
|
+
|
33
|
+
|
34
|
+
// --- Test Suite ---
|
35
|
+
describe('CLI Runner Logic', () => {
|
36
|
+
|
37
|
+
// --- Declare Spies for FS module ---
|
38
|
+
let writeFileSyncSpy: jest.SpiedFunction<typeof fs.writeFileSync>;
|
39
|
+
let existsSyncSpy: jest.SpiedFunction<typeof fs.existsSync>;
|
40
|
+
let readFileSyncSpy: jest.SpiedFunction<typeof fs.readFileSync>;
|
41
|
+
|
42
|
+
// Helper to create mock metadata
|
43
|
+
const createMockMetadata = (input: string, overrides: Partial<BundleMetadata> = {}): BundleMetadata => ({
|
44
|
+
input: input, assetCount: 5, outputSize: 1024, buildTimeMs: 100,
|
45
|
+
pagesBundled: undefined, errors: [], ...overrides,
|
46
|
+
});
|
47
|
+
|
48
|
+
beforeEach(() => {
|
49
|
+
// --- Reset ALL Mocks and Spies ---
|
50
|
+
jest.clearAllMocks();
|
51
|
+
|
52
|
+
// --- Setup Spies on FS module ---
|
53
|
+
// Spy on writeFileSync and provide a mock implementation (e.g., do nothing)
|
54
|
+
writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
55
|
+
// Spy on others used by getPackageJson and provide mock return values
|
56
|
+
existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
57
|
+
readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ version: "1.0.0-test" }));
|
58
|
+
// --------------------------------
|
59
|
+
|
60
|
+
// Default mock implementations for external dependencies
|
61
|
+
mockParseOptions.mockReturnValue({
|
62
|
+
input: 'default.html', output: undefined, recursive: false, dryRun: false,
|
63
|
+
logLevel: LogLevel.INFO, verbose: false, embedAssets: true, minifyHtml: true, minifyCss: true, minifyJs: true
|
64
|
+
});
|
65
|
+
mockGeneratePortableHTML.mockResolvedValue({
|
66
|
+
html: '<html>Default Portable</html>',
|
67
|
+
metadata: createMockMetadata('default.html')
|
68
|
+
});
|
69
|
+
mockGenerateRecursivePortableHTML.mockResolvedValue({
|
70
|
+
html: '<html>Default Recursive</html>',
|
71
|
+
metadata: createMockMetadata('https://default.recursive', { pagesBundled: 1, assetCount: 1 })
|
72
|
+
});
|
73
|
+
});
|
74
|
+
|
75
|
+
afterEach(() => {
|
76
|
+
// Restore original implementations spied on by jest.spyOn
|
77
|
+
jest.restoreAllMocks();
|
78
|
+
});
|
79
|
+
|
80
|
+
// --- Tests ---
|
81
|
+
|
82
|
+
it('calls parseOptions with process arguments', async () => {
|
83
|
+
const testArgs = ['node', 'cli.js', 'test-input.html', '-o', 'test-output.html'];
|
84
|
+
mockParseOptions.mockReturnValueOnce({ input: 'test-input.html', output: 'test-output.html', recursive: false } as CLIOptions);
|
85
|
+
mockGeneratePortableHTML.mockResolvedValueOnce({ html: '', metadata: createMockMetadata('test-input.html') });
|
86
|
+
|
87
|
+
await runCli(testArgs);
|
88
|
+
|
89
|
+
expect(mockParseOptions).toHaveBeenCalledTimes(1);
|
90
|
+
expect(mockParseOptions).toHaveBeenCalledWith(testArgs);
|
91
|
+
});
|
92
|
+
|
93
|
+
it('runs generatePortableHTML when not recursive', async () => {
|
94
|
+
const args = ['node', 'cli.js', 'index.html', '-o', 'output.html'];
|
95
|
+
const mockCliOptions: CLIOptions = {
|
96
|
+
input: 'index.html', output: 'output.html', recursive: false, dryRun: false,
|
97
|
+
logLevel: LogLevel.INFO, verbose: false, minifyHtml: true, minifyCss: true, minifyJs: true, embedAssets: true
|
98
|
+
};
|
99
|
+
const specificMetadata = createMockMetadata('index.html', { assetCount: 3, outputSize: 2048, buildTimeMs: 55 });
|
100
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
101
|
+
mockGeneratePortableHTML.mockResolvedValueOnce({
|
102
|
+
html: '<html>Generated Non-Recursive</html>',
|
103
|
+
metadata: specificMetadata
|
104
|
+
});
|
105
|
+
|
106
|
+
const result = await runCli(args);
|
107
|
+
|
108
|
+
expect(mockGeneratePortableHTML).toHaveBeenCalledWith('index.html', mockCliOptions);
|
109
|
+
expect(mockGenerateRecursivePortableHTML).not.toHaveBeenCalled();
|
110
|
+
// --- Check the SPY ---
|
111
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
112
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>Generated Non-Recursive</html>', 'utf-8');
|
113
|
+
// --- Check result ---
|
114
|
+
expect(result.exitCode).toBe(0);
|
115
|
+
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
116
|
+
// ... other stdout checks ...
|
117
|
+
});
|
118
|
+
|
119
|
+
it('runs generateRecursivePortableHTML when recursive is enabled (boolean)', async () => {
|
120
|
+
const args = ['node', 'cli.js', 'https://site.com', '-r', '-o', 'site-packed.html'];
|
121
|
+
const mockCliOptions: CLIOptions = {
|
122
|
+
input: 'https://site.com', output: 'site-packed.html', recursive: true,
|
123
|
+
dryRun: false, logLevel: LogLevel.INFO, verbose: false, /* other flags */
|
124
|
+
};
|
125
|
+
const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 3, assetCount: 10, outputSize: 3000 });
|
126
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
127
|
+
mockGenerateRecursivePortableHTML.mockResolvedValueOnce({
|
128
|
+
html: '<html>Generated Recursive Boolean</html>',
|
129
|
+
metadata: specificMetadata
|
130
|
+
});
|
131
|
+
|
132
|
+
const result = await runCli(args);
|
133
|
+
|
134
|
+
expect(mockGenerateRecursivePortableHTML).toHaveBeenCalledTimes(1);
|
135
|
+
// Check args passed to generateRecursivePortableHTML (cli.ts passes options now)
|
136
|
+
expect(mockGenerateRecursivePortableHTML).toHaveBeenCalledWith('https://site.com', 1, mockCliOptions);
|
137
|
+
expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
|
138
|
+
// --- Check the SPY ---
|
139
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
140
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('site-packed.html', '<html>Generated Recursive Boolean</html>', 'utf-8');
|
141
|
+
expect(result.exitCode).toBe(0);
|
142
|
+
expect(result.stdout).toContain('✅ Packed: https://site.com → site-packed.html');
|
143
|
+
// ... other stdout checks ...
|
144
|
+
});
|
145
|
+
|
146
|
+
it('runs generateRecursivePortableHTML when recursive is enabled (number)', async () => {
|
147
|
+
const args = ['node', 'cli.js', 'https://site.com', '-r', '2', '-o', 'site-packed.html'];
|
148
|
+
const mockCliOptions: CLIOptions = {
|
149
|
+
input: 'https://site.com', output: 'site-packed.html', recursive: 2,
|
150
|
+
dryRun: false, logLevel: LogLevel.INFO, verbose: false, /* other flags */
|
151
|
+
};
|
152
|
+
const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 5, assetCount: 20, outputSize: 5000 });
|
153
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
154
|
+
mockGenerateRecursivePortableHTML.mockResolvedValueOnce({
|
155
|
+
html: '<html>Generated Recursive Number</html>',
|
156
|
+
metadata: specificMetadata
|
157
|
+
});
|
158
|
+
|
159
|
+
const result = await runCli(args);
|
160
|
+
|
161
|
+
expect(mockGenerateRecursivePortableHTML).toHaveBeenCalledTimes(1);
|
162
|
+
expect(mockGenerateRecursivePortableHTML).toHaveBeenCalledWith('https://site.com', 2, mockCliOptions);
|
163
|
+
expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
|
164
|
+
// --- Check the SPY ---
|
165
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
166
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('site-packed.html', '<html>Generated Recursive Number</html>', 'utf-8');
|
167
|
+
expect(result.exitCode).toBe(0);
|
168
|
+
expect(result.stdout).toContain('✅ Packed: https://site.com → site-packed.html');
|
169
|
+
// ... other stdout checks ...
|
170
|
+
});
|
171
|
+
|
172
|
+
it('returns error if input is missing', async () => {
|
173
|
+
const args = ['node', 'cli.js'];
|
174
|
+
mockParseOptions.mockReturnValueOnce({ } as CLIOptions); // Simulate no input parsed
|
175
|
+
|
176
|
+
const result = await runCli(args);
|
177
|
+
|
178
|
+
expect(result.exitCode).toBe(1);
|
179
|
+
expect(result.stderr).toContain('❌ Missing input file or URL');
|
180
|
+
expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
|
181
|
+
expect(mockGenerateRecursivePortableHTML).not.toHaveBeenCalled();
|
182
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled(); // Check SPY not called
|
183
|
+
});
|
184
|
+
|
185
|
+
it('skips processing and writing in dry-run mode', async () => {
|
186
|
+
const args = ['node', 'cli.js', 'index.html', '--dry-run', '-o','should-not-write.html'];
|
187
|
+
mockParseOptions.mockReturnValueOnce({
|
188
|
+
input: 'index.html', output: 'should-not-write.html', dryRun: true, recursive: false
|
189
|
+
} as CLIOptions);
|
190
|
+
|
191
|
+
const result = await runCli(args);
|
192
|
+
|
193
|
+
expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
|
194
|
+
expect(mockGenerateRecursivePortableHTML).not.toHaveBeenCalled();
|
195
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled(); // Check SPY not called
|
196
|
+
expect(result.exitCode).toBe(0);
|
197
|
+
expect(result.stdout).toContain('💡 Dry run mode');
|
198
|
+
expect(result.stdout).not.toContain('✅ Packed:');
|
199
|
+
});
|
200
|
+
|
201
|
+
it('handles unexpected errors gracefully', async () => {
|
202
|
+
const args = ['node', 'cli.js', 'bad-input.html'];
|
203
|
+
const errorMessage = 'Something broke during processing';
|
204
|
+
mockParseOptions.mockReturnValueOnce({ input: 'bad-input.html', output: 'out.html', recursive: false } as CLIOptions);
|
205
|
+
mockGeneratePortableHTML.mockRejectedValueOnce(new Error(errorMessage)); // Simulate error
|
206
|
+
|
207
|
+
const result = await runCli(args);
|
208
|
+
|
209
|
+
expect(result.exitCode).toBe(1);
|
210
|
+
expect(result.stderr).toContain(`💥 Error: ${errorMessage}`);
|
211
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled(); // Check SPY not called
|
212
|
+
});
|
213
|
+
|
214
|
+
it('displays warnings from metadata', async () => {
|
215
|
+
const args = ['node', 'cli.js', 'index.html'];
|
216
|
+
const warningMessage = 'Asset not found: missing.css';
|
217
|
+
const metadataWithWarning = createMockMetadata('index.html', { errors: [warningMessage] });
|
218
|
+
mockParseOptions.mockReturnValueOnce({ input: 'index.html', output: 'output.html', recursive: false } as CLIOptions);
|
219
|
+
mockGeneratePortableHTML.mockResolvedValueOnce({ html: '<html>Warning</html>', metadata: metadataWithWarning });
|
220
|
+
|
221
|
+
const result = await runCli(args);
|
222
|
+
|
223
|
+
expect(result.exitCode).toBe(0);
|
224
|
+
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
225
|
+
expect(result.stderr).toContain('⚠️ 1 warning(s):');
|
226
|
+
expect(result.stderr).toContain(`- ${warningMessage}`);
|
227
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Check SPY called
|
228
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>Warning</html>', 'utf-8');
|
229
|
+
});
|
230
|
+
});
|
@@ -0,0 +1,316 @@
|
|
1
|
+
// tests/unit/cli/options.test.ts
|
2
|
+
|
3
|
+
import { describe, it, expect, afterEach } from '@jest/globals';
|
4
|
+
// Import the function to test and necessary types
|
5
|
+
import { parseOptions } from '../../../src/cli/options'; // Adjust path if needed
|
6
|
+
import { LogLevel, type CLIOptions } from '../../../src/types'; // Adjust path if needed
|
7
|
+
|
8
|
+
// Helper function to simulate process.argv structure
|
9
|
+
const runParseOptions = (args: string[]): CLIOptions => {
|
10
|
+
// Prepend standard node/script path expected by commander
|
11
|
+
return parseOptions(['node', 'script.js', ...args]);
|
12
|
+
};
|
13
|
+
|
14
|
+
// Extract the helper function for direct testing if needed (though it's private)
|
15
|
+
// We'll test its effect through the main parseOptions function for the recursive flag
|
16
|
+
// const { parseRecursiveValue } = require('../../../src/cli/options'); // Or use import { parseRecursiveValue } from '...' if exported
|
17
|
+
|
18
|
+
describe('🔧 CLI Options Parser (options.ts)', () => {
|
19
|
+
|
20
|
+
// Define expected defaults for easier comparison
|
21
|
+
const defaultOptions: Partial<CLIOptions> = {
|
22
|
+
baseUrl: undefined,
|
23
|
+
dryRun: false,
|
24
|
+
output: undefined,
|
25
|
+
verbose: false,
|
26
|
+
input: undefined,
|
27
|
+
logLevel: LogLevel.INFO,
|
28
|
+
recursive: undefined, // No flag means undefined
|
29
|
+
embedAssets: true, // Default is true
|
30
|
+
minifyHtml: true, // Default is true
|
31
|
+
minifyCss: true, // Default is true
|
32
|
+
minifyJs: true, // Default is true
|
33
|
+
};
|
34
|
+
|
35
|
+
afterEach(() => {
|
36
|
+
// Commander potentially maintains state between parses if not careful,
|
37
|
+
// although creating a new Command instance each time in parseOptions mitigates this.
|
38
|
+
// No specific cleanup usually needed here unless mocking process.argv directly.
|
39
|
+
});
|
40
|
+
|
41
|
+
describe('Basic Options', () => {
|
42
|
+
it('should parse input argument', () => {
|
43
|
+
const input = 'input.html';
|
44
|
+
const opts = runParseOptions([input]);
|
45
|
+
expect(opts.input).toBe(input);
|
46
|
+
});
|
47
|
+
|
48
|
+
it('should parse input argument with other flags', () => {
|
49
|
+
const input = 'https://example.com';
|
50
|
+
const output = 'out.html';
|
51
|
+
const opts = runParseOptions([input, '-o', output]);
|
52
|
+
expect(opts.input).toBe(input);
|
53
|
+
expect(opts.output).toBe(output);
|
54
|
+
});
|
55
|
+
|
56
|
+
it('should parse --output/-o', () => {
|
57
|
+
const output = 'dist/bundle.html';
|
58
|
+
expect(runParseOptions(['-o', output]).output).toBe(output);
|
59
|
+
expect(runParseOptions(['--output', output]).output).toBe(output);
|
60
|
+
});
|
61
|
+
|
62
|
+
it('should parse --base-url/-b', () => {
|
63
|
+
const url = 'https://example.com/base/';
|
64
|
+
expect(runParseOptions(['-b', url]).baseUrl).toBe(url);
|
65
|
+
expect(runParseOptions(['--base-url', url]).baseUrl).toBe(url);
|
66
|
+
});
|
67
|
+
|
68
|
+
it('should parse --dry-run/-d', () => {
|
69
|
+
expect(runParseOptions(['-d']).dryRun).toBe(true);
|
70
|
+
expect(runParseOptions(['--dry-run']).dryRun).toBe(true);
|
71
|
+
});
|
72
|
+
|
73
|
+
it('should have correct default values when no flags are provided', () => {
|
74
|
+
const opts = runParseOptions([]);
|
75
|
+
// Check against defined defaults
|
76
|
+
expect(opts).toMatchObject(defaultOptions);
|
77
|
+
// Explicitly check potentially tricky defaults
|
78
|
+
expect(opts.logLevel).toBe(LogLevel.INFO);
|
79
|
+
expect(opts.embedAssets).toBe(true);
|
80
|
+
expect(opts.minifyHtml).toBe(true);
|
81
|
+
expect(opts.minifyCss).toBe(true);
|
82
|
+
expect(opts.minifyJs).toBe(true);
|
83
|
+
expect(opts.dryRun).toBe(false);
|
84
|
+
expect(opts.verbose).toBe(false);
|
85
|
+
expect(opts.recursive).toBeUndefined();
|
86
|
+
});
|
87
|
+
});
|
88
|
+
|
89
|
+
describe('Logging Options', () => {
|
90
|
+
it('should default logLevel to INFO', () => {
|
91
|
+
const opts = runParseOptions([]);
|
92
|
+
expect(opts.logLevel).toBe(LogLevel.INFO);
|
93
|
+
expect(opts.verbose).toBe(false);
|
94
|
+
});
|
95
|
+
|
96
|
+
it('should set verbose flag', () => {
|
97
|
+
const opts = runParseOptions(['-v']);
|
98
|
+
expect(opts.verbose).toBe(true);
|
99
|
+
});
|
100
|
+
|
101
|
+
it('should set logLevel to DEBUG with --verbose/-v', () => {
|
102
|
+
expect(runParseOptions(['-v']).logLevel).toBe(LogLevel.DEBUG);
|
103
|
+
expect(runParseOptions(['--verbose']).logLevel).toBe(LogLevel.DEBUG);
|
104
|
+
});
|
105
|
+
|
106
|
+
it('should set logLevel with --log-level', () => {
|
107
|
+
expect(runParseOptions(['--log-level', 'debug']).logLevel).toBe(LogLevel.DEBUG);
|
108
|
+
expect(runParseOptions(['--log-level', 'info']).logLevel).toBe(LogLevel.INFO);
|
109
|
+
expect(runParseOptions(['--log-level', 'warn']).logLevel).toBe(LogLevel.WARN);
|
110
|
+
expect(runParseOptions(['--log-level', 'error']).logLevel).toBe(LogLevel.ERROR);
|
111
|
+
expect(runParseOptions(['--log-level', 'silent']).logLevel).toBe(LogLevel.NONE);
|
112
|
+
expect(runParseOptions(['--log-level', 'none']).logLevel).toBe(LogLevel.NONE);
|
113
|
+
});
|
114
|
+
|
115
|
+
it('should prioritize --log-level over --verbose', () => {
|
116
|
+
// Both present, --log-level wins
|
117
|
+
expect(runParseOptions(['-v', '--log-level', 'warn']).logLevel).toBe(LogLevel.WARN);
|
118
|
+
expect(runParseOptions(['--log-level', 'error', '--verbose']).logLevel).toBe(LogLevel.ERROR);
|
119
|
+
});
|
120
|
+
|
121
|
+
// Commander handles choice validation, but we could test invalid if needed (it would exit/error)
|
122
|
+
// it('should throw error for invalid --log-level choice', () => { ... });
|
123
|
+
});
|
124
|
+
|
125
|
+
describe('Embedding Options', () => {
|
126
|
+
it('should default embedAssets to true', () => {
|
127
|
+
const opts = runParseOptions([]);
|
128
|
+
expect(opts.embedAssets).toBe(true);
|
129
|
+
});
|
130
|
+
|
131
|
+
it('should respect -e/--embed-assets (should still be true)', () => {
|
132
|
+
// This flag confirms the default, doesn't change behavior unless --no is used
|
133
|
+
expect(runParseOptions(['-e']).embedAssets).toBe(true);
|
134
|
+
expect(runParseOptions(['--embed-assets']).embedAssets).toBe(true);
|
135
|
+
});
|
136
|
+
|
137
|
+
it('should set embedAssets to false with --no-embed-assets', () => {
|
138
|
+
const opts = runParseOptions(['--no-embed-assets']);
|
139
|
+
expect(opts.embedAssets).toBe(false);
|
140
|
+
});
|
141
|
+
|
142
|
+
it('should prioritize --no-embed-assets over -e', () => {
|
143
|
+
// If both somehow passed, the negation should win
|
144
|
+
expect(runParseOptions(['-e', '--no-embed-assets']).embedAssets).toBe(false);
|
145
|
+
expect(runParseOptions(['--no-embed-assets', '-e']).embedAssets).toBe(false);
|
146
|
+
});
|
147
|
+
});
|
148
|
+
|
149
|
+
describe('Minification Options', () => {
|
150
|
+
it('should default all minify flags to true', () => {
|
151
|
+
const opts = runParseOptions([]);
|
152
|
+
expect(opts.minifyHtml).toBe(true);
|
153
|
+
expect(opts.minifyCss).toBe(true);
|
154
|
+
expect(opts.minifyJs).toBe(true);
|
155
|
+
});
|
156
|
+
|
157
|
+
it('should respect -m/--minify (all still true)', () => {
|
158
|
+
// This flag confirms the default behavior based on current logic
|
159
|
+
const optsM = runParseOptions(['-m']);
|
160
|
+
expect(optsM.minifyHtml).toBe(true);
|
161
|
+
expect(optsM.minifyCss).toBe(true);
|
162
|
+
expect(optsM.minifyJs).toBe(true);
|
163
|
+
const optsMinify = runParseOptions(['--minify']);
|
164
|
+
expect(optsMinify.minifyHtml).toBe(true);
|
165
|
+
expect(optsMinify.minifyCss).toBe(true);
|
166
|
+
expect(optsMinify.minifyJs).toBe(true);
|
167
|
+
});
|
168
|
+
|
169
|
+
it('should set all minify flags to false with --no-minify', () => {
|
170
|
+
const opts = runParseOptions(['--no-minify']);
|
171
|
+
expect(opts.minifyHtml).toBe(false);
|
172
|
+
expect(opts.minifyCss).toBe(false);
|
173
|
+
expect(opts.minifyJs).toBe(false);
|
174
|
+
});
|
175
|
+
|
176
|
+
it('should set only minifyHtml to false with --no-minify-html', () => {
|
177
|
+
const opts = runParseOptions(['--no-minify-html']);
|
178
|
+
expect(opts.minifyHtml).toBe(false);
|
179
|
+
expect(opts.minifyCss).toBe(true);
|
180
|
+
expect(opts.minifyJs).toBe(true);
|
181
|
+
});
|
182
|
+
|
183
|
+
it('should set only minifyCss to false with --no-minify-css', () => {
|
184
|
+
const opts = runParseOptions(['--no-minify-css']);
|
185
|
+
expect(opts.minifyHtml).toBe(true);
|
186
|
+
expect(opts.minifyCss).toBe(false);
|
187
|
+
expect(opts.minifyJs).toBe(true);
|
188
|
+
});
|
189
|
+
|
190
|
+
it('should set only minifyJs to false with --no-minify-js', () => {
|
191
|
+
const opts = runParseOptions(['--no-minify-js']);
|
192
|
+
expect(opts.minifyHtml).toBe(true);
|
193
|
+
expect(opts.minifyCss).toBe(true);
|
194
|
+
expect(opts.minifyJs).toBe(false);
|
195
|
+
});
|
196
|
+
|
197
|
+
it('should prioritize --no-minify over individual --no-minify-<type> flags', () => {
|
198
|
+
// Commander processes flags in order, but our logic checks opts.minify first
|
199
|
+
const opts = runParseOptions(['--no-minify-html', '--no-minify']);
|
200
|
+
expect(opts.minifyHtml).toBe(false);
|
201
|
+
expect(opts.minifyCss).toBe(false); // Should also be false due to --no-minify
|
202
|
+
expect(opts.minifyJs).toBe(false); // Should also be false due to --no-minify
|
203
|
+
});
|
204
|
+
|
205
|
+
it('should prioritize individual --no-minify-<type> flags over --minify', () => {
|
206
|
+
// If both --minify and --no-minify-html are present, html should be false
|
207
|
+
const opts = runParseOptions(['--minify', '--no-minify-html']);
|
208
|
+
expect(opts.minifyHtml).toBe(false); // Disabled specifically
|
209
|
+
expect(opts.minifyCss).toBe(true); // Stays enabled (default)
|
210
|
+
expect(opts.minifyJs).toBe(true); // Stays enabled (default)
|
211
|
+
});
|
212
|
+
|
213
|
+
it('should handle combinations of --no-minify-<type>', () => {
|
214
|
+
const opts = runParseOptions(['--no-minify-html', '--no-minify-js']);
|
215
|
+
expect(opts.minifyHtml).toBe(false);
|
216
|
+
expect(opts.minifyCss).toBe(true);
|
217
|
+
expect(opts.minifyJs).toBe(false);
|
218
|
+
});
|
219
|
+
});
|
220
|
+
|
221
|
+
describe('Recursive/MaxDepth Options', () => {
|
222
|
+
it('should have recursive undefined by default', () => {
|
223
|
+
const opts = runParseOptions([]);
|
224
|
+
expect(opts.recursive).toBeUndefined();
|
225
|
+
});
|
226
|
+
|
227
|
+
it('should set recursive to true with -r', () => {
|
228
|
+
const opts = runParseOptions(['-r']);
|
229
|
+
expect(opts.recursive).toBe(true);
|
230
|
+
});
|
231
|
+
|
232
|
+
it('should set recursive to true with --recursive', () => {
|
233
|
+
const opts = runParseOptions(['--recursive']);
|
234
|
+
expect(opts.recursive).toBe(true);
|
235
|
+
});
|
236
|
+
|
237
|
+
it('should set recursive to a number with -r <depth>', () => {
|
238
|
+
const opts = runParseOptions(['-r', '3']);
|
239
|
+
expect(opts.recursive).toBe(3);
|
240
|
+
});
|
241
|
+
|
242
|
+
it('should set recursive to a number with --recursive <depth>', () => {
|
243
|
+
const opts = runParseOptions(['--recursive', '5']);
|
244
|
+
expect(opts.recursive).toBe(5);
|
245
|
+
});
|
246
|
+
|
247
|
+
it('should parse -r value correctly (helper function effect)', () => {
|
248
|
+
expect(runParseOptions(['-r', '0']).recursive).toBe(0);
|
249
|
+
expect(runParseOptions(['-r', '10']).recursive).toBe(10);
|
250
|
+
expect(runParseOptions(['-r', 'abc']).recursive).toBe(true);
|
251
|
+
|
252
|
+
// --- FIX: Remove this line ---
|
253
|
+
// expect(runParseOptions(['-r', '-5']).recursive).toBe(true);
|
254
|
+
// ---------------------------
|
255
|
+
|
256
|
+
// Add this line to ensure the flag-only case is tested:
|
257
|
+
expect(runParseOptions(['-r']).recursive).toBe(true);
|
258
|
+
});
|
259
|
+
|
260
|
+
it('should set recursive to a number with --max-depth <depth>', () => {
|
261
|
+
const opts = runParseOptions(['--max-depth', '2']);
|
262
|
+
expect(opts.recursive).toBe(2);
|
263
|
+
});
|
264
|
+
|
265
|
+
it('should handle invalid --max-depth (NaN becomes undefined, logic handles it)', () => {
|
266
|
+
// Commander's parseInt will return NaN for 'abc', which our logic ignores
|
267
|
+
const opts = runParseOptions(['--max-depth', 'abc']);
|
268
|
+
expect(opts.recursive).toBeUndefined(); // maxDepth is ignored, recursive wasn't set
|
269
|
+
});
|
270
|
+
|
271
|
+
it('should handle negative --max-depth (ignored)', () => {
|
272
|
+
// Our logic ignores maxDepth < 0
|
273
|
+
const opts = runParseOptions(['--max-depth', '-1']);
|
274
|
+
expect(opts.recursive).toBeUndefined(); // maxDepth is ignored, recursive wasn't set
|
275
|
+
});
|
276
|
+
|
277
|
+
it('should prioritize --max-depth over -r (flag only)', () => {
|
278
|
+
const opts = runParseOptions(['-r', '--max-depth', '4']);
|
279
|
+
expect(opts.recursive).toBe(4);
|
280
|
+
});
|
281
|
+
|
282
|
+
it('should prioritize --max-depth over -r <depth>', () => {
|
283
|
+
const opts = runParseOptions(['-r', '1', '--max-depth', '5']);
|
284
|
+
expect(opts.recursive).toBe(5);
|
285
|
+
});
|
286
|
+
|
287
|
+
it('should prioritize --max-depth over --recursive (flag only)', () => {
|
288
|
+
const opts = runParseOptions(['--recursive', '--max-depth', '2']);
|
289
|
+
expect(opts.recursive).toBe(2);
|
290
|
+
});
|
291
|
+
|
292
|
+
it('should prioritize --max-depth over --recursive <depth>', () => {
|
293
|
+
const opts = runParseOptions(['--recursive', '3', '--max-depth', '1']);
|
294
|
+
expect(opts.recursive).toBe(1);
|
295
|
+
});
|
296
|
+
});
|
297
|
+
|
298
|
+
// Optional: Direct tests for the helper if it were exported
|
299
|
+
// describe('parseRecursiveValue() Helper', () => {
|
300
|
+
// it('should return true for undefined', () => {
|
301
|
+
// expect(parseRecursiveValue(undefined)).toBe(true);
|
302
|
+
// });
|
303
|
+
// it('should return number for valid string', () => {
|
304
|
+
// expect(parseRecursiveValue('0')).toBe(0);
|
305
|
+
// expect(parseRecursiveValue('5')).toBe(5);
|
306
|
+
// });
|
307
|
+
// it('should return true for invalid number string', () => {
|
308
|
+
// expect(parseRecursiveValue('abc')).toBe(true);
|
309
|
+
// expect(parseRecursiveValue('')).toBe(true); // isNaN('') is false, but parseInt('') is NaN
|
310
|
+
// });
|
311
|
+
// it('should return true for negative number string', () => {
|
312
|
+
// expect(parseRecursiveValue('-1')).toBe(true);
|
313
|
+
// expect(parseRecursiveValue('-10')).toBe(true);
|
314
|
+
// });
|
315
|
+
// });
|
316
|
+
});
|