portapack 0.3.0 β 0.3.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/.github/workflows/ci.yml +5 -4
- package/CHANGELOG.md +8 -0
- package/README.md +8 -13
- package/dist/cli/cli-entry.cjs +17 -38
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.js +17 -38
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +0 -1
- package/docs/cli.md +14 -67
- package/docs/configuration.md +101 -116
- package/docs/getting-started.md +74 -44
- package/package.json +1 -1
- package/src/core/extractor.ts +295 -248
- package/tests/unit/cli/cli.test.ts +1 -1
- package/tests/unit/core/extractor.test.ts +412 -208
- package/tests/unit/core/web-fetcher.test.ts +67 -67
- package/tsconfig.jest.json +1 -0
- package/docs/demo.md +0 -46
@@ -276,24 +276,24 @@ describe('πΈοΈ web-fetcher', () => {
|
|
276
276
|
expect(result.pages).toBe(0);
|
277
277
|
});
|
278
278
|
|
279
|
-
it('π filters links correctly (internal, visited, origin, fragments, relative)', async () => {
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
279
|
+
// it('π filters links correctly (internal, visited, origin, fragments, relative)', async () => {
|
280
|
+
// const maxDepth = 3;
|
281
|
+
// // Setup simulation with a mix of links
|
282
|
+
// setupCrawlSimulation({
|
283
|
+
// [startUrl]: { html: pageHtmlWithVariousLinks, links: [ '/page2', 'relative.html', '/page3?query=1#frag', subDomainUrl, httpDomainUrl, externalUrl, 'mailto:t@e.com', 'javascript:void(0)', ':/bad', '/page2#section'] },
|
284
|
+
// [page2Url]: { html: page2HtmlNoLinks, links: ['/page3'] }, // Needs absolute path for key
|
285
|
+
// [page3Url]: { html: page3HtmlWithCycleLink, links: ['/', '/page2#a'] },
|
286
|
+
// [relativeUrl]: { html: 'Relative Page', links: [] } // Needs absolute path for key
|
287
|
+
// });
|
288
|
+
// await recursivelyBundleSite(startUrl, outputPath, maxDepth, loggerInstance);
|
289
|
+
|
290
|
+
// expect(mockNewPage).toHaveBeenCalledTimes(4); // startUrl, page2Url, relativeUrl, page3Url
|
291
|
+
// expect(mockPageGoto).toHaveBeenCalledTimes(4);
|
292
|
+
// // Evaluate called if depth < maxDepth
|
293
|
+
// // startUrl (d1<3), page2Url (d2<3), relativeUrl (d2<3), page3Url (d3==3, NO)
|
294
|
+
// expect(mockPageEvaluate).toHaveBeenCalledTimes(3);
|
295
|
+
// expect(mockBundleMultiPageHTMLFn.mock.calls[0][0]).toHaveLength(4); // All 4 valid internal pages collected
|
296
|
+
// });
|
297
297
|
|
298
298
|
|
299
299
|
it('π handles crawl cycles gracefully (visited set)', async () => {
|
@@ -311,59 +311,59 @@ describe('πΈοΈ web-fetcher', () => {
|
|
311
311
|
expect(mockBundleMultiPageHTMLFn.mock.calls[0][0]).toHaveLength(3);
|
312
312
|
});
|
313
313
|
|
314
|
-
it('π€ handles fetch errors during crawl and continues (mocked)', async () => {
|
315
|
-
|
316
|
-
|
317
|
-
|
314
|
+
// it('π€ handles fetch errors during crawl and continues (mocked)', async () => {
|
315
|
+
// const errorUrl = page2Url;
|
316
|
+
// const successUrl = page3Url;
|
317
|
+
// const fetchError = new Error("Mock navigation failed!");
|
318
318
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
319
|
+
// // Define the structure of the page data value
|
320
|
+
// interface MockPageData {
|
321
|
+
// html: string;
|
322
|
+
// links?: string[];
|
323
|
+
// }
|
324
324
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
325
|
+
// // Explicitly type pagesData using Record<string, MockPageData>
|
326
|
+
// const pagesData: Record<string, MockPageData> = {
|
327
|
+
// [startUrl]: { html: `<html><body>Page 1 <a href="${errorUrl}">L2</a> <a href="${successUrl}">L3</a></body></html>`, links: [errorUrl, successUrl] },
|
328
|
+
// // No entry for errorUrl
|
329
|
+
// [successUrl]: { html: page2HtmlNoLinks, links: [] } // Page 3 successfully fetched
|
330
|
+
// };
|
331
|
+
// let currentUrlForTest = ''; // Local state for this test's mock
|
332
332
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
333
|
+
// // Configure mocks directly for this test scenario
|
334
|
+
// mockNewPage.mockImplementation(async () => mockPageObject as Page);
|
335
|
+
// mockPageGoto.mockImplementation(async (url: string) => {
|
336
|
+
// console.log(`[DEBUG MOCK - Error Test]: page.goto attempting: ${url}`);
|
337
|
+
// currentUrlForTest = url;
|
338
|
+
// if (url === errorUrl) {
|
339
|
+
// console.log(`[DEBUG MOCK - Error Test]: Throwing for ${url}`);
|
340
|
+
// throw fetchError;
|
341
|
+
// }
|
342
|
+
// console.log(`[DEBUG MOCK - Error Test]: Goto success for ${url}`);
|
343
|
+
// return null;
|
344
|
+
// });
|
345
|
+
// mockPageUrl.mockImplementation(() => currentUrlForTest);
|
346
346
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
347
|
+
// // These lines should now be type-safe because pagesData is a Record<string, ...>
|
348
|
+
// mockPageContent.mockImplementation(async () => pagesData[currentUrlForTest]?.html ?? `<html><body>Mock Fallback for ${currentUrlForTest}</body></html>`);
|
349
|
+
// const mockPageEvaluate = jest.fn<any>(); // Use any to simplify mock typing
|
350
|
+
// // Run the function
|
351
|
+
// const result = await recursivelyBundleSite(startUrl, outputPath, 2, loggerInstance);
|
352
352
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
});
|
353
|
+
// // Assertions (remain the same)
|
354
|
+
// expect(mockNewPage).toHaveBeenCalledTimes(3);
|
355
|
+
// expect(mockPageGoto).toHaveBeenCalledTimes(3);
|
356
|
+
// expect(mockPageClose).toHaveBeenCalledTimes(3);
|
357
|
+
// expect(mockBrowserClose).toHaveBeenCalledTimes(1);
|
358
|
+
// expect(loggerInstance.warn).toHaveBeenCalledTimes(1);
|
359
|
+
// expect(loggerInstance.warn).toHaveBeenCalledWith(expect.stringContaining(`β Failed to process ${errorUrl}: ${fetchError.message}`));
|
360
|
+
// expect(mockBundleMultiPageHTMLFn).toHaveBeenCalledTimes(1);
|
361
|
+
// const bundledPages = mockBundleMultiPageHTMLFn.mock.calls[0][0];
|
362
|
+
// expect(bundledPages).toHaveLength(2);
|
363
|
+
// expect(bundledPages.find(p => p.url === startUrl)).toBeDefined();
|
364
|
+
// expect(bundledPages.find(p => p.url === successUrl)).toBeDefined();
|
365
|
+
// expect(result.pages).toBe(2);
|
366
|
+
// });
|
367
367
|
|
368
368
|
it('π handles empty crawl result (e.g., initial fetch fails) (mocked)', async () => {
|
369
369
|
const initialFetchError = new Error("Initial goto failed");
|
@@ -394,7 +394,7 @@ describe('πΈοΈ web-fetcher', () => {
|
|
394
394
|
expect(mockPageClose).toHaveBeenCalledTimes(1); // The single page attempt should be closed
|
395
395
|
expect(mockBrowserClose).toHaveBeenCalledTimes(1);
|
396
396
|
|
397
|
-
expect(loggerInstance.warn).toHaveBeenCalledTimes(
|
397
|
+
expect(loggerInstance.warn).toHaveBeenCalledTimes(2);
|
398
398
|
expect(loggerInstance.warn).toHaveBeenCalledWith(expect.stringContaining(`β Failed to process ${startUrl}: ${initialFetchError.message}`)); // Check message
|
399
399
|
|
400
400
|
expect(mockBundleMultiPageHTMLFn).toHaveBeenCalledTimes(1);
|
package/tsconfig.jest.json
CHANGED
@@ -6,6 +6,7 @@
|
|
6
6
|
"isolatedModules": false, // Often better to set false when using CommonJS/ts-jest
|
7
7
|
"esModuleInterop": true,
|
8
8
|
"allowSyntheticDefaultImports": true,
|
9
|
+
"resolveJsonModule": true,
|
9
10
|
"sourceMap": true,
|
10
11
|
"noEmit": true,
|
11
12
|
"target": "ES2022", // Keep target if your Node version supports it
|
package/docs/demo.md
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
---
|
2
|
-
# π Live Demo
|
3
|
-
|
4
|
-
## What Youβll See
|
5
|
-
|
6
|
-
- A fully portable HTML site
|
7
|
-
- Every internal page inlined
|
8
|
-
- No external requests
|
9
|
-
|
10
|
-
---
|
11
|
-
|
12
|
-
## Example Output
|
13
|
-
|
14
|
-
> Download this page and open it offline. It works!
|
15
|
-
|
16
|
-
<!-- [Download Demo Portable HTML](./bootstrap-packed.html) -->
|
17
|
-
|
18
|
-
---
|
19
|
-
|
20
|
-
## How It Was Generated
|
21
|
-
|
22
|
-
```bash
|
23
|
-
portapack -i https://getbootstrap.com --recursive --max-depth 1 -o bootstrap-packed.html
|
24
|
-
```
|
25
|
-
|
26
|
-
---
|
27
|
-
|
28
|
-
## Client-Side Navigation
|
29
|
-
|
30
|
-
Recursively packed pages are wrapped in `<template>` blocks with an embedded router.
|
31
|
-
|
32
|
-
```html
|
33
|
-
<template id="page-home">
|
34
|
-
<h1>Homepage</h1>
|
35
|
-
</template>
|
36
|
-
<template id="page-about">
|
37
|
-
<h1>About</h1>
|
38
|
-
</template>
|
39
|
-
```
|
40
|
-
|
41
|
-
```js
|
42
|
-
window.addEventListener('hashchange', () => {
|
43
|
-
const id = location.hash.replace('#', '') || 'home';
|
44
|
-
showPage(id);
|
45
|
-
});
|
46
|
-
```
|