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
package/dist/index.js CHANGED
@@ -112,7 +112,9 @@ var Logger = class _Logger {
112
112
  case "none":
113
113
  return new _Logger(0 /* NONE */);
114
114
  default:
115
- console.warn(`[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`);
115
+ console.warn(
116
+ `[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`
117
+ );
116
118
  return new _Logger(defaultLevel);
117
119
  }
118
120
  }
@@ -194,7 +196,6 @@ function isUtf8DecodingLossy(originalBuffer, decodedString) {
194
196
  }
195
197
  }
196
198
  function determineBaseUrl(inputPathOrUrl, logger) {
197
- console.log(`[DEBUG determineBaseUrl] Input: "${inputPathOrUrl}"`);
198
199
  logger?.debug(`Determining base URL for input: ${inputPathOrUrl}`);
199
200
  if (!inputPathOrUrl) {
200
201
  logger?.warn("Cannot determine base URL: inputPathOrUrl is empty or invalid.");
@@ -208,11 +209,11 @@ function determineBaseUrl(inputPathOrUrl, logger) {
208
209
  url.hash = "";
209
210
  const baseUrl = url.href;
210
211
  logger?.debug(`Determined remote base URL: ${baseUrl}`);
211
- console.log(`[DEBUG determineBaseUrl] Determined Remote URL: "${baseUrl}"`);
212
212
  return baseUrl;
213
213
  } else if (inputPathOrUrl.includes("://") && !inputPathOrUrl.startsWith("file:")) {
214
- logger?.warn(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
215
- console.log(`[DEBUG determineBaseUrl] Unsupported protocol.`);
214
+ logger?.warn(
215
+ `Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`
216
+ );
216
217
  return void 0;
217
218
  } else {
218
219
  let resourcePath;
@@ -228,9 +229,7 @@ function determineBaseUrl(inputPathOrUrl, logger) {
228
229
  isInputLikelyDirectory = false;
229
230
  }
230
231
  }
231
- console.log(`[DEBUG determineBaseUrl] resourcePath: "${resourcePath}", isInputLikelyDirectory: ${isInputLikelyDirectory}`);
232
232
  const baseDirPath = isInputLikelyDirectory ? resourcePath : path2.dirname(resourcePath);
233
- console.log(`[DEBUG determineBaseUrl] Calculated baseDirPath: "${baseDirPath}"`);
234
233
  let normalizedPathForURL = baseDirPath.replace(/\\/g, "/");
235
234
  if (/^[A-Z]:\//i.test(normalizedPathForURL) && !normalizedPathForURL.startsWith("/")) {
236
235
  normalizedPathForURL = "/" + normalizedPathForURL;
@@ -240,14 +239,16 @@ function determineBaseUrl(inputPathOrUrl, logger) {
240
239
  }
241
240
  const fileUrl = new URL2("file://" + normalizedPathForURL);
242
241
  const fileUrlString = fileUrl.href;
243
- logger?.debug(`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`);
244
- console.log(`[DEBUG determineBaseUrl] Determined File URL: "${fileUrlString}"`);
242
+ logger?.debug(
243
+ `Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`
244
+ );
245
245
  return fileUrlString;
246
246
  }
247
247
  } catch (error) {
248
248
  const message = error instanceof Error ? error.message : String(error);
249
- console.error(`[DEBUG determineBaseUrl] Error determining base URL: ${message}`);
250
- logger?.error(`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`);
249
+ logger?.error(
250
+ `\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`
251
+ );
251
252
  return void 0;
252
253
  }
253
254
  }
@@ -262,7 +263,9 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
262
263
  const base = new URL2(baseContextUrl);
263
264
  resolvableUrl = base.protocol + resolvableUrl;
264
265
  } catch (e) {
265
- logger?.warn(`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`);
266
+ logger?.warn(
267
+ `Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`
268
+ );
266
269
  return null;
267
270
  }
268
271
  }
