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
package/.github/workflows/ci.yml
CHANGED
@@ -23,11 +23,12 @@ jobs:
|
|
23
23
|
cache: 'npm'
|
24
24
|
|
25
25
|
- run: npm ci
|
26
|
-
|
27
|
-
# - run: npm run lint
|
28
|
-
# - run: npm run format:check
|
29
|
-
- run: npm run build # for now we keep it simple to release
|
26
|
+
- run: npm run test:ci
|
30
27
|
|
28
|
+
- name: Upload coverage to Codecov
|
29
|
+
uses: codecov/codecov-action@v4
|
30
|
+
with:
|
31
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
31
32
|
# coverage:
|
32
33
|
# needs: build
|
33
34
|
# runs-on: ubuntu-latest
|
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## [0.3.1](https://github.com/manicinc/portapack/compare/v0.3.0...v0.3.1) (2025-04-14)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* **ci:** update Codecov config [skip release] ([593b126](https://github.com/manicinc/portapack/commit/593b1262183d05a9a7099463b6da0f4deb916576))
|
7
|
+
* **extractor:** resolve test failures and coverage issues ([40ea42c](https://github.com/manicinc/portapack/commit/40ea42cbdbeec67657225c50eb97ef0965cd2769))
|
8
|
+
|
1
9
|
# [0.3.0](https://github.com/manicinc/portapack/compare/v0.2.1...v0.3.0) (2025-04-13)
|
2
10
|
|
3
11
|
|
package/README.md
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
[](https://www.npmjs.com/package/portapack)
|
4
4
|
[](https://github.com/manicinc/portapack/actions)
|
5
|
-
[](https://codecov.io/gh/manicinc/portapack)
|
6
|
+
[](./LICENSE)
|
6
7
|
|
7
8
|
**PortaPack** bundles your entire website — HTML, CSS, JS, images, and fonts — into one self-contained HTML file. Perfect for snapshots, demos, testing, and offline apps.
|
8
9
|
|
@@ -125,15 +126,6 @@ import {
|
|
125
126
|
| `fetchAndPackWebPage()` | Just fetch HTML (no asset processing) |
|
126
127
|
| `bundleMultiPageHTML()` | Combine multiple HTMLs with router |
|
127
128
|
|
128
|
-
## 🧪 Use Cases
|
129
|
-
|
130
|
-
- Archive pages for offline use
|
131
|
-
- Create demo bundles without a web server
|
132
|
-
- Simplify distribution of small apps
|
133
|
-
- QA test static assets
|
134
|
-
- Embed pages in PDFs or ebooks
|
135
|
-
- Analyze asset weight impact
|
136
|
-
|
137
129
|
## 🤝 Contribute
|
138
130
|
|
139
131
|
```bash
|
@@ -146,10 +138,13 @@ npm run dev
|
|
146
138
|
|
147
139
|
## 📊 Project Health
|
148
140
|
|
149
|
-
|
141
|
+
| Metric | Value |
|
142
|
+
|--------------|-------|
|
143
|
+
| 📦 Version | [](https://www.npmjs.com/package/portapack) |
|
144
|
+
| ✅ Build | [](https://github.com/manicinc/portapack/actions) |
|
145
|
+
| 🧪 Coverage | [](https://codecov.io/gh/manicinc/portapack) |
|
150
146
|
|
151
147
|
## 📄 License
|
152
148
|
|
153
|
-
MIT — Built
|
149
|
+
MIT — Built by Manic.agency
|
154
150
|
|
155
|
-
*Open Source Empowering Designers and Developers 🖥️*
|
package/dist/cli/cli-entry.cjs
CHANGED
@@ -326,7 +326,6 @@ function isUtf8DecodingLossy(originalBuffer, decodedString) {
|
|
326
326
|
}
|
327
327
|
}
|
328
328
|
function determineBaseUrl(inputPathOrUrl, logger) {
|
329
|
-
console.log(`[DEBUG determineBaseUrl] Input: "${inputPathOrUrl}"`);
|
330
329
|
logger?.debug(`Determining base URL for input: ${inputPathOrUrl}`);
|
331
330
|
if (!inputPathOrUrl) {
|
332
331
|
logger?.warn("Cannot determine base URL: inputPathOrUrl is empty or invalid.");
|
@@ -340,11 +339,9 @@ function determineBaseUrl(inputPathOrUrl, logger) {
|
|
340
339
|
url.hash = "";
|
341
340
|
const baseUrl = url.href;
|
342
341
|
logger?.debug(`Determined remote base URL: ${baseUrl}`);
|
343
|
-
console.log(`[DEBUG determineBaseUrl] Determined Remote URL: "${baseUrl}"`);
|
344
342
|
return baseUrl;
|
345
343
|
} else if (inputPathOrUrl.includes("://") && !inputPathOrUrl.startsWith("file:")) {
|
346
344
|
logger?.warn(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
|
347
|
-
console.log(`[DEBUG determineBaseUrl] Unsupported protocol.`);
|
348
345
|
return void 0;
|
349
346
|
} else {
|
350
347
|
let resourcePath;
|
@@ -360,9 +357,7 @@ function determineBaseUrl(inputPathOrUrl, logger) {
|
|
360
357
|
isInputLikelyDirectory = false;
|
361
358
|
}
|
362
359
|
}
|
363
|
-
console.log(`[DEBUG determineBaseUrl] resourcePath: "${resourcePath}", isInputLikelyDirectory: ${isInputLikelyDirectory}`);
|
364
360
|
const baseDirPath = isInputLikelyDirectory ? resourcePath : import_path2.default.dirname(resourcePath);
|
365
|
-
console.log(`[DEBUG determineBaseUrl] Calculated baseDirPath: "${baseDirPath}"`);
|
366
361
|
let normalizedPathForURL = baseDirPath.replace(/\\/g, "/");
|
367
362
|
if (/^[A-Z]:\//i.test(normalizedPathForURL) && !normalizedPathForURL.startsWith("/")) {
|
368
363
|
normalizedPathForURL = "/" + normalizedPathForURL;
|
@@ -373,12 +368,10 @@ function determineBaseUrl(inputPathOrUrl, logger) {
|
|
373
368
|
const fileUrl = new import_url.URL("file://" + normalizedPathForURL);
|
374
369
|
const fileUrlString = fileUrl.href;
|
375
370
|
logger?.debug(`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`);
|
376
|
-
console.log(`[DEBUG determineBaseUrl] Determined File URL: "${fileUrlString}"`);
|
377
371
|
return fileUrlString;
|
378
372
|
}
|
379
373
|
} catch (error) {
|
380
374
|
const message = error instanceof Error ? error.message : String(error);
|
381
|
-
console.error(`[DEBUG determineBaseUrl] Error determining base URL: ${message}`);
|
382
375
|
logger?.error(`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`);
|
383
376
|
return void 0;
|
384
377
|
}
|
@@ -416,82 +409,69 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
|
|
416
409
|
}
|
417
410
|
}
|
418
411
|
function resolveCssRelativeUrl(relativeUrl, cssBaseContextUrl, logger) {
|
419
|
-
console.log(`[DEBUG resolveCssRelativeUrl] Input: relative="${relativeUrl}", base="${cssBaseContextUrl}"`);
|
420
412
|
if (!relativeUrl || relativeUrl.startsWith("data:") || relativeUrl.startsWith("#")) {
|
421
413
|
return null;
|
422
414
|
}
|
423
415
|
try {
|
424
416
|
const resolvedUrl = new import_url.URL(relativeUrl, cssBaseContextUrl);
|
425
|
-
console.log(`[DEBUG resolveCssRelativeUrl] Resolved URL object href: "${resolvedUrl.href}"`);
|
426
417
|
return resolvedUrl.href;
|
427
418
|
} catch (error) {
|
428
419
|
logger?.warn(
|
429
420
|
`Failed to resolve CSS URL: "${relativeUrl}" relative to "${cssBaseContextUrl}": ${String(error)}`
|
430
421
|
);
|
431
|
-
console.error(`[DEBUG resolveCssRelativeUrl] Error resolving: ${String(error)}`);
|
432
422
|
return null;
|
433
423
|
}
|
434
424
|
}
|
435
425
|
async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
|
436
|
-
console.log(`[DEBUG fetchAsset] Attempting fetch for URL: ${resolvedUrl.href}`);
|
437
426
|
logger?.debug(`Attempting to fetch asset: ${resolvedUrl.href}`);
|
438
427
|
const protocol = resolvedUrl.protocol;
|
439
428
|
try {
|
440
429
|
if (protocol === "http:" || protocol === "https:") {
|
441
430
|
const response = await axiosNs.default.get(resolvedUrl.href, {
|
442
431
|
responseType: "arraybuffer",
|
432
|
+
// Fetch as binary data
|
443
433
|
timeout
|
434
|
+
// Apply network timeout
|
444
435
|
});
|
445
436
|
logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data?.byteLength ?? 0} bytes)`);
|
446
|
-
console.log(`[DEBUG fetchAsset] HTTP fetch SUCCESS for: ${resolvedUrl.href}, Status: ${response.status}`);
|
447
437
|
return Buffer.from(response.data);
|
448
438
|
} else if (protocol === "file:") {
|
449
439
|
let filePath;
|
450
440
|
try {
|
451
441
|
filePath = (0, import_url.fileURLToPath)(resolvedUrl);
|
452
442
|
} catch (e) {
|
453
|
-
console.error(`[DEBUG fetchAsset] fileURLToPath FAILED for: ${resolvedUrl.href}`, e);
|
454
443
|
logger?.error(`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`);
|
455
444
|
return null;
|
456
445
|
}
|
457
446
|
const normalizedForLog = import_path2.default.normalize(filePath);
|
458
|
-
console.log(`[DEBUG fetchAsset] Attempting readFile with path: "${normalizedForLog}" (Original from URL: "${filePath}")`);
|
459
447
|
const data = await (0, import_promises.readFile)(filePath);
|
460
|
-
console.log(`[DEBUG fetchAsset] readFile call SUCCEEDED for path: "${normalizedForLog}". Data length: ${data?.byteLength}`);
|
461
448
|
logger?.debug(`Read local file ${filePath} (${data.byteLength} bytes)`);
|
462
449
|
return data;
|
463
450
|
} else {
|
464
|
-
console.log(`[DEBUG fetchAsset] Unsupported protocol: ${protocol}`);
|
465
451
|
logger?.warn(`Unsupported protocol "${protocol}" in URL: ${resolvedUrl.href}`);
|
466
452
|
return null;
|
467
453
|
}
|
468
454
|
} catch (error) {
|
469
455
|
const failedId = protocol === "file:" ? import_path2.default.normalize((0, import_url.fileURLToPath)(resolvedUrl)) : resolvedUrl.href;
|
470
|
-
|
471
|
-
|
472
|
-
const status =
|
473
|
-
const
|
474
|
-
const
|
475
|
-
const message = error.message;
|
476
|
-
const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: Status ${status} - ${statusText}. Code: ${code}, Message: ${message}`;
|
456
|
+
if ((protocol === "http:" || protocol === "https:") && error?.isAxiosError === true) {
|
457
|
+
const axiosError = error;
|
458
|
+
const status = axiosError.response?.status ?? "N/A";
|
459
|
+
const code = axiosError.code ?? "N/A";
|
460
|
+
const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: ${axiosError.message} (Code: ${code})`;
|
477
461
|
logger?.warn(logMessage);
|
478
|
-
}
|
479
|
-
if (error instanceof Error && error.code === "ENOENT") {
|
462
|
+
} else if (protocol === "file:" && error instanceof Error) {
|
480
463
|
let failedPath = resolvedUrl.href;
|
481
464
|
try {
|
482
465
|
failedPath = (0, import_url.fileURLToPath)(resolvedUrl);
|
483
466
|
} catch {
|
484
467
|
}
|
485
468
|
failedPath = import_path2.default.normalize(failedPath);
|
486
|
-
if (error
|
469
|
+
if (error.code === "ENOENT") {
|
487
470
|
logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
|
488
|
-
} else if (error
|
471
|
+
} else if (error.code === "EACCES") {
|
489
472
|
logger?.warn(`\u26A0\uFE0F Permission denied (EACCES) reading asset: ${failedPath}.`);
|
490
|
-
logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
|
491
|
-
} else if (error instanceof Error) {
|
492
|
-
logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
|
493
473
|
} else {
|
494
|
-
logger?.warn(`\u26A0\uFE0F
|
474
|
+
logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
|
495
475
|
}
|
496
476
|
} else if (error instanceof Error) {
|
497
477
|
logger?.warn(`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`);
|
@@ -507,7 +487,7 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
507
487
|
const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
508
488
|
const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
|
509
489
|
const processFoundUrl = (rawUrl, ruleType) => {
|
510
|
-
if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:")) return;
|
490
|
+
if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#")) return;
|
511
491
|
const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
|
512
492
|
if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
|
513
493
|
processedInThisParse.add(resolvedUrl);
|
@@ -515,7 +495,7 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
515
495
|
newlyDiscovered.push({
|
516
496
|
type: assetType,
|
517
497
|
url: resolvedUrl,
|
518
|
-
//
|
498
|
+
// Store the resolved absolute URL string
|
519
499
|
content: void 0
|
520
500
|
// Content will be fetched later if needed
|
521
501
|
});
|
@@ -552,7 +532,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
552
532
|
continue;
|
553
533
|
}
|
554
534
|
const urlToQueue = resolvedUrlObj.href;
|
555
|
-
if (!
|
535
|
+
if (!processedOrQueuedUrls.has(urlToQueue)) {
|
556
536
|
processedOrQueuedUrls.add(urlToQueue);
|
557
537
|
const { assetType: guessedType } = guessMimeType(urlToQueue);
|
558
538
|
const initialType = asset.type ?? guessedType;
|
@@ -561,10 +541,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
561
541
|
// Use the resolved URL
|
562
542
|
type: initialType,
|
563
543
|
content: void 0
|
544
|
+
// Content is initially undefined
|
564
545
|
});
|
565
546
|
logger?.debug(` -> Queued initial asset: ${urlToQueue} (Original raw: ${asset.url})`);
|
566
|
-
} else if (urlToQueue.startsWith("data:")) {
|
567
|
-
logger?.debug(` -> Skipping data URI: ${urlToQueue.substring(0, 50)}...`);
|
568
547
|
} else {
|
569
548
|
logger?.debug(` -> Skipping already processed/queued initial asset: ${urlToQueue}`);
|
570
549
|
}
|
@@ -684,7 +663,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
684
663
|
const newlyDiscoveredAssets = extractUrlsFromCSS(
|
685
664
|
cssContentForParsing,
|
686
665
|
cssBaseContextUrl,
|
687
|
-
// Use CSS file's
|
666
|
+
// Use the CSS file's own URL as the base
|
688
667
|
logger
|
689
668
|
);
|
690
669
|
if (newlyDiscoveredAssets.length > 0) {
|
@@ -705,7 +684,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
705
684
|
}
|
706
685
|
}
|
707
686
|
}
|
708
|
-
const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ?
|
687
|
+
const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? `${MAX_ASSET_EXTRACTION_ITERATIONS}+ (limit hit)` : iterationCount;
|
709
688
|
logger?.info(`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`);
|
710
689
|
return {
|
711
690
|
htmlContent: parsed.htmlContent,
|