portapack 0.2.1 → 0.3.0

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.
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
2
4
  var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
9
  var __esm = (fn, res) => function __init() {
5
10
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
11
  };
@@ -8,6 +13,23 @@ var __export = (target, all) => {
8
13
  for (var name in all)
9
14
  __defProp(target, name, { get: all[name], enumerable: true });
10
15
  };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
11
33
 
12
34
  // src/types.ts
13
35
  var LogLevel;
@@ -26,15 +48,14 @@ var init_types = __esm({
26
48
  });
27
49
 
28
50
  // src/cli/options.ts
29
- import { Command, Option } from "commander";
30
51
  function parseRecursiveValue(val) {
31
52
  if (val === void 0) return true;
32
53
  const parsed = parseInt(val, 10);
33
54
  return isNaN(parsed) || parsed < 0 ? true : parsed;
34
55
  }
35
56
  function parseOptions(argv = process.argv) {
36
- const program = new Command();
37
- program.name("portapack").version("0.0.0").description("\u{1F4E6} Bundle HTML and its dependencies into a portable file").argument("[input]", "Input HTML file or URL").option("-o, --output <file>", "Output file path").option("-m, --minify", "Enable all minification (HTML, CSS, JS)").option("--no-minify", "Disable all minification").option("--no-minify-html", "Disable HTML minification").option("--no-minify-css", "Disable CSS minification").option("--no-minify-js", "Disable JavaScript minification").option("-e, --embed-assets", "Embed assets as data URIs").option("--no-embed-assets", "Keep asset links relative/absolute").option("-r, --recursive [depth]", "Recursively crawl site (optional depth)", parseRecursiveValue).option("--max-depth <n>", "Set max depth for recursive crawl (alias for -r <n>)", parseInt).option("-b, --base-url <url>", "Base URL for resolving relative links").option("-d, --dry-run", "Run without writing output file").option("-v, --verbose", "Enable verbose (debug) logging").addOption(new Option("--log-level <level>", "Set logging level").choices(logLevels));
57
+ const program = new import_commander.Command();
58
+ program.name("portapack").version("0.0.0").description("\u{1F4E6} Bundle HTML and its dependencies into a portable file").argument("[input]", "Input HTML file or URL").option("-o, --output <file>", "Output file path").option("-m, --minify", "Enable all minification (HTML, CSS, JS)").option("--no-minify", "Disable all minification").option("--no-minify-html", "Disable HTML minification").option("--no-minify-css", "Disable CSS minification").option("--no-minify-js", "Disable JavaScript minification").option("-e, --embed-assets", "Embed assets as data URIs").option("--no-embed-assets", "Keep asset links relative/absolute").option("-r, --recursive [depth]", "Recursively crawl site (optional depth)", parseRecursiveValue).option("--max-depth <n>", "Set max depth for recursive crawl (alias for -r <n>)", parseInt).option("-b, --base-url <url>", "Base URL for resolving relative links").option("-d, --dry-run", "Run without writing output file").option("-v, --verbose", "Enable verbose (debug) logging").addOption(new import_commander.Option("--log-level <level>", "Set logging level").choices(logLevels));
38
59
  program.parse(argv);
39
60
  const opts = program.opts();
40
61
  const inputArg = program.args.length > 0 ? program.args[0] : void 0;
@@ -110,17 +131,130 @@ function parseOptions(argv = process.argv) {
110
131
  // minifyHtml, minifyCss, minifyJs (commander's raw boolean flags)
111
132
  };
112
133
  }
113
- var logLevels;
134
+ var import_commander, logLevels;
114
135
  var init_options = __esm({
115
136
  "src/cli/options.ts"() {
116
137
  "use strict";
138
+ import_commander = require("commander");
117
139
  init_types();
118
140
  logLevels = ["debug", "info", "warn", "error", "silent", "none"];
119
141
  }
120
142
  });
121
143
 
144
+ // src/utils/logger.ts
145
+ var Logger;
146
+ var init_logger = __esm({
147
+ "src/utils/logger.ts"() {
148
+ "use strict";
149
+ init_types();
150
+ Logger = class _Logger {
151
+ /** The current minimum log level required for a message to be output. */
152
+ level;
153
+ /**
154
+ * Creates a new Logger instance.
155
+ * Defaults to LogLevel.INFO if no level is provided.
156
+ *
157
+ * @param {LogLevel} [level=LogLevel.INFO] - The initial log level for this logger instance.
158
+ * Must be one of the values from the LogLevel enum.
159
+ */
160
+ constructor(level = 3 /* INFO */) {
161
+ this.level = level !== void 0 && LogLevel[level] !== void 0 ? level : 3 /* INFO */;
162
+ }
163
+ /**
164
+ * Updates the logger's current level. Messages below this level will be suppressed.
165
+ *
166
+ * @param {LogLevel} level - The new log level to set. Must be a LogLevel enum member.
167
+ */
168
+ setLevel(level) {
169
+ this.level = level;
170
+ }
171
+ /**
172
+ * Logs a debug message if the current log level is DEBUG or higher.
173
+ *
174
+ * @param {string} message - The debug message string.
175
+ */
176
+ debug(message) {
177
+ if (this.level >= 4 /* DEBUG */) {
178
+ console.debug(`[DEBUG] ${message}`);
179
+ }
180
+ }
181
+ /**
182
+ * Logs an informational message if the current log level is INFO or higher.
183
+ *
184
+ * @param {string} message - The informational message string.
185
+ */
186
+ info(message) {
187
+ if (this.level >= 3 /* INFO */) {
188
+ console.info(`[INFO] ${message}`);
189
+ }
190
+ }
191
+ /**
192
+ * Logs a warning message if the current log level is WARN or higher.
193
+ *
194
+ * @param {string} message - The warning message string.
195
+ */
196
+ warn(message) {
197
+ if (this.level >= 2 /* WARN */) {
198
+ console.warn(`[WARN] ${message}`);
199
+ }
200
+ }
201
+ /**
202
+ * Logs an error message if the current log level is ERROR or higher.
203
+ *
204
+ * @param {string} message - The error message string.
205
+ */
206
+ error(message) {
207
+ if (this.level >= 1 /* ERROR */) {
208
+ console.error(`[ERROR] ${message}`);
209
+ }
210
+ }
211
+ /**
212
+ * Static factory method to create a Logger instance based on a simple boolean `verbose` flag.
213
+ *
214
+ * @static
215
+ * @param {{ verbose?: boolean }} [options={}] - An object potentially containing a `verbose` flag.
216
+ * @returns {Logger} A new Logger instance set to LogLevel.DEBUG if options.verbose is true,
217
+ * otherwise set to LogLevel.INFO.
218
+ */
219
+ static fromVerboseFlag(options = {}) {
220
+ return new _Logger(options.verbose ? 4 /* DEBUG */ : 3 /* INFO */);
221
+ }
222
+ /**
223
+ * Static factory method to create a Logger instance based on a LogLevel string name.
224
+ * Useful for creating a logger from config files or environments variables.
225
+ *
226
+ * @static
227
+ * @param {string | undefined} levelName - The name of the log level (e.g., 'debug', 'info', 'warn', 'error', 'silent'/'none'). Case-insensitive.
228
+ * @param {LogLevel} [defaultLevel=LogLevel.INFO] - The level to use if levelName is invalid or undefined.
229
+ * @returns {Logger} A new Logger instance set to the corresponding LogLevel.
230
+ */
231
+ static fromLevelName(levelName, defaultLevel = 3 /* INFO */) {
232
+ if (!levelName) {
233
+ return new _Logger(defaultLevel);
234
+ }
235
+ switch (levelName.toLowerCase()) {
236
+ // Return enum members
237
+ case "debug":
238
+ return new _Logger(4 /* DEBUG */);
239
+ case "info":
240
+ return new _Logger(3 /* INFO */);
241
+ case "warn":
242
+ return new _Logger(2 /* WARN */);
243
+ case "error":
244
+ return new _Logger(1 /* ERROR */);
245
+ case "silent":
246
+ case "none":
247
+ return new _Logger(0 /* NONE */);
248
+ default:
249
+ console.warn(`[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`);
250
+ return new _Logger(defaultLevel);
251
+ }
252
+ }
253
+ };
254
+ }
255
+ });
256
+
122
257
  // src/utils/mime.ts
