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.
- package/.eslintrc.json +67 -8
- package/.github/workflows/ci.yml +5 -4
- package/.releaserc.js +25 -27
- package/CHANGELOG.md +12 -19
- package/LICENSE.md +21 -0
- package/README.md +34 -36
- package/commitlint.config.js +30 -34
- package/dist/cli/cli-entry.cjs +199 -135
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +194 -134
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +36 -34
- package/docs/.vitepress/sidebar-generator.ts +89 -38
- package/docs/cli.md +29 -82
- package/docs/code-of-conduct.md +7 -1
- package/docs/configuration.md +103 -117
- package/docs/contributing.md +6 -2
- package/docs/deployment.md +10 -5
- package/docs/development.md +8 -5
- package/docs/getting-started.md +76 -45
- package/docs/index.md +1 -1
- package/docs/public/android-chrome-192x192.png +0 -0
- package/docs/public/android-chrome-512x512.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/site.webmanifest +1 -0
- package/docs/troubleshooting.md +12 -1
- package/examples/main.ts +7 -10
- package/examples/sample-project/script.js +1 -1
- package/jest.config.ts +8 -13
- package/nodemon.json +5 -10
- package/package.json +2 -5
- package/src/cli/cli-entry.ts +2 -2
- package/src/cli/cli.ts +21 -16
- package/src/cli/options.ts +127 -113
- package/src/core/bundler.ts +254 -221
- package/src/core/extractor.ts +639 -520
- package/src/core/minifier.ts +173 -162
- package/src/core/packer.ts +141 -137
- package/src/core/parser.ts +74 -73
- package/src/core/web-fetcher.ts +270 -258
- package/src/index.ts +18 -17
- package/src/types.ts +9 -11
- package/src/utils/font.ts +12 -6
- package/src/utils/logger.ts +110 -105
- package/src/utils/meta.ts +75 -76
- package/src/utils/mime.ts +50 -50
- package/src/utils/slugify.ts +33 -34
- package/tests/unit/cli/cli-entry.test.ts +72 -70
- package/tests/unit/cli/cli.test.ts +314 -278
- package/tests/unit/cli/options.test.ts +294 -301
- package/tests/unit/core/bundler.test.ts +426 -329
- package/tests/unit/core/extractor.test.ts +828 -380
- package/tests/unit/core/minifier.test.ts +374 -274
- package/tests/unit/core/packer.test.ts +298 -264
- package/tests/unit/core/parser.test.ts +538 -150
- package/tests/unit/core/web-fetcher.test.ts +389 -359
- package/tests/unit/index.test.ts +238 -197
- package/tests/unit/utils/font.test.ts +26 -21
- package/tests/unit/utils/logger.test.ts +267 -260
- package/tests/unit/utils/meta.test.ts +29 -28
- package/tests/unit/utils/mime.test.ts +73 -74
- package/tests/unit/utils/slugify.test.ts +14 -12
- package/tsconfig.build.json +9 -10
- package/tsconfig.jest.json +2 -1
- package/tsconfig.json +2 -2
- package/tsup.config.ts +8 -8
- package/typedoc.json +5 -9
- package/docs/demo.md +0 -46
- /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
- /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
package/dist/cli/cli-entry.cjs
CHANGED
@@ -55,7 +55,11 @@ function parseRecursiveValue(val) {
|
|
55
55
|
}
|
56
56
|
function parseOptions(argv = process.argv) {
|
57
57
|
const program = new import_commander.Command();
|
58
|
-
program.name("portapack").version("0.0.0").description("\u{1F4E6} Bundle HTML and its dependencies into a portable file").argument("[input]", "Input HTML file or URL").option("-o, --output <file>", "Output file path").option("-m, --minify", "Enable all minification (HTML, CSS, JS)").option("--no-minify", "Disable all minification").option("--no-minify-html", "Disable HTML minification").option("--no-minify-css", "Disable CSS minification").option("--no-minify-js", "Disable JavaScript minification").option("-e, --embed-assets", "Embed assets as data URIs").option("--no-embed-assets", "Keep asset links relative/absolute").option(
|
58
|
+
program.name("portapack").version("0.0.0").description("\u{1F4E6} Bundle HTML and its dependencies into a portable file").argument("[input]", "Input HTML file or URL").option("-o, --output <file>", "Output file path").option("-m, --minify", "Enable all minification (HTML, CSS, JS)").option("--no-minify", "Disable all minification").option("--no-minify-html", "Disable HTML minification").option("--no-minify-css", "Disable CSS minification").option("--no-minify-js", "Disable JavaScript minification").option("-e, --embed-assets", "Embed assets as data URIs").option("--no-embed-assets", "Keep asset links relative/absolute").option(
|
59
|
+
"-r, --recursive [depth]",
|
60
|
+
"Recursively crawl site (optional depth)",
|
61
|
+
parseRecursiveValue
|
62
|
+
).option("--max-depth <n>", "Set max depth for recursive crawl (alias for -r <n>)", parseInt).option("-b, --base-url <url>", "Base URL for resolving relative links").option("-d, --dry-run", "Run without writing output file").option("-v, --verbose", "Enable verbose (debug) logging").addOption(new import_commander.Option("--log-level <level>", "Set logging level").choices(logLevels));
|
59
63
|
program.parse(argv);
|
60
64
|
const opts = program.opts();
|
61
65
|
const inputArg = program.args.length > 0 ? program.args[0] : void 0;
|
@@ -246,7 +250,9 @@ var init_logger = __esm({
|
|
246
250
|
case "none":
|
247
251
|
return new _Logger(0 /* NONE */);
|
248
252
|
default:
|
249
|
-
console.warn(
|
253
|
+
console.warn(
|
254
|
+
`[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`
|
255
|
+
);
|
250
256
|
return new _Logger(defaultLevel);
|
251
257
|
}
|
252
258
|
}
|
@@ -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(
|
347
|
-
|
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(
|
376
|
-
|
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
|
-
|
382
|
-
|
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(
|
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(
|
418
|
+
logger?.warn(
|
419
|
+
`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`
|
420
|
+
);
|
412
421
|
} else {
|
413
|
-
logger?.warn(
|
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(
|
446
|
-
|
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
|
-
|
454
|
-
|
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
|
-
|
471
|
-
|
472
|
-
const status =
|
473
|
-
const
|
474
|
-
const
|
475
|
-
const message = error.message;
|
476
|
-
const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: Status ${status} - ${statusText}. Code: ${code}, Message: ${message}`;
|
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
|
491
|
+
if (error.code === "ENOENT") {
|
487
492
|
logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
|
488
|
-
} else if (error
|
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
|
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(
|
499
|
+
logger?.warn(
|
500
|
+
`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`
|
501
|
+
);
|
498
502
|
} else {
|
499
|
-
logger?.warn(
|
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:"))
|
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
|
-
//
|
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(
|
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(
|
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(
|
543
|
-
|
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 (!
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
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(
|
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(
|
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(
|
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 ?
|
709
|
-
logger?.info(
|
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
|
-
|
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(
|
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(
|
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(
|
822
|
+
logger?.warn(
|
823
|
+
`\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`
|
824
|
+
);
|
783
825
|
} else {
|
784
|
-
logger?.warn(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
1129
|
-
|
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
|
-
|
1135
|
-
|
1136
|
-
|
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
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
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
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
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
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1212
|
+
// Update active link styling
|
1213
|
+
navLinks.forEach(link => {
|
1214
|
+
link.classList.toggle('active', link.getAttribute('data-page') === slug);
|
1215
|
+
});
|
1161
1216
|
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
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
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
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
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
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
|
-
|
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(
|
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(
|
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(
|
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(
|
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(
|
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}`);
|