portapack 0.3.0 → 0.3.2

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/.github/workflows/ci.yml +5 -4
  3. package/.releaserc.js +25 -27
  4. package/CHANGELOG.md +12 -19
  5. package/LICENSE.md +21 -0
  6. package/README.md +34 -36
  7. package/commitlint.config.js +30 -34
  8. package/dist/cli/cli-entry.cjs +199 -135
  9. package/dist/cli/cli-entry.cjs.map +1 -1
  10. package/dist/index.d.ts +0 -3
  11. package/dist/index.js +194 -134
  12. package/dist/index.js.map +1 -1
  13. package/docs/.vitepress/config.ts +36 -34
  14. package/docs/.vitepress/sidebar-generator.ts +89 -38
  15. package/docs/cli.md +29 -82
  16. package/docs/code-of-conduct.md +7 -1
  17. package/docs/configuration.md +103 -117
  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 +76 -45
  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/site.webmanifest +1 -0
  30. package/docs/troubleshooting.md +12 -1
  31. package/examples/main.ts +7 -10
  32. package/examples/sample-project/script.js +1 -1
  33. package/jest.config.ts +8 -13
  34. package/nodemon.json +5 -10
  35. package/package.json +2 -5
  36. package/src/cli/cli-entry.ts +2 -2
  37. package/src/cli/cli.ts +21 -16
  38. package/src/cli/options.ts +127 -113
  39. package/src/core/bundler.ts +254 -221
  40. package/src/core/extractor.ts +639 -520
  41. package/src/core/minifier.ts +173 -162
  42. package/src/core/packer.ts +141 -137
  43. package/src/core/parser.ts +74 -73
  44. package/src/core/web-fetcher.ts +270 -258
  45. package/src/index.ts +18 -17
  46. package/src/types.ts +9 -11
  47. package/src/utils/font.ts +12 -6
  48. package/src/utils/logger.ts +110 -105
  49. package/src/utils/meta.ts +75 -76
  50. package/src/utils/mime.ts +50 -50
  51. package/src/utils/slugify.ts +33 -34
  52. package/tests/unit/cli/cli-entry.test.ts +72 -70
  53. package/tests/unit/cli/cli.test.ts +314 -278
  54. package/tests/unit/cli/options.test.ts +294 -301
  55. package/tests/unit/core/bundler.test.ts +426 -329
  56. package/tests/unit/core/extractor.test.ts +828 -380
  57. package/tests/unit/core/minifier.test.ts +374 -274
  58. package/tests/unit/core/packer.test.ts +298 -264
  59. package/tests/unit/core/parser.test.ts +538 -150
  60. package/tests/unit/core/web-fetcher.test.ts +389 -359
  61. package/tests/unit/index.test.ts +238 -197
  62. package/tests/unit/utils/font.test.ts +26 -21
  63. package/tests/unit/utils/logger.test.ts +267 -260
  64. package/tests/unit/utils/meta.test.ts +29 -28
  65. package/tests/unit/utils/mime.test.ts +73 -74
  66. package/tests/unit/utils/slugify.test.ts +14 -12
  67. package/tsconfig.build.json +9 -10
  68. package/tsconfig.jest.json +2 -1
  69. package/tsconfig.json +2 -2
  70. package/tsup.config.ts +8 -8
  71. package/typedoc.json +5 -9
  72. package/docs/demo.md +0 -46
  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
  }
@@ -326,7 +332,6 @@ function isUtf8DecodingLossy(originalBuffer, decodedString) {
326
332
  }
327
333
  }
