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.
Files changed (74) hide show
  1. package/.eslintrc.json +67 -8
  2. package/.releaserc.js +25 -27
  3. package/CHANGELOG.md +14 -22
  4. package/LICENSE.md +21 -0
  5. package/README.md +22 -53
  6. package/commitlint.config.js +30 -34
  7. package/dist/cli/cli-entry.cjs +183 -98
  8. package/dist/cli/cli-entry.cjs.map +1 -1
  9. package/dist/index.d.ts +0 -3
  10. package/dist/index.js +178 -97
  11. package/dist/index.js.map +1 -1
  12. package/docs/.vitepress/config.ts +38 -33
  13. package/docs/.vitepress/sidebar-generator.ts +89 -38
  14. package/docs/architecture.md +186 -0
  15. package/docs/cli.md +23 -23
  16. package/docs/code-of-conduct.md +7 -1
  17. package/docs/configuration.md +12 -11
  18. package/docs/contributing.md +6 -2
  19. package/docs/deployment.md +10 -5
  20. package/docs/development.md +8 -5
  21. package/docs/getting-started.md +13 -13
  22. package/docs/index.md +1 -1
  23. package/docs/public/android-chrome-192x192.png +0 -0
  24. package/docs/public/android-chrome-512x512.png +0 -0
  25. package/docs/public/apple-touch-icon.png +0 -0
  26. package/docs/public/favicon-16x16.png +0 -0
  27. package/docs/public/favicon-32x32.png +0 -0
  28. package/docs/public/favicon.ico +0 -0
  29. package/docs/roadmap.md +233 -0
  30. package/docs/site.webmanifest +1 -0
  31. package/docs/troubleshooting.md +12 -1
  32. package/examples/main.ts +5 -30
  33. package/examples/sample-project/script.js +1 -1
  34. package/jest.config.ts +8 -13
  35. package/nodemon.json +5 -10
  36. package/package.json +2 -5
  37. package/src/cli/cli-entry.ts +2 -2
  38. package/src/cli/cli.ts +21 -16
  39. package/src/cli/options.ts +127 -113
  40. package/src/core/bundler.ts +253 -222
  41. package/src/core/extractor.ts +632 -565
  42. package/src/core/minifier.ts +173 -162
  43. package/src/core/packer.ts +141 -137
  44. package/src/core/parser.ts +74 -73
  45. package/src/core/web-fetcher.ts +270 -258
  46. package/src/index.ts +18 -17
  47. package/src/types.ts +9 -11
  48. package/src/utils/font.ts +12 -6
  49. package/src/utils/logger.ts +110 -105
  50. package/src/utils/meta.ts +75 -76
  51. package/src/utils/mime.ts +50 -50
  52. package/src/utils/slugify.ts +33 -34
  53. package/tests/unit/cli/cli-entry.test.ts +72 -70
  54. package/tests/unit/cli/cli.test.ts +314 -278
  55. package/tests/unit/cli/options.test.ts +294 -301
  56. package/tests/unit/core/bundler.test.ts +426 -329
  57. package/tests/unit/core/extractor.test.ts +793 -549
  58. package/tests/unit/core/minifier.test.ts +374 -274
  59. package/tests/unit/core/packer.test.ts +298 -264
  60. package/tests/unit/core/parser.test.ts +538 -150
  61. package/tests/unit/core/web-fetcher.test.ts +389 -359
  62. package/tests/unit/index.test.ts +238 -197
  63. package/tests/unit/utils/font.test.ts +26 -21
  64. package/tests/unit/utils/logger.test.ts +267 -260
  65. package/tests/unit/utils/meta.test.ts +29 -28
  66. package/tests/unit/utils/mime.test.ts +73 -74
  67. package/tests/unit/utils/slugify.test.ts +14 -12
  68. package/tsconfig.build.json +9 -10
  69. package/tsconfig.jest.json +1 -1
  70. package/tsconfig.json +2 -2
  71. package/tsup.config.ts +8 -9
  72. package/typedoc.json +5 -9
  73. /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
  74. /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
