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/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
|
}
|
@@ -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(
|
215
|
-
|
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(
|
244
|
-
|
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
|
-
|
250
|
-
|
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(
|
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(
|
282
|
+
logger?.warn(
|
283
|
+
`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`
|
284
|
+
);
|
280
285
|
} else {
|
281
|
-
logger?.warn(
|
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(
|
314
|
-
|
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
|
-
|
322
|
-
|
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
|
-
|
339
|
-
|
340
|
-
const status =
|
341
|
-
const
|
342
|
-
const
|
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
|
355
|
+
if (error.code === "ENOENT") {
|
355
356
|
logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
|
356
|
-
} else if (error
|
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
|
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(
|
363
|
+
logger?.warn(
|
364
|
+
`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`
|
365
|
+
);
|
366
366
|
} else {
|
367
|
-
logger?.warn(
|
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:"))
|
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
|
-
//
|
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(
|
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(
|
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(
|
411
|
-
|
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 (!
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
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(
|
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(
|
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(
|
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 ?
|
577
|
-
logger?.info(
|
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
|
-
|
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(
|
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(
|
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(
|
727
|
+
logger?.warn(
|
728
|
+
`\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`
|
729
|
+
);
|
692
730
|
} else {
|
693
|
-
logger?.warn(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
965
|
-
|
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
|
-
|
971
|
-
|
972
|
-
|
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
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
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
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
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
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
1044
|
+
// Update active link styling
|
1045
|
+
navLinks.forEach(link => {
|
1046
|
+
link.classList.toggle('active', link.getAttribute('data-page') === slug);
|
1047
|
+
});
|
997
1048
|
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
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
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
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
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
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
|
-
|
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(
|
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(
|
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(
|
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(
|
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(
|
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}`);
|