@@ -276,95 +279,94 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
276
279
  } catch (error) {
277
280
  const message = error instanceof Error ? error.message : String(error);
278
281
  if (!/^[a-z]+:/i.test(resolvableUrl) && !resolvableUrl.startsWith("/") && !baseContextUrl) {
279
- logger?.warn(`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`);
282
+ logger?.warn(
283
+ `Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`
284
+ );
280
285
  } else {
281
- logger?.warn(`\u26A0\uFE0F Failed to parse/resolve URL "${resolvableUrl}" ${baseContextUrl ? 'against base "' + baseContextUrl + '"' : "(no base provided)"}: ${message}`);
286
+ logger?.warn(
287
+ `\u26A0\uFE0F Failed to parse/resolve URL "${resolvableUrl}" ${baseContextUrl ? 'against base "' + baseContextUrl + '"' : "(no base provided)"}: ${message}`
288
+ );
282
289
  }
283
290
  return null;
284
291
  }
285
292
  }
286
293
  function resolveCssRelativeUrl(relativeUrl, cssBaseContextUrl, logger) {
287
- console.log(`[DEBUG resolveCssRelativeUrl] Input: relative="${relativeUrl}", base="${cssBaseContextUrl}"`);
288
294
  if (!relativeUrl || relativeUrl.startsWith("data:") || relativeUrl.startsWith("#")) {
289
295
  return null;
290
296
  }
291
297
  try {
292
298
  const resolvedUrl = new URL2(relativeUrl, cssBaseContextUrl);
293
- console.log(`[DEBUG resolveCssRelativeUrl] Resolved URL object href: "${resolvedUrl.href}"`);
294
299
  return resolvedUrl.href;
295
300
  } catch (error) {
296
301
  logger?.warn(
297
302
  `Failed to resolve CSS URL: "${relativeUrl}" relative to "${cssBaseContextUrl}": ${String(error)}`
298
303
  );
299
- console.error(`[DEBUG resolveCssRelativeUrl] Error resolving: ${String(error)}`);
300
304
  return null;
301
305
  }
302
306
  }
303
307
  async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
304
- console.log(`[DEBUG fetchAsset] Attempting fetch for URL: ${resolvedUrl.href}`);
305
308
  logger?.debug(`Attempting to fetch asset: ${resolvedUrl.href}`);
306
309
  const protocol = resolvedUrl.protocol;