328
334
  function determineBaseUrl(inputPathOrUrl, logger) {
329
- console.log(`[DEBUG determineBaseUrl] Input: "${inputPathOrUrl}"`);
330
335
  logger?.debug(`Determining base URL for input: ${inputPathOrUrl}`);
331
336
  if (!inputPathOrUrl) {
332
337
  logger?.warn("Cannot determine base URL: inputPathOrUrl is empty or invalid.");
@@ -340,11 +345,11 @@ function determineBaseUrl(inputPathOrUrl, logger) {
340
345
  url.hash = "";
341
346
  const baseUrl = url.href;
342
347
  logger?.debug(`Determined remote base URL: ${baseUrl}`);
343
- console.log(`[DEBUG determineBaseUrl] Determined Remote URL: "${baseUrl}"`);
344
348
  return baseUrl;
345
349
  } else if (inputPathOrUrl.includes("://") && !inputPathOrUrl.startsWith("file:")) {
346
- logger?.warn(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
347
- console.log(`[DEBUG determineBaseUrl] Unsupported protocol.`);
350
+ logger?.warn(
351
+ `Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`
352
+ );
348
353
  return void 0;
349
354
  } else {
350
355
  let resourcePath;
@@ -360,9 +365,7 @@ function determineBaseUrl(inputPathOrUrl, logger) {
360
365
  isInputLikelyDirectory = false;
361
366
  }
362
367
  }
363
- console.log(`[DEBUG determineBaseUrl] resourcePath: "${resourcePath}", isInputLikelyDirectory: ${isInputLikelyDirectory}`);
364
368
  const baseDirPath = isInputLikelyDirectory ? resourcePath : import_path2.default.dirname(resourcePath);
365
- console.log(`[DEBUG determineBaseUrl] Calculated baseDirPath: "${baseDirPath}"`);
366
369
  let normalizedPathForURL = baseDirPath.replace(/\\/g, "/");
367
370
  if (/^[A-Z]:\//i.test(normalizedPathForURL) && !normalizedPathForURL.startsWith("/")) {
368
371
  normalizedPathForURL = "/" + normalizedPathForURL;
@@ -372,14 +375,16 @@ function determineBaseUrl(inputPathOrUrl, logger) {
372
375
  }
373
376
  const fileUrl = new import_url.URL("file://" + normalizedPathForURL);
374
377
  const fileUrlString = fileUrl.href;
375
- logger?.debug(`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`);
376
- console.log(`[DEBUG determineBaseUrl] Determined File URL: "${fileUrlString}"`);
378
+ logger?.debug(
379
+ `Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`
380
+ );
377
381
  return fileUrlString;
378
382
  }
379
383
  } catch (error) {
380
384
  const message = error instanceof Error ? error.message : String(error);
381
- console.error(`[DEBUG determineBaseUrl] Error determining base URL: ${message}`);
382
- 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
+ );
383
388
  return void 0;
384
389
  }
385
390
  }
@@ -394,7 +399,9 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
394
399
  const base = new import_url.URL(baseContextUrl);
395
400
  resolvableUrl = base.protocol + resolvableUrl;
396
401
  } catch (e) {
397
- 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
+ );
398
405
  return null;
399
406
  }
400
407
  }
@@ -408,95 +415,94 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
408
415
  } catch (error) {
409
416
  const message = error instanceof Error ? error.message : String(error);
410
417
  if (!/^[a-z]+:/i.test(resolvableUrl) && !resolvableUrl.startsWith("/") && !baseContextUrl) {
411
- 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
+ );
412
421
  } else {
413
- 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
+ );
414
425
  }
415
426
  return null;
416
427
  }
417
428
  }
418
429
  function resolveCssRelativeUrl(relativeUrl, cssBaseContextUrl, logger) {
419
- console.log(`[DEBUG resolveCssRelativeUrl] Input: relative="${relativeUrl}", base="${cssBaseContextUrl}"`);
420
430
  if (!relativeUrl || relativeUrl.startsWith("data:") || relativeUrl.startsWith("#")) {
421
431
  return null;
422
432
  }
423
433
  try {
424
434
  const resolvedUrl = new import_url.URL(relativeUrl, cssBaseContextUrl);
425
- console.log(`[DEBUG resolveCssRelativeUrl] Resolved URL object href: "${resolvedUrl.href}"`);
426
435
  return resolvedUrl.href;
427
436
  } catch (error) {
428
437
  logger?.warn(
429
438
  `Failed to resolve CSS URL: "${relativeUrl}" relative to "${cssBaseContextUrl}": ${String(error)}`
430
439
  );
431
- console.error(`[DEBUG resolveCssRelativeUrl] Error resolving: ${String(error)}`);
432
440
  return null;
433
441
  }
434
442
  }
