portapack 0.3.1 → 0.3.3
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 +67 -8
- package/.releaserc.js +25 -27
- package/CHANGELOG.md +14 -22
- package/LICENSE.md +21 -0
- package/README.md +22 -53
- package/commitlint.config.js +30 -34
- package/dist/cli/cli-entry.cjs +183 -98
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +178 -97
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +38 -33
- package/docs/.vitepress/sidebar-generator.ts +89 -38
- package/docs/architecture.md +186 -0
- package/docs/cli.md +23 -23
- package/docs/code-of-conduct.md +7 -1
- package/docs/configuration.md +12 -11
- package/docs/contributing.md +6 -2
- package/docs/deployment.md +10 -5
- package/docs/development.md +8 -5
- package/docs/getting-started.md +13 -13
- package/docs/index.md +1 -1
- package/docs/public/android-chrome-192x192.png +0 -0
- package/docs/public/android-chrome-512x512.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/roadmap.md +233 -0
- package/docs/site.webmanifest +1 -0
- package/docs/troubleshooting.md +12 -1
- package/examples/main.ts +5 -30
- package/examples/sample-project/script.js +1 -1
- package/jest.config.ts +8 -13
- package/nodemon.json +5 -10
- package/package.json +2 -5
- package/src/cli/cli-entry.ts +2 -2
- package/src/cli/cli.ts +21 -16
- package/src/cli/options.ts +127 -113
- package/src/core/bundler.ts +253 -222
- package/src/core/extractor.ts +632 -565
- package/src/core/minifier.ts +173 -162
- package/src/core/packer.ts +141 -137
- package/src/core/parser.ts +74 -73
- package/src/core/web-fetcher.ts +270 -258
- package/src/index.ts +18 -17
- package/src/types.ts +9 -11
- package/src/utils/font.ts +12 -6
- package/src/utils/logger.ts +110 -105
- package/src/utils/meta.ts +75 -76
- package/src/utils/mime.ts +50 -50
- package/src/utils/slugify.ts +33 -34
- package/tests/unit/cli/cli-entry.test.ts +72 -70
- package/tests/unit/cli/cli.test.ts +314 -278
- package/tests/unit/cli/options.test.ts +294 -301
- package/tests/unit/core/bundler.test.ts +426 -329
- package/tests/unit/core/extractor.test.ts +793 -549
- package/tests/unit/core/minifier.test.ts +374 -274
- package/tests/unit/core/packer.test.ts +298 -264
- package/tests/unit/core/parser.test.ts +538 -150
- package/tests/unit/core/web-fetcher.test.ts +389 -359
- package/tests/unit/index.test.ts +238 -197
- package/tests/unit/utils/font.test.ts +26 -21
- package/tests/unit/utils/logger.test.ts +267 -260
- package/tests/unit/utils/meta.test.ts +29 -28
- package/tests/unit/utils/mime.test.ts +73 -74
- package/tests/unit/utils/slugify.test.ts +14 -12
- package/tsconfig.build.json +9 -10
- package/tsconfig.jest.json +1 -1
- package/tsconfig.json +2 -2
- package/tsup.config.ts +8 -9
- package/typedoc.json +5 -9
- /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
- /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
package/dist/cli/cli-entry.cjs
CHANGED
@@ -55,7 +55,11 @@ function parseRecursiveValue(val) {
|
|
55
55
|
}
|
56
56
|
function parseOptions(argv = process.argv) {
|
57
57
|
const program = new import_commander.Command();
|
58
|
-
program.name("portapack").version("0.0.0").description("\u{1F4E6} Bundle HTML and its dependencies into a portable file").argument("[input]", "Input HTML file or URL").option("-o, --output <file>", "Output file path").option("-m, --minify", "Enable all minification (HTML, CSS, JS)").option("--no-minify", "Disable all minification").option("--no-minify-html", "Disable HTML minification").option("--no-minify-css", "Disable CSS minification").option("--no-minify-js", "Disable JavaScript minification").option("-e, --embed-assets", "Embed assets as data URIs").option("--no-embed-assets", "Keep asset links relative/absolute").option(
|
58
|
+
program.name("portapack").version("0.0.0").description("\u{1F4E6} Bundle HTML and its dependencies into a portable file").argument("[input]", "Input HTML file or URL").option("-o, --output <file>", "Output file path").option("-m, --minify", "Enable all minification (HTML, CSS, JS)").option("--no-minify", "Disable all minification").option("--no-minify-html", "Disable HTML minification").option("--no-minify-css", "Disable CSS minification").option("--no-minify-js", "Disable JavaScript minification").option("-e, --embed-assets", "Embed assets as data URIs").option("--no-embed-assets", "Keep asset links relative/absolute").option(
|
59
|
+
"-r, --recursive [depth]",
|
60
|
+
"Recursively crawl site (optional depth)",
|
61
|
+
parseRecursiveValue
|
62
|
+
).option("--max-depth <n>", "Set max depth for recursive crawl (alias for -r <n>)", parseInt).option("-b, --base-url <url>", "Base URL for resolving relative links").option("-d, --dry-run", "Run without writing output file").option("-v, --verbose", "Enable verbose (debug) logging").addOption(new import_commander.Option("--log-level <level>", "Set logging level").choices(logLevels));
|
59
63
|
program.parse(argv);
|
60
64
|
const opts = program.opts();
|
61
65
|
const inputArg = program.args.length > 0 ? program.args[0] : void 0;
|
@@ -246,7 +250,9 @@ var init_logger = __esm({
|
|
246
250
|
case "none":
|
247
251
|
return new _Logger(0 /* NONE */);
|
248
252
|
default:
|
249
|
-
console.warn(
|
253
|
+
console.warn(
|
254
|
+
`[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`
|
255
|
+
);
|
250
256
|
return new _Logger(defaultLevel);
|
251
257
|
}
|
252
258
|
}
|
@@ -341,7 +347,9 @@ function determineBaseUrl(inputPathOrUrl, logger) {
|
|
341
347
|
logger?.debug(`Determined remote base URL: ${baseUrl}`);
|
342
348
|
return baseUrl;
|
343
349
|
} else if (inputPathOrUrl.includes("://") && !inputPathOrUrl.startsWith("file:")) {
|
344
|
-
logger?.warn(
|
350
|
+
logger?.warn(
|
351
|
+
`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`
|
352
|
+
);
|
345
353
|
return void 0;
|
346
354
|
} else {
|
347
355
|
let resourcePath;
|
@@ -367,12 +375,16 @@ function determineBaseUrl(inputPathOrUrl, logger) {
|
|
367
375
|
}
|
368
376
|
const fileUrl = new import_url.URL("file://" + normalizedPathForURL);
|
369
377
|
const fileUrlString = fileUrl.href;
|
370
|
-
logger?.debug(
|
378
|
+
logger?.debug(
|
379
|
+
`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`
|
380
|
+
);
|
371
381
|
return fileUrlString;
|
372
382
|
}
|
373
383
|
} catch (error) {
|
374
384
|
const message = error instanceof Error ? error.message : String(error);
|
375
|
-
logger?.error(
|
385
|
+
logger?.error(
|
386
|
+
`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`
|
387
|
+
);
|
376
388
|
return void 0;
|
377
389
|
}
|
378
390
|
}
|
@@ -387,7 +399,9 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
|
|
387
399
|
const base = new import_url.URL(baseContextUrl);
|
388
400
|
resolvableUrl = base.protocol + resolvableUrl;
|
389
401
|
} catch (e) {
|
390
|
-
logger?.warn(
|
402
|
+
logger?.warn(
|
403
|
+
`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`
|
404
|
+
);
|
391
405
|
return null;
|
392
406
|
}
|
393
407
|
}
|
@@ -401,9 +415,13 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
|
|
401
415
|
} catch (error) {
|
402
416
|
const message = error instanceof Error ? error.message : String(error);
|
403
417
|
if (!/^[a-z]+:/i.test(resolvableUrl) && !resolvableUrl.startsWith("/") && !baseContextUrl) {
|
404
|
-
logger?.warn(
|
418
|
+
logger?.warn(
|
419
|
+
`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`
|
420
|
+
);
|
405
421
|
} else {
|
406
|
-
logger?.warn(
|
422
|
+
logger?.warn(
|
423
|
+
`\u26A0\uFE0F Failed to parse/resolve URL "${resolvableUrl}" ${baseContextUrl ? 'against base "' + baseContextUrl + '"' : "(no base provided)"}: ${message}`
|
424
|
+
);
|
407
425
|
}
|
408
426
|
return null;
|
409
427
|
}
|
@@ -433,14 +451,18 @@ async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
|
|
433
451
|
timeout
|
434
452
|
// Apply network timeout
|
435
453
|
});
|
436
|
-
logger?.debug(
|
454
|
+
logger?.debug(
|
455
|
+
`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data?.byteLength ?? 0} bytes)`
|
456
|
+
);
|
437
457
|
return Buffer.from(response.data);
|
438
458
|
} else if (protocol === "file:") {
|
439
459
|
let filePath;
|
440
460
|
try {
|
441
461
|
filePath = (0, import_url.fileURLToPath)(resolvedUrl);
|
442
462
|
} catch (e) {
|
443
|
-
logger?.error(
|
463
|
+
logger?.error(
|
464
|
+
`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`
|
465
|
+
);
|
444
466
|
return null;
|
445
467
|
}
|
446
468
|
const normalizedForLog = import_path2.default.normalize(filePath);
|
@@ -474,9 +496,13 @@ async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
|
|
474
496
|
logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
|
475
497
|
}
|
476
498
|
} else if (error instanceof Error) {
|
477
|
-
logger?.warn(
|
499
|
+
logger?.warn(
|
500
|
+
`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`
|
501
|
+
);
|
478
502
|
} else {
|
479
|
-
logger?.warn(
|
503
|
+
logger?.warn(
|
504
|
+
`\u26A0\uFE0F An unknown and unexpected error occurred processing asset ${resolvedUrl.href}: ${String(error)}`
|
505
|
+
);
|
480
506
|
}
|
481
507
|
return null;
|
482
508
|
}
|
@@ -487,7 +513,8 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
487
513
|
const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
488
514
|
const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
|
489
515
|
const processFoundUrl = (rawUrl, ruleType) => {
|
490
|
-
if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#"))
|
516
|
+
if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#"))
|
517
|
+
return;
|
491
518
|
const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
|
492
519
|
if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
|
493
520
|
processedInThisParse.add(resolvedUrl);
|
@@ -499,7 +526,9 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
499
526
|
content: void 0
|
500
527
|
// Content will be fetched later if needed
|
501
528
|
});
|
502
|
-
logger?.debug(
|
529
|
+
logger?.debug(
|
530
|
+
`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`
|
531
|
+
);
|
503
532
|
}
|
504
533
|
};
|
505
534
|
let match;
|
@@ -513,14 +542,20 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
513
542
|
return newlyDiscovered;
|
514
543
|
}
|
515
544
|
async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger) {
|
516
|
-
logger?.info(
|
545
|
+
logger?.info(
|
546
|
+
`\u{1F680} Starting asset extraction! Embed: ${embedAssets}. Input: ${inputPathOrUrl || "(HTML content only)"}`
|
547
|
+
);
|
517
548
|
const initialAssets = parsed.assets || [];
|
518
549
|
const finalAssetsMap = /* @__PURE__ */ new Map();
|
519
550
|
let assetsToProcess = [];
|
520
551
|
const processedOrQueuedUrls = /* @__PURE__ */ new Set();
|
521
552
|
const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || "", logger);
|
522
|
-
if (!htmlBaseContextUrl && initialAssets.some(
|
523
|
-
|
553
|
+
if (!htmlBaseContextUrl && initialAssets.some(
|
554
|
+
(a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/")
|
555
|
+
)) {
|
556
|
+
logger?.warn(
|
557
|
+
"\u{1F6A8} No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail."
|
558
|
+
);
|
524
559
|
} else if (htmlBaseContextUrl) {
|
525
560
|
logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
|
526
561
|
}
|
@@ -552,9 +587,13 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
552
587
|
while (assetsToProcess.length > 0) {
|
553
588
|
iterationCount++;
|
554
589
|
if (iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS) {
|
555
|
-
logger?.error(
|
590
|
+
logger?.error(
|
591
|
+
`\u{1F6D1} Asset extraction loop limit hit (${MAX_ASSET_EXTRACTION_ITERATIONS})! Aborting.`
|
592
|
+
);
|
556
593
|
const remainingUrls = assetsToProcess.map((a) => a.url).slice(0, 10).join(", ");
|
557
|
-
logger?.error(
|
594
|
+
logger?.error(
|
595
|
+
`Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`
|
596
|
+
);
|
558
597
|
assetsToProcess.forEach((asset) => {
|
559
598
|
if (!finalAssetsMap.has(asset.url)) {
|
560
599
|
finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
|
@@ -580,7 +619,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
580
619
|
try {
|
581
620
|
assetUrlObj = new import_url.URL(asset.url);
|
582
621
|
} catch (urlError) {
|
583
|
-
logger?.warn(
|
622
|
+
logger?.warn(
|
623
|
+
`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`
|
624
|
+
);
|
584
625
|
finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
|
585
626
|
continue;
|
586
627
|
}
|
@@ -616,7 +657,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
616
657
|
cssContentForParsing = textContent;
|
617
658
|
}
|
618
659
|
} else {
|
619
|
-
logger?.warn(
|
660
|
+
logger?.warn(
|
661
|
+
`Could not decode ${asset.type} asset ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`
|
662
|
+
);
|
620
663
|
cssContentForParsing = void 0;
|
621
664
|
if (embedAssets) {
|
622
665
|
finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
|
@@ -637,14 +680,18 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
637
680
|
try {
|
638
681
|
const attemptedTextContent = assetContentBuffer.toString("utf-8");
|
639
682
|
if (isUtf8DecodingLossy(assetContentBuffer, attemptedTextContent)) {
|
640
|
-
logger?.warn(
|
683
|
+
logger?.warn(
|
684
|
+
`Couldn't embed unclassified asset ${asset.url} as text due to invalid UTF-8 sequences. Falling back to base64 (octet-stream).`
|
685
|
+
);
|
641
686
|
finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
|
642
687
|
} else {
|
643
688
|
finalContent = attemptedTextContent;
|
644
689
|
logger?.debug(`Successfully embedded unclassified asset ${asset.url} as text.`);
|
645
690
|
}
|
646
691
|
} catch (decodeError) {
|
647
|
-
logger?.warn(
|
692
|
+
logger?.warn(
|
693
|
+
`Error during text decoding for unclassified asset ${asset.url}: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}. Falling back to base64.`
|
694
|
+
);
|
648
695
|
finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
|
649
696
|
}
|
650
697
|
} else {
|
@@ -658,7 +705,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
658
705
|
finalAssetsMap.set(asset.url, { ...asset, url: asset.url, content: finalContent });
|
659
706
|
if (asset.type === "css" && cssContentForParsing) {
|
660
707
|
const cssBaseContextUrl = determineBaseUrl(asset.url, logger);
|
661
|
-
logger?.debug(
|
708
|
+
logger?.debug(
|
709
|
+
`CSS base context for resolving nested assets within ${asset.url}: ${cssBaseContextUrl}`
|
710
|
+
);
|
662
711
|
if (cssBaseContextUrl) {
|
663
712
|
const newlyDiscoveredAssets = extractUrlsFromCSS(
|
664
713
|
cssContentForParsing,
|
@@ -667,25 +716,33 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
667
716
|
logger
|
668
717
|
);
|
669
718
|
if (newlyDiscoveredAssets.length > 0) {
|
670
|
-
logger?.debug(
|
719
|
+
logger?.debug(
|
720
|
+
`Discovered ${newlyDiscoveredAssets.length} nested assets in CSS ${asset.url}. Checking against queue...`
|
721
|
+
);
|
671
722
|
for (const newAsset of newlyDiscoveredAssets) {
|
672
723
|
if (!processedOrQueuedUrls.has(newAsset.url)) {
|
673
724
|
processedOrQueuedUrls.add(newAsset.url);
|
674
725
|
assetsToProcess.push(newAsset);
|
675
726
|
logger?.debug(` -> Queued new nested asset: ${newAsset.url}`);
|
676
727
|
} else {
|
677
|
-
logger?.debug(
|
728
|
+
logger?.debug(
|
729
|
+
` -> Skipping already processed/queued nested asset: ${newAsset.url}`
|
730
|
+
);
|
678
731
|
}
|
679
732
|
}
|
680
733
|
}
|
681
734
|
} else {
|
682
|
-
logger?.warn(
|
735
|
+
logger?.warn(
|
736
|
+
`Could not determine base URL context for CSS file ${asset.url}. Cannot resolve nested relative paths within it.`
|
737
|
+
);
|
683
738
|
}
|
684
739
|
}
|
685
740
|
}
|
686
741
|
}
|
687
742
|
const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? `${MAX_ASSET_EXTRACTION_ITERATIONS}+ (limit hit)` : iterationCount;
|
688
|
-
logger?.info(
|
743
|
+
logger?.info(
|
744
|
+
`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`
|
745
|
+
);
|
689
746
|
return {
|
690
747
|
htmlContent: parsed.htmlContent,
|
691
748
|
assets: Array.from(finalAssetsMap.values())
|
@@ -724,7 +781,7 @@ async function minifyAssets(parsed, options = {}, logger) {
|
|
724
781
|
logger?.debug(`Minification flags: ${JSON.stringify(minifyFlags)}`);
|
725
782
|
const minifiedAssets = await Promise.all(
|
726
783
|
currentAssets.map(async (asset) => {
|
727
|
-
|
784
|
+
const processedAsset = { ...asset };
|
728
785
|
if (typeof processedAsset.content !== "string" || processedAsset.content.length === 0) {
|
729
786
|
return processedAsset;
|
730
787
|
}
|
@@ -739,13 +796,17 @@ async function minifyAssets(parsed, options = {}, logger) {
|
|
739
796
|
logger?.warn(`\u26A0\uFE0F CleanCSS failed for ${assetIdentifier}: ${result.errors.join(", ")}`);
|
740
797
|
} else {
|
741
798
|
if (result.warnings && result.warnings.length > 0) {
|
742
|
-
logger?.debug(
|
799
|
+
logger?.debug(
|
800
|
+
`CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`
|
801
|
+
);
|
743
802
|
}
|
744
803
|
if (result.styles) {
|
745
804
|
newContent = result.styles;
|
746
805
|
logger?.debug(`CSS minified successfully: ${assetIdentifier}`);
|
747
806
|
} else {
|
748
|
-
logger?.warn(
|
807
|
+
logger?.warn(
|
808
|
+
`\u26A0\uFE0F CleanCSS produced no styles but reported no errors for ${assetIdentifier}. Keeping original.`
|
809
|
+
);
|
749
810
|
}
|
750
811
|
}
|
751
812
|
}
|
@@ -758,15 +819,21 @@ async function minifyAssets(parsed, options = {}, logger) {
|
|
758
819
|
} else {
|
759
820
|
const terserError = result.error;
|
760
821
|
if (terserError) {
|
761
|
-
logger?.warn(
|
822
|
+
logger?.warn(
|
823
|
+
`\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`
|
824
|
+
);
|
762
825
|
} else {
|
763
|
-
logger?.warn(
|
826
|
+
logger?.warn(
|
827
|
+
`\u26A0\uFE0F Terser produced no code but reported no errors for ${assetIdentifier}. Keeping original.`
|
828
|
+
);
|
764
829
|
}
|
765
830
|
}
|
766
831
|
}
|
767
832
|
} catch (err) {
|
768
833
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
769
|
-
logger?.warn(
|
834
|
+
logger?.warn(
|
835
|
+
`\u26A0\uFE0F Failed to minify asset ${assetIdentifier} (${processedAsset.type}): ${errorMessage}`
|
836
|
+
);
|
770
837
|
}
|
771
838
|
processedAsset.content = newContent;
|
772
839
|
return processedAsset;
|
@@ -929,7 +996,9 @@ function inlineAssets($, assets, logger) {
|
|
929
996
|
logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
|
930
997
|
element.attr(srcAttr, asset.content);
|
931
998
|
} else if (src) {
|
932
|
-
logger?.warn(
|
999
|
+
logger?.warn(
|
1000
|
+
`Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`
|
1001
|
+
);
|
933
1002
|
}
|
934
1003
|
});
|
935
1004
|
$("img[srcset], source[srcset]").each((_, el) => {
|
@@ -1063,7 +1132,9 @@ function bundleMultiPageHTML(pages, logger) {
|
|
1063
1132
|
} else if (!baseSlug) {
|
1064
1133
|
if (isRootIndex) {
|
1065
1134
|
baseSlug = "index";
|
1066
|
-
logger?.debug(
|
1135
|
+
logger?.debug(
|
1136
|
+
`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`
|
1137
|
+
);
|
1067
1138
|
} else {
|
1068
1139
|
baseSlug = "page";
|
1069
1140
|
logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
|
@@ -1071,14 +1142,18 @@ function bundleMultiPageHTML(pages, logger) {
|
|
1071
1142
|
}
|
1072
1143
|
if (!baseSlug) {
|
1073
1144
|
baseSlug = `page-${pageCounterForFallback++}`;
|
1074
|
-
logger?.warn(
|
1145
|
+
logger?.warn(
|
1146
|
+
`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`
|
1147
|
+
);
|
1075
1148
|
}
|
1076
1149
|
let slug = baseSlug;
|
1077
1150
|
let collisionCounter = 1;
|
1078
1151
|
const originalBaseSlugForLog = baseSlug;
|
1079
1152
|
while (usedSlugs.has(slug)) {
|
1080
1153
|
const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
|
1081
|
-
logger?.warn(
|
1154
|
+
logger?.warn(
|
1155
|
+
`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`
|
1156
|
+
);
|
1082
1157
|
slug = newSlug;
|
1083
1158
|
}
|
1084
1159
|
usedSlugs.add(slug);
|
@@ -1088,7 +1163,8 @@ function bundleMultiPageHTML(pages, logger) {
|
|
1088
1163
|
}
|
1089
1164
|
}
|
1090
1165
|
const defaultPageSlug = usedSlugs.has("index") ? "index" : firstValidSlug || "page";
|
1091
|
-
|
1166
|
+
const output = `
|
1167
|
+
<!DOCTYPE html>
|
1092
1168
|
<html lang="en">
|
1093
1169
|
<head>
|
1094
1170
|
<meta charset="UTF-8">
|
@@ -1104,74 +1180,74 @@ function bundleMultiPageHTML(pages, logger) {
|
|
1104
1180
|
</style>
|
1105
1181
|
</head>
|
1106
1182
|
<body>
|
1107
|
-
|
1108
|
-
|
1183
|
+
<nav id="main-nav">
|
1184
|
+
${validPages.map((p) => {
|
1109
1185
|
const slug = slugMap.get(p.url);
|
1110
1186
|
const label = slug;
|
1111
1187
|
return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
|
1112
1188
|
}).join("\n ")}
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1189
|
+
</nav>
|
1190
|
+
<div id="page-container"></div>
|
1191
|
+
${validPages.map((p) => {
|
1116
1192
|
const slug = slugMap.get(p.url);
|
1117
1193
|
return `<template id="page-${slug}">${p.html}</template>`;
|
1118
1194
|
}).join("\n ")}
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1195
|
+
<script id="router-script">
|
1196
|
+
document.addEventListener('DOMContentLoaded', function() {
|
1197
|
+
const pageContainer = document.getElementById('page-container');
|
1198
|
+
const navLinks = document.querySelectorAll('#main-nav a');
|
1123
1199
|
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1200
|
+
function navigateTo(slug) {
|
1201
|
+
const template = document.getElementById('page-' + slug);
|
1202
|
+
if (!template || !pageContainer) {
|
1203
|
+
console.warn('Navigation failed: Template or container not found for slug:', slug);
|
1204
|
+
// Maybe try navigating to default page? Or just clear container?
|
1205
|
+
if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
|
1206
|
+
return;
|
1207
|
+
}
|
1208
|
+
// Clear previous content and append new content
|
1209
|
+
pageContainer.innerHTML = ''; // Clear reliably
|
1210
|
+
pageContainer.appendChild(template.content.cloneNode(true));
|
1135
1211
|
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1212
|
+
// Update active link styling
|
1213
|
+
navLinks.forEach(link => {
|
1214
|
+
link.classList.toggle('active', link.getAttribute('data-page') === slug);
|
1215
|
+
});
|
1140
1216
|
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
}
|
1217
|
+
// Update URL hash without triggering hashchange if already correct
|
1218
|
+
if (window.location.hash.substring(1) !== slug) {
|
1219
|
+
// Use pushState for cleaner history
|
1220
|
+
history.pushState({ slug: slug }, '', '#' + slug);
|
1146
1221
|
}
|
1222
|
+
}
|
1147
1223
|
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1224
|
+
// Handle back/forward navigation
|
1225
|
+
window.addEventListener('popstate', (event) => {
|
1226
|
+
let slug = window.location.hash.substring(1);
|
1227
|
+
// If popstate event has state use it, otherwise fallback to hash or default
|
1228
|
+
if (event && event.state && event.state.slug) { // Check event exists
|
1229
|
+
slug = event.state.slug;
|
1230
|
+
}
|
1231
|
+
// Ensure the target page exists before navigating, fallback to default slug
|
1232
|
+
const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
|
1233
|
+
navigateTo(targetSlug);
|
1234
|
+
});
|
1159
1235
|
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
});
|
1236
|
+
// Handle direct link clicks
|
1237
|
+
navLinks.forEach(link => {
|
1238
|
+
link.addEventListener('click', function(e) {
|
1239
|
+
e.preventDefault();
|
1240
|
+
const slug = this.getAttribute('data-page');
|
1241
|
+
if (slug) navigateTo(slug);
|
1167
1242
|
});
|
1168
|
-
|
1169
|
-
// Initial page load
|
1170
|
-
const initialHash = window.location.hash.substring(1);
|
1171
|
-
const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
|
1172
|
-
navigateTo(initialSlug);
|
1173
1243
|
});
|
1174
|
-
|
1244
|
+
|
1245
|
+
// Initial page load
|
1246
|
+
const initialHash = window.location.hash.substring(1);
|
1247
|
+
const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
|
1248
|
+
navigateTo(initialSlug);
|
1249
|
+
});
|
1250
|
+
</script>
|
1175
1251
|
</body>
|
1176
1252
|
</html>`;
|
1177
1253
|
logger?.info(`Multi-page bundle generated. Size: ${Buffer.byteLength(output, "utf-8")} bytes.`);
|
@@ -1237,7 +1313,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
|
|
1237
1313
|
throw pageError;
|
1238
1314
|
}
|
1239
1315
|
} catch (launchError) {
|
1240
|
-
logger?.error(
|
1316
|
+
logger?.error(
|
1317
|
+
`Critical error during browser launch or page setup for ${url}: ${launchError.message}`
|
1318
|
+
);
|
1241
1319
|
if (browser) {
|
1242
1320
|
try {
|
1243
1321
|
await browser.close();
|
@@ -1250,7 +1328,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
|
|
1250
1328
|
throw launchError;
|
1251
1329
|
} finally {
|
1252
1330
|
if (browser) {
|
1253
|
-
logger?.warn(
|
1331
|
+
logger?.warn(
|
1332
|
+
`Closing browser in final cleanup for ${url}. This might indicate an unusual error path.`
|
1333
|
+
);
|
1254
1334
|
try {
|
1255
1335
|
await browser.close();
|
1256
1336
|
} catch (closeErr) {
|
@@ -1367,21 +1447,26 @@ async function crawlWebsite(startUrl, options) {
|
|
1367
1447
|
}
|
1368
1448
|
async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1, loggerInstance) {
|
1369
1449
|
const logger = loggerInstance || new Logger();
|
1370
|
-
logger.info(
|
1450
|
+
logger.info(
|
1451
|
+
`Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`
|
1452
|
+
);
|
1371
1453
|
try {
|
1372
1454
|
const crawlOptions = {
|
1373
1455
|
maxDepth,
|
1374
1456
|
logger
|
1375
|
-
/* Add other options like timeout, userAgent if needed */
|
1376
1457
|
};
|
1377
1458
|
const pages = await crawlWebsite(startUrl, crawlOptions);
|
1378
1459
|
if (pages.length === 0) {
|
1379
|
-
logger.warn(
|
1460
|
+
logger.warn(
|
1461
|
+
"Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle."
|
1462
|
+
);
|
1380
1463
|
} else {
|
1381
1464
|
logger.info(`Crawl successful, found ${pages.length} pages. Starting bundling.`);
|
1382
1465
|
}
|
1383
1466
|
const bundledHtml = bundleMultiPageHTML(pages, logger);
|
1384
|
-
logger.info(
|
1467
|
+
logger.info(
|
1468
|
+
`Bundling complete. Output size: ${Buffer.byteLength(bundledHtml, "utf-8")} bytes.`
|
1469
|
+
);
|
1385
1470
|
logger.info(`Writing bundled HTML to ${outputFile}`);
|
1386
1471
|
await fs2.writeFile(outputFile, bundledHtml, "utf-8");
|
1387
1472
|
logger.info(`Successfully wrote bundled output to ${outputFile}`);
|