307
310
  try {
308
311
  if (protocol === "http:" || protocol === "https:") {
309
312
  const response = await axiosNs.default.get(resolvedUrl.href, {
310
313
  responseType: "arraybuffer",
314
+ // Fetch as binary data
311
315
  timeout
316
+ // Apply network timeout
312
317
  });
313
- logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data?.byteLength ?? 0} bytes)`);
314
- console.log(`[DEBUG fetchAsset] HTTP fetch SUCCESS for: ${resolvedUrl.href}, Status: ${response.status}`);
318
+ logger?.debug(
319
+ `Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data?.byteLength ?? 0} bytes)`
320
+ );
315
321
  return Buffer.from(response.data);
316
322
  } else if (protocol === "file:") {
317
323
  let filePath;
318
324
  try {
319
325
  filePath = fileURLToPath(resolvedUrl);
320
326
  } catch (e) {
321
- console.error(`[DEBUG fetchAsset] fileURLToPath FAILED for: ${resolvedUrl.href}`, e);
322
- logger?.error(`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`);
327
+ logger?.error(
328
+ `Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`
329
+ );
323
330
  return null;
324
331
  }
325
332
  const normalizedForLog = path2.normalize(filePath);
326
- console.log(`[DEBUG fetchAsset] Attempting readFile with path: "${normalizedForLog}" (Original from URL: "${filePath}")`);
327
333
  const data = await readFile(filePath);
328
- console.log(`[DEBUG fetchAsset] readFile call SUCCEEDED for path: "${normalizedForLog}". Data length: ${data?.byteLength}`);
329
334
  logger?.debug(`Read local file ${filePath} (${data.byteLength} bytes)`);
330
335
  return data;
331
336
  } else {
332
- console.log(`[DEBUG fetchAsset] Unsupported protocol: ${protocol}`);
333
337
  logger?.warn(`Unsupported protocol "${protocol}" in URL: ${resolvedUrl.href}`);
334
338
  return null;
335
339
  }
336
340
  } catch (error) {
337
341
  const failedId = protocol === "file:" ? path2.normalize(fileURLToPath(resolvedUrl)) : resolvedUrl.href;
338
- console.error(`[DEBUG fetchAsset] fetch/read FAILED for: "${failedId}". Error:`, error);
339
- if ((protocol === "http:" || protocol === "https:") && axiosNs.isAxiosError(error)) {
340
- const status = error.response?.status ?? "N/A";
341
- const statusText = error.response?.statusText ?? "Error";
342
- const code = error.code ?? "N/A";
343
- const message = error.message;
344
- const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: Status ${status} - ${statusText}. Code: ${code}, Message: ${message}`;
342
+ if ((protocol === "http:" || protocol === "https:") && error?.isAxiosError === true) {
343
+ const axiosError = error;
344
+ const status = axiosError.response?.status ?? "N/A";
345
+ const code = axiosError.code ?? "N/A";
346
+ const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: ${axiosError.message} (Code: ${code})`;
345
347
  logger?.warn(logMessage);
346
- }
347
- if (error instanceof Error && error.code === "ENOENT") {
348
+ } else if (protocol === "file:" && error instanceof Error) {
348
349
  let failedPath = resolvedUrl.href;
349
350
  try {
350
351
  failedPath = fileURLToPath(resolvedUrl);
351
352
  } catch {
352
353
  }
353
354
  failedPath = path2.normalize(failedPath);
354
- if (error instanceof Error && error.code === "ENOENT") {
355
+ if (error.code === "ENOENT") {
355
356
  logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
356
- } else if (error instanceof Error && error.code === "EACCES") {
357
+ } else if (error.code === "EACCES") {
357
358
  logger?.warn(`\u26A0\uFE0F Permission denied (EACCES) reading asset: ${failedPath}.`);
358
- logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
359
- } else if (error instanceof Error) {
360
- logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
361
359
  } else {
362
- logger?.warn(`\u26A0\uFE0F An unknown error occurred while reading local asset ${failedPath}: ${String(error)}`);
360
+ logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
363
361
  }
364
362
  } else if (error instanceof Error) {
365
- logger?.warn(`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`);
363
+ logger?.warn(
364
+ `\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`
365
+ );
366
366
  } else {
367
- logger?.warn(`\u26A0\uFE0F An unknown and unexpected error occurred processing asset ${resolvedUrl.href}: ${String(error)}`);
367
+ logger?.warn(
368
+ `\u26A0\uFE0F An unknown and unexpected error occurred processing asset ${resolvedUrl.href}: ${String(error)}`
369
+ );
368
370
  }
369
371
  return null;
370
372
  }
@@ -375,7 +377,8 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
375
377
  const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
376
378
  const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
377
379
  const processFoundUrl = (rawUrl, ruleType) => {
378
- if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:")) return;
380
+ if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#"))
381
+ return;
379
382
  const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
380
383
  if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
381
384
  processedInThisParse.add(resolvedUrl);
@@ -383,11 +386,13 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
383
386
  newlyDiscovered.push({
384
387
  type: assetType,
385
388
  url: resolvedUrl,
386
- // The resolved absolute URL string
389
+ // Store the resolved absolute URL string
387
390
  content: void 0
388
391
  // Content will be fetched later if needed
389
392
  });
390
- logger?.debug(`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`);
393
+ logger?.debug(
394
+ `Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`
395
+ );
391
396
  }
392
397
  };
393
398
  let match;
@@ -401,14 +406,20 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
401
406
  return newlyDiscovered;
402
407
  }
403
408
  async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger) {
404
- logger?.info(`\u{1F680} Starting asset extraction! Embed: ${embedAssets}. Input: ${inputPathOrUrl || "(HTML content only)"}`);
409
+ logger?.info(
410
+ `\u{1F680} Starting asset extraction! Embed: ${embedAssets}. Input: ${inputPathOrUrl || "(HTML content only)"}`
411
+ );
405
412
  const initialAssets = parsed.assets || [];
406
413
  const finalAssetsMap = /* @__PURE__ */ new Map();
407
414
  let assetsToProcess = [];
408
415
  const processedOrQueuedUrls = /* @__PURE__ */ new Set();
409
416
  const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || "", logger);
410
- if (!htmlBaseContextUrl && initialAssets.some((a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/"))) {
411
- logger?.warn("\u{1F6A8} No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail.");
417
+ if (!htmlBaseContextUrl && initialAssets.some(
418
+ (a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/")
419
+ )) {
420
+ logger?.warn(
421
+ "\u{1F6A8} No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail."
422
+ );
412
423
  } else if (htmlBaseContextUrl) {
413
424
  logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
414
425
  }
@@ -420,7 +431,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
420
431
  continue;
421
432
  }
422
433
  const urlToQueue = resolvedUrlObj.href;
423
- if (!urlToQueue.startsWith("data:") && !processedOrQueuedUrls.has(urlToQueue)) {
434
+ if (!processedOrQueuedUrls.has(urlToQueue)) {
424
435
  processedOrQueuedUrls.add(urlToQueue);
425
436
  const { assetType: guessedType } = guessMimeType(urlToQueue);
426
437
  const initialType = asset.type ?? guessedType;
@@ -429,10 +440,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
429
440
  // Use the resolved URL
430
441
  type: initialType,
431
442
  content: void 0
443
+ // Content is initially undefined
432
444
  });
433
445
  logger?.debug(` -> Queued initial asset: ${urlToQueue} (Original raw: ${asset.url})`);
434
- } else if (urlToQueue.startsWith("data:")) {
435
- logger?.debug(` -> Skipping data URI: ${urlToQueue.substring(0, 50)}...`);
436
446
  } else {
437
447
  logger?.debug(` -> Skipping already processed/queued initial asset: ${urlToQueue}`);
438
448
  }
@@ -441,9 +451,13 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
441
451
  while (assetsToProcess.length > 0) {
442
452
  iterationCount++;
443
453
  if (iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS) {
444
- logger?.error(`\u{1F6D1} Asset extraction loop limit hit (${MAX_ASSET_EXTRACTION_ITERATIONS})! Aborting.`);
454
+ logger?.error(
455
+ `\u{1F6D1} Asset extraction loop limit hit (${MAX_ASSET_EXTRACTION_ITERATIONS})! Aborting.`
456
+ );
445
457
  const remainingUrls = assetsToProcess.map((a) => a.url).slice(0, 10).join(", ");
446
- logger?.error(`Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`);
458
+ logger?.error(
459
+ `Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`
460
+ );
447
461
  assetsToProcess.forEach((asset) => {
448
462
  if (!finalAssetsMap.has(asset.url)) {
449
463
  finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
@@ -469,7 +483,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
469
483
  try {
470
484
  assetUrlObj = new URL2(asset.url);
471
485
  } catch (urlError) {
472
- logger?.warn(`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
486
+ logger?.warn(
487
+ `Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`
488
+ );
473
489
  finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
474
490
  continue;
475
491
  }