435
443
  async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
436
- console.log(`[DEBUG fetchAsset] Attempting fetch for URL: ${resolvedUrl.href}`);
437
444
  logger?.debug(`Attempting to fetch asset: ${resolvedUrl.href}`);
438
445
  const protocol = resolvedUrl.protocol;
439
446
  try {
440
447
  if (protocol === "http:" || protocol === "https:") {
441
448
  const response = await axiosNs.default.get(resolvedUrl.href, {
442
449
  responseType: "arraybuffer",
450
+ // Fetch as binary data
443
451
  timeout
452
+ // Apply network timeout
444
453
  });
445
- 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}`);
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
+ );
447
457
  return Buffer.from(response.data);
448
458
  } else if (protocol === "file:") {
449
459
  let filePath;
450
460
  try {
451
461
  filePath = (0, import_url.fileURLToPath)(resolvedUrl);
452
462
  } catch (e) {
453
- console.error(`[DEBUG fetchAsset] fileURLToPath FAILED for: ${resolvedUrl.href}`, e);
454
- 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
+ );
455
466
  return null;
456
467
  }
457
468
  const normalizedForLog = import_path2.default.normalize(filePath);
458
- console.log(`[DEBUG fetchAsset] Attempting readFile with path: "${normalizedForLog}" (Original from URL: "${filePath}")`);
459
469
  const data = await (0, import_promises.readFile)(filePath);
460
- console.log(`[DEBUG fetchAsset] readFile call SUCCEEDED for path: "${normalizedForLog}". Data length: ${data?.byteLength}`);
461
470
  logger?.debug(`Read local file ${filePath} (${data.byteLength} bytes)`);
462
471
  return data;
463
472
  } else {
464
- console.log(`[DEBUG fetchAsset] Unsupported protocol: ${protocol}`);
465
473
  logger?.warn(`Unsupported protocol "${protocol}" in URL: ${resolvedUrl.href}`);
466
474
  return null;
467
475
  }
468
476
  } catch (error) {
469
477
  const failedId = protocol === "file:" ? import_path2.default.normalize((0, import_url.fileURLToPath)(resolvedUrl)) : resolvedUrl.href;
470
- console.error(`[DEBUG fetchAsset] fetch/read FAILED for: "${failedId}". Error:`, error);
471
- if ((protocol === "http:" || protocol === "https:") && axiosNs.isAxiosError(error)) {
472
- const status = error.response?.status ?? "N/A";
473
- const statusText = error.response?.statusText ?? "Error";
474
- const code = error.code ?? "N/A";
475
- const message = error.message;
476
- const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: Status ${status} - ${statusText}. Code: ${code}, Message: ${message}`;
478
+ if ((protocol === "http:" || protocol === "https:") && error?.isAxiosError === true) {
479
+ const axiosError = error;
480
+ const status = axiosError.response?.status ?? "N/A";
481
+ const code = axiosError.code ?? "N/A";
482
+ const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: ${axiosError.message} (Code: ${code})`;
477
483
  logger?.warn(logMessage);
478
- }
479
- if (error instanceof Error && error.code === "ENOENT") {
484
+ } else if (protocol === "file:" && error instanceof Error) {
480
485
  let failedPath = resolvedUrl.href;
481
486
  try {
482
487
  failedPath = (0, import_url.fileURLToPath)(resolvedUrl);
483
488
  } catch {
484
489
  }
485
490
  failedPath = import_path2.default.normalize(failedPath);
486
- if (error instanceof Error && error.code === "ENOENT") {
491
+ if (error.code === "ENOENT") {
487
492
  logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
488
- } else if (error instanceof Error && error.code === "EACCES") {
493
+ } else if (error.code === "EACCES") {
489
494
  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
495
  } else {
494
- logger?.warn(`\u26A0\uFE0F An unknown error occurred while reading local asset ${failedPath}: ${String(error)}`);
496
+ logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
495
497
  }
496
498
  } else if (error instanceof Error) {
497
- 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
+ );
498
502
  } else {
499
- 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
+ );
500
506
  }
501
507
  return null;
502
508
  }
@@ -507,7 +513,8 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
507
513
  const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
508
514
  const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
509
515
  const processFoundUrl = (rawUrl, ruleType) => {
510
- if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:")) return;
516
+ if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#"))
517
+ return;
511
518
  const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
512
519
  if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
513
520
  processedInThisParse.add(resolvedUrl);
@@ -515,11 +522,13 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
515
522
  newlyDiscovered.push({
516
523
  type: assetType,
517
524
  url: resolvedUrl,
518
- // The resolved absolute URL string
525
+ // Store the resolved absolute URL string
519
526
  content: void 0
520
527
  // Content will be fetched later if needed
521
528
  });
522
- 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
+ );
523
532
  }
524
533
  };
525
534
  let match;
@@ -533,14 +542,20 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
533
542
  return newlyDiscovered;
534
543
  }
535
544
  async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger) {
536
- 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
+ );
537
548
  const initialAssets = parsed.assets || [];
538
549
  const finalAssetsMap = /* @__PURE__ */ new Map();
539
550
  let assetsToProcess = [];
540
551
  const processedOrQueuedUrls = /* @__PURE__ */ new Set();
541
552
  const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || "", logger);
542
- if (!htmlBaseContextUrl && initialAssets.some((a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/"))) {
543
- 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
+ );
544
559
  } else if (htmlBaseContextUrl) {
545
560
  logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
546
561
  }
@@ -552,7 +567,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
552
567
  continue;
553
568
  }
554
569
  const urlToQueue = resolvedUrlObj.href;
555
- if (!urlToQueue.startsWith("data:") && !processedOrQueuedUrls.has(urlToQueue)) {
570
+ if (!processedOrQueuedUrls.has(urlToQueue)) {
556
571
  processedOrQueuedUrls.add(urlToQueue);
557
572
  const { assetType: guessedType } = guessMimeType(urlToQueue);
558
573
  const initialType = asset.type ?? guessedType;
@@ -561,10 +576,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
561
576
  // Use the resolved URL
562
577
  type: initialType,
563
578
  content: void 0
579
+ // Content is initially undefined
564
580
  });
565
581
  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
582
  } else {
569
583
  logger?.debug(` -> Skipping already processed/queued initial asset: ${urlToQueue}`);
570
584
  }
@@ -573,9 +587,13 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
573
587
  while (assetsToProcess.length > 0) {
574
588
  iterationCount++;
575
589
  if (iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS) {
576
- 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
+ );
577
593
  const remainingUrls = assetsToProcess.map((a) => a.url).slice(0, 10).join(", ");
578
- logger?.error(`Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`);
594
+ logger?.error(
595
+ `Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`
596
+ );
579
597
  assetsToProcess.forEach((asset) => {
580
598
  if (!finalAssetsMap.has(asset.url)) {
581
599
  finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
@@ -601,7 +619,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
601
619
  try {
602
620
  assetUrlObj = new import_url.URL(asset.url);
603
621
  } catch (urlError) {
604
- 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
+ );
605
625
  finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
606
626
  continue;
607
627
  }
@@ -637,7 +657,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
637
657
  cssContentForParsing = textContent;
638
658
  }
639
659
  } else {
640
- 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
+ );
641
663
  cssContentForParsing = void 0;
642
664
  if (embedAssets) {
643
665
  finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
@@ -658,14 +680,18 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
658
680
  try {
659
681
  const attemptedTextContent = assetContentBuffer.toString("utf-8");
660
682
  if (isUtf8DecodingLossy(assetContentBuffer, attemptedTextContent)) {
661
- 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
+ );
662
686
  finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
663
687
  } else {
664
688
  finalContent = attemptedTextContent;
665
689
  logger?.debug(`Successfully embedded unclassified asset ${asset.url} as text.`);
666
690
  }
667
691
  } catch (decodeError) {
668
- 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
+ );
669
695
  finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
670
696
  }
671
697
  } else {
@@ -679,34 +705,44 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
679
705
  finalAssetsMap.set(asset.url, { ...asset, url: asset.url, content: finalContent });
680
706
  if (asset.type === "css" && cssContentForParsing) {
681
707
  const cssBaseContextUrl = determineBaseUrl(asset.url, logger);
682
- 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
+ );
683
711
  if (cssBaseContextUrl) {
684
712
  const newlyDiscoveredAssets = extractUrlsFromCSS(
685
713
  cssContentForParsing,
686
714
  cssBaseContextUrl,
687
- // Use CSS file's base URL
715
+ // Use the CSS file's own URL as the base
688
716
  logger
689
717
  );
690
718
  if (newlyDiscoveredAssets.length > 0) {
691
- 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
+ );
692
722
  for (const newAsset of newlyDiscoveredAssets) {
693
723
  if (!processedOrQueuedUrls.has(newAsset.url)) {
694
724
  processedOrQueuedUrls.add(newAsset.url);
695
725
  assetsToProcess.push(newAsset);
696
726
  logger?.debug(` -> Queued new nested asset: ${newAsset.url}`);
697
727
  } else {
698
- logger?.debug(` -> Skipping already processed/queued nested asset: ${newAsset.url}`);
728
+ logger?.debug(
729
+ ` -> Skipping already processed/queued nested asset: ${newAsset.url}`
730
+ );
699
731
  }
700
732
  }
701
733
  }
702
734
  } else {
703
- 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
+ );
704
738
  }
705
739
  }
706
740
  }
707
741
  }
708
- const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? "MAX+" : iterationCount;
709
- logger?.info(`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`);
742
+ const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? `${MAX_ASSET_EXTRACTION_ITERATIONS}+ (limit hit)` : iterationCount;
743
+ logger?.info(
744
+ `\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`
745
+ );
710
746
  return {
711
747
  htmlContent: parsed.htmlContent,
712
748
  assets: Array.from(finalAssetsMap.values())
@@ -745,7 +781,7 @@ async function minifyAssets(parsed, options = {}, logger) {
745
781
  logger?.debug(`Minification flags: ${JSON.stringify(minifyFlags)}`);
746
782
  const minifiedAssets = await Promise.all(
747
783
  currentAssets.map(async (asset) => {
748
- let processedAsset = { ...asset };
784
+ const processedAsset = { ...asset };
749
785
  if (typeof processedAsset.content !== "string" || processedAsset.content.length === 0) {
750
786
  return processedAsset;
751
787
  }
@@ -760,13 +796,17 @@ async function minifyAssets(parsed, options = {}, logger) {
760
796
  logger?.warn(`\u26A0\uFE0F CleanCSS failed for ${assetIdentifier}: ${result.errors.join(", ")}`);
761
797
  } else {
762
798
  if (result.warnings && result.warnings.length > 0) {
763
- logger?.debug(`CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`);
799
+ logger?.debug(
800
+ `CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`
801
+ );
764
802
  }
765
803
  if (result.styles) {
766
804
  newContent = result.styles;
767
805
  logger?.debug(`CSS minified successfully: ${assetIdentifier}`);
768
806
  } else {
769
- 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
+ );
770
810
  }
771
811
  }
772
812
  }
@@ -779,15 +819,21 @@ async function minifyAssets(parsed, options = {}, logger) {
779
819
  } else {
780
820
  const terserError = result.error;
781
821
  if (terserError) {
782
- 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
+ );
783
825
  } else {
784
- 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
+ );
785
829
  }
786
830
  }
787
831
  }
788
832
  } catch (err) {
789
833
  const errorMessage = err instanceof Error ? err.message : String(err);
790
- 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
+ );
791
837
  }
792
838
  processedAsset.content = newContent;
793
839
  return processedAsset;
@@ -950,7 +996,9 @@ function inlineAssets($, assets, logger) {
950
996
  logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
951
997
  element.attr(srcAttr, asset.content);
952
998
  } else if (src) {
953
- 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
+ );
954
1002
  }
955
1003
  });
956
1004
  $("img[srcset], source[srcset]").each((_, el) => {
@@ -1084,7 +1132,9 @@ function bundleMultiPageHTML(pages, logger) {
1084
1132
  } else if (!baseSlug) {
1085
1133
  if (isRootIndex) {
1086
1134
  baseSlug = "index";
1087
- 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
+ );
1088
1138
  } else {
1089
1139
  baseSlug = "page";
1090
1140
  logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
@@ -1092,14 +1142,18 @@ function bundleMultiPageHTML(pages, logger) {
1092
1142
  }
1093
1143
  if (!baseSlug) {
1094
1144
  baseSlug = `page-${pageCounterForFallback++}`;
1095
- 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
+ );
1096
1148
  }
1097
1149
  let slug = baseSlug;
1098
1150
  let collisionCounter = 1;
1099
1151
  const originalBaseSlugForLog = baseSlug;
1100
1152
  while (usedSlugs.has(slug)) {
1101
1153
  const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
1102
- 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
+ );
1103
1157
  slug = newSlug;
1104
1158
  }
1105
1159
  usedSlugs.add(slug);
@@ -1109,7 +1163,8 @@ function bundleMultiPageHTML(pages, logger) {
1109
1163
  }
1110
1164
  }
1111
1165
  const defaultPageSlug = usedSlugs.has("index") ? "index" : firstValidSlug || "page";
1112
- let output = `<!DOCTYPE html>
1166
+ const output = `
1167
+ <!DOCTYPE html>
1113
1168
  <html lang="en">
1114
1169
  <head>
1115
1170
  <meta charset="UTF-8">
@@ -1125,74 +1180,74 @@ function bundleMultiPageHTML(pages, logger) {
1125
1180
  </style>
1126
1181
  </head>
1127
1182
  <body>
1128
- <nav id="main-nav">
1129
- ${validPages.map((p) => {
1183
+ <nav id="main-nav">
1184
+ ${validPages.map((p) => {
1130
1185
  const slug = slugMap.get(p.url);
1131
1186
  const label = slug;
1132
1187
  return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
1133
1188
  }).join("\n ")}
1134
- </nav>
1135
- <div id="page-container"></div>
1136
- ${validPages.map((p) => {
1189
+ </nav>
1190
+ <div id="page-container"></div>
1191
+ ${validPages.map((p) => {
1137
1192
  const slug = slugMap.get(p.url);
1138
1193
  return `<template id="page-${slug}">${p.html}</template>`;
1139
1194
  }).join("\n ")}
1140
- <script id="router-script">
1141
- document.addEventListener('DOMContentLoaded', function() {
1142
- const pageContainer = document.getElementById('page-container');
1143
- 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');
1144
1199
 
1145
- function navigateTo(slug) {
1146
- const template = document.getElementById('page-' + slug);
1147
- if (!template || !pageContainer) {
1148
- console.warn('Navigation failed: Template or container not found for slug:', slug);
1149
- // Maybe try navigating to default page? Or just clear container?
1150
- if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
1151
- return;
1152
- }
1153
- // Clear previous content and append new content
1154
- pageContainer.innerHTML = ''; // Clear reliably
1155
- 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));
1156
1211
 
1157
- // Update active link styling
1158
- navLinks.forEach(link => {
1159
- link.classList.toggle('active', link.getAttribute('data-page') === slug);
1160
- });
1212
+ // Update active link styling
1213
+ navLinks.forEach(link => {
1214
+ link.classList.toggle('active', link.getAttribute('data-page') === slug);
1215
+ });
1161
1216
 
1162
- // Update URL hash without triggering hashchange if already correct
1163
- if (window.location.hash.substring(1) !== slug) {
1164
- // Use pushState for cleaner history
1165
- history.pushState({ slug: slug }, '', '#' + slug);
1166
- }
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);
1167
1221
  }
1222
+ }
1168
1223
 
1169
- // Handle back/forward navigation
1170
- window.addEventListener('popstate', (event) => {
1171
- let slug = window.location.hash.substring(1);
1172
- // If popstate event has state use it, otherwise fallback to hash or default
1173
- if (event && event.state && event.state.slug) { // Check event exists
1174
- slug = event.state.slug;
1175
- }
1176
- // Ensure the target page exists before navigating, fallback to default slug
1177
- const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
1178
- navigateTo(targetSlug);
1179
- });
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
+ });
1180
1235
 
1181
- // Handle direct link clicks
1182
- navLinks.forEach(link => {
1183
- link.addEventListener('click', function(e) {
1184
- e.preventDefault();
1185
- const slug = this.getAttribute('data-page');
1186
- if (slug) navigateTo(slug);
1187
- });
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);
1188
1242
  });
1189
-
1190
- // Initial page load
1191
- const initialHash = window.location.hash.substring(1);
1192
- const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
1193
- navigateTo(initialSlug);
1194
1243
  });
1195
- </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>
1196
1251
  </body>
1197
1252
  </html>`;
1198
1253
  logger?.info(`Multi-page bundle generated. Size: ${Buffer.byteLength(output, "utf-8")} bytes.`);
@@ -1258,7 +1313,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
1258
1313
  throw pageError;
1259
1314
  }
1260
1315
  } catch (launchError) {
1261
- 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
+ );
1262
1319
  if (browser) {
1263
1320
  try {
1264
1321
  await browser.close();
@@ -1271,7 +1328,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
1271
1328
  throw launchError;
1272
1329
  } finally {
1273
1330
  if (browser) {
1274
- 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
+ );
1275
1334
  try {
1276
1335
  await browser.close();
1277
1336
  } catch (closeErr) {
@@ -1388,21 +1447,26 @@ async function crawlWebsite(startUrl, options) {
1388
1447
  }
1389
1448
  async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1, loggerInstance) {
1390
1449
  const logger = loggerInstance || new Logger();
1391
- 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
+ );
1392
1453
  try {
1393
1454
  const crawlOptions = {
1394
1455
  maxDepth,
1395
1456
  logger
1396
- /* Add other options like timeout, userAgent if needed */
1397
1457
  };
1398
1458
  const pages = await crawlWebsite(startUrl, crawlOptions);
1399
1459
  if (pages.length === 0) {
1400
- 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
+ );
1401
1463
  } else {
1402
1464
  logger.info(`Crawl successful, found ${pages.length} pages. Starting bundling.`);
1403
1465
  }
1404
1466
  const bundledHtml = bundleMultiPageHTML(pages, logger);
1405
- 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
+ );
1406
1470
  logger.info(`Writing bundled HTML to ${outputFile}`);
1407
1471
  await fs2.writeFile(outputFile, bundledHtml, "utf-8");
1408
1472
  logger.info(`Successfully wrote bundled output to ${outputFile}`);