portapack 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +67 -8
- package/.releaserc.js +25 -27
- package/CHANGELOG.md +14 -22
- package/LICENSE.md +21 -0
- package/README.md +22 -53
- package/commitlint.config.js +30 -34
- package/dist/cli/cli-entry.cjs +183 -98
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +178 -97
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +38 -33
- package/docs/.vitepress/sidebar-generator.ts +89 -38
- package/docs/architecture.md +186 -0
- package/docs/cli.md +23 -23
- package/docs/code-of-conduct.md +7 -1
- package/docs/configuration.md +12 -11
- package/docs/contributing.md +6 -2
- package/docs/deployment.md +10 -5
- package/docs/development.md +8 -5
- package/docs/getting-started.md +13 -13
- package/docs/index.md +1 -1
- package/docs/public/android-chrome-192x192.png +0 -0
- package/docs/public/android-chrome-512x512.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/roadmap.md +233 -0
- package/docs/site.webmanifest +1 -0
- package/docs/troubleshooting.md +12 -1
- package/examples/main.ts +5 -30
- package/examples/sample-project/script.js +1 -1
- package/jest.config.ts +8 -13
- package/nodemon.json +5 -10
- package/package.json +2 -5
- package/src/cli/cli-entry.ts +2 -2
- package/src/cli/cli.ts +21 -16
- package/src/cli/options.ts +127 -113
- package/src/core/bundler.ts +253 -222
- package/src/core/extractor.ts +632 -565
- package/src/core/minifier.ts +173 -162
- package/src/core/packer.ts +141 -137
- package/src/core/parser.ts +74 -73
- package/src/core/web-fetcher.ts +270 -258
- package/src/index.ts +18 -17
- package/src/types.ts +9 -11
- package/src/utils/font.ts +12 -6
- package/src/utils/logger.ts +110 -105
- package/src/utils/meta.ts +75 -76
- package/src/utils/mime.ts +50 -50
- package/src/utils/slugify.ts +33 -34
- package/tests/unit/cli/cli-entry.test.ts +72 -70
- package/tests/unit/cli/cli.test.ts +314 -278
- package/tests/unit/cli/options.test.ts +294 -301
- package/tests/unit/core/bundler.test.ts +426 -329
- package/tests/unit/core/extractor.test.ts +793 -549
- package/tests/unit/core/minifier.test.ts +374 -274
- package/tests/unit/core/packer.test.ts +298 -264
- package/tests/unit/core/parser.test.ts +538 -150
- package/tests/unit/core/web-fetcher.test.ts +389 -359
- package/tests/unit/index.test.ts +238 -197
- package/tests/unit/utils/font.test.ts +26 -21
- package/tests/unit/utils/logger.test.ts +267 -260
- package/tests/unit/utils/meta.test.ts +29 -28
- package/tests/unit/utils/mime.test.ts +73 -74
- package/tests/unit/utils/slugify.test.ts +14 -12
- package/tsconfig.build.json +9 -10
- package/tsconfig.jest.json +1 -1
- package/tsconfig.json +2 -2
- package/tsup.config.ts +8 -9
- package/typedoc.json +5 -9
- /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
- /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
package/dist/index.d.ts
CHANGED
@@ -204,7 +204,6 @@ declare class Logger {
|
|
204
204
|
/**
|
205
205
|
* @file bundler.ts
|
206
206
|
* @description Core bundling functions to handle both single and multi-page HTML documents. This includes asset extraction, optional minification, and full inlining into a self-contained HTML file.
|
207
|
-
* @version 1.3.0 // Assuming version based on previous context
|
208
207
|
*/
|
209
208
|
|
210
209
|
/**
|
@@ -221,8 +220,6 @@ declare function bundleMultiPageHTML(pages: PageEntry[], logger?: Logger): strin
|
|
221
220
|
* @file index.ts
|
222
221
|
* @description Public API surface for PortaPack.
|
223
222
|
* Exposes the unified `pack()` method and advanced helpers like recursive crawling and multi-page bundling.
|
224
|
-
* @version 1.0.0 - (Add version if applicable)
|
225
|
-
* @date 2025-04-11
|
226
223
|
*/
|
227
224
|
|
228
225
|
/**
|
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(
|
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
|
}
|
@@ -209,7 +211,9 @@ function determineBaseUrl(inputPathOrUrl, logger) {
|
|
209
211
|
logger?.debug(`Determined remote base URL: ${baseUrl}`);
|
210
212
|
return baseUrl;
|
211
213
|
} else if (inputPathOrUrl.includes("://") && !inputPathOrUrl.startsWith("file:")) {
|
212
|
-
logger?.warn(
|
214
|
+
logger?.warn(
|
215
|
+
`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`
|
216
|
+
);
|
213
217
|
return void 0;
|
214
218
|
} else {
|
215
219
|
let resourcePath;
|
@@ -235,12 +239,16 @@ function determineBaseUrl(inputPathOrUrl, logger) {
|
|
235
239
|
}
|
236
240
|
const fileUrl = new URL2("file://" + normalizedPathForURL);
|
237
241
|
const fileUrlString = fileUrl.href;
|
238
|
-
logger?.debug(
|
242
|
+
logger?.debug(
|
243
|
+
`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`
|
244
|
+
);
|
239
245
|
return fileUrlString;
|
240
246
|
}
|
241
247
|
} catch (error) {
|
242
248
|
const message = error instanceof Error ? error.message : String(error);
|
243
|
-
logger?.error(
|
249
|
+
logger?.error(
|
250
|
+
`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`
|
251
|
+
);
|
244
252
|
return void 0;
|
245
253
|
}
|
246
254
|
}
|
@@ -255,7 +263,9 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
|
|
255
263
|
const base = new URL2(baseContextUrl);
|
256
264
|
resolvableUrl = base.protocol + resolvableUrl;
|
257
265
|
} catch (e) {
|
258
|
-
logger?.warn(
|
266
|
+
logger?.warn(
|
267
|
+
`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`
|
268
|
+
);
|
259
269
|
return null;
|
260
270
|
}
|
261
271
|
}
|
@@ -269,9 +279,13 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
|
|
269
279
|
} catch (error) {
|
270
280
|
const message = error instanceof Error ? error.message : String(error);
|
271
281
|
if (!/^[a-z]+:/i.test(resolvableUrl) && !resolvableUrl.startsWith("/") && !baseContextUrl) {
|
272
|
-
logger?.warn(
|
282
|
+
logger?.warn(
|
283
|
+
`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`
|
284
|
+
);
|
273
285
|
} else {
|
274
|
-
logger?.warn(
|
286
|
+
logger?.warn(
|
287
|
+
`\u26A0\uFE0F Failed to parse/resolve URL "${resolvableUrl}" ${baseContextUrl ? 'against base "' + baseContextUrl + '"' : "(no base provided)"}: ${message}`
|
288
|
+
);
|
275
289
|
}
|
276
290
|
return null;
|
277
291
|
}
|
@@ -301,14 +315,18 @@ async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
|
|
301
315
|
timeout
|
302
316
|
// Apply network timeout
|
303
317
|
});
|
304
|
-
logger?.debug(
|
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
|
+
);
|
305
321
|
return Buffer.from(response.data);
|
306
322
|
} else if (protocol === "file:") {
|
307
323
|
let filePath;
|
308
324
|
try {
|
309
325
|
filePath = fileURLToPath(resolvedUrl);
|
310
326
|
} catch (e) {
|
311
|
-
logger?.error(
|
327
|
+
logger?.error(
|
328
|
+
`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`
|
329
|
+
);
|
312
330
|
return null;
|
313
331
|
}
|
314
332
|
const normalizedForLog = path2.normalize(filePath);
|
@@ -342,9 +360,13 @@ async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
|
|
342
360
|
logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
|
343
361
|
}
|
344
362
|
} else if (error instanceof Error) {
|
345
|
-
logger?.warn(
|
363
|
+
logger?.warn(
|
364
|
+
`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`
|
365
|
+
);
|
346
366
|
} else {
|
347
|
-
logger?.warn(
|
367
|
+
logger?.warn(
|
368
|
+
`\u26A0\uFE0F An unknown and unexpected error occurred processing asset ${resolvedUrl.href}: ${String(error)}`
|
369
|
+
);
|
348
370
|
}
|
349
371
|
return null;
|
350
372
|
}
|
@@ -355,7 +377,8 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
355
377
|
const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
356
378
|
const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
|
357
379
|
const processFoundUrl = (rawUrl, ruleType) => {
|
358
|
-
if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#"))
|
380
|
+
if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#"))
|
381
|
+
return;
|
359
382
|
const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
|
360
383
|
if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
|
361
384
|
processedInThisParse.add(resolvedUrl);
|
@@ -367,7 +390,9 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
367
390
|
content: void 0
|
368
391
|
// Content will be fetched later if needed
|
369
392
|
});
|
370
|
-
logger?.debug(
|
393
|
+
logger?.debug(
|
394
|
+
`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`
|
395
|
+
);
|
371
396
|
}
|
372
397
|
};
|
373
398
|
let match;
|
@@ -381,14 +406,20 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
|
381
406
|
return newlyDiscovered;
|
382
407
|
}
|
383
408
|
async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger) {
|
384
|
-
logger?.info(
|
409
|
+
logger?.info(
|
410
|
+
`\u{1F680} Starting asset extraction! Embed: ${embedAssets}. Input: ${inputPathOrUrl || "(HTML content only)"}`
|
411
|
+
);
|
385
412
|
const initialAssets = parsed.assets || [];
|
386
413
|
const finalAssetsMap = /* @__PURE__ */ new Map();
|
387
414
|
let assetsToProcess = [];
|
388
415
|
const processedOrQueuedUrls = /* @__PURE__ */ new Set();
|
389
416
|
const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || "", logger);
|
390
|
-
if (!htmlBaseContextUrl && initialAssets.some(
|
391
|
-
|
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
|
+
);
|
392
423
|
} else if (htmlBaseContextUrl) {
|
393
424
|
logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
|
394
425
|
}
|
@@ -420,9 +451,13 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
420
451
|
while (assetsToProcess.length > 0) {
|
421
452
|
iterationCount++;
|
422
453
|
if (iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS) {
|
423
|
-
logger?.error(
|
454
|
+
logger?.error(
|
455
|
+
`\u{1F6D1} Asset extraction loop limit hit (${MAX_ASSET_EXTRACTION_ITERATIONS})! Aborting.`
|
456
|
+
);
|
424
457
|
const remainingUrls = assetsToProcess.map((a) => a.url).slice(0, 10).join(", ");
|
425
|
-
logger?.error(
|
458
|
+
logger?.error(
|
459
|
+
`Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`
|
460
|
+
);
|
426
461
|
assetsToProcess.forEach((asset) => {
|
427
462
|
if (!finalAssetsMap.has(asset.url)) {
|
428
463
|
finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
|
@@ -448,7 +483,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
448
483
|
try {
|
449
484
|
assetUrlObj = new URL2(asset.url);
|
450
485
|
} catch (urlError) {
|
451
|
-
logger?.warn(
|
486
|
+
logger?.warn(
|
487
|
+
`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`
|
488
|
+
);
|
452
489
|
finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
|
453
490
|
continue;
|
454
491
|
}
|
@@ -484,7 +521,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
484
521
|
cssContentForParsing = textContent;
|
485
522
|
}
|
486
523
|
} else {
|
487
|
-
logger?.warn(
|
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
|
+
);
|
488
527
|
cssContentForParsing = void 0;
|
489
528
|
if (embedAssets) {
|
490
529
|
finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
|
@@ -505,14 +544,18 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
505
544
|
try {
|
506
545
|
const attemptedTextContent = assetContentBuffer.toString("utf-8");
|
507
546
|
if (isUtf8DecodingLossy(assetContentBuffer, attemptedTextContent)) {
|
508
|
-
logger?.warn(
|
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
|
+
);
|
509
550
|
finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
|
510
551
|
} else {
|
511
552
|
finalContent = attemptedTextContent;
|
512
553
|
logger?.debug(`Successfully embedded unclassified asset ${asset.url} as text.`);
|
513
554
|
}
|
514
555
|
} catch (decodeError) {
|
515
|
-
logger?.warn(
|
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
|
+
);
|
516
559
|
finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
|
517
560
|
}
|
518
561
|
} else {
|
@@ -526,7 +569,9 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
526
569
|
finalAssetsMap.set(asset.url, { ...asset, url: asset.url, content: finalContent });
|
527
570
|
if (asset.type === "css" && cssContentForParsing) {
|
528
571
|
const cssBaseContextUrl = determineBaseUrl(asset.url, logger);
|
529
|
-
logger?.debug(
|
572
|
+
logger?.debug(
|
573
|
+
`CSS base context for resolving nested assets within ${asset.url}: ${cssBaseContextUrl}`
|
574
|
+
);
|
530
575
|
if (cssBaseContextUrl) {
|
531
576
|
const newlyDiscoveredAssets = extractUrlsFromCSS(
|
532
577
|
cssContentForParsing,
|
@@ -535,25 +580,33 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
|
|
535
580
|
logger
|
536
581
|
);
|
537
582
|
if (newlyDiscoveredAssets.length > 0) {
|
538
|
-
logger?.debug(
|
583
|
+
logger?.debug(
|
584
|
+
`Discovered ${newlyDiscoveredAssets.length} nested assets in CSS ${asset.url}. Checking against queue...`
|
585
|
+
);
|
539
586
|
for (const newAsset of newlyDiscoveredAssets) {
|
540
587
|
if (!processedOrQueuedUrls.has(newAsset.url)) {
|
541
588
|
processedOrQueuedUrls.add(newAsset.url);
|
542
589
|
assetsToProcess.push(newAsset);
|
543
590
|
logger?.debug(` -> Queued new nested asset: ${newAsset.url}`);
|
544
591
|
} else {
|
545
|
-
logger?.debug(
|
592
|
+
logger?.debug(
|
593
|
+
` -> Skipping already processed/queued nested asset: ${newAsset.url}`
|
594
|
+
);
|
546
595
|
}
|
547
596
|
}
|
548
597
|
}
|
549
598
|
} else {
|
550
|
-
logger?.warn(
|
599
|
+
logger?.warn(
|
600
|
+
`Could not determine base URL context for CSS file ${asset.url}. Cannot resolve nested relative paths within it.`
|
601
|
+
);
|
551
602
|
}
|
552
603
|
}
|
553
604
|
}
|
554
605
|
}
|
555
606
|
const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? `${MAX_ASSET_EXTRACTION_ITERATIONS}+ (limit hit)` : iterationCount;
|
556
|
-
logger?.info(
|
607
|
+
logger?.info(
|
608
|
+
`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`
|
609
|
+
);
|
557
610
|
return {
|
558
611
|
htmlContent: parsed.htmlContent,
|
559
612
|
assets: Array.from(finalAssetsMap.values())
|
@@ -633,7 +686,7 @@ async function minifyAssets(parsed, options = {}, logger) {
|
|
633
686
|
logger?.debug(`Minification flags: ${JSON.stringify(minifyFlags)}`);
|
634
687
|
const minifiedAssets = await Promise.all(
|
635
688
|
currentAssets.map(async (asset) => {
|
636
|
-
|
689
|
+
const processedAsset = { ...asset };
|
637
690
|
if (typeof processedAsset.content !== "string" || processedAsset.content.length === 0) {
|
638
691
|
return processedAsset;
|
639
692
|
}
|
@@ -648,13 +701,17 @@ async function minifyAssets(parsed, options = {}, logger) {
|
|
648
701
|
logger?.warn(`\u26A0\uFE0F CleanCSS failed for ${assetIdentifier}: ${result.errors.join(", ")}`);
|
649
702
|
} else {
|
650
703
|
if (result.warnings && result.warnings.length > 0) {
|
651
|
-
logger?.debug(
|
704
|
+
logger?.debug(
|
705
|
+
`CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`
|
706
|
+
);
|
652
707
|
}
|
653
708
|
if (result.styles) {
|
654
709
|
newContent = result.styles;
|
655
710
|
logger?.debug(`CSS minified successfully: ${assetIdentifier}`);
|
656
711
|
} else {
|
657
|
-
logger?.warn(
|
712
|
+
logger?.warn(
|
713
|
+
`\u26A0\uFE0F CleanCSS produced no styles but reported no errors for ${assetIdentifier}. Keeping original.`
|
714
|
+
);
|
658
715
|
}
|
659
716
|
}
|
660
717
|
}
|
@@ -667,15 +724,21 @@ async function minifyAssets(parsed, options = {}, logger) {
|
|
667
724
|
} else {
|
668
725
|
const terserError = result.error;
|
669
726
|
if (terserError) {
|
670
|
-
logger?.warn(
|
727
|
+
logger?.warn(
|
728
|
+
`\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`
|
729
|
+
);
|
671
730
|
} else {
|
672
|
-
logger?.warn(
|
731
|
+
logger?.warn(
|
732
|
+
`\u26A0\uFE0F Terser produced no code but reported no errors for ${assetIdentifier}. Keeping original.`
|
733
|
+
);
|
673
734
|
}
|
674
735
|
}
|
675
736
|
}
|
676
737
|
} catch (err) {
|
677
738
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
678
|
-
logger?.warn(
|
739
|
+
logger?.warn(
|
740
|
+
`\u26A0\uFE0F Failed to minify asset ${assetIdentifier} (${processedAsset.type}): ${errorMessage}`
|
741
|
+
);
|
679
742
|
}
|
680
743
|
processedAsset.content = newContent;
|
681
744
|
return processedAsset;
|
@@ -777,7 +840,9 @@ function inlineAssets($, assets, logger) {
|
|
777
840
|
logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
|
778
841
|
element.attr(srcAttr, asset.content);
|
779
842
|
} else if (src) {
|
780
|
-
logger?.warn(
|
843
|
+
logger?.warn(
|
844
|
+
`Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`
|
845
|
+
);
|
781
846
|
}
|
782
847
|
});
|
783
848
|
$("img[srcset], source[srcset]").each((_, el) => {
|
@@ -899,7 +964,9 @@ function bundleMultiPageHTML(pages, logger) {
|
|
899
964
|
} else if (!baseSlug) {
|
900
965
|
if (isRootIndex) {
|
901
966
|
baseSlug = "index";
|
902
|
-
logger?.debug(
|
967
|
+
logger?.debug(
|
968
|
+
`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`
|
969
|
+
);
|
903
970
|
} else {
|
904
971
|
baseSlug = "page";
|
905
972
|
logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
|
@@ -907,14 +974,18 @@ function bundleMultiPageHTML(pages, logger) {
|
|
907
974
|
}
|
908
975
|
if (!baseSlug) {
|
909
976
|
baseSlug = `page-${pageCounterForFallback++}`;
|
910
|
-
logger?.warn(
|
977
|
+
logger?.warn(
|
978
|
+
`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`
|
979
|
+
);
|
911
980
|
}
|
912
981
|
let slug = baseSlug;
|
913
982
|
let collisionCounter = 1;
|
914
983
|
const originalBaseSlugForLog = baseSlug;
|
915
984
|
while (usedSlugs.has(slug)) {
|
916
985
|
const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
|
917
|
-
logger?.warn(
|
986
|
+
logger?.warn(
|
987
|
+
`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`
|
988
|
+
);
|
918
989
|
slug = newSlug;
|
919
990
|
}
|
920
991
|
usedSlugs.add(slug);
|
@@ -924,7 +995,8 @@ function bundleMultiPageHTML(pages, logger) {
|
|
924
995
|
}
|
925
996
|
}
|
926
997
|
const defaultPageSlug = usedSlugs.has("index") ? "index" : firstValidSlug || "page";
|
927
|
-
|
998
|
+
const output = `
|
999
|
+
<!DOCTYPE html>
|
928
1000
|
<html lang="en">
|
929
1001
|
<head>
|
930
1002
|
<meta charset="UTF-8">
|
@@ -940,74 +1012,74 @@ function bundleMultiPageHTML(pages, logger) {
|
|
940
1012
|
</style>
|
941
1013
|
</head>
|
942
1014
|
<body>
|
943
|
-
|
944
|
-
|
1015
|
+
<nav id="main-nav">
|
1016
|
+
${validPages.map((p) => {
|
945
1017
|
const slug = slugMap.get(p.url);
|
946
1018
|
const label = slug;
|
947
1019
|
return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
|
948
1020
|
}).join("\n ")}
|
949
|
-
|
950
|
-
|
951
|
-
|
1021
|
+
</nav>
|
1022
|
+
<div id="page-container"></div>
|
1023
|
+
${validPages.map((p) => {
|
952
1024
|
const slug = slugMap.get(p.url);
|
953
1025
|
return `<template id="page-${slug}">${p.html}</template>`;
|
954
1026
|
}).join("\n ")}
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
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');
|
959
1031
|
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
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));
|
971
1043
|
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
1044
|
+
// Update active link styling
|
1045
|
+
navLinks.forEach(link => {
|
1046
|
+
link.classList.toggle('active', link.getAttribute('data-page') === slug);
|
1047
|
+
});
|
976
1048
|
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
}
|
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);
|
982
1053
|
}
|
1054
|
+
}
|
983
1055
|
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
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
|
+
});
|
995
1067
|
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
});
|
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);
|
1003
1074
|
});
|
1004
|
-
|
1005
|
-
// Initial page load
|
1006
|
-
const initialHash = window.location.hash.substring(1);
|
1007
|
-
const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
|
1008
|
-
navigateTo(initialSlug);
|
1009
1075
|
});
|
1010
|
-
|
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>
|
1011
1083
|
</body>
|
1012
1084
|
</html>`;
|
1013
1085
|
logger?.info(`Multi-page bundle generated. Size: ${Buffer.byteLength(output, "utf-8")} bytes.`);
|
@@ -1074,7 +1146,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
|
|
1074
1146
|
throw pageError;
|
1075
1147
|
}
|
1076
1148
|
} catch (launchError) {
|
1077
|
-
logger?.error(
|
1149
|
+
logger?.error(
|
1150
|
+
`Critical error during browser launch or page setup for ${url}: ${launchError.message}`
|
1151
|
+
);
|
1078
1152
|
if (browser) {
|
1079
1153
|
try {
|
1080
1154
|
await browser.close();
|
@@ -1087,7 +1161,9 @@ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT,
|
|
1087
1161
|
throw launchError;
|
1088
1162
|
} finally {
|
1089
1163
|
if (browser) {
|
1090
|
-
logger?.warn(
|
1164
|
+
logger?.warn(
|
1165
|
+
`Closing browser in final cleanup for ${url}. This might indicate an unusual error path.`
|
1166
|
+
);
|
1091
1167
|
try {
|
1092
1168
|
await browser.close();
|
1093
1169
|
} catch (closeErr) {
|
@@ -1204,21 +1280,26 @@ async function crawlWebsite(startUrl, options) {
|
|
1204
1280
|
}
|
1205
1281
|
async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1, loggerInstance) {
|
1206
1282
|
const logger = loggerInstance || new Logger();
|
1207
|
-
logger.info(
|
1283
|
+
logger.info(
|
1284
|
+
`Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`
|
1285
|
+
);
|
1208
1286
|
try {
|
1209
1287
|
const crawlOptions = {
|
1210
1288
|
maxDepth,
|
1211
1289
|
logger
|
1212
|
-
/* Add other options like timeout, userAgent if needed */
|
1213
1290
|
};
|
1214
1291
|
const pages = await crawlWebsite(startUrl, crawlOptions);
|
1215
1292
|
if (pages.length === 0) {
|
1216
|
-
logger.warn(
|
1293
|
+
logger.warn(
|
1294
|
+
"Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle."
|
1295
|
+
);
|
1217
1296
|
} else {
|
1218
1297
|
logger.info(`Crawl successful, found ${pages.length} pages. Starting bundling.`);
|
1219
1298
|
}
|
1220
1299
|
const bundledHtml = bundleMultiPageHTML(pages, logger);
|
1221
|
-
logger.info(
|
1300
|
+
logger.info(
|
1301
|
+
`Bundling complete. Output size: ${Buffer.byteLength(bundledHtml, "utf-8")} bytes.`
|
1302
|
+
);
|
1222
1303
|
logger.info(`Writing bundled HTML to ${outputFile}`);
|
1223
1304
|
await fs2.writeFile(outputFile, bundledHtml, "utf-8");
|
1224
1305
|
logger.info(`Successfully wrote bundled output to ${outputFile}`);
|