@@ -505,7 +521,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
505
521
  cssContentForParsing = textContent;
506
522
  }
507
523
  } else {
508
- logger?.warn(`Could not decode ${asset.type} asset ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`);
524
+ logger?.warn(
525
+ `Could not decode ${asset.type} asset ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`
526
+ );
509
527
  cssContentForParsing = void 0;
510
528
  if (embedAssets) {
511
529
  finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
@@ -526,14 +544,18 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
526
544
  try {
527
545
  const attemptedTextContent = assetContentBuffer.toString("utf-8");
528
546
  if (isUtf8DecodingLossy(assetContentBuffer, attemptedTextContent)) {
529
- logger?.warn(`Couldn't embed unclassified asset ${asset.url} as text due to invalid UTF-8 sequences. Falling back to base64 (octet-stream).`);
547
+ logger?.warn(
548
+ `Couldn't embed unclassified asset ${asset.url} as text due to invalid UTF-8 sequences. Falling back to base64 (octet-stream).`
549
+ );
530
550
  finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
531
551
  } else {
532
552
  finalContent = attemptedTextContent;
533
553
  logger?.debug(`Successfully embedded unclassified asset ${asset.url} as text.`);
534
554
  }
535
555
  } catch (decodeError) {
536
- logger?.warn(`Error during text decoding for unclassified asset ${asset.url}: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}. Falling back to base64.`);
556
+ logger?.warn(
557
+ `Error during text decoding for unclassified asset ${asset.url}: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}. Falling back to base64.`
558
+ );
537
559
  finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
538
560
  }
539
561
  } else {
@@ -547,34 +569,44 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
547
569
  finalAssetsMap.set(asset.url, { ...asset, url: asset.url, content: finalContent });
548
570
  if (asset.type === "css" && cssContentForParsing) {
549
571
  const cssBaseContextUrl = determineBaseUrl(asset.url, logger);
550
- logger?.debug(`CSS base context for resolving nested assets within ${asset.url}: ${cssBaseContextUrl}`);
572
+ logger?.debug(
573
+ `CSS base context for resolving nested assets within ${asset.url}: ${cssBaseContextUrl}`
574
+ );
551
575
  if (cssBaseContextUrl) {
552
576
  const newlyDiscoveredAssets = extractUrlsFromCSS(
553
577
  cssContentForParsing,
554
578
  cssBaseContextUrl,
555
- // Use CSS file's base URL
579
+ // Use the CSS file's own URL as the base
556
580
  logger
557
581
  );
558
582
  if (newlyDiscoveredAssets.length > 0) {
559
- logger?.debug(`Discovered ${newlyDiscoveredAssets.length} nested assets in CSS ${asset.url}. Checking against queue...`);
583
+ logger?.debug(
584
+ `Discovered ${newlyDiscoveredAssets.length} nested assets in CSS ${asset.url}. Checking against queue...`
585
+ );
560
586
  for (const newAsset of newlyDiscoveredAssets) {
561
587
  if (!processedOrQueuedUrls.has(newAsset.url)) {
562
588
  processedOrQueuedUrls.add(newAsset.url);
563
589
  assetsToProcess.push(newAsset);
564
590
  logger?.debug(` -> Queued new nested asset: ${newAsset.url}`);
565
591
  } else {
566
- logger?.debug(` -> Skipping already processed/queued nested asset: ${newAsset.url}`);
592
+ logger?.debug(
593
+ ` -> Skipping already processed/queued nested asset: ${newAsset.url}`
594
+ );
567
595
  }
568
596
  }
569
597
  }
570
598
  } else {
571
- logger?.warn(`Could not determine base URL context for CSS file ${asset.url}. Cannot resolve nested relative paths within it.`);
599
+ logger?.warn(
600
+ `Could not determine base URL context for CSS file ${asset.url}. Cannot resolve nested relative paths within it.`
601
+ );
572
602
  }
573
603
  }
574
604
  }
575
605
  }
