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.
Files changed (76) hide show
  1. package/.eslintrc.json +9 -0
  2. package/.github/workflows/ci.yml +73 -0
  3. package/.github/workflows/deploy-pages.yml +56 -0
  4. package/.prettierrc +9 -0
  5. package/.releaserc.js +29 -0
  6. package/CHANGELOG.md +21 -0
  7. package/README.md +288 -0
  8. package/commitlint.config.js +36 -0
  9. package/dist/cli/cli-entry.js +1694 -0
  10. package/dist/cli/cli-entry.js.map +1 -0
  11. package/dist/index.d.ts +275 -0
  12. package/dist/index.js +1405 -0
  13. package/dist/index.js.map +1 -0
  14. package/docs/.vitepress/config.ts +89 -0
  15. package/docs/.vitepress/sidebar-generator.ts +73 -0
  16. package/docs/cli.md +117 -0
  17. package/docs/code-of-conduct.md +65 -0
  18. package/docs/configuration.md +151 -0
  19. package/docs/contributing.md +107 -0
  20. package/docs/demo.md +46 -0
  21. package/docs/deployment.md +132 -0
  22. package/docs/development.md +168 -0
  23. package/docs/getting-started.md +106 -0
  24. package/docs/index.md +40 -0
  25. package/docs/portapack-transparent.png +0 -0
  26. package/docs/portapack.jpg +0 -0
  27. package/docs/troubleshooting.md +107 -0
  28. package/examples/main.ts +118 -0
  29. package/examples/sample-project/index.html +12 -0
  30. package/examples/sample-project/logo.png +1 -0
  31. package/examples/sample-project/script.js +1 -0
  32. package/examples/sample-project/styles.css +1 -0
  33. package/jest.config.ts +124 -0
  34. package/jest.setup.cjs +211 -0
  35. package/nodemon.json +11 -0
  36. package/output.html +1 -0
  37. package/package.json +161 -0
  38. package/site-packed.html +1 -0
  39. package/src/cli/cli-entry.ts +28 -0
  40. package/src/cli/cli.ts +139 -0
  41. package/src/cli/options.ts +151 -0
  42. package/src/core/bundler.ts +201 -0
  43. package/src/core/extractor.ts +618 -0
  44. package/src/core/minifier.ts +233 -0
  45. package/src/core/packer.ts +191 -0
  46. package/src/core/parser.ts +115 -0
  47. package/src/core/web-fetcher.ts +292 -0
  48. package/src/index.ts +262 -0
  49. package/src/types.ts +163 -0
  50. package/src/utils/font.ts +41 -0
  51. package/src/utils/logger.ts +139 -0
  52. package/src/utils/meta.ts +100 -0
  53. package/src/utils/mime.ts +90 -0
  54. package/src/utils/slugify.ts +70 -0
  55. package/test-output.html +0 -0
  56. package/tests/__fixtures__/sample-project/index.html +5 -0
  57. package/tests/unit/cli/cli-entry.test.ts +104 -0
  58. package/tests/unit/cli/cli.test.ts +230 -0
  59. package/tests/unit/cli/options.test.ts +316 -0
  60. package/tests/unit/core/bundler.test.ts +287 -0
  61. package/tests/unit/core/extractor.test.ts +1129 -0
  62. package/tests/unit/core/minifier.test.ts +414 -0
  63. package/tests/unit/core/packer.test.ts +193 -0
  64. package/tests/unit/core/parser.test.ts +540 -0
  65. package/tests/unit/core/web-fetcher.test.ts +374 -0
  66. package/tests/unit/index.test.ts +339 -0
  67. package/tests/unit/utils/font.test.ts +81 -0
  68. package/tests/unit/utils/logger.test.ts +275 -0
  69. package/tests/unit/utils/meta.test.ts +70 -0
  70. package/tests/unit/utils/mime.test.ts +96 -0
  71. package/tests/unit/utils/slugify.test.ts +71 -0
  72. package/tsconfig.build.json +11 -0
  73. package/tsconfig.jest.json +17 -0
  74. package/tsconfig.json +20 -0
  75. package/tsup.config.ts +71 -0
  76. 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
+ }