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,275 @@
|
|
1
|
+
/**
|
2
|
+
* Tests for the logger module
|
3
|
+
*/
|
4
|
+
import { Logger } from '../../../src/utils/logger';
|
5
|
+
import { LogLevel } from '../../../src/types';
|
6
|
+
import { jest, describe, it, beforeEach, afterEach, expect } from '@jest/globals'; // Ensure expect is imported
|
7
|
+
|
8
|
+
describe('Logger', () => {
|
9
|
+
// Save original console methods
|
10
|
+
const originalConsole = {
|
11
|
+
log: console.log,
|
12
|
+
warn: console.warn,
|
13
|
+
error: console.error,
|
14
|
+
info: console.info,
|
15
|
+
debug: console.debug
|
16
|
+
};
|
17
|
+
|
18
|
+
// Create mock functions for console methods
|
19
|
+
let mockLog: jest.Mock;
|
20
|
+
let mockWarn: jest.Mock;
|
21
|
+
let mockError: jest.Mock;
|
22
|
+
let mockInfo: jest.Mock;
|
23
|
+
let mockDebug: jest.Mock;
|
24
|
+
|
25
|
+
beforeEach(() => {
|
26
|
+
// Setup mocks
|
27
|
+
mockLog = jest.fn();
|
28
|
+
mockWarn = jest.fn();
|
29
|
+
mockError = jest.fn();
|
30
|
+
mockInfo = jest.fn();
|
31
|
+
mockDebug = jest.fn();
|
32
|
+
|
33
|
+
// Override console methods
|
34
|
+
console.log = mockLog;
|
35
|
+
console.warn = mockWarn;
|
36
|
+
console.error = mockError;
|
37
|
+
console.info = mockInfo;
|
38
|
+
console.debug = mockDebug;
|
39
|
+
});
|
40
|
+
|
41
|
+
afterEach(() => {
|
42
|
+
// Restore original console methods
|
43
|
+
console.log = originalConsole.log;
|
44
|
+
console.warn = originalConsole.warn;
|
45
|
+
console.error = originalConsole.error;
|
46
|
+
console.info = originalConsole.info;
|
47
|
+
console.debug = originalConsole.debug;
|
48
|
+
});
|
49
|
+
|
50
|
+
describe('Logger instantiation', () => {
|
51
|
+
it('creates a logger with default log level INFO', () => {
|
52
|
+
const logger = new Logger();
|
53
|
+
expect(logger).toBeDefined();
|
54
|
+
// @ts-expect-error Access private member for testing
|
55
|
+
expect(logger.level).toBe(LogLevel.INFO);
|
56
|
+
});
|
57
|
+
|
58
|
+
it('creates a logger with specific log level', () => {
|
59
|
+
const logger = new Logger(LogLevel.DEBUG);
|
60
|
+
expect(logger).toBeDefined();
|
61
|
+
// @ts-expect-error Access private member for testing
|
62
|
+
expect(logger.level).toBe(LogLevel.DEBUG);
|
63
|
+
});
|
64
|
+
|
65
|
+
// Test constructor guards against invalid levels
|
66
|
+
it('defaults to INFO if constructor receives invalid level', () => {
|
67
|
+
// @ts-expect-error Testing invalid input
|
68
|
+
const logger = new Logger(99);
|
69
|
+
// @ts-expect-error Access private member for testing
|
70
|
+
expect(logger.level).toBe(LogLevel.INFO);
|
71
|
+
});
|
72
|
+
it('defaults to INFO if constructor receives undefined', () => {
|
73
|
+
const logger = new Logger(undefined);
|
74
|
+
// @ts-expect-error Access private member for testing
|
75
|
+
expect(logger.level).toBe(LogLevel.INFO);
|
76
|
+
});
|
77
|
+
});
|
78
|
+
|
79
|
+
describe('Log methods', () => {
|
80
|
+
// Existing tests for error, warn, info, debug, none are good... keep them
|
81
|
+
|
82
|
+
it('logs error messages only when level >= ERROR', () => {
|
83
|
+
const loggerError = new Logger(LogLevel.ERROR);
|
84
|
+
const loggerNone = new Logger(LogLevel.NONE);
|
85
|
+
|
86
|
+
loggerError.error('Test error');
|
87
|
+
expect(mockError).toHaveBeenCalledTimes(1);
|
88
|
+
expect(mockError).toHaveBeenCalledWith('[ERROR] Test error');
|
89
|
+
|
90
|
+
loggerError.warn('Test warn'); // Should not log
|
91
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
92
|
+
|
93
|
+
mockError.mockClear(); // Clear for next check
|
94
|
+
loggerNone.error('Test error none'); // Should not log
|
95
|
+
expect(mockError).not.toHaveBeenCalled();
|
96
|
+
});
|
97
|
+
|
98
|
+
it('logs warn messages only when level >= WARN', () => {
|
99
|
+
const loggerWarn = new Logger(LogLevel.WARN);
|
100
|
+
const loggerError = new Logger(LogLevel.ERROR); // Lower level
|
101
|
+
|
102
|
+
loggerWarn.warn('Test warn');
|
103
|
+
expect(mockWarn).toHaveBeenCalledTimes(1);
|
104
|
+
expect(mockWarn).toHaveBeenCalledWith('[WARN] Test warn');
|
105
|
+
loggerWarn.error('Test error'); // Should also log
|
106
|
+
expect(mockError).toHaveBeenCalledTimes(1);
|
107
|
+
|
108
|
+
mockWarn.mockClear();
|
109
|
+
loggerError.warn('Test warn error level'); // Should not log
|
110
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
111
|
+
});
|
112
|
+
|
113
|
+
it('logs info messages only when level >= INFO', () => {
|
114
|
+
const loggerInfo = new Logger(LogLevel.INFO);
|
115
|
+
const loggerWarn = new Logger(LogLevel.WARN); // Lower level
|
116
|
+
|
117
|
+
loggerInfo.info('Test info');
|
118
|
+
expect(mockInfo).toHaveBeenCalledTimes(1);
|
119
|
+
expect(mockInfo).toHaveBeenCalledWith('[INFO] Test info');
|
120
|
+
loggerInfo.warn('Test warn'); // Should also log
|
121
|
+
expect(mockWarn).toHaveBeenCalledTimes(1);
|
122
|
+
|
123
|
+
mockInfo.mockClear();
|
124
|
+
loggerWarn.info('Test info warn level'); // Should not log
|
125
|
+
expect(mockInfo).not.toHaveBeenCalled();
|
126
|
+
});
|
127
|
+
|
128
|
+
it('logs debug messages only when level >= DEBUG', () => {
|
129
|
+
const loggerDebug = new Logger(LogLevel.DEBUG);
|
130
|
+
const loggerInfo = new Logger(LogLevel.INFO); // Lower level
|
131
|
+
|
132
|
+
loggerDebug.debug('Test debug');
|
133
|
+
expect(mockDebug).toHaveBeenCalledTimes(1);
|
134
|
+
expect(mockDebug).toHaveBeenCalledWith('[DEBUG] Test debug');
|
135
|
+
loggerDebug.info('Test info'); // Should also log
|
136
|
+
expect(mockInfo).toHaveBeenCalledTimes(1);
|
137
|
+
|
138
|
+
mockDebug.mockClear();
|
139
|
+
loggerInfo.debug('Test debug info level'); // Should not log
|
140
|
+
expect(mockDebug).not.toHaveBeenCalled();
|
141
|
+
});
|
142
|
+
|
143
|
+
it('does not log anything at NONE level', () => {
|
144
|
+
const logger = new Logger(LogLevel.NONE);
|
145
|
+
|
146
|
+
logger.error('Test error');
|
147
|
+
logger.warn('Test warn');
|
148
|
+
logger.info('Test info');
|
149
|
+
logger.debug('Test debug');
|
150
|
+
|
151
|
+
expect(mockError).not.toHaveBeenCalled();
|
152
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
153
|
+
expect(mockInfo).not.toHaveBeenCalled();
|
154
|
+
expect(mockDebug).not.toHaveBeenCalled();
|
155
|
+
});
|
156
|
+
});
|
157
|
+
|
158
|
+
describe('setLevel method', () => {
|
159
|
+
// Existing test for setLevel is good... keep it
|
160
|
+
it('changes log level dynamically', () => {
|
161
|
+
const logger = new Logger(LogLevel.NONE);
|
162
|
+
|
163
|
+
// Nothing should log at NONE level
|
164
|
+
logger.error('Test error 1');
|
165
|
+
expect(mockError).not.toHaveBeenCalled();
|
166
|
+
|
167
|
+
// Change to ERROR level
|
168
|
+
logger.setLevel(LogLevel.ERROR);
|
169
|
+
|
170
|
+
// Now ERROR should log
|
171
|
+
logger.error('Test error 2');
|
172
|
+
expect(mockError).toHaveBeenCalledTimes(1); // Called once now
|
173
|
+
expect(mockError).toHaveBeenCalledWith('[ERROR] Test error 2');
|
174
|
+
|
175
|
+
// But WARN still should not
|
176
|
+
logger.warn('Test warn');
|
177
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
178
|
+
|
179
|
+
// Change to DEBUG level
|
180
|
+
logger.setLevel(LogLevel.DEBUG);
|
181
|
+
|
182
|
+
// Now all levels should log
|
183
|
+
logger.warn('Test warn 2');
|
184
|
+
logger.info('Test info');
|
185
|
+
logger.debug('Test debug');
|
186
|
+
|
187
|
+
expect(mockWarn).toHaveBeenCalledTimes(1); // Called once now
|
188
|
+
expect(mockWarn).toHaveBeenCalledWith('[WARN] Test warn 2');
|
189
|
+
expect(mockInfo).toHaveBeenCalledTimes(1);
|
190
|
+
expect(mockInfo).toHaveBeenCalledWith('[INFO] Test info');
|
191
|
+
expect(mockDebug).toHaveBeenCalledTimes(1);
|
192
|
+
expect(mockDebug).toHaveBeenCalledWith('[DEBUG] Test debug');
|
193
|
+
});
|
194
|
+
});
|
195
|
+
|
196
|
+
// --- NEW: Tests for static factory methods ---
|
197
|
+
describe('Static factory methods', () => {
|
198
|
+
describe('Logger.fromVerboseFlag()', () => {
|
199
|
+
it('creates logger with DEBUG level if verbose is true', () => {
|
200
|
+
const logger = Logger.fromVerboseFlag({ verbose: true });
|
201
|
+
// @ts-expect-error Access private member
|
202
|
+
expect(logger.level).toBe(LogLevel.DEBUG);
|
203
|
+
});
|
204
|
+
|
205
|
+
it('creates logger with INFO level if verbose is false', () => {
|
206
|
+
const logger = Logger.fromVerboseFlag({ verbose: false });
|
207
|
+
// @ts-expect-error Access private member
|
208
|
+
expect(logger.level).toBe(LogLevel.INFO);
|
209
|
+
});
|
210
|
+
|
211
|
+
it('creates logger with INFO level if verbose is undefined', () => {
|
212
|
+
const logger = Logger.fromVerboseFlag({}); // Empty options
|
213
|
+
// @ts-expect-error Access private member
|
214
|
+
expect(logger.level).toBe(LogLevel.INFO);
|
215
|
+
});
|
216
|
+
|
217
|
+
it('creates logger with INFO level if options is undefined', () => {
|
218
|
+
const logger = Logger.fromVerboseFlag(); // No options arg
|
219
|
+
// @ts-expect-error Access private member
|
220
|
+
expect(logger.level).toBe(LogLevel.INFO);
|
221
|
+
});
|
222
|
+
});
|
223
|
+
|
224
|
+
describe('Logger.fromLevelName()', () => {
|
225
|
+
it.each([
|
226
|
+
['debug', LogLevel.DEBUG],
|
227
|
+
['info', LogLevel.INFO],
|
228
|
+
['warn', LogLevel.WARN],
|
229
|
+
['error', LogLevel.ERROR],
|
230
|
+
['none', LogLevel.NONE],
|
231
|
+
['silent', LogLevel.NONE],
|
232
|
+
['DEBUG', LogLevel.DEBUG], // Case-insensitive
|
233
|
+
['InFo', LogLevel.INFO],
|
234
|
+
])('creates logger with correct level for valid name "%s"', (name, expectedLevel) => {
|
235
|
+
const logger = Logger.fromLevelName(name);
|
236
|
+
// @ts-expect-error Access private member
|
237
|
+
expect(logger.level).toBe(expectedLevel);
|
238
|
+
expect(mockWarn).not.toHaveBeenCalled(); // No warning for valid names
|
239
|
+
});
|
240
|
+
|
241
|
+
it('defaults to INFO level if levelName is undefined', () => {
|
242
|
+
const logger = Logger.fromLevelName(undefined);
|
243
|
+
// @ts-expect-error Access private member
|
244
|
+
expect(logger.level).toBe(LogLevel.INFO);
|
245
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
246
|
+
});
|
247
|
+
|
248
|
+
it('uses provided defaultLevel if levelName is undefined', () => {
|
249
|
+
const logger = Logger.fromLevelName(undefined, LogLevel.WARN);
|
250
|
+
// @ts-expect-error Access private member
|
251
|
+
expect(logger.level).toBe(LogLevel.WARN);
|
252
|
+
expect(mockWarn).not.toHaveBeenCalled();
|
253
|
+
});
|
254
|
+
|
255
|
+
|
256
|
+
it('defaults to INFO level and logs warning for invalid name', () => {
|
257
|
+
const logger = Logger.fromLevelName('invalidLevel');
|
258
|
+
// @ts-expect-error Access private member
|
259
|
+
expect(logger.level).toBe(LogLevel.INFO); // Falls back to default INFO
|
260
|
+
// Check that console.warn was called *directly* by the static method
|
261
|
+
expect(mockWarn).toHaveBeenCalledTimes(1);
|
262
|
+
expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('[Logger] Invalid log level name "invalidLevel". Defaulting to INFO.'));
|
263
|
+
});
|
264
|
+
|
265
|
+
it('uses provided defaultLevel and logs warning for invalid name', () => {
|
266
|
+
const logger = Logger.fromLevelName('invalidLevel', LogLevel.ERROR);
|
267
|
+
// @ts-expect-error Access private member
|
268
|
+
expect(logger.level).toBe(LogLevel.ERROR); // Falls back to provided default ERROR
|
269
|
+
// Check that console.warn was called *directly* by the static method
|
270
|
+
expect(mockWarn).toHaveBeenCalledTimes(1);
|
271
|
+
expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('[Logger] Invalid log level name "invalidLevel". Defaulting to ERROR.'));
|
272
|
+
});
|
273
|
+
});
|
274
|
+
});
|
275
|
+
});
|
@@ -0,0 +1,70 @@
|
|
1
|
+
// tests/unit/utils/meta.test.ts
|
2
|
+
import { jest } from '@jest/globals';
|
3
|
+
import { BuildTimer } from '../../../src/utils/meta';
|
4
|
+
|
5
|
+
describe('BuildTimer', () => {
|
6
|
+
const mockInput = 'input.html';
|
7
|
+
|
8
|
+
it('initializes and returns correct metadata', () => {
|
9
|
+
const timer = new BuildTimer(mockInput);
|
10
|
+
|
11
|
+
// Use extra object in finish to set these, closer to real usage
|
12
|
+
// timer.setAssetCount(5); // These might be set externally
|
13
|
+
// timer.setPageCount(3); // These might be set externally
|
14
|
+
timer.addError('Test warning 1');
|
15
|
+
timer.addError('Test warning 2');
|
16
|
+
|
17
|
+
const html = '<html><body>Test</body></html>';
|
18
|
+
// Simulate passing extra metadata calculated elsewhere
|
19
|
+
const metadata = timer.finish(html, {
|
20
|
+
assetCount: 5,
|
21
|
+
pagesBundled: 3,
|
22
|
+
errors: ['External warning'] // Add an external error to test merging
|
23
|
+
});
|
24
|
+
|
25
|
+
|
26
|
+
expect(metadata.input).toBe(mockInput);
|
27
|
+
expect(metadata.assetCount).toBe(5);
|
28
|
+
expect(metadata.pagesBundled).toBe(3);
|
29
|
+
// Check merged and deduplicated errors
|
30
|
+
expect(metadata.errors).toEqual(['Test warning 1', 'Test warning 2', 'External warning']);
|
31
|
+
expect(metadata.outputSize).toBe(Buffer.byteLength(html));
|
32
|
+
expect(typeof metadata.buildTimeMs).toBe('number');
|
33
|
+
});
|
34
|
+
|
35
|
+
it('handles no errors or page count gracefully', () => {
|
36
|
+
const timer = new BuildTimer(mockInput);
|
37
|
+
// Simulate finish called without explicit counts/errors in 'extra'
|
38
|
+
const result = timer.finish('<html></html>', { assetCount: 0 }); // Pass assetCount 0 explicitly
|
39
|
+
|
40
|
+
// FIX: Expect errors to be undefined when none are added
|
41
|
+
expect(result.errors).toBeUndefined(); // <<< CHANGED from toEqual([])
|
42
|
+
expect(result.pagesBundled).toBeUndefined();
|
43
|
+
expect(result.assetCount).toBe(0); // Check the explicitly passed 0
|
44
|
+
});
|
45
|
+
|
46
|
+
// Add a test case to check internal assetCount if not provided in extra
|
47
|
+
it('uses internal asset count if not provided in extra', () => {
|
48
|
+
const timer = new BuildTimer(mockInput);
|
49
|
+
timer.setAssetCount(10); // Set internal count
|
50
|
+
const result = timer.finish('html'); // Don't provide assetCount in extra
|
51
|
+
expect(result.assetCount).toBe(10);
|
52
|
+
});
|
53
|
+
|
54
|
+
// Add a test case to check internal pageCount if not provided in extra
|
55
|
+
it('uses internal page count if not provided in extra', () => {
|
56
|
+
const timer = new BuildTimer(mockInput);
|
57
|
+
timer.setPageCount(2); // Set internal count
|
58
|
+
const result = timer.finish('html'); // Don't provide pageCount in extra
|
59
|
+
expect(result.pagesBundled).toBe(2);
|
60
|
+
});
|
61
|
+
|
62
|
+
// Add a test case to check internal errors if not provided in extra
|
63
|
+
it('uses internal errors if not provided in extra', () => {
|
64
|
+
const timer = new BuildTimer(mockInput);
|
65
|
+
timer.addError("Internal Error"); // Add internal error
|
66
|
+
const result = timer.finish('html'); // Don't provide errors in extra
|
67
|
+
expect(result.errors).toEqual(["Internal Error"]);
|
68
|
+
});
|
69
|
+
|
70
|
+
});
|
@@ -0,0 +1,96 @@
|
|
1
|
+
/**
|
2
|
+
* @file tests/unit/utils/mime.test.ts
|
3
|
+
* @description Unit tests for the MIME type guessing utility.
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { guessMimeType, getFontMimeType } from '../../../src/utils/mime';
|
7
|
+
import { describe, it, expect } from '@jest/globals';
|
8
|
+
import type { Asset } from '../../../src/types'; // Import Asset type
|
9
|
+
|
10
|
+
describe('🧪 MIME Utilities', () => {
|
11
|
+
|
12
|
+
describe('guessMimeType()', () => {
|
13
|
+
const defaultResult = { mime: 'application/octet-stream', assetType: 'other' as Asset['type'] };
|
14
|
+
|
15
|
+
// Test cases: [input, expectedMime, expectedAssetType]
|
16
|
+
const testCases: [string, string, Asset['type']][] = [
|
17
|
+
// CSS
|
18
|
+
['style.css', 'text/css', 'css'],
|
19
|
+
['path/to/style.CSS', 'text/css', 'css'], // Case-insensitive extension
|
20
|
+
['style.css?v=1.0', 'text/css', 'css'], // With query string
|
21
|
+
['/path/style.css#id', 'text/css', 'css'], // With fragment
|
22
|
+
['https://example.com/a/b/c/style.css?q=1', 'text/css', 'css'], // Remote URL
|
23
|
+
|
24
|
+
// JS
|
25
|
+
['script.js', 'application/javascript', 'js'],
|
26
|
+
['script.mjs', 'application/javascript', 'js'],
|
27
|
+
['https://cdn.com/lib.js', 'application/javascript', 'js'],
|
28
|
+
|
29
|
+
// Images
|
30
|
+
['logo.png', 'image/png', 'image'],
|
31
|
+
['photo.jpg', 'image/jpeg', 'image'],
|
32
|
+
['image.jpeg', 'image/jpeg', 'image'],
|
33
|
+
['anim.gif', 'image/gif', 'image'],
|
34
|
+
['icon.svg', 'image/svg+xml', 'image'],
|
35
|
+
['image.webp', 'image/webp', 'image'],
|
36
|
+
['favicon.ico', 'image/x-icon', 'image'],
|
37
|
+
['image.avif', 'image/avif', 'image'],
|
38
|
+
|
39
|
+
// Fonts
|
40
|
+
['font.woff', 'font/woff', 'font'],
|
41
|
+
['font.woff2', 'font/woff2', 'font'],
|
42
|
+
['font.ttf', 'font/ttf', 'font'],
|
43
|
+
['font.otf', 'font/otf', 'font'],
|
44
|
+
['font.eot', 'application/vnd.ms-fontobject', 'font'],
|
45
|
+
|
46
|
+
// Audio/Video ('other')
|
47
|
+
['audio.mp3', 'audio/mpeg', 'other'],
|
48
|
+
['audio.ogg', 'audio/ogg', 'other'],
|
49
|
+
['audio.wav', 'audio/wav', 'other'],
|
50
|
+
['video.mp4', 'video/mp4', 'other'],
|
51
|
+
['video.webm', 'video/webm', 'other'],
|
52
|
+
|
53
|
+
// Other ('other')
|
54
|
+
['data.json', 'application/json', 'other'],
|
55
|
+
['manifest.webmanifest', 'application/manifest+json', 'other'],
|
56
|
+
['document.xml', 'application/xml', 'other'],
|
57
|
+
['page.html', 'text/html', 'other'],
|
58
|
+
['notes.txt', 'text/plain', 'other'],
|
59
|
+
|
60
|
+
// Edge cases
|
61
|
+
['file_without_extension', defaultResult.mime, defaultResult.assetType],
|
62
|
+
['file.unknown', defaultResult.mime, defaultResult.assetType],
|
63
|
+
['.', defaultResult.mime, defaultResult.assetType], // Just a dot
|
64
|
+
['image.', defaultResult.mime, defaultResult.assetType], // Dot at the end
|
65
|
+
// URLs with complex paths/queries but known extensions
|
66
|
+
['https://example.com/complex/path.with.dots/image.png?a=1&b=2#frag', 'image/png', 'image'],
|
67
|
+
['file:///C:/Users/Test/Documents/my%20font.ttf', 'font/ttf', 'font'], // File URI
|
68
|
+
];
|
69
|
+
|
70
|
+
// it.each(testCases)('should return correct type for "%s"', (input, expectedMime, expectedAssetType) => {
|
71
|
+
// const result = guessMimeType(input);
|
72
|
+
// expect(result.mime).toBe(expectedMime);
|
73
|
+
// expect(result.assetType).toBe(expectedAssetType);
|
74
|
+
// });
|
75
|
+
|
76
|
+
it('should return default for null or empty input', () => {
|
77
|
+
// @ts-expect-error Testing invalid input
|
78
|
+
expect(guessMimeType(null)).toEqual(defaultResult);
|
79
|
+
expect(guessMimeType('')).toEqual(defaultResult);
|
80
|
+
expect(guessMimeType(undefined as any)).toEqual(defaultResult); // Test undefined
|
81
|
+
});
|
82
|
+
});
|
83
|
+
|
84
|
+
// Test deprecated getFontMimeType (should just delegate)
|
85
|
+
describe('getFontMimeType() [Deprecated]', () => {
|
86
|
+
it('should return correct font MIME type', () => {
|
87
|
+
expect(getFontMimeType('font.woff2')).toBe('font/woff2');
|
88
|
+
expect(getFontMimeType('font.ttf')).toBe('font/ttf');
|
89
|
+
});
|
90
|
+
|
91
|
+
it('should delegate to guessMimeType and return default for non-fonts', () => {
|
92
|
+
expect(getFontMimeType('style.css')).toBe('text/css'); // Returns CSS mime
|
93
|
+
expect(getFontMimeType('unknown.ext')).toBe('application/octet-stream'); // Returns default
|
94
|
+
});
|
95
|
+
});
|
96
|
+
});
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import { slugify } from '../../../src/utils/slugify'; // Adjust path if needed
|
2
|
+
import { describe, it, expect } from '@jest/globals';
|
3
|
+
|
4
|
+
describe('slugify()', () => {
|
5
|
+
it('should handle typical URLs', () => {
|
6
|
+
// --- Expectations matching the corrected slugify logic ---
|
7
|
+
expect(slugify('https://site.com/path/page.html')).toBe('path-page');
|
8
|
+
expect(slugify('products/item-1.html')).toBe('products-item-1');
|
9
|
+
expect(slugify(' search?q=test page 2 ')).toBe('search-q-test-page-2');
|
10
|
+
expect(slugify('/path/with/slashes/')).toBe('path-with-slashes');
|
11
|
+
// ----------------------------------------------------------
|
12
|
+
expect(slugify('')).toBe('index');
|
13
|
+
});
|
14
|
+
|
15
|
+
});
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
// describe('🔧 sanitizeSlug()', () => {
|
20
|
+
// const tests: Array<[string, string]> = [
|
21
|
+
// // Basic pages
|
22
|
+
// ['about.html', 'about'],
|
23
|
+
// ['/contact.html', 'contact'],
|
24
|
+
// ['index.htm', 'index'],
|
25
|
+
// ['home.php', 'home'],
|
26
|
+
|
27
|
+
// // Complex paths
|
28
|
+
// ['/products/item-1.html', 'products-item-1'],
|
29
|
+
// ['search/q/test-page-2', 'search-q-test-page-2'],
|
30
|
+
// ['/path/with spaces/', 'path-with-spaces'],
|
31
|
+
// ['leading/trailing/', 'leading-trailing'],
|
32
|
+
// ['multiple////slashes//page', 'multiple-slashes-page'],
|
33
|
+
|
34
|
+
// // URL with query strings and fragments
|
35
|
+
// ['about.html?ref=123', 'about'],
|
36
|
+
// ['page.html#section', 'page'],
|
37
|
+
// ['dir/page.html?x=1#top', 'dir-page'],
|
38
|
+
|
39
|
+
// // Weird extensions
|
40
|
+
// ['weird.jsp', 'weird'],
|
41
|
+
// ['page.aspx', 'page'],
|
42
|
+
// ['form.asp', 'form'],
|
43
|
+
|
44
|
+
// // Already clean
|
45
|
+
// ['docs/getting-started', 'docs-getting-started'],
|
46
|
+
|
47
|
+
// // Empty or garbage
|
48
|
+
// ['', 'index'],
|
49
|
+
// [' ', 'index'],
|
50
|
+
// ['?ref=abc', 'index'],
|
51
|
+
// ['#anchor', 'index'],
|
52
|
+
|
53
|
+
// // Slug collisions (the function itself doesn't track collisions, that's the caller's job)
|
54
|
+
// ['duplicate.html', 'duplicate'],
|
55
|
+
// ['duplicate.html?x=1', 'duplicate'],
|
56
|
+
|
57
|
+
// // URL-style strings
|
58
|
+
// ['https://example.com/about.html', 'about'],
|
59
|
+
// ['https://example.com/dir/page.html?ref=42#main', 'dir-page'],
|
60
|
+
|
61
|
+
// // Strange symbols
|
62
|
+
// ['some@strange!file$name.html', 'some-strange-file-name'],
|
63
|
+
// ['complex/path/with_underscores-and.dots.html', 'complex-path-with_underscores-and.dots']
|
64
|
+
// ];
|
65
|
+
|
66
|
+
// tests.forEach(([input, expected]) => {
|
67
|
+
// // it(`should sanitize "${input}" to "${expected}"`, () => {
|
68
|
+
// // expect(sanitizeSlug(input)).toBe(expected);
|
69
|
+
// // });
|
70
|
+
// });
|
71
|
+
// });
|
@@ -0,0 +1,11 @@
|
|
1
|
+
{
|
2
|
+
"extends": "./tsconfig.json", // Inherit from tsconfig.json
|
3
|
+
"compilerOptions": {
|
4
|
+
"noEmit": false, // Ensure files are emitted
|
5
|
+
"declaration": true, // Generate type declaration files
|
6
|
+
"emitDeclarationOnly": true, // Only generate .d.ts files
|
7
|
+
"outDir": "dist/types" // Output directory for declaration files
|
8
|
+
},
|
9
|
+
"include": ["src/**/*"] // Include all TypeScript files in src
|
10
|
+
}
|
11
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
{
|
2
|
+
"extends": "./tsconfig.json",
|
3
|
+
"compilerOptions": {
|
4
|
+
"module": "NodeNext", // Recommended for Jest ESM
|
5
|
+
"moduleResolution": "NodeNext", // Recommended for Jest ESM
|
6
|
+
"isolatedModules": true, // Addresses ts-jest warning
|
7
|
+
"esModuleInterop": true,
|
8
|
+
"allowSyntheticDefaultImports": true,
|
9
|
+
"sourceMap": true,
|
10
|
+
"noEmit": true, // Jest/ts-jest handles output
|
11
|
+
// Ensure target/lib/strict etc. are compatible with your code
|
12
|
+
"target": "ES2022",
|
13
|
+
"lib": ["es2022", "dom"]
|
14
|
+
},
|
15
|
+
"include": ["src/**/*", "tests/**/*"],
|
16
|
+
"exclude": ["node_modules", "dist"]
|
17
|
+
}
|
package/tsconfig.json
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ES2022",
|
4
|
+
"module": "ESNext", // ✅ ESM output
|
5
|
+
"moduleResolution": "Bundler", // ✅ lets TS handle extensions properly
|
6
|
+
"declaration": true,
|
7
|
+
"sourceMap": true,
|
8
|
+
"outDir": "./dist",
|
9
|
+
"strict": true,
|
10
|
+
"esModuleInterop": true,
|
11
|
+
"skipLibCheck": true,
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
13
|
+
"baseUrl": "./",
|
14
|
+
"paths": {
|
15
|
+
"portapack": ["dist/index"]
|
16
|
+
}
|
17
|
+
},
|
18
|
+
"include": ["src/**/*"],
|
19
|
+
"exclude": ["dist", "node_modules"]
|
20
|
+
}
|
package/tsup.config.ts
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
/**
|
2
|
+
* @file tsup.config.ts
|
3
|
+
* @description
|
4
|
+
* Build configuration for tsup bundler.
|
5
|
+
* Configures:
|
6
|
+
* - Separate CLI and API builds
|
7
|
+
* - ESM output for Node.js
|
8
|
+
* - TypeScript declaration files for API only
|
9
|
+
* - Source maps for debugging
|
10
|
+
* - Shebang for CLI binary
|
11
|
+
*/
|
12
|
+
|
13
|
+
import { defineConfig } from 'tsup';
|
14
|
+
|
15
|
+
export default defineConfig([
|
16
|
+
{
|
17
|
+
// CLI build configuration
|
18
|
+
entry: {
|
19
|
+
'cli-entry': 'src/cli/cli-entry.ts', // Entry point for CLI binary
|
20
|
+
},
|
21
|
+
format: ['esm'],
|
22
|
+
target: 'node18',
|
23
|
+
platform: 'node',
|
24
|
+
splitting: false,
|
25
|
+
clean: true, // Clean the output directory
|
26
|
+
dts: false, // No types for CLI output
|
27
|
+
sourcemap: true,
|
28
|
+
outDir: 'dist/cli',
|
29
|
+
banner: {
|
30
|
+
js: '#!/usr/bin/env node', // Include shebang for CLI executable
|
31
|
+
},
|
32
|
+
outExtension({ format }) {
|
33
|
+
return {
|
34
|
+
js: '.js', // Keep .js extension for ESM imports
|
35
|
+
};
|
36
|
+
},
|
37
|
+
esbuildOptions(options) {
|
38
|
+
// Make sure to preserve import.meta.url
|
39
|
+
options.supported = {
|
40
|
+
...options.supported,
|
41
|
+
'import-meta': true,
|
42
|
+
};
|
43
|
+
},
|
44
|
+
},
|
45
|
+
{
|
46
|
+
// API build configuration
|
47
|
+
entry: {
|
48
|
+
index: 'src/index.ts',
|
49
|
+
},
|
50
|
+
format: ['esm'],
|
51
|
+
target: 'node18',
|
52
|
+
platform: 'node',
|
53
|
+
splitting: false,
|
54
|
+
clean: false, // Don't wipe CLI output
|
55
|
+
dts: true, // Generate TypeScript declarations
|
56
|
+
sourcemap: true,
|
57
|
+
outDir: 'dist',
|
58
|
+
outExtension({ format }) {
|
59
|
+
return {
|
60
|
+
js: '.js', // Keep .js extension for ESM imports
|
61
|
+
};
|
62
|
+
},
|
63
|
+
esbuildOptions(options) {
|
64
|
+
// Make sure to preserve import.meta.url
|
65
|
+
options.supported = {
|
66
|
+
...options.supported,
|
67
|
+
'import-meta': true,
|
68
|
+
};
|
69
|
+
},
|
70
|
+
}
|
71
|
+
]);
|
package/typedoc.json
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
{
|
2
|
+
"entryPoints": [
|
3
|
+
"src/index.ts",
|
4
|
+
"src/types.ts",
|
5
|
+
"src/cli/cli.ts"
|
6
|
+
],
|
7
|
+
"out": "docs/api",
|
8
|
+
"plugin": ["typedoc-plugin-markdown"],
|
9
|
+
"tsconfig": "tsconfig.json",
|
10
|
+
"readme": "none",
|
11
|
+
"excludePrivate": true,
|
12
|
+
"excludeInternal": true,
|
13
|
+
"excludeProtected": true,
|
14
|
+
"hideGenerator": true,
|
15
|
+
"theme": "markdown",
|
16
|
+
"entryPointStrategy": "expand",
|
17
|
+
"exclude": [
|
18
|
+
"**/node_modules/**",
|
19
|
+
"**/test/**",
|
20
|
+
"**/tests/**",
|
21
|
+
"**/dist/**",
|
22
|
+
"**/*.spec.ts",
|
23
|
+
"**/*.test.ts"
|
24
|
+
],
|
25
|
+
"sort": ["alphabetical"],
|
26
|
+
"categorizeByGroup": true,
|
27
|
+
"sourceLinkTemplate": "https://github.com/manicinc/portapack/blob/master/{path}#L{line}"
|
28
|
+
}
|