576
- const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? "MAX+" : iterationCount;
577
- logger?.info(`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`);
606
+ const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? `${MAX_ASSET_EXTRACTION_ITERATIONS}+ (limit hit)` : iterationCount;
607
+ logger?.info(
608
+ `\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`
609
+ );
578
610
  return {
579
611
  htmlContent: parsed.htmlContent,
580
612
  assets: Array.from(finalAssetsMap.values())
@@ -654,7 +686,7 @@ async function minifyAssets(parsed, options = {}, logger) {
654
686
  logger?.debug(`Minification flags: ${JSON.stringify(minifyFlags)}`);
655
687
  const minifiedAssets = await Promise.all(
656
688
  currentAssets.map(async (asset) => {
657
- let processedAsset = { ...asset };
689
+ const processedAsset = { ...asset };
658
690
  if (typeof processedAsset.content !== "string" || processedAsset.content.length === 0) {
659
691
  return processedAsset;
660
692
  }
@@ -669,13 +701,17 @@ async function minifyAssets(parsed, options = {}, logger) {
669
701
  logger?.warn(`\u26A0\uFE0F CleanCSS failed for ${assetIdentifier}: ${result.errors.join(", ")}`);
670
702
  } else {
671
703
  if (result.warnings && result.warnings.length > 0) {
672
- logger?.debug(`CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`);
704
+ logger?.debug(
705
+ `CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`
706
+ );
673
707
  }
674
708
  if (result.styles) {
675
709
  newContent = result.styles;
676
710
  logger?.debug(`CSS minified successfully: ${assetIdentifier}`);
677
711
  } else {
678
- logger?.warn(`\u26A0\uFE0F CleanCSS produced no styles but reported no errors for ${assetIdentifier}. Keeping original.`);
712
+ logger?.warn(
713
+ `\u26A0\uFE0F CleanCSS produced no styles but reported no errors for ${assetIdentifier}. Keeping original.`
714
+ );
679
715
  }
680
716
  }
681
717
  }
@@ -688,15 +724,21 @@ async function minifyAssets(parsed, options = {}, logger) {
688
724
  } else {
689
725
  const terserError = result.error;
690
726
  if (terserError) {
691
- logger?.warn(`\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`);
727
+ logger?.warn(
728
+ `\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`
729
+ );
692
730
  } else {
693
- logger?.warn(`\u26A0\uFE0F Terser produced no code but reported no errors for ${assetIdentifier}. Keeping original.`);
731
+ logger?.warn(
732
+ `\u26A0\uFE0F Terser produced no code but reported no errors for ${assetIdentifier}. Keeping original.`
733
+ );
694
734
  }
695
735
  }
696
736
  }
697
737
  } catch (err) {
698
738
  const errorMessage = err instanceof Error ? err.message : String(err);
699
- logger?.warn(`\u26A0\uFE0F Failed to minify asset ${assetIdentifier} (${processedAsset.type}): ${errorMessage}`);
739
+ logger?.warn(
740
+ `\u26A0\uFE0F Failed to minify asset ${assetIdentifier} (${processedAsset.type}): ${errorMessage}`
741
+ );
700
742
  }
701
743
  processedAsset.content = newContent;
702
744
  return processedAsset;
@@ -798,7 +840,9 @@ function inlineAssets($, assets, logger) {
798
840
  logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
799
841
  element.attr(srcAttr, asset.content);
800
842
  } else if (src) {
801
- logger?.warn(`Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`);
843
+ logger?.warn(
844
+ `Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`
845
+ );
802
846
  }
803
847
  });