@@ -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("-r, --recursive [depth]", "Recursively crawl site (optional depth)", parseRecursiveValue).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));
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(`[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`);
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(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
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(`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`);
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(`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`);
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(`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`);
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(`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`);
418
+ logger?.warn(
419
+ `Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`
420
+ );
405
421
  } else {
406
- logger?.warn(`\u26A0\uFE0F Failed to parse/resolve URL "${resolvableUrl}" ${baseContextUrl ? 'against base "' + baseContextUrl + '"' : "(no base provided)"}: ${message}`);
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(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data?.byteLength ?? 0} bytes)`);
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(`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`);
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(`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`);
499
+ logger?.warn(
500
+ `\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`
501
+ );
478
502
  } else {
479
- logger?.warn(`\u26A0\uFE0F An unknown and unexpected error occurred processing asset ${resolvedUrl.href}: ${String(error)}`);
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("#")) return;
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(`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`);
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(`\u{1F680} Starting asset extraction! Embed: ${embedAssets}. Input: ${inputPathOrUrl || "(HTML content only)"}`);
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((a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/"))) {
523
- logger?.warn("\u{1F6A8} No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail.");
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(`\u{1F6D1} Asset extraction loop limit hit (${MAX_ASSET_EXTRACTION_ITERATIONS})! Aborting.`);
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(`Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`);
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(`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
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(`Could not decode ${asset.type} asset ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`);
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(`Couldn't embed unclassified asset ${asset.url} as text due to invalid UTF-8 sequences. Falling back to base64 (octet-stream).`);
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(`Error during text decoding for unclassified asset ${asset.url}: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}. Falling back to base64.`);
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(`CSS base context for resolving nested assets within ${asset.url}: ${cssBaseContextUrl}`);
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(`Discovered ${newlyDiscoveredAssets.length} nested assets in CSS ${asset.url}. Checking against queue...`);
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(` -> Skipping already processed/queued nested asset: ${newAsset.url}`);
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(`Could not determine base URL context for CSS file ${asset.url}. Cannot resolve nested relative paths within it.`);
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(`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`);
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
- let processedAsset = { ...asset };
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(`CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`);
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(`\u26A0\uFE0F CleanCSS produced no styles but reported no errors for ${assetIdentifier}. Keeping original.`);
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(`\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`);
822
+ logger?.warn(
823
+ `\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`
824
+ );
762
825
  } else {
763
- logger?.warn(`\u26A0\uFE0F Terser produced no code but reported no errors for ${assetIdentifier}. Keeping original.`);
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(`\u26A0\uFE0F Failed to minify asset ${assetIdentifier} (${processedAsset.type}): ${errorMessage}`);
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(`Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`);
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(`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`);
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(`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`);
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(`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`);
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
- let output = `<!DOCTYPE html>
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
- <nav id="main-nav">
1108
- ${validPages.map((p) => {
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
- </nav>
1114
- <div id="page-container"></div>
1115
- ${validPages.map((p) => {
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
- <script id="router-script">
1120
- document.addEventListener('DOMContentLoaded', function() {
1121
- const pageContainer = document.getElementById('page-container');
1122
- const navLinks = document.querySelectorAll('#main-nav a');
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
- function navigateTo(slug) {
1125
- const template = document.getElementById('page-' + slug);
1126
- if (!template || !pageContainer) {
1127
- console.warn('Navigation failed: Template or container not found for slug:', slug);
1128
- // Maybe try navigating to default page? Or just clear container?
1129
- if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
1130
- return;
1131
- }
1132
- // Clear previous content and append new content
1133
- pageContainer.innerHTML = ''; // Clear reliably
1134
- pageContainer.appendChild(template.content.cloneNode(true));
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
- // Update active link styling
1137
- navLinks.forEach(link => {
1138
- link.classList.toggle('active', link.getAttribute('data-page') === slug);
1139
- });
1212
+ // Update active link styling
1213
+ navLinks.forEach(link => {
1214
+ link.classList.toggle('active', link.getAttribute('data-page') === slug);
1215
+ });
1140
1216
 
1141
- // Update URL hash without triggering hashchange if already correct
1142
- if (window.location.hash.substring(1) !== slug) {
1143
- // Use pushState for cleaner history
1144
- history.pushState({ slug: slug }, '', '#' + slug);
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
- // Handle back/forward navigation
1149
- window.addEventListener('popstate', (event) => {
1150
- let slug = window.location.hash.substring(1);
1151
- // If popstate event has state use it, otherwise fallback to hash or default
1152
- if (event && event.state && event.state.slug) { // Check event exists
1153
- slug = event.state.slug;
1154
- }
1155
- // Ensure the target page exists before navigating, fallback to default slug
1156
- const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
1157
- navigateTo(targetSlug);
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
- // Handle direct link clicks
1161
- navLinks.forEach(link => {
1162
- link.addEventListener('click', function(e) {
1163
- e.preventDefault();
1164
- const slug = this.getAttribute('data-page');
1165
- if (slug) navigateTo(slug);
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
- </script>
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(`Critical error during browser launch or page setup for ${url}: ${launchError.message}`);
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(`Closing browser in final cleanup for ${url}. This might indicate an unusual error path.`);
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(`Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`);
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("Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle.");
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(`Bundling complete. Output size: ${Buffer.byteLength(bundledHtml, "utf-8")} bytes.`);
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}`);