123
- import path from "path";
124
258
  function guessMimeType(urlOrPath) {
125
259
  if (!urlOrPath) {
126
260
  return DEFAULT_MIME_TYPE;
@@ -128,16 +262,17 @@ function guessMimeType(urlOrPath) {
128
262
  let ext = "";
129
263
  try {
130
264
  const parsedUrl = new URL(urlOrPath);
131
- ext = path.extname(parsedUrl.pathname).toLowerCase();
265
+ ext = import_path.default.extname(parsedUrl.pathname).toLowerCase();
132
266
  } catch {
133
- ext = path.extname(urlOrPath).toLowerCase();
267
+ ext = import_path.default.extname(urlOrPath).toLowerCase();
134
268
  }
135
269
  return MIME_MAP[ext] || DEFAULT_MIME_TYPE;
136
270
  }
137
- var MIME_MAP, DEFAULT_MIME_TYPE;
271
+ var import_path, MIME_MAP, DEFAULT_MIME_TYPE;
138
272
  var init_mime = __esm({
139
273
  "src/utils/mime.ts"() {
140
274
  "use strict";
275
+ import_path = __toESM(require("path"), 1);
141
276
  MIME_MAP = {
142
277
  // CSS
143
278
  ".css": { mime: "text/css", assetType: "css" },
@@ -181,84 +316,7 @@ var init_mime = __esm({
181
316
  }
182
317
  });
183
318
 
184
- // src/core/parser.ts
185
- import { readFile } from "fs/promises";
186
- import * as cheerio from "cheerio";
187
- async function parseHTML(entryFilePath, logger) {
188
- logger?.debug(`Parsing HTML file: ${entryFilePath}`);
189
- let htmlContent;
190
- try {
191
- htmlContent = await readFile(entryFilePath, "utf-8");
192
- logger?.debug(`Successfully read HTML file (${Buffer.byteLength(htmlContent)} bytes).`);
193
- } catch (err) {
194
- logger?.error(`Failed to read HTML file "${entryFilePath}": ${err.message}`);
195
- throw new Error(`Could not read input HTML file: ${entryFilePath}`, { cause: err });
196
- }
197
- const $ = cheerio.load(htmlContent);
198
- const assets = [];
199
- const addedUrls = /* @__PURE__ */ new Set();
200
- const addAsset = (url, forcedType) => {
201
- if (!url || url.trim() === "" || url.startsWith("data:")) {
202
- return;
203
- }
204
- if (!addedUrls.has(url)) {
205
- addedUrls.add(url);
206
- const mimeInfo = guessMimeType(url);
207
- const type = forcedType ?? mimeInfo.assetType;
208
- assets.push({ type, url });
209
- logger?.debug(`Discovered asset: Type='${type}', URL='${url}'`);
210
- } else {
211
- logger?.debug(`Skipping duplicate asset URL: ${url}`);
212
- }
213
- };
214
- logger?.debug("Extracting assets from HTML tags...");
215
- $('link[rel="stylesheet"][href]').each((_, el) => {
216
- addAsset($(el).attr("href"), "css");
217
- });
218
- $("script[src]").each((_, el) => {
219
- addAsset($(el).attr("src"), "js");
220
- });
221
- $("img[src]").each((_, el) => addAsset($(el).attr("src"), "image"));
222
- $('input[type="image"][src]').each((_, el) => addAsset($(el).attr("src"), "image"));
223
- $("img[srcset], picture source[srcset]").each((_, el) => {
224
- const srcset = $(el).attr("srcset");
225
- srcset?.split(",").forEach((entry) => {
226
- const [url] = entry.trim().split(/\s+/);
227
- addAsset(url, "image");
228
- });
229
- });
230
- $("video[src]").each((_, el) => addAsset($(el).attr("src"), "video"));
231
- $("video[poster]").each((_, el) => addAsset($(el).attr("poster"), "image"));
232
- $("audio[src]").each((_, el) => addAsset($(el).attr("src"), "audio"));
233
- $("video > source[src]").each((_, el) => addAsset($(el).attr("src"), "video"));
234
- $("audio > source[src]").each((_, el) => addAsset($(el).attr("src"), "audio"));
235
- $("link[href]").filter((_, el) => {
236
- const rel = $(el).attr("rel")?.toLowerCase() ?? "";
237
- return ["icon", "shortcut icon", "apple-touch-icon", "manifest"].includes(rel);
238
- }).each((_, el) => {
239
- const rel = $(el).attr("rel")?.toLowerCase() ?? "";
240
- const isIcon = ["icon", "shortcut icon", "apple-touch-icon"].includes(rel);
241
- addAsset($(el).attr("href"), isIcon ? "image" : void 0);
242
- });
243
- $('link[rel="preload"][as="font"][href]').each((_, el) => {
244
- addAsset($(el).attr("href"), "font");
245
- });
246
- logger?.info(`HTML parsing complete. Discovered ${assets.length} unique asset links.`);
247
- return { htmlContent, assets };
248
- }
249
- var init_parser = __esm({
250
- "src/core/parser.ts"() {
251
- "use strict";
252
- init_mime();
253
- }
254
- });
255
-
256
319
  // src/core/extractor.ts
257
- import { readFile as readFile2 } from "fs/promises";
258
- import * as fs from "fs";
259
- import path2 from "path";
260
- import { fileURLToPath, URL as URL2 } from "url";
261
- import * as axios from "axios";
262
320
  function isUtf8DecodingLossy(originalBuffer, decodedString) {
263
321
  try {
264
322
  const reEncodedBuffer = Buffer.from(decodedString, "utf-8");
@@ -268,6 +326,7 @@ function isUtf8DecodingLossy(originalBuffer, decodedString) {
268
326
  }
269
327
  }
270
328
  function determineBaseUrl(inputPathOrUrl, logger) {
329
+ console.log(`[DEBUG determineBaseUrl] Input: "${inputPathOrUrl}"`);
271
330
  logger?.debug(`Determining base URL for input: ${inputPathOrUrl}`);
272
331
  if (!inputPathOrUrl) {
273
332
  logger?.warn("Cannot determine base URL: inputPathOrUrl is empty or invalid.");
@@ -275,55 +334,52 @@ function determineBaseUrl(inputPathOrUrl, logger) {
275
334
  }
276
335
  try {
277
336
  if (/^https?:\/\//i.test(inputPathOrUrl)) {
278
- const url = new URL2(inputPathOrUrl);
337
+ const url = new import_url.URL(inputPathOrUrl);
279
338
  url.pathname = url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1);
280
339
  url.search = "";
281
340
  url.hash = "";
282
341
  const baseUrl = url.href;
283
342
  logger?.debug(`Determined remote base URL: ${baseUrl}`);
343
+ console.log(`[DEBUG determineBaseUrl] Determined Remote URL: "${baseUrl}"`);
284
344
  return baseUrl;
285
345
  } else if (inputPathOrUrl.includes("://") && !inputPathOrUrl.startsWith("file:")) {
286
346
  logger?.warn(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
347
+ console.log(`[DEBUG determineBaseUrl] Unsupported protocol.`);
287
348
  return void 0;
288
349
  } else {
289
- let absolutePath;
350
+ let resourcePath;
351
+ let isInputLikelyDirectory = false;
290
352
  if (inputPathOrUrl.startsWith("file:")) {
291
- try {
292
- absolutePath = fileURLToPath(inputPathOrUrl);
293
- } catch (e) {
294
- logger?.error(`\u{1F480} Failed to convert file URL "${inputPathOrUrl}" to path: ${e.message}`);
295
- return void 0;
296
- }
353
+ resourcePath = (0, import_url.fileURLToPath)(inputPathOrUrl);
354
+ isInputLikelyDirectory = inputPathOrUrl.endsWith("/");
297
355
  } else {
298
- absolutePath = path2.resolve(inputPathOrUrl);
299
- }
300
- let isDirectory = false;
301
- try {
302
- isDirectory = fs.statSync(absolutePath).isDirectory();
303
- } catch (statError) {
304
- if (statError instanceof Error && statError.code === "ENOENT") {
305
- logger?.debug(`Path "${absolutePath}" not found. Assuming input represents a file, using its parent directory as base.`);
306
- } else {
307
- logger?.warn(`Could not stat local path "${absolutePath}" during base URL determination: ${statError instanceof Error ? statError.message : String(statError)}. Assuming input represents a file.`);
356
+ resourcePath = import_path2.default.resolve(inputPathOrUrl);
357
+ try {
358
+ isInputLikelyDirectory = fs.statSync(resourcePath).isDirectory();
359
+ } catch {
360
+ isInputLikelyDirectory = false;
308
361
  }
309
- isDirectory = false;
310
362
  }
311
- const dirPath = isDirectory ? absolutePath : path2.dirname(absolutePath);
312
- let normalizedPathForURL = dirPath.replace(/\\/g, "/");
363
+ console.log(`[DEBUG determineBaseUrl] resourcePath: "${resourcePath}", isInputLikelyDirectory: ${isInputLikelyDirectory}`);
364
+ const baseDirPath = isInputLikelyDirectory ? resourcePath : import_path2.default.dirname(resourcePath);
365
+ console.log(`[DEBUG determineBaseUrl] Calculated baseDirPath: "${baseDirPath}"`);
366
+ let normalizedPathForURL = baseDirPath.replace(/\\/g, "/");
313
367
  if (/^[A-Z]:\//i.test(normalizedPathForURL) && !normalizedPathForURL.startsWith("/")) {
314
368
  normalizedPathForURL = "/" + normalizedPathForURL;
315
369
  }
316
- const fileUrl = new URL2("file://" + normalizedPathForURL);
317
- let fileUrlString = fileUrl.href;
318
- if (!fileUrlString.endsWith("/")) {
319
- fileUrlString += "/";
370
+ if (!normalizedPathForURL.endsWith("/")) {
371
+ normalizedPathForURL += "/";
320
372
  }
321
- logger?.debug(`Determined local base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved dir: ${dirPath}, isDir: ${isDirectory})`);
373
+ const fileUrl = new import_url.URL("file://" + normalizedPathForURL);
374
+ const fileUrlString = fileUrl.href;
375
+ logger?.debug(`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`);
376
+ console.log(`[DEBUG determineBaseUrl] Determined File URL: "${fileUrlString}"`);
322
377
  return fileUrlString;
323
378
  }
324
379
  } catch (error) {
325
380
  const message = error instanceof Error ? error.message : String(error);
326
- logger?.error(`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error ? ` - Stack: ${error.stack}` : ""}`);
381
+ console.error(`[DEBUG determineBaseUrl] Error determining base URL: ${message}`);
382
+ logger?.error(`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`);
327
383
  return void 0;
328
384
  }
329
385
  }
@@ -335,7 +391,7 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
335
391
  let resolvableUrl = trimmedUrl;
336
392
  if (resolvableUrl.startsWith("//") && baseContextUrl) {
337
393
  try {
338
- const base = new URL2(baseContextUrl);
394
+ const base = new import_url.URL(baseContextUrl);
339
395
  resolvableUrl = base.protocol + resolvableUrl;
340
396
  } catch (e) {
341
397
  logger?.warn(`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`);
@@ -343,7 +399,11 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
343
399
  }
344
400
  }
345
401
  try {
346
- const resolved = new URL2(resolvableUrl, baseContextUrl);
402
+ const resolved = new import_url.URL(resolvableUrl, baseContextUrl);
403
+ if (!["http:", "https:", "file:"].includes(resolved.protocol)) {
404
+ logger?.debug(`Skipping asset with unsupported protocol: ${resolved.href}`);
405
+ return null;
406
+ }
347
407
  return resolved;
348
408
  } catch (error) {
349
409
  const message = error instanceof Error ? error.message : String(error);
@@ -356,83 +416,78 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
356
416
  }
357
417
  }
358
418
  function resolveCssRelativeUrl(relativeUrl, cssBaseContextUrl, logger) {
359
- if (!relativeUrl || relativeUrl.startsWith("data:")) {
419
+ console.log(`[DEBUG resolveCssRelativeUrl] Input: relative="${relativeUrl}", base="${cssBaseContextUrl}"`);
420
+ if (!relativeUrl || relativeUrl.startsWith("data:") || relativeUrl.startsWith("#")) {
360
421
  return null;
361
422
  }
362
423
  try {
363
- if (cssBaseContextUrl.startsWith("file:")) {
364
- const basePath = fileURLToPath(cssBaseContextUrl);
365
- let cssDir;
366
- try {
367
- const stat = fs.statSync(basePath);
368
- if (stat.isDirectory()) {
369
- cssDir = basePath;
370
- } else {
371
- cssDir = path2.dirname(basePath);
372
- }
373
- } catch {
374
- cssDir = path2.dirname(basePath);
375
- }
376
- let resolvedPath = path2.resolve(cssDir, relativeUrl);
377
- resolvedPath = resolvedPath.replace(/\\/g, "/");
378
- if (/^[A-Z]:/i.test(resolvedPath) && !resolvedPath.startsWith("/")) {
379
- resolvedPath = "/" + resolvedPath;
380
- }
381
- return `file://${resolvedPath}`;
382
- } else {
383
- return new URL2(relativeUrl, cssBaseContextUrl).href;
384
- }
424
+ const resolvedUrl = new import_url.URL(relativeUrl, cssBaseContextUrl);
425
+ console.log(`[DEBUG resolveCssRelativeUrl] Resolved URL object href: "${resolvedUrl.href}"`);
426
+ return resolvedUrl.href;
385
427
  } catch (error) {
386
428
  logger?.warn(
387
- `Failed to resolve CSS URL: "${relativeUrl}" against "${cssBaseContextUrl}": ${String(error)}`
429
+ `Failed to resolve CSS URL: "${relativeUrl}" relative to "${cssBaseContextUrl}": ${String(error)}`
388
430
  );
431
+ console.error(`[DEBUG resolveCssRelativeUrl] Error resolving: ${String(error)}`);
389
432
  return null;
390
433
  }
391
434
  }
392
435
  async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
436
+ console.log(`[DEBUG fetchAsset] Attempting fetch for URL: ${resolvedUrl.href}`);
393
437
  logger?.debug(`Attempting to fetch asset: ${resolvedUrl.href}`);
394
438
  const protocol = resolvedUrl.protocol;
395
439
  try {
396
440
  if (protocol === "http:" || protocol === "https:") {
397
- const response = await axios.default.get(resolvedUrl.href, {
441
+ const response = await axiosNs.default.get(resolvedUrl.href, {
398
442
  responseType: "arraybuffer",
399
443
  timeout
400
444
  });
401
- logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data.byteLength} bytes)`);
445
+ logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data?.byteLength ?? 0} bytes)`);
446
+ console.log(`[DEBUG fetchAsset] HTTP fetch SUCCESS for: ${resolvedUrl.href}, Status: ${response.status}`);
402
447
  return Buffer.from(response.data);
403
448
  } else if (protocol === "file:") {
404
449
  let filePath;
405
450
  try {
406
- filePath = fileURLToPath(resolvedUrl);
451
+ filePath = (0, import_url.fileURLToPath)(resolvedUrl);
407
452
  } catch (e) {
453
+ console.error(`[DEBUG fetchAsset] fileURLToPath FAILED for: ${resolvedUrl.href}`, e);
408
454
  logger?.error(`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`);
409
455
  return null;
410
456
  }
411
- const data = await readFile2(filePath);
457
+ const normalizedForLog = import_path2.default.normalize(filePath);
458
+ console.log(`[DEBUG fetchAsset] Attempting readFile with path: "${normalizedForLog}" (Original from URL: "${filePath}")`);
459
+ const data = await (0, import_promises.readFile)(filePath);
460
+ console.log(`[DEBUG fetchAsset] readFile call SUCCEEDED for path: "${normalizedForLog}". Data length: ${data?.byteLength}`);
412
461
  logger?.debug(`Read local file ${filePath} (${data.byteLength} bytes)`);
413
462
  return data;
414
463
  } else {
464
+ console.log(`[DEBUG fetchAsset] Unsupported protocol: ${protocol}`);
415
465
  logger?.warn(`Unsupported protocol "${protocol}" in URL: ${resolvedUrl.href}`);
416
466
  return null;
417
467
  }
418
468
  } catch (error) {
419
- if ((protocol === "http:" || protocol === "https:") && axios.default.isAxiosError(error)) {
469
+ const failedId = protocol === "file:" ? import_path2.default.normalize((0, import_url.fileURLToPath)(resolvedUrl)) : resolvedUrl.href;
470
+ console.error(`[DEBUG fetchAsset] fetch/read FAILED for: "${failedId}". Error:`, error);
471
+ if ((protocol === "http:" || protocol === "https:") && axiosNs.isAxiosError(error)) {
420
472
  const status = error.response?.status ?? "N/A";
421
473
  const statusText = error.response?.statusText ?? "Error";
422
474
  const code = error.code ?? "N/A";
423
475
  const message = error.message;
424
476
  const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: Status ${status} - ${statusText}. Code: ${code}, Message: ${message}`;
425
477
  logger?.warn(logMessage);
426
- } else if (protocol === "file:") {
478
+ }
479
+ if (error instanceof Error && error.code === "ENOENT") {
427
480
  let failedPath = resolvedUrl.href;
428
481
  try {
429
- failedPath = fileURLToPath(resolvedUrl);
482
+ failedPath = (0, import_url.fileURLToPath)(resolvedUrl);
430
483
  } catch {
431
484
  }
485
+ failedPath = import_path2.default.normalize(failedPath);
432
486
  if (error instanceof Error && error.code === "ENOENT") {
433
487
  logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
434
488
  } else if (error instanceof Error && error.code === "EACCES") {
435
489
  logger?.warn(`\u26A0\uFE0F Permission denied (EACCES) reading asset: ${failedPath}.`);
490
+ logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
436
491
  } else if (error instanceof Error) {
437
492
  logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
438
493
  } else {
@@ -460,14 +515,13 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
460
515
  newlyDiscovered.push({
461
516
  type: assetType,
462
517
  url: resolvedUrl,
463
- // The resolved URL string
518
+ // The resolved absolute URL string
464
519
  content: void 0
520
+ // Content will be fetched later if needed
465
521
  });
466
522
  logger?.debug(`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`);
467
523
  }
468
524
  };
469
- urlRegex.lastIndex = 0;
470
- importRegex.lastIndex = 0;
471
525
  let match;
472
526
  while ((match = urlRegex.exec(cssContent)) !== null) {
473
527
  processFoundUrl(match[2], "url()");
@@ -483,23 +537,28 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
483
537
  const initialAssets = parsed.assets || [];
484
538
  const finalAssetsMap = /* @__PURE__ */ new Map();
485
539
  let assetsToProcess = [];
540
+ const processedOrQueuedUrls = /* @__PURE__ */ new Set();
486
541
  const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || "", logger);
487
542
  if (!htmlBaseContextUrl && initialAssets.some((a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/"))) {
488
543
  logger?.warn("\u{1F6A8} No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail.");
489
544
  } else if (htmlBaseContextUrl) {
490
545
  logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
491
546
  }
492
- const processedOrQueuedUrls = /* @__PURE__ */ new Set();
493
547
  logger?.debug(`Queueing ${initialAssets.length} initial assets parsed from HTML...`);
494
548
  for (const asset of initialAssets) {
495
549
  const resolvedUrlObj = resolveAssetUrl(asset.url, htmlBaseContextUrl, logger);
496
- const urlToQueue = resolvedUrlObj ? resolvedUrlObj.href : asset.url;
550
+ if (!resolvedUrlObj) {
551
+ logger?.debug(` -> Skipping initial asset with unresolvable/ignorable URL: ${asset.url}`);
552
+ continue;
553
+ }
554
+ const urlToQueue = resolvedUrlObj.href;
497
555
  if (!urlToQueue.startsWith("data:") && !processedOrQueuedUrls.has(urlToQueue)) {
498
556
  processedOrQueuedUrls.add(urlToQueue);
499
557
  const { assetType: guessedType } = guessMimeType(urlToQueue);
500
558
  const initialType = asset.type ?? guessedType;
501
559
  assetsToProcess.push({
502
560
  url: urlToQueue,
561
+ // Use the resolved URL
503
562
  type: initialType,
504
563
  content: void 0
505
564
  });
@@ -507,7 +566,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
507
566
  } else if (urlToQueue.startsWith("data:")) {
508
567
  logger?.debug(` -> Skipping data URI: ${urlToQueue.substring(0, 50)}...`);
509
568
  } else {
510
- logger?.debug(` -> Skipping already queued initial asset: ${urlToQueue}`);
569
+ logger?.debug(` -> Skipping already processed/queued initial asset: ${urlToQueue}`);
511
570
  }
512
571
  }
513
572
  let iterationCount = 0;
@@ -540,7 +599,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
540
599
  let assetUrlObj = null;
541
600
  if (needsFetching) {
542
601
  try {
543
- assetUrlObj = new URL2(asset.url);
602
+ assetUrlObj = new import_url.URL(asset.url);
544
603
  } catch (urlError) {
545
604
  logger?.warn(`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
546
605
  finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
@@ -578,7 +637,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
578
637
  cssContentForParsing = textContent;
579
638
  }
580
639
  } else {
581
- logger?.warn(`Could not decode ${asset.type} ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`);
640
+ logger?.warn(`Could not decode ${asset.type} asset ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`);
582
641
  cssContentForParsing = void 0;
583
642
  if (embedAssets) {
584
643
  finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
@@ -625,6 +684,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
625
684
  const newlyDiscoveredAssets = extractUrlsFromCSS(
626
685
  cssContentForParsing,
627
686
  cssBaseContextUrl,
687
+ // Use CSS file's base URL
628
688
  logger
629
689
  );
630
690
  if (newlyDiscoveredAssets.length > 0) {
@@ -652,10 +712,15 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
652
712
  assets: Array.from(finalAssetsMap.values())
653
713
  };
654
714
  }
655
- var TEXT_ASSET_TYPES, BINARY_ASSET_TYPES, MAX_ASSET_EXTRACTION_ITERATIONS;
715
+ var import_promises, fs, import_path2, import_url, axiosNs, TEXT_ASSET_TYPES, BINARY_ASSET_TYPES, MAX_ASSET_EXTRACTION_ITERATIONS;
656
716
  var init_extractor = __esm({
657
717
  "src/core/extractor.ts"() {
658
718
  "use strict";
719
+ import_promises = require("fs/promises");
720
+ fs = __toESM(require("fs"), 1);
721
+ import_path2 = __toESM(require("path"), 1);
722
+ import_url = require("url");
723
+ axiosNs = __toESM(require("axios"), 1);
659
724
  init_mime();
660
725
  TEXT_ASSET_TYPES = /* @__PURE__ */ new Set(["css", "js"]);
661
726
  BINARY_ASSET_TYPES = /* @__PURE__ */ new Set(["image", "font", "video", "audio"]);
@@ -664,9 +729,6 @@ var init_extractor = __esm({
664
729
  });
665
730
 
666
731
  // src/core/minifier.ts
667
- import { minify as htmlMinify } from "html-minifier-terser";
668
- import CleanCSS from "clean-css";
669
- import { minify as jsMinify } from "terser";
670
732
  async function minifyAssets(parsed, options = {}, logger) {
671
733
  const { htmlContent, assets } = parsed;
672
734
  const currentHtmlContent = htmlContent ?? "";
@@ -692,7 +754,7 @@ async function minifyAssets(parsed, options = {}, logger) {
692
754
  try {
693
755
  if (minifyFlags.minifyCss && processedAsset.type === "css") {
694
756
  logger?.debug(`Minifying CSS: ${assetIdentifier}`);
695
- const cssMinifier = new CleanCSS(CSS_MINIFY_OPTIONS);
757
+ const cssMinifier = new import_clean_css.default(CSS_MINIFY_OPTIONS);
696
758
  const result = cssMinifier.minify(processedAsset.content);
697
759
  if (result.errors && result.errors.length > 0) {
698
760
  logger?.warn(`\u26A0\uFE0F CleanCSS failed for ${assetIdentifier}: ${result.errors.join(", ")}`);
@@ -710,7 +772,7 @@ async function minifyAssets(parsed, options = {}, logger) {
710
772
  }
711
773
  if (minifyFlags.minifyJs && processedAsset.type === "js") {
712
774
  logger?.debug(`Minifying JS: ${assetIdentifier}`);
713
- const result = await jsMinify(processedAsset.content, JS_MINIFY_OPTIONS);
775
+ const result = await (0, import_terser.minify)(processedAsset.content, JS_MINIFY_OPTIONS);
714
776
  if (result.code) {
715
777
  newContent = result.code;
716
778
  logger?.debug(`JS minified successfully: ${assetIdentifier}`);
@@ -735,7 +797,7 @@ async function minifyAssets(parsed, options = {}, logger) {
735
797
  if (minifyFlags.minifyHtml && finalHtml.length > 0) {
736
798
  logger?.debug("Minifying HTML content...");
737
799
  try {
738
- finalHtml = await htmlMinify(finalHtml, {
800
+ finalHtml = await (0, import_html_minifier_terser.minify)(finalHtml, {
739
801
  ...HTML_MINIFY_OPTIONS,
740
802
  minifyCSS: minifyFlags.minifyCss,
741
803
  minifyJS: minifyFlags.minifyJs
@@ -754,10 +816,13 @@ async function minifyAssets(parsed, options = {}, logger) {
754
816
  // The array of processed asset copies
755
817
  };
756
818
  }
757
- var HTML_MINIFY_OPTIONS, CSS_MINIFY_OPTIONS, JS_MINIFY_OPTIONS;
819
+ var import_html_minifier_terser, import_clean_css, import_terser, HTML_MINIFY_OPTIONS, CSS_MINIFY_OPTIONS, JS_MINIFY_OPTIONS;
758
820
  var init_minifier = __esm({
759
821
  "src/core/minifier.ts"() {
760
822
  "use strict";
823
+ import_html_minifier_terser = require("html-minifier-terser");
824
+ import_clean_css = __toESM(require("clean-css"), 1);
825
+ import_terser = require("terser");
761
826
  HTML_MINIFY_OPTIONS = {
762
827
  collapseWhitespace: true,
763
828
  removeComments: true,
@@ -815,7 +880,6 @@ var init_minifier = __esm({
815
880
  });
816
881
 
817
882
  // src/core/packer.ts
818
- import * as cheerio2 from "cheerio";
819
883
  function escapeScriptContent(code) {
820
884
  return code.replace(/<\/(script)/gi, "<\\/$1");
821
885
  }
@@ -928,7 +992,7 @@ function packHTML(parsed, logger) {
928
992
  return '<!DOCTYPE html><html><head><base href="./"></head><body></body></html>';
929
993
  }
930
994
  logger?.debug("Loading HTML content into Cheerio for packing...");
931
- const $ = cheerio2.load(htmlContent);
995
+ const $ = cheerio.load(htmlContent);
932
996
  logger?.debug("Ensuring <base> tag exists...");
933
997
  ensureBaseTag($, logger);
934
998
  logger?.debug("Starting asset inlining...");
@@ -938,122 +1002,11 @@ function packHTML(parsed, logger) {
938
1002
  logger?.debug(`Packing complete. Final size: ${Buffer.byteLength(finalHtml)} bytes.`);
939
1003
  return finalHtml;
940
1004
  }
1005
+ var cheerio;
941
1006
  var init_packer = __esm({
942
1007
  "src/core/packer.ts"() {
943
1008
  "use strict";
944
- }
945
- });
946
-
947
- // src/utils/logger.ts
948
- var Logger;
949
- var init_logger = __esm({
950
- "src/utils/logger.ts"() {
951
- "use strict";
952
- init_types();
953
- Logger = class _Logger {
954
- /** The current minimum log level required for a message to be output. */
955
- level;
956
- /**
957
- * Creates a new Logger instance.
958
- * Defaults to LogLevel.INFO if no level is provided.
959
- *
960
- * @param {LogLevel} [level=LogLevel.INFO] - The initial log level for this logger instance.
961
- * Must be one of the values from the LogLevel enum.
962
- */
963
- constructor(level = 3 /* INFO */) {
964
- this.level = level !== void 0 && LogLevel[level] !== void 0 ? level : 3 /* INFO */;
965
- }
966
- /**
967
- * Updates the logger's current level. Messages below this level will be suppressed.
968
- *
969
- * @param {LogLevel} level - The new log level to set. Must be a LogLevel enum member.
970
- */
971
- setLevel(level) {
972
- this.level = level;
973
- }
974
- /**
975
- * Logs a debug message if the current log level is DEBUG or higher.
976
- *
977
- * @param {string} message - The debug message string.
978
- */
979
- debug(message) {
980
- if (this.level >= 4 /* DEBUG */) {
981
- console.debug(`[DEBUG] ${message}`);
982
- }
983
- }
984
- /**
985
- * Logs an informational message if the current log level is INFO or higher.
986
- *
987
- * @param {string} message - The informational message string.
988
- */
989
- info(message) {
990
- if (this.level >= 3 /* INFO */) {
991
- console.info(`[INFO] ${message}`);
992
- }
993
- }
994
- /**
995
- * Logs a warning message if the current log level is WARN or higher.
996
- *
997
- * @param {string} message - The warning message string.
998
- */
999
- warn(message) {
1000
- if (this.level >= 2 /* WARN */) {
1001
- console.warn(`[WARN] ${message}`);
1002
- }
1003
- }
1004
- /**
1005
- * Logs an error message if the current log level is ERROR or higher.
1006
- *
1007
- * @param {string} message - The error message string.
1008
- */
1009
- error(message) {
1010
- if (this.level >= 1 /* ERROR */) {
1011
- console.error(`[ERROR] ${message}`);
1012
- }
1013
- }
1014
- /**
1015
- * Static factory method to create a Logger instance based on a simple boolean `verbose` flag.
1016
- *
1017
- * @static
1018
- * @param {{ verbose?: boolean }} [options={}] - An object potentially containing a `verbose` flag.
1019
- * @returns {Logger} A new Logger instance set to LogLevel.DEBUG if options.verbose is true,
1020
- * otherwise set to LogLevel.INFO.
1021
- */
1022
- static fromVerboseFlag(options = {}) {
1023
- return new _Logger(options.verbose ? 4 /* DEBUG */ : 3 /* INFO */);
1024
- }
1025
- /**
1026
- * Static factory method to create a Logger instance based on a LogLevel string name.
1027
- * Useful for creating a logger from config files or environments variables.
1028
- *
1029
- * @static
1030
- * @param {string | undefined} levelName - The name of the log level (e.g., 'debug', 'info', 'warn', 'error', 'silent'/'none'). Case-insensitive.
1031
- * @param {LogLevel} [defaultLevel=LogLevel.INFO] - The level to use if levelName is invalid or undefined.
1032
- * @returns {Logger} A new Logger instance set to the corresponding LogLevel.
1033
- */
1034
- static fromLevelName(levelName, defaultLevel = 3 /* INFO */) {
1035
- if (!levelName) {
1036
- return new _Logger(defaultLevel);
1037
- }
1038
- switch (levelName.toLowerCase()) {
1039
- // Return enum members
1040
- case "debug":
1041
- return new _Logger(4 /* DEBUG */);
1042
- case "info":
1043
- return new _Logger(3 /* INFO */);
1044
- case "warn":
1045
- return new _Logger(2 /* WARN */);
1046
- case "error":
1047
- return new _Logger(1 /* ERROR */);
1048
- case "silent":
1049
- case "none":
1050
- return new _Logger(0 /* NONE */);
1051
- default:
1052
- console.warn(`[Logger] Invalid log level name "${levelName}". Defaulting to ${LogLevel[defaultLevel]}.`);
1053
- return new _Logger(defaultLevel);
1054
- }
1055
- }
1056
- };
1009
+ cheerio = __toESM(require("cheerio"), 1);
1057
1010
  }
1058
1011
  });
1059
1012
 
@@ -1093,9 +1046,11 @@ function bundleMultiPageHTML(pages, logger) {
1093
1046
  throw new Error(errorMsg);
1094
1047
  }
1095
1048
  logger?.info(`Bundling ${pages.length} pages into a multi-page HTML document.`);
1049
+ let pageIndex = 0;
1096
1050
  const validPages = pages.filter((page) => {
1097
1051
  const isValid = page && typeof page === "object" && typeof page.url === "string" && typeof page.html === "string";
1098
- if (!isValid) logger?.warn("Skipping invalid page entry");
1052
+ if (!isValid) logger?.warn(`Skipping invalid page entry at index ${pageIndex}`);
1053
+ pageIndex++;
1099
1054
  return isValid;
1100
1055
  });
1101
1056
  if (validPages.length === 0) {
@@ -1105,70 +1060,137 @@ function bundleMultiPageHTML(pages, logger) {
1105
1060
  }
1106
1061
  const slugMap = /* @__PURE__ */ new Map();
1107
1062
  const usedSlugs = /* @__PURE__ */ new Set();
1063
+ let firstValidSlug = void 0;
1064
+ let pageCounterForFallback = 1;
1108
1065
  for (const page of validPages) {
1109
- const baseSlug = sanitizeSlug(page.url);
1066
+ let baseSlug = sanitizeSlug(page.url);
1067
+ const isRootIndex = page.url === "/" || page.url === "index.html" || page.url.endsWith("/index.html");
1068
+ if (baseSlug === "index" && !isRootIndex) {
1069
+ logger?.debug(`URL "${page.url}" sanitized to "index", attempting to find alternative slug.`);
1070
+ const pathParts = page.url.replace(/\/$/, "").split("/").filter((p) => p && p.toLowerCase() !== "index.html" && p.toLowerCase() !== "index");
1071
+ if (pathParts.length > 0) {
1072
+ const lastPartSlug = sanitizeSlug(pathParts[pathParts.length - 1]);
1073
+ if (lastPartSlug && lastPartSlug !== "index") {
1074
+ baseSlug = lastPartSlug;
1075
+ logger?.debug(`Using last path part slug "${baseSlug}" instead.`);
1076
+ } else {
1077
+ baseSlug = "page";
1078
+ logger?.debug(`Last path part invalid ("${lastPartSlug}"), using fallback slug "page".`);
1079
+ }
1080
+ } else {
1081
+ baseSlug = "page";
1082
+ logger?.debug(`No valid path parts found, using fallback slug "page".`);
1083
+ }
1084
+ } else if (!baseSlug) {
1085
+ if (isRootIndex) {
1086
+ baseSlug = "index";
1087
+ logger?.debug(`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`);
1088
+ } else {
1089
+ baseSlug = "page";
1090
+ logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
1091
+ }
1092
+ }
1093
+ if (!baseSlug) {
1094
+ baseSlug = `page-${pageCounterForFallback++}`;
1095
+ logger?.warn(`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`);
1096
+ }
1110
1097
  let slug = baseSlug;
1111
- let counter = 1;
1098
+ let collisionCounter = 1;
1099
+ const originalBaseSlugForLog = baseSlug;
1112
1100
  while (usedSlugs.has(slug)) {
1113
- slug = `${baseSlug}-${counter++}`;
1114
- logger?.warn(`Slug collision detected for "${page.url}". Using "${slug}" instead.`);
1101
+ const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
1102
+ logger?.warn(`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`);
1103
+ slug = newSlug;
1115
1104
  }
1116
1105
  usedSlugs.add(slug);
1117
1106
  slugMap.set(page.url, slug);
1107
+ if (firstValidSlug === void 0) {
1108
+ firstValidSlug = slug;
1109
+ }
1118
1110
  }
1119
- const defaultPageSlug = slugMap.get(validPages[0].url);
1111
+ const defaultPageSlug = usedSlugs.has("index") ? "index" : firstValidSlug || "page";
1120
1112
  let output = `<!DOCTYPE html>
1121
1113
  <html lang="en">
1122
1114
  <head>
1123
1115
  <meta charset="UTF-8">
1124
1116
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1125
1117
  <title>Multi-Page Bundle</title>
1118
+ <style>
1119
+ body { font-family: sans-serif; margin: 0; }
1120
+ #main-nav { background-color: #f0f0f0; padding: 10px; border-bottom: 1px solid #ccc; }
1121
+ #main-nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
1122
+ #main-nav a.active { font-weight: bold; text-decoration: underline; }
1123
+ #page-container { padding: 20px; }
1124
+ template { display: none; }
1125
+ </style>
1126
1126
  </head>
1127
1127
  <body>
1128
1128
  <nav id="main-nav">
1129
1129
  ${validPages.map((p) => {
1130
1130
  const slug = slugMap.get(p.url);
1131
- const label = p.url.split("/").pop()?.split(".")[0] || "Page";
1131
+ const label = slug;
1132
1132
  return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
1133
- }).join("\n")}
1133
+ }).join("\n ")}
1134
1134
  </nav>
1135
1135
  <div id="page-container"></div>
1136
1136
  ${validPages.map((p) => {
1137
1137
  const slug = slugMap.get(p.url);
1138
1138
  return `<template id="page-${slug}">${p.html}</template>`;
1139
- }).join("\n")}
1139
+ }).join("\n ")}
1140
1140
  <script id="router-script">
1141
1141
  document.addEventListener('DOMContentLoaded', function() {
1142
+ const pageContainer = document.getElementById('page-container');
1143
+ const navLinks = document.querySelectorAll('#main-nav a');
1144
+
1142
1145
  function navigateTo(slug) {
1143
1146
  const template = document.getElementById('page-' + slug);
1144
- const container = document.getElementById('page-container');
1145
- if (!template || !container) return;
1146
- container.innerHTML = '';
1147
- container.appendChild(template.content.cloneNode(true));
1148
- document.querySelectorAll('#main-nav a').forEach(link => {
1149
- if (link.getAttribute('data-page') === slug) link.classList.add('active');
1150
- else link.classList.remove('active');
1147
+ if (!template || !pageContainer) {
1148
+ console.warn('Navigation failed: Template or container not found for slug:', slug);
1149
+ // Maybe try navigating to default page? Or just clear container?
1150
+ if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
1151
+ return;
1152
+ }
1153
+ // Clear previous content and append new content
1154
+ pageContainer.innerHTML = ''; // Clear reliably
1155
+ pageContainer.appendChild(template.content.cloneNode(true));
1156
+
1157
+ // Update active link styling
1158
+ navLinks.forEach(link => {
1159
+ link.classList.toggle('active', link.getAttribute('data-page') === slug);
1151
1160
  });
1161
+
1162
+ // Update URL hash without triggering hashchange if already correct
1152
1163
  if (window.location.hash.substring(1) !== slug) {
1153
- history.pushState(null, '', '#' + slug);
1164
+ // Use pushState for cleaner history
1165
+ history.pushState({ slug: slug }, '', '#' + slug);
1154
1166
  }
1155
1167
  }
1156
1168
 
1157
- window.addEventListener('hashchange', () => {
1158
- const slug = window.location.hash.substring(1);
1159
- if (document.getElementById('page-' + slug)) navigateTo(slug);
1169
+ // Handle back/forward navigation
1170
+ window.addEventListener('popstate', (event) => {
1171
+ let slug = window.location.hash.substring(1);
1172
+ // If popstate event has state use it, otherwise fallback to hash or default
1173
+ if (event && event.state && event.state.slug) { // Check event exists
1174
+ slug = event.state.slug;
1175
+ }
1176
+ // Ensure the target page exists before navigating, fallback to default slug
1177
+ const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
1178
+ navigateTo(targetSlug);
1160
1179
  });
1161
1180
 
1162
- document.querySelectorAll('#main-nav a').forEach(link => {
1181
+ // Handle direct link clicks
1182
+ navLinks.forEach(link => {
1163
1183
  link.addEventListener('click', function(e) {
1164
1184
  e.preventDefault();
1165
1185
  const slug = this.getAttribute('data-page');
1166
- navigateTo(slug);
1186
+ if (slug) navigateTo(slug);
1167
1187
  });
1168
1188
  });
1169
1189
 
1170
- const initial = window.location.hash.substring(1);
1171
- navigateTo(document.getElementById('page-' + initial) ? initial : '${defaultPageSlug}');
1190
+ // Initial page load
1191
+ const initialHash = window.location.hash.substring(1);
1192
+ const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
1193
+ navigateTo(initialSlug);
1172
1194
  });
1173
1195
  </script>
1174
1196
  </body>
@@ -1182,58 +1204,69 @@ var init_bundler = __esm({
1182
1204
  init_extractor();
1183
1205
  init_minifier();
1184
1206
  init_packer();
1207
+ init_types();
1185
1208
  init_slugify();
1186
1209
  }
1187
1210
  });
1188
1211
 
1189
1212
  // src/core/web-fetcher.ts
1190
- import * as puppeteer from "puppeteer";
1191
- import * as fs2 from "fs/promises";
1192
- async function fetchAndPackWebPage(url, logger, timeout = 3e4) {
1213
+ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT, userAgent) {
1193
1214
  let browser = null;
1194
1215
  const start = Date.now();
1195
- logger?.debug(`Initiating fetch for single page: ${url}`);
1216
+ logger?.info(`Initiating fetch for single page: ${url}`);
1196
1217
  try {
1197
- browser = await puppeteer.launch({ headless: true });
1198
- logger?.debug(`Browser launched for ${url}`);
1218
+ logger?.debug("Launching browser...");
1219
+ browser = await puppeteer.launch(PUPPETEER_LAUNCH_OPTIONS);
1220
+ logger?.debug(`Browser launched successfully (PID: ${browser.process()?.pid}).`);
1199
1221
  const page = await browser.newPage();
1200
- logger?.debug(`Page created for ${url}`);
1222
+ logger?.debug(`New page created for ${url}`);
1223
+ if (userAgent) {
1224
+ await page.setUserAgent(userAgent);
1225
+ logger?.debug(`User-Agent set to: "${userAgent}"`);
1226
+ }
1201
1227
  try {
1202
1228
  logger?.debug(`Navigating to ${url} with timeout ${timeout}ms`);
1203
1229
  await page.goto(url, { waitUntil: "networkidle2", timeout });
1204
1230
  logger?.debug(`Navigation successful for ${url}`);
1205
1231
  const html = await page.content();
1206
- logger?.debug(`Content retrieved for ${url}`);
1232
+ logger?.debug(`Content retrieved for ${url} (${Buffer.byteLength(html, "utf-8")} bytes)`);
1207
1233
  const metadata = {
1208
1234
  input: url,
1209
1235
  outputSize: Buffer.byteLength(html, "utf-8"),
1210
1236
  assetCount: 0,
1211
- // Basic fetch doesn't track assets
1237
+ // Basic fetch doesn't track assets processed by *this* tool
1212
1238
  buildTimeMs: Date.now() - start,
1213
1239
  errors: []
1214
1240
  // No errors if we reached this point
1215
1241
  };
1216
1242
  await page.close();
1217
1243
  logger?.debug(`Page closed for ${url}`);
1244
+ await browser.close();
1218
1245
  logger?.debug(`Browser closed for ${url}`);
1219
1246
  browser = null;
1220
1247
  return { html, metadata };
1221
1248
  } catch (pageError) {
1222
1249
  logger?.error(`Error during page processing for ${url}: ${pageError.message}`);
1223
- try {
1224
- await page.close();
1225
- } catch (closeErr) {
1226
- throw closeErr;
1250
+ if (page && !page.isClosed()) {
1251
+ try {
1252
+ await page.close();
1253
+ logger?.debug(`Page closed after error for ${url}`);
1254
+ } catch (closeErr) {
1255
+ logger?.error(`Failed to close page after error for ${url}: ${closeErr.message}`);
1256
+ }
1227
1257
  }
1228
1258
  throw pageError;
1229
1259
  }
1230
1260
  } catch (launchError) {
1231
- logger?.error(`Critical error during browser launch or page creation for ${url}: ${launchError.message}`);
1261
+ logger?.error(`Critical error during browser launch or page setup for ${url}: ${launchError.message}`);
1232
1262
  if (browser) {
1233
1263
  try {
1234
1264
  await browser.close();
1265
+ logger?.debug("Browser closed after launch/setup error.");
1235
1266
  } catch (closeErr) {
1267
+ logger?.warn(`Failed to close browser after launch/setup error: ${closeErr.message}`);
1236
1268
  }
1269
+ browser = null;
1237
1270
  }
1238
1271
  throw launchError;
1239
1272
  } finally {
@@ -1246,99 +1279,123 @@ async function fetchAndPackWebPage(url, logger, timeout = 3e4) {
1246
1279
  }
1247
1280
  }
1248
1281
  }
1249
- async function crawlWebsite(startUrl, maxDepth, logger) {
1282
+ async function crawlWebsite(startUrl, options) {
1283
+ const {
1284
+ maxDepth = 1,
1285
+ timeout = DEFAULT_PAGE_TIMEOUT,
1286
+ // include = ['**'], // TODO: Implement glob filtering
1287
+ // exclude = [],
1288
+ userAgent,
1289
+ logger
1290
+ } = options;
1250
1291
  logger?.info(`Starting crawl for ${startUrl} with maxDepth ${maxDepth}`);
1251
1292
  if (maxDepth <= 0) {
1252
- logger?.info("maxDepth is 0 or negative, no pages will be crawled.");
1293
+ logger?.warn("maxDepth is 0 or negative, no pages will be crawled.");
1253
1294
  return [];
1254
1295
  }
1255
- const browser = await puppeteer.launch({ headless: true });
1296
+ let browser = null;
1256
1297
  const visited = /* @__PURE__ */ new Set();
1257
1298
  const results = [];
1258
1299
  const queue = [];
1259
1300
  let startOrigin;
1260
1301
  try {
1261
- startOrigin = new URL(startUrl).origin;
1262
- } catch (e) {
1263
- logger?.error(`Invalid start URL: ${startUrl}. ${e.message}`);
1264
- await browser.close();
1265
- return [];
1266
- }
1267
- let normalizedStartUrl;
1268
- try {
1269
- const parsedStartUrl = new URL(startUrl);
1270
- parsedStartUrl.hash = "";
1271
- normalizedStartUrl = parsedStartUrl.href;
1272
- } catch (e) {
1273
- logger?.error(`Invalid start URL: ${startUrl}. ${e.message}`);
1274
- await browser.close();
1275
- return [];
1276
- }
1277
- visited.add(normalizedStartUrl);
1278
- queue.push({ url: normalizedStartUrl, depth: 1 });
1279
- logger?.debug(`Queued initial URL: ${normalizedStartUrl} (depth 1)`);
1280
- while (queue.length > 0) {
1281
- const { url, depth } = queue.shift();
1282
- logger?.info(`Processing: ${url} (depth ${depth})`);
1283
- let page = null;
1284
1302
  try {
1285
- page = await browser.newPage();
1286
- await page.setViewport({ width: 1280, height: 800 });
1287
- await page.goto(url, { waitUntil: "networkidle2", timeout: 3e4 });
1288
- const html = await page.content();
1289
- results.push({ url, html });
1290
- logger?.debug(`Successfully fetched content for ${url}`);
1291
- if (depth < maxDepth) {
1292
- logger?.debug(`Discovering links on ${url} (current depth ${depth}, maxDepth ${maxDepth})`);
1293
- const hrefs = await page.evaluate(
1294
- () => Array.from(document.querySelectorAll("a[href]"), (a) => a.getAttribute("href"))
1295
- );
1296
- logger?.debug(`Found ${hrefs.length} potential hrefs on ${url}`);
1297
- let linksAdded = 0;
1298
- for (const href of hrefs) {
1299
- if (!href) continue;
1300
- let absoluteUrl;
1301
- try {
1302
- const resolved = new URL(href, url);
1303
- resolved.hash = "";
1304
- absoluteUrl = resolved.href;
1305
- } catch (e) {
1306
- logger?.debug(`Ignoring invalid URL syntax: "${href}" on page ${url}`);
1307
- continue;
1308
- }
1309
- if (absoluteUrl.startsWith(startOrigin) && !visited.has(absoluteUrl)) {
1310
- visited.add(absoluteUrl);
1311
- queue.push({ url: absoluteUrl, depth: depth + 1 });
1312
- linksAdded++;
1313
- } else {
1303
+ startOrigin = new URL(startUrl).origin;
1304
+ } catch (e) {
1305
+ logger?.error(`Invalid start URL: ${startUrl}. ${e.message}`);
1306
+ throw new Error(`Invalid start URL: ${startUrl}`);
1307
+ }
1308
+ let normalizedStartUrl;
1309
+ try {
1310
+ const parsedStartUrl = new URL(startUrl);
1311
+ parsedStartUrl.hash = "";
1312
+ normalizedStartUrl = parsedStartUrl.href;
1313
+ } catch (e) {
1314
+ logger?.error(`Invalid start URL: ${startUrl}. ${e.message}`);
1315
+ throw new Error(`Invalid start URL: ${startUrl}`);
1316
+ }
1317
+ logger?.debug("Launching browser for crawl...");
1318
+ browser = await puppeteer.launch(PUPPETEER_LAUNCH_OPTIONS);
1319
+ logger?.debug(`Browser launched for crawl (PID: ${browser.process()?.pid}).`);
1320
+ visited.add(normalizedStartUrl);
1321
+ queue.push({ url: normalizedStartUrl, depth: 1 });
1322
+ logger?.debug(`Queued initial URL: ${normalizedStartUrl} (depth 1)`);
1323
+ while (queue.length > 0) {
1324
+ const { url, depth } = queue.shift();
1325
+ logger?.info(`Processing: ${url} (depth ${depth})`);
1326
+ let page = null;
1327
+ try {
1328
+ page = await browser.newPage();
1329
+ if (userAgent) {
1330
+ await page.setUserAgent(userAgent);
1331
+ }
1332
+ await page.goto(url, { waitUntil: "networkidle2", timeout });
1333
+ const html = await page.content();
1334
+ results.push({ url, html });
1335
+ logger?.debug(`Successfully fetched content for ${url}`);
1336
+ if (depth < maxDepth) {
1337
+ logger?.debug(`Discovering links on ${url} (depth ${depth}/${maxDepth})`);
1338
+ const hrefs = await page.evaluate(
1339
+ () => Array.from(document.querySelectorAll("a[href]"), (a) => a.getAttribute("href"))
1340
+ );
1341
+ logger?.debug(`Found ${hrefs.length} potential hrefs on ${url}`);
1342
+ let linksAdded = 0;
1343
+ for (const href of hrefs) {
1344
+ if (!href) continue;
1345
+ let absoluteUrl;
1346
+ try {
1347
+ const resolved = new URL(href, url);
1348
+ resolved.hash = "";
1349
+ absoluteUrl = resolved.href;
1350
+ } catch (e) {
1351
+ logger?.debug(`Ignoring invalid URL syntax: "${href}" on page ${url}`);
1352
+ continue;
1353
+ }
1354
+ if (absoluteUrl.startsWith(startOrigin) && !visited.has(absoluteUrl)) {
1355
+ visited.add(absoluteUrl);
1356
+ queue.push({ url: absoluteUrl, depth: depth + 1 });
1357
+ linksAdded++;
1358
+ }
1314
1359
  }
1360
+ logger?.debug(`Added ${linksAdded} new unique internal links to queue from ${url}`);
1361
+ } else {
1362
+ logger?.debug(`Max depth (${maxDepth}) reached, not discovering links on ${url}`);
1315
1363
  }
1316
- logger?.debug(`Added ${linksAdded} new unique internal links to queue from ${url}`);
1317
- } else {
1318
- logger?.debug(`Max depth (${maxDepth}) reached, not discovering links on ${url}`);
1319
- }
1320
- } catch (err) {
1321
- logger?.warn(`\u274C Failed to process ${url}: ${err.message}`);
1322
- } finally {
1323
- if (page) {
1324
- try {
1325
- await page.close();
1326
- } catch (pageCloseError) {
1327
- logger?.error(`Failed to close page for ${url}: ${pageCloseError.message}`);
1364
+ } catch (err) {
1365
+ logger?.warn(`\u274C Failed to process ${url}: ${err.message}`);
1366
+ } finally {
1367
+ if (page && !page.isClosed()) {
1368
+ try {
1369
+ await page.close();
1370
+ } catch (pageCloseError) {
1371
+ logger?.error(`Failed to close page for ${url}: ${pageCloseError.message}`);
1372
+ }
1328
1373
  }
1329
1374
  }
1330
1375
  }
1376
+ } catch (error) {
1377
+ logger?.error(`Critical crawl error: ${error instanceof Error ? error.message : error}`);
1378
+ throw error;
1379
+ } finally {
1380
+ if (browser) {
1381
+ logger?.info(`Crawl finished or errored. Closing browser.`);
1382
+ await browser.close();
1383
+ logger?.debug(`Browser closed after crawl.`);
1384
+ }
1331
1385
  }
1332
- logger?.info(`Crawl finished. Closing browser.`);
1333
- await browser.close();
1334
- logger?.info(`Found ${results.length} pages.`);
1386
+ logger?.info(`Crawl found ${results.length} pages.`);
1335
1387
  return results;
1336
1388
  }
1337
- async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1) {
1338
- const logger = new Logger();
1389
+ async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1, loggerInstance) {
1390
+ const logger = loggerInstance || new Logger();
1339
1391
  logger.info(`Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`);
1340
1392
  try {
1341
- const pages = await crawlWebsite(startUrl, maxDepth, logger);
1393
+ const crawlOptions = {
1394
+ maxDepth,
1395
+ logger
1396
+ /* Add other options like timeout, userAgent if needed */
1397
+ };
1398
+ const pages = await crawlWebsite(startUrl, crawlOptions);
1342
1399
  if (pages.length === 0) {
1343
1400
  logger.warn("Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle.");
1344
1401
  } else {
@@ -1361,11 +1418,98 @@ async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1) {
1361
1418
  throw error;
1362
1419
  }
1363
1420
  }
1421
+ var puppeteer, fs2, PUPPETEER_LAUNCH_OPTIONS, DEFAULT_PAGE_TIMEOUT;
1364
1422
  var init_web_fetcher = __esm({
1365
1423
  "src/core/web-fetcher.ts"() {
1366
1424
  "use strict";
1425
+ puppeteer = __toESM(require("puppeteer"), 1);
1426
+ fs2 = __toESM(require("fs/promises"), 1);
1367
1427
  init_logger();
1368
1428
  init_bundler();
1429
+ PUPPETEER_LAUNCH_OPTIONS = {
1430
+ headless: true,
1431
+ args: [
1432
+ "--no-sandbox",
1433
+ // Often required in containerized environments
1434
+ "--disable-setuid-sandbox",
1435
+ "--disable-dev-shm-usage"
1436
+ // Recommended for Docker/CI
1437
+ ]
1438
+ };
1439
+ DEFAULT_PAGE_TIMEOUT = 3e4;
1440
+ }
1441
+ });
1442
+
1443
+ // src/core/parser.ts
1444
+ async function parseHTML(entryFilePath, logger) {
1445
+ logger?.debug(`Parsing HTML file: ${entryFilePath}`);
1446
+ let htmlContent;
1447
+ try {
1448
+ htmlContent = await (0, import_promises2.readFile)(entryFilePath, "utf-8");
1449
+ logger?.debug(`Successfully read HTML file (${Buffer.byteLength(htmlContent)} bytes).`);
1450
+ } catch (err) {
1451
+ logger?.error(`Failed to read HTML file "${entryFilePath}": ${err.message}`);
1452
+ throw new Error(`Could not read input HTML file: ${entryFilePath}`, { cause: err });
1453
+ }
1454
+ const $ = cheerio2.load(htmlContent);
1455
+ const assets = [];
1456
+ const addedUrls = /* @__PURE__ */ new Set();
1457
+ const addAsset = (url, forcedType) => {
1458
+ if (!url || url.trim() === "" || url.startsWith("data:")) {
1459
+ return;
1460
+ }
1461
+ if (!addedUrls.has(url)) {
1462
+ addedUrls.add(url);
1463
+ const mimeInfo = guessMimeType(url);
1464
+ const type = forcedType ?? mimeInfo.assetType;
1465
+ assets.push({ type, url });
1466
+ logger?.debug(`Discovered asset: Type='${type}', URL='${url}'`);
1467
+ } else {
1468
+ logger?.debug(`Skipping duplicate asset URL: ${url}`);
1469
+ }
1470
+ };
1471
+ logger?.debug("Extracting assets from HTML tags...");
1472
+ $('link[rel="stylesheet"][href]').each((_, el) => {
1473
+ addAsset($(el).attr("href"), "css");
1474
+ });
1475
+ $("script[src]").each((_, el) => {
1476
+ addAsset($(el).attr("src"), "js");
1477
+ });
1478
+ $("img[src]").each((_, el) => addAsset($(el).attr("src"), "image"));
1479
+ $('input[type="image"][src]').each((_, el) => addAsset($(el).attr("src"), "image"));
1480
+ $("img[srcset], picture source[srcset]").each((_, el) => {
1481
+ const srcset = $(el).attr("srcset");
1482
+ srcset?.split(",").forEach((entry) => {
1483
+ const [url] = entry.trim().split(/\s+/);
1484
+ addAsset(url, "image");
1485
+ });
1486
+ });
1487
+ $("video[src]").each((_, el) => addAsset($(el).attr("src"), "video"));
1488
+ $("video[poster]").each((_, el) => addAsset($(el).attr("poster"), "image"));
1489
+ $("audio[src]").each((_, el) => addAsset($(el).attr("src"), "audio"));
1490
+ $("video > source[src]").each((_, el) => addAsset($(el).attr("src"), "video"));
1491
+ $("audio > source[src]").each((_, el) => addAsset($(el).attr("src"), "audio"));
1492
+ $("link[href]").filter((_, el) => {
1493
+ const rel = $(el).attr("rel")?.toLowerCase() ?? "";
1494
+ return ["icon", "shortcut icon", "apple-touch-icon", "manifest"].includes(rel);
1495
+ }).each((_, el) => {
1496
+ const rel = $(el).attr("rel")?.toLowerCase() ?? "";
1497
+ const isIcon = ["icon", "shortcut icon", "apple-touch-icon"].includes(rel);
1498
+ addAsset($(el).attr("href"), isIcon ? "image" : void 0);
1499
+ });
1500
+ $('link[rel="preload"][as="font"][href]').each((_, el) => {
1501
+ addAsset($(el).attr("href"), "font");
1502
+ });
1503
+ logger?.info(`HTML parsing complete. Discovered ${assets.length} unique asset links.`);
1504
+ return { htmlContent, assets };
1505
+ }
1506
+ var import_promises2, cheerio2;
1507
+ var init_parser = __esm({
1508
+ "src/core/parser.ts"() {
1509
+ "use strict";
1510
+ import_promises2 = require("fs/promises");
1511
+ cheerio2 = __toESM(require("cheerio"), 1);
1512
+ init_mime();
1369
1513
  }
1370
1514
  });
1371
1515
 
@@ -1455,117 +1599,91 @@ var init_meta = __esm({
1455
1599
  });
1456
1600
 
1457
1601
  // src/index.ts
1602
+ async function pack(input, options = {}) {
1603
+ const logger = options.loggerInstance || new Logger(options.logLevel);
1604
+ const isHttp = /^https?:\/\//i.test(input);
1605
+ if (!isHttp && /:\/\//.test(input) && !input.startsWith("file://")) {
1606
+ const errorMsg = `Unsupported protocol or input type: ${input}`;
1607
+ logger.error(errorMsg);
1608
+ throw new Error(errorMsg);
1609
+ }
1610
+ const isRemote = /^https?:\/\//i.test(input);
1611
+ const recursive = options.recursive === true || typeof options.recursive === "number";
1612
+ if (isRemote && recursive) {
1613
+ const depth = typeof options.recursive === "number" ? options.recursive : 1;
1614
+ logger.info(`Starting recursive fetch for ${input} up to depth ${depth}`);
1615
+ return generateRecursivePortableHTML(input, depth, options, logger);
1616
+ }
1617
+ logger.info(`Starting single page processing for: ${input}`);
1618
+ return generatePortableHTML(input, options, logger);
1619
+ }
1458
1620
  async function generatePortableHTML(input, options = {}, loggerInstance) {
1459
1621
  const logger = loggerInstance || new Logger(options.logLevel);
1460
- logger.info(`Generating portable HTML for: ${input}`);
1461
1622
  const timer = new BuildTimer(input);
1462
- const isRemote = /^https?:\/\//i.test(input);
1463
- if (isRemote) {
1464
- logger.info(`Input is a remote URL. Fetching page content directly...`);
1623
+ if (/^https?:\/\//i.test(input)) {
1624
+ logger.info(`Workspaceing remote page: ${input}`);
1465
1625
  try {
1466
- const result = await fetchAndPackWebPage2(input, options, logger);
1467
- logger.info(`Remote fetch complete. Input: ${input}, Size: ${result.metadata.outputSize} bytes, Time: ${result.metadata.buildTimeMs}ms`);
1468
- return result;
1626
+ const result = await fetchAndPackWebPage(input, logger);
1627
+ const metadata = timer.finish(result.html, result.metadata);
1628
+ logger.info(`Finished fetching and packing remote page: ${input}`);
1629
+ return { html: result.html, metadata };
1469
1630
  } catch (error) {
1470
- logger.error(`Failed to fetch remote URL ${input}: ${error.message}`);
1631
+ logger.error(`Error fetching remote page ${input}: ${error.message}`);
1471
1632
  throw error;
1472
1633
  }
1473
1634
  }
1474
- logger.info(`Input is a local file path. Starting local processing pipeline...`);
1475
- const basePath = options.baseUrl || input;
1476
- logger.debug(`Using base path for asset resolution: ${basePath}`);
1635
+ logger.info(`Processing local file: ${input}`);
1477
1636
  try {
1637
+ const baseUrl = options.baseUrl || input;
1478
1638
  const parsed = await parseHTML(input, logger);
1479
- const enriched = await extractAssets(parsed, options.embedAssets ?? true, basePath, logger);
1639
+ const enriched = await extractAssets(parsed, options.embedAssets ?? true, baseUrl, logger);
1480
1640
  const minified = await minifyAssets(enriched, options, logger);
1481
1641
  const finalHtml = packHTML(minified, logger);
1482
1642
  const metadata = timer.finish(finalHtml, {
1483
1643
  assetCount: minified.assets.length
1484
- // FIX: Removed incorrect attempt to get errors from logger
1485
- // Errors collected by the timer itself (via timer.addError) will be included automatically.
1486
1644
  });
1487
- logger.info(`Local processing complete. Input: ${input}, Size: ${metadata.outputSize} bytes, Assets: ${metadata.assetCount}, Time: ${metadata.buildTimeMs}ms`);
1488
- if (metadata.errors && metadata.errors.length > 0) {
1489
- logger.warn(`Completed with ${metadata.errors.length} warning(s) logged in metadata.`);
1490
- }
1645
+ logger.info(`Finished processing local file: ${input}`);
1491
1646
  return { html: finalHtml, metadata };
1492
1647
  } catch (error) {
1493
- logger.error(`Error during local processing for ${input}: ${error.message}`);
1648
+ logger.error(`Error processing local file ${input}: ${error.message}`);
1494
1649
  throw error;
1495
1650
  }
1496
1651
  }
1497
1652
  async function generateRecursivePortableHTML(url, depth = 1, options = {}, loggerInstance) {
1498
1653
  const logger = loggerInstance || new Logger(options.logLevel);
1499
- logger.info(`Generating recursive portable HTML for: ${url}, Max Depth: ${depth}`);
1500
1654
  const timer = new BuildTimer(url);
1501
1655
  if (!/^https?:\/\//i.test(url)) {
1502
- const errMsg = `Invalid input URL for recursive bundling: ${url}. Must start with http(s)://`;
1503
- logger.error(errMsg);
1504
- throw new Error(errMsg);
1656
+ const errorMsg = `Invalid URL for recursive bundling. Must start with http:// or https://. Received: ${url}`;
1657
+ logger.error(errorMsg);
1658
+ throw new Error(errorMsg);
1505
1659
  }
1506
- const internalOutputPathPlaceholder = `${new URL(url).hostname}_recursive.html`;
1660
+ logger.info(`Starting recursive bundle for ${url} up to depth ${depth}`);
1507
1661
  try {
1508
- const { html, pages } = await recursivelyBundleSite(url, internalOutputPathPlaceholder, depth);
1509
- logger.info(`Recursive crawl complete. Discovered and bundled ${pages} pages.`);
1662
+ const { html, pages } = await recursivelyBundleSite(url, "output.html", depth, logger);
1510
1663
  timer.setPageCount(pages);
1511
1664
  const metadata = timer.finish(html, {
1512
1665
  assetCount: 0,
1513
- // NOTE: Asset count across multiple pages is not currently aggregated.
1514
1666
  pagesBundled: pages
1515
- // TODO: Potentially collect errors from the core function if it returns them
1516
1667
  });
1517
- logger.info(`Recursive bundling complete. Input: ${url}, Size: ${metadata.outputSize} bytes, Pages: ${metadata.pagesBundled}, Time: ${metadata.buildTimeMs}ms`);
1518
- if (metadata.errors && metadata.errors.length > 0) {
1519
- logger.warn(`Completed with ${metadata.errors.length} warning(s) logged in metadata.`);
1520
- }
1668
+ logger.info(`Finished recursive bundle for ${url}. Bundled ${pages} pages.`);
1521
1669
  return { html, metadata };
1522
1670
  } catch (error) {
1523
- logger.error(`Error during recursive generation for ${url}: ${error.message}`);
1524
- if (error.cause instanceof Error) {
1525
- logger.error(`Cause: ${error.cause.message}`);
1526
- }
1527
- throw error;
1528
- }
1529
- }
1530
- async function fetchAndPackWebPage2(url, options = {}, loggerInstance) {
1531
- const logger = loggerInstance || new Logger(options.logLevel);
1532
- logger.info(`Workspaceing single remote page: ${url}`);
1533
- const timer = new BuildTimer(url);
1534
- if (!/^https?:\/\//i.test(url)) {
1535
- const errMsg = `Invalid input URL for fetchAndPackWebPage: ${url}. Must start with http(s)://`;
1536
- logger.error(errMsg);
1537
- throw new Error(errMsg);
1538
- }
1539
- try {
1540
- const result = await fetchAndPackWebPage(url, logger);
1541
- const metadata = timer.finish(result.html, {
1542
- // Use assetCount and errors from core metadata if available
1543
- assetCount: result.metadata?.assetCount ?? 0,
1544
- errors: result.metadata?.errors ?? []
1545
- // Ensure errors array exists
1546
- });
1547
- logger.info(`Single page fetch complete. Input: ${url}, Size: ${metadata.outputSize} bytes, Assets: ${metadata.assetCount}, Time: ${metadata.buildTimeMs}ms`);
1548
- if (metadata.errors && metadata.errors.length > 0) {
1549
- logger.warn(`Completed with ${metadata.errors.length} warning(s) logged in metadata.`);
1550
- }
1551
- return { html: result.html, metadata };
1552
- } catch (error) {
1553
- logger.error(`Error during single page fetch for ${url}: ${error.message}`);
1671
+ logger.error(`Error during recursive bundle for ${url}: ${error.message}`);
1554
1672
  throw error;
1555
1673
  }
1556
1674
  }
1557
1675
  var init_src = __esm({
1558
1676
  "src/index.ts"() {
1559
1677
  "use strict";
1678
+ init_web_fetcher();
1560
1679
  init_parser();
1561
1680
  init_extractor();
1562
1681
  init_minifier();
1563
1682
  init_packer();
1564
- init_web_fetcher();
1565
1683
  init_bundler();
1684
+ init_logger();
1566
1685
  init_meta();
1567
1686
  init_logger();
1568
- init_types();
1569
1687
  }
1570
1688
  });
1571
1689
 
@@ -1575,20 +1693,15 @@ __export(cli_exports, {
1575
1693
  main: () => main,
1576
1694
  runCli: () => runCli
1577
1695
  });
1578
- import fs3 from "fs";
1579
- import path3 from "path";
1580
- import { fileURLToPath as fileURLToPath2 } from "url";
1581
1696
  function getPackageJson() {
1582
1697
  try {
1583
- const __filename = fileURLToPath2(import.meta.url);
1584
- const __dirname = path3.dirname(__filename);
1585
- const pkgPath = path3.resolve(__dirname, "../../package.json");
1586
- if (fs3.existsSync(pkgPath)) {
1587
- return JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1588
- }
1589
- } catch (_) {
1698
+ const searchPath = typeof __dirname !== "undefined" ? import_path3.default.join(__dirname, "..", "..") : process.cwd();
1699
+ const pkgJsonPath = require.resolve("portapack/package.json", { paths: [searchPath] });
1700
+ return require(pkgJsonPath);
1701
+ } catch (err) {
1702
+ console.error("Warning: Could not dynamically load package.json for version.", err);
1703
+ return { version: "0.0.0-unknown" };
1590
1704
  }
1591
- return { version: "0.1.0" };
1592
1705
  }
1593
1706
  async function runCli(argv = process.argv) {
1594
1707
  let stdout = "";
@@ -1597,6 +1710,11 @@ async function runCli(argv = process.argv) {
1597
1710
  const originalLog = console.log;
1598
1711
  const originalErr = console.error;
1599
1712
  const originalWarn = console.warn;
1713
+ const restoreConsole = () => {
1714
+ console.log = originalLog;
1715
+ console.error = originalErr;
1716
+ console.warn = originalWarn;
1717
+ };
1600
1718
  console.log = (...args) => {
1601
1719
  stdout += args.join(" ") + "\n";
1602
1720
  };
@@ -1606,40 +1724,38 @@ async function runCli(argv = process.argv) {
1606
1724
  console.warn = (...args) => {
1607
1725
  stderr += args.join(" ") + "\n";
1608
1726
  };
1609
- let opts;
1727
+ let cliOptions;
1610
1728
  try {
1611
- opts = parseOptions(argv);
1612
- const version = getPackageJson().version || "0.1.0";
1613
- if (opts.verbose) {
1729
+ cliOptions = parseOptions(argv);
1730
+ const version = getPackageJson().version || "0.0.0";
1731
+ if (cliOptions.verbose) {
1614
1732
  console.log(`\u{1F4E6} PortaPack v${version}`);
1615
1733
  }
1616
- if (!opts.input) {
1734
+ if (!cliOptions.input) {
1617
1735
  console.error("\u274C Missing input file or URL");
1618
- console.log = originalLog;
1619
- console.error = originalErr;
1620
- console.warn = originalWarn;
1736
+ restoreConsole();
1621
1737
  return { stdout, stderr, exitCode: 1 };
1622
1738
  }
1623
- const outputPath = opts.output ?? `${path3.basename(opts.input).split(".")[0] || "output"}.packed.html`;
1624
- if (opts.verbose) {
1625
- console.log(`\u{1F4E5} Input: ${opts.input}`);
1739
+ const inputBasename = import_path3.default.basename(cliOptions.input);
1740
+ const outputDefaultBase = inputBasename.includes(".") ? inputBasename.substring(0, inputBasename.lastIndexOf(".")) : inputBasename;
1741
+ const outputPath = cliOptions.output ?? `${outputDefaultBase || "output"}.packed.html`;
1742
+ if (cliOptions.verbose) {
1743
+ console.log(`\u{1F4E5} Input: ${cliOptions.input}`);
1626
1744
  console.log(`\u{1F4E4} Output: ${outputPath}`);
1627
- console.log(` Recursive: ${opts.recursive ?? false}`);
1628
- console.log(` Embed Assets: ${opts.embedAssets}`);
1629
- console.log(` Minify HTML: ${opts.minifyHtml}`);
1630
- console.log(` Minify CSS: ${opts.minifyCss}`);
1631
- console.log(` Minify JS: ${opts.minifyJs}`);
1632
- console.log(` Log Level: ${LogLevel[opts.logLevel ?? 3 /* INFO */]}`);
1745
+ console.log(` Recursive: ${cliOptions.recursive ?? false}`);
1746
+ console.log(` Embed Assets: ${cliOptions.embedAssets}`);
1747
+ console.log(` Minify HTML: ${cliOptions.minifyHtml}`);
1748
+ console.log(` Minify CSS: ${cliOptions.minifyCss}`);
1749
+ console.log(` Minify JS: ${cliOptions.minifyJs}`);
1750
+ console.log(` Log Level: ${cliOptions.logLevel}`);
1633
1751
  }
1634
- if (opts.dryRun) {
1752
+ if (cliOptions.dryRun) {
1635
1753
  console.log("\u{1F4A1} Dry run mode \u2014 no output will be written");
1636
- console.log = originalLog;
1637
- console.error = originalErr;
1638
- console.warn = originalWarn;
1754
+ restoreConsole();
1639
1755
  return { stdout, stderr, exitCode: 0 };
1640
1756
  }
1641
- const result = opts.recursive ? await generateRecursivePortableHTML(opts.input, typeof opts.recursive === "boolean" ? 1 : opts.recursive, opts) : await generatePortableHTML(opts.input, opts);
1642
- fs3.writeFileSync(outputPath, result.html, "utf-8");
1757
+ const result = await pack(cliOptions.input, cliOptions);
1758
+ import_fs.default.writeFileSync(outputPath, result.html, "utf-8");
1643
1759
  const meta = result.metadata;
1644
1760
  console.log(`\u2705 Packed: ${meta.input} \u2192 ${outputPath}`);
1645
1761
  console.log(`\u{1F4E6} Size: ${(meta.outputSize / 1024).toFixed(2)} KB`);
@@ -1648,7 +1764,7 @@ async function runCli(argv = process.argv) {
1648
1764
  if (meta.pagesBundled && meta.pagesBundled > 0) {
1649
1765
  console.log(`\u{1F9E9} Pages: ${meta.pagesBundled}`);
1650
1766
  }
1651
- if (meta.errors && meta.errors.length > 0) {
1767
+ if (meta.errors?.length) {
1652
1768
  console.warn(`
1653
1769
  \u26A0\uFE0F ${meta.errors.length} warning(s):`);
1654
1770
  for (const err of meta.errors) {
@@ -1658,37 +1774,49 @@ async function runCli(argv = process.argv) {
1658
1774
  } catch (err) {
1659
1775
  console.error(`
1660
1776
  \u{1F4A5} Error: ${err?.message || "Unknown failure"}`);
1661
- if (err?.stack && opts?.verbose) {
1777
+ if (err?.stack && cliOptions?.verbose) {
1662
1778
  console.error(err.stack);
1663
1779
  }
1664
1780
  exitCode = 1;
1665
1781
  } finally {
1666
- console.log = originalLog;
1667
- console.error = originalErr;
1668
- console.warn = originalWarn;
1782
+ restoreConsole();
1669
1783
  }
1670
1784
  return { stdout, stderr, exitCode };
1671
1785
  }
1672
- var main;
1786
+ var import_fs, import_path3, main;
1673
1787
  var init_cli = __esm({
1674
1788
  "src/cli/cli.ts"() {
1675
1789
  "use strict";
1790
+ import_fs = __toESM(require("fs"), 1);
1791
+ import_path3 = __toESM(require("path"), 1);
1676
1792
  init_options();
1677
1793
  init_src();
1678
- init_types();
1679
1794
  main = runCli;
1680
1795
  }
1681
1796
  });
1682
1797
 
1683
1798
  // src/cli/cli-entry.ts
1799
+ var cli_entry_exports = {};
1800
+ __export(cli_entry_exports, {
1801
+ startCLI: () => startCLI
1802
+ });
1803
+ module.exports = __toCommonJS(cli_entry_exports);
1684
1804
  var startCLI = async () => {
1685
1805
  const { main: main2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
1686
1806
  return await main2(process.argv);
1687
1807
  };
1688
- if (import.meta.url === `file://${process.argv[1]}`) {
1689
- startCLI().then(({ exitCode }) => process.exit(Number(exitCode)));
1808
+ if (require.main === module) {
1809
+ startCLI().then(({ stdout, stderr, exitCode }) => {
1810
+ if (stdout) process.stdout.write(stdout);
1811
+ if (stderr) process.stderr.write(stderr);
1812
+ process.exit(Number(exitCode));
1813
+ }).catch((err) => {
1814
+ console.error("\u{1F4A5} Unhandled CLI error:", err);
1815
+ process.exit(1);
1816
+ });
1690
1817
  }
1691
- export {
1818
+ // Annotate the CommonJS export names for ESM import in node:
1819
+ 0 && (module.exports = {
1692
1820
  startCLI
1693
- };
1694
- //# sourceMappingURL=cli-entry.js.map
1821
+ });
1822
+ //# sourceMappingURL=cli-entry.cjs.map