804
848
  $("img[srcset], source[srcset]").each((_, el) => {
@@ -920,7 +964,9 @@ function bundleMultiPageHTML(pages, logger) {
920
964
  } else if (!baseSlug) {
921
965
  if (isRootIndex) {
922
966
  baseSlug = "index";
923
- logger?.debug(`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`);
967
+ logger?.debug(
968
+ `URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`
969
+ );
924
970
  } else {
925
971
  baseSlug = "page";
926
972
  logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
@@ -928,14 +974,18 @@ function bundleMultiPageHTML(pages, logger) {
928
974
  }
929
975
  if (!baseSlug) {
930
976
  baseSlug = `page-${pageCounterForFallback++}`;
931
- logger?.warn(`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`);
977
+ logger?.warn(
978
+ `Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`
979
+ );
932
980
  }
933
981
  let slug = baseSlug;
934
982
  let collisionCounter = 1;
935
983
  const originalBaseSlugForLog = baseSlug;
936
984
  while (usedSlugs.has(slug)) {
937
985
  const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
938
- logger?.warn(`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`);
986
+ logger?.warn(
987
+ `Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`
988
+ );
939
989
  slug = newSlug;
940
990
  }
941
991
  usedSlugs.add(slug);
@@ -945,7 +995,8 @@ function bundleMultiPageHTML(pages, logger) {
945
995
  }
946
996
  }
947
997
  const defaultPageSlug = usedSlugs.has("index") ? "index" : firstValidSlug || "page";
948
- let output = `<!DOCTYPE html>
998
+ const output = `
999
+ <!DOCTYPE html>
949
1000
  <html lang="en">
950
1001
  <head>
951
1002
  <meta charset="UTF-8">
@@ -961,74 +1012,74 @@ function bundleMultiPageHTML(pages, logger) {
961
1012
  </style>
962
1013
  </head>
963
1014
  <body>
964
- <nav id="main-nav">
965
- ${validPages.map((p) => {
1015
+ <nav id="main-nav">
1016
+ ${validPages.map((p) => {
966
1017
  const slug = slugMap.get(p.url);
967
1018
  const label = slug;
968
1019
  return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
969
1020
  }).join("\n ")}
970
- </nav>
971
- <div id="page-container"></div>
972
- ${validPages.map((p) => {
1021
+ </nav>
1022
+ <div id="page-container"></div>
1023
+ ${validPages.map((p) => {
973
1024
  const slug = slugMap.get(p.url);
974
1025
  return `<template id="page-${slug}">${p.html}</template>`;
975
1026
  }).join("\n ")}
976
- <script id="router-script">
977
- document.addEventListener('DOMContentLoaded', function() {
978
- const pageContainer = document.getElementById('page-container');
979
- const navLinks = document.querySelectorAll('#main-nav a');
1027
+ <script id="router-script">
1028
+ document.addEventListener('DOMContentLoaded', function() {
1029
+ const pageContainer = document.getElementById('page-container');
1030
+ const navLinks = document.querySelectorAll('#main-nav a');
980
1031
 
981
- function navigateTo(slug) {
982
- const template = document.getElementById('page-' + slug);
983
- if (!template || !pageContainer) {
984
- console.warn('Navigation failed: Template or container not found for slug:', slug);
985
- // Maybe try navigating to default page? Or just clear container?
986
- if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
987
- return;
988
- }
989
- // Clear previous content and append new content
990
- pageContainer.innerHTML = ''; // Clear reliably
991
- pageContainer.appendChild(template.content.cloneNode(true));
1032
+ function navigateTo(slug) {
1033
+ const template = document.getElementById('page-' + slug);
1034
+ if (!template || !pageContainer) {
1035
+ console.warn('Navigation failed: Template or container not found for slug:', slug);
1036
+ // Maybe try navigating to default page? Or just clear container?
1037
+ if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
1038
+ return;
1039
+ }
1040
+ // Clear previous content and append new content
1041
+ pageContainer.innerHTML = ''; // Clear reliably
1042
+ pageContainer.appendChild(template.content.cloneNode(true));
992
1043
 
993
- // Update active link styling
994
- navLinks.forEach(link => {
995
- link.classList.toggle('active', link.getAttribute('data-page') === slug);
996
- });
1044
+ // Update active link styling
1045
+ navLinks.forEach(link => {
1046
+ link.classList.toggle('active', link.getAttribute('data-page') === slug);
1047
+ });
997
1048
 
998
- // Update URL hash without triggering hashchange if already correct
999
- if (window.location.hash.substring(1) !== slug) {
1000
- // Use pushState for cleaner history
1001
- history.pushState({ slug: slug }, '', '#' + slug);
1002
- }
1049
+ // Update URL hash without triggering hashchange if already correct
1050
+ if (window.location.hash.substring(1) !== slug) {
1051
+ // Use pushState for cleaner history
1052
+ history.pushState({ slug: slug }, '', '#' + slug);
1003
1053
  }
1054
+ }
1004
1055
 
1005
- // Handle back/forward navigation
1006
- window.addEventListener('popstate', (event) => {
1007
- let slug = window.location.hash.substring(1);
1008
- // If popstate event has state use it, otherwise fallback to hash or default
1009
- if (event && event.state && event.state.slug) { // Check event exists
1010
- slug = event.state.slug;
1011
- }
1012
- // Ensure the target page exists before navigating, fallback to default slug
1013
- const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
1014
- navigateTo(targetSlug);
1015
- });
1056
+ // Handle back/forward navigation
1057
+ window.addEventListener('popstate', (event) => {
1058
+ let slug = window.location.hash.substring(1);
1059
+ // If popstate event has state use it, otherwise fallback to hash or default
1060
+ if (event && event.state && event.state.slug) { // Check event exists
1061
+ slug = event.state.slug;
1062
+ }
1063
+ // Ensure the target page exists before navigating, fallback to default slug
1064
+ const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
1065
+ navigateTo(targetSlug);
1066
+ });
1016
1067
 
1017
- // Handle direct link clicks
1018
- navLinks.forEach(link => {
1019
- link.addEventListener('click', function(e) {
1020
- e.preventDefault();
1021
- const slug = this.getAttribute('data-page');
1022
- if (slug) navigateTo(slug);
1023
- });
1068
+ // Handle direct link clicks
1069
+ navLinks.forEach(link => {
1070
+ link.addEventListener('click', function(e) {
1071
+ e.preventDefault();
1072
+ const slug = this.getAttribute('data-page');
1073
+ if (slug) navigateTo(slug);
1024
1074
  });
1025
-
1026
- // Initial page load
1027
- const initialHash = window.location.hash.substring(1);
1028
- const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
1029
- navigateTo(initialSlug);
1030
1075
  });
1031
- </script>
1076
+
1077
+ // Initial page load
1078
+ const initialHash = window.location.hash.substring(1);
1079
+ const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
1080
+ navigateTo(initialSlug);
1081
+ });
1082
+ </script>
1032
1083
  </body>
1033
1084
  </html>`;
1034
1085
  logger?.info(`Multi-page bundle generated. Size: ${Buffer.byteLength(output, "utf-8")} bytes.`);
@@ -1095,7 +1146,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
1095
1146
  throw pageError;
1096
1147
  }
1097
1148
  } catch (launchError) {
1098
- logger?.error(`Critical error during browser launch or page setup for ${url}: ${launchError.message}`);
1149
+ logger?.error(
1150
+ `Critical error during browser launch or page setup for ${url}: ${launchError.message}`
1151
+ );
1099
1152
  if (browser) {
1100
1153
  try {
1101
1154
  await browser.close();
@@ -1108,7 +1161,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
1108
1161
  throw launchError;
1109
1162
  } finally {
1110
1163
  if (browser) {
1111
- logger?.warn(`Closing browser in final cleanup for ${url}. This might indicate an unusual error path.`);
1164
+ logger?.warn(
1165
+ `Closing browser in final cleanup for ${url}. This might indicate an unusual error path.`
1166
+ );
1112
1167
  try {
1113
1168
  await browser.close();
1114
1169
  } catch (closeErr) {
@@ -1225,21 +1280,26 @@ async function crawlWebsite(startUrl, options) {
1225
1280
  }
1226
1281
  async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1, loggerInstance) {
1227
1282
  const logger = loggerInstance || new Logger();
1228
- logger.info(`Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`);
1283
+ logger.info(
1284
+ `Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`
1285
+ );
1229
1286
  try {
1230
1287
  const crawlOptions = {
1231
1288
  maxDepth,
1232
1289
  logger
1233
- /* Add other options like timeout, userAgent if needed */
1234
1290
  };
1235
1291
  const pages = await crawlWebsite(startUrl, crawlOptions);
1236
1292
  if (pages.length === 0) {
1237
- logger.warn("Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle.");
1293
+ logger.warn(
1294
+ "Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle."
1295
+ );
1238
1296
  } else {
1239
1297
  logger.info(`Crawl successful, found ${pages.length} pages. Starting bundling.`);
1240
1298
  }
1241
1299
  const bundledHtml = bundleMultiPageHTML(pages, logger);
1242
- logger.info(`Bundling complete. Output size: ${Buffer.byteLength(bundledHtml, "utf-8")} bytes.`);
1300
+ logger.info(
1301
+ `Bundling complete. Output size: ${Buffer.byteLength(bundledHtml, "utf-8")} bytes.`
1302
+ );
1243
1303
  logger.info(`Writing bundled HTML to ${outputFile}`);
1244
1304
  await fs2.writeFile(outputFile, bundledHtml, "utf-8");
1245
1305
  logger.info(`Successfully wrote bundled output to ${outputFile}`);