portapack 0.2.1 → 0.3.1

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");
@@ -275,7 +333,7 @@ function determineBaseUrl(inputPathOrUrl, logger) {
275
333
  }
276
334
  try {
277
335
  if (/^https?:\/\//i.test(inputPathOrUrl)) {
278
- const url = new URL2(inputPathOrUrl);
336
+ const url = new import_url.URL(inputPathOrUrl);
279
337
  url.pathname = url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1);
280
338
  url.search = "";
281
339
  url.hash = "";
@@ -286,44 +344,35 @@ function determineBaseUrl(inputPathOrUrl, logger) {
286
344
  logger?.warn(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
287
345
  return void 0;
288
346
  } else {
289
- let absolutePath;
347
+ let resourcePath;
348
+ let isInputLikelyDirectory = false;
290
349
  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
- }
350
+ resourcePath = (0, import_url.fileURLToPath)(inputPathOrUrl);
351
+ isInputLikelyDirectory = inputPathOrUrl.endsWith("/");
297
352
  } 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.`);
353
+ resourcePath = import_path2.default.resolve(inputPathOrUrl);
354
+ try {
355
+ isInputLikelyDirectory = fs.statSync(resourcePath).isDirectory();
356
+ } catch {
357
+ isInputLikelyDirectory = false;
308
358
  }
309
- isDirectory = false;
310
359
  }
311
- const dirPath = isDirectory ? absolutePath : path2.dirname(absolutePath);
312
- let normalizedPathForURL = dirPath.replace(/\\/g, "/");
360
+ const baseDirPath = isInputLikelyDirectory ? resourcePath : import_path2.default.dirname(resourcePath);
361
+ let normalizedPathForURL = baseDirPath.replace(/\\/g, "/");
313
362
  if (/^[A-Z]:\//i.test(normalizedPathForURL) && !normalizedPathForURL.startsWith("/")) {
314
363
  normalizedPathForURL = "/" + normalizedPathForURL;
315
364
  }
316
- const fileUrl = new URL2("file://" + normalizedPathForURL);
317
- let fileUrlString = fileUrl.href;
318
- if (!fileUrlString.endsWith("/")) {
319
- fileUrlString += "/";
365
+ if (!normalizedPathForURL.endsWith("/")) {
366
+ normalizedPathForURL += "/";
320
367
  }
321
- logger?.debug(`Determined local base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved dir: ${dirPath}, isDir: ${isDirectory})`);
368
+ const fileUrl = new import_url.URL("file://" + normalizedPathForURL);
369
+ const fileUrlString = fileUrl.href;
370
+ logger?.debug(`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`);
322
371
  return fileUrlString;
323
372
  }
324
373
  } catch (error) {
325
374
  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}` : ""}`);
375
+ logger?.error(`\u{1F480} Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ""}`);
327
376
  return void 0;
328
377
  }
329
378
  }
@@ -335,7 +384,7 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
335
384
  let resolvableUrl = trimmedUrl;
336
385
  if (resolvableUrl.startsWith("//") && baseContextUrl) {
337
386
  try {
338
- const base = new URL2(baseContextUrl);
387
+ const base = new import_url.URL(baseContextUrl);
339
388
  resolvableUrl = base.protocol + resolvableUrl;
340
389
  } catch (e) {
341
390
  logger?.warn(`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`);
@@ -343,7 +392,11 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
343
392
  }
344
393
  }
345
394
  try {
346
- const resolved = new URL2(resolvableUrl, baseContextUrl);
395
+ const resolved = new import_url.URL(resolvableUrl, baseContextUrl);
396
+ if (!["http:", "https:", "file:"].includes(resolved.protocol)) {
397
+ logger?.debug(`Skipping asset with unsupported protocol: ${resolved.href}`);
398
+ return null;
399
+ }
347
400
  return resolved;
348
401
  } catch (error) {
349
402
  const message = error instanceof Error ? error.message : String(error);
@@ -356,35 +409,15 @@ function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
356
409
  }
357
410
  }
358
411
  function resolveCssRelativeUrl(relativeUrl, cssBaseContextUrl, logger) {
359
- if (!relativeUrl || relativeUrl.startsWith("data:")) {
412
+ if (!relativeUrl || relativeUrl.startsWith("data:") || relativeUrl.startsWith("#")) {
360
413
  return null;
361
414
  }
362
415
  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
- }
416
+ const resolvedUrl = new import_url.URL(relativeUrl, cssBaseContextUrl);
417
+ return resolvedUrl.href;
385
418
  } catch (error) {
386
419
  logger?.warn(
387
- `Failed to resolve CSS URL: "${relativeUrl}" against "${cssBaseContextUrl}": ${String(error)}`
420
+ `Failed to resolve CSS URL: "${relativeUrl}" relative to "${cssBaseContextUrl}": ${String(error)}`
388
421
  );
389
422
  return null;
390
423
  }
@@ -394,21 +427,24 @@ async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
394
427
  const protocol = resolvedUrl.protocol;
395
428
  try {
396
429
  if (protocol === "http:" || protocol === "https:") {
397
- const response = await axios.default.get(resolvedUrl.href, {
430
+ const response = await axiosNs.default.get(resolvedUrl.href, {
398
431
  responseType: "arraybuffer",
432
+ // Fetch as binary data
399
433
  timeout
434
+ // Apply network timeout
400
435
  });
401
- logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data.byteLength} bytes)`);
436
+ logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data?.byteLength ?? 0} bytes)`);
402
437
  return Buffer.from(response.data);
403
438
  } else if (protocol === "file:") {
404
439
  let filePath;
405
440
  try {
406
- filePath = fileURLToPath(resolvedUrl);
441
+ filePath = (0, import_url.fileURLToPath)(resolvedUrl);
407
442
  } catch (e) {
408
443
  logger?.error(`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`);
409
444
  return null;
410
445
  }
411
- const data = await readFile2(filePath);
446
+ const normalizedForLog = import_path2.default.normalize(filePath);
447
+ const data = await (0, import_promises.readFile)(filePath);
412
448
  logger?.debug(`Read local file ${filePath} (${data.byteLength} bytes)`);
413
449
  return data;
414
450
  } else {
@@ -416,27 +452,26 @@ async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
416
452
  return null;
417
453
  }
418
454
  } catch (error) {
419
- if ((protocol === "http:" || protocol === "https:") && axios.default.isAxiosError(error)) {
420
- const status = error.response?.status ?? "N/A";
421
- const statusText = error.response?.statusText ?? "Error";
422
- const code = error.code ?? "N/A";
423
- const message = error.message;
424
- const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: Status ${status} - ${statusText}. Code: ${code}, Message: ${message}`;
455
+ const failedId = protocol === "file:" ? import_path2.default.normalize((0, import_url.fileURLToPath)(resolvedUrl)) : resolvedUrl.href;
456
+ if ((protocol === "http:" || protocol === "https:") && error?.isAxiosError === true) {
457
+ const axiosError = error;
458
+ const status = axiosError.response?.status ?? "N/A";
459
+ const code = axiosError.code ?? "N/A";
460
+ const logMessage = `\u26A0\uFE0F Failed to fetch remote asset ${resolvedUrl.href}: ${axiosError.message} (Code: ${code})`;
425
461
  logger?.warn(logMessage);
426
- } else if (protocol === "file:") {
462
+ } else if (protocol === "file:" && error instanceof Error) {
427
463
  let failedPath = resolvedUrl.href;
428
464
  try {
429
- failedPath = fileURLToPath(resolvedUrl);
465
+ failedPath = (0, import_url.fileURLToPath)(resolvedUrl);
430
466
  } catch {
431
467
  }
432
- if (error instanceof Error && error.code === "ENOENT") {
468
+ failedPath = import_path2.default.normalize(failedPath);
469
+ if (error.code === "ENOENT") {
433
470
  logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
434
- } else if (error instanceof Error && error.code === "EACCES") {
471
+ } else if (error.code === "EACCES") {
435
472
  logger?.warn(`\u26A0\uFE0F Permission denied (EACCES) reading asset: ${failedPath}.`);
436
- } else if (error instanceof Error) {
437
- logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
438
473
  } else {
439
- logger?.warn(`\u26A0\uFE0F An unknown error occurred while reading local asset ${failedPath}: ${String(error)}`);
474
+ logger?.warn(`\u26A0\uFE0F Failed to read local asset ${failedPath}: ${error.message}`);
440
475
  }
441
476
  } else if (error instanceof Error) {
442
477
  logger?.warn(`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`);
@@ -452,7 +487,7 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
452
487
  const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
453
488
  const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
454
489
  const processFoundUrl = (rawUrl, ruleType) => {
455
- if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:")) return;
490
+ if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:") || rawUrl.startsWith("#")) return;
456
491
  const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
457
492
  if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
458
493
  processedInThisParse.add(resolvedUrl);
@@ -460,14 +495,13 @@ function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
460
495
  newlyDiscovered.push({
461
496
  type: assetType,
462
497
  url: resolvedUrl,
463
- // The resolved URL string
498
+ // Store the resolved absolute URL string
464
499
  content: void 0
500
+ // Content will be fetched later if needed
465
501
  });
466
502
  logger?.debug(`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`);
467
503
  }
468
504
  };
469
- urlRegex.lastIndex = 0;
470
- importRegex.lastIndex = 0;
471
505
  let match;
472
506
  while ((match = urlRegex.exec(cssContent)) !== null) {
473
507
  processFoundUrl(match[2], "url()");
@@ -483,31 +517,35 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
483
517
  const initialAssets = parsed.assets || [];
484
518
  const finalAssetsMap = /* @__PURE__ */ new Map();
485
519
  let assetsToProcess = [];
520
+ const processedOrQueuedUrls = /* @__PURE__ */ new Set();
486
521
  const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || "", logger);
487
522
  if (!htmlBaseContextUrl && initialAssets.some((a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/"))) {
488
523
  logger?.warn("\u{1F6A8} No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail.");
489
524
  } else if (htmlBaseContextUrl) {
490
525
  logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
491
526
  }
492
- const processedOrQueuedUrls = /* @__PURE__ */ new Set();
493
527
  logger?.debug(`Queueing ${initialAssets.length} initial assets parsed from HTML...`);
494
528
  for (const asset of initialAssets) {
495
529
  const resolvedUrlObj = resolveAssetUrl(asset.url, htmlBaseContextUrl, logger);
496
- const urlToQueue = resolvedUrlObj ? resolvedUrlObj.href : asset.url;
497
- if (!urlToQueue.startsWith("data:") && !processedOrQueuedUrls.has(urlToQueue)) {
530
+ if (!resolvedUrlObj) {
531
+ logger?.debug(` -> Skipping initial asset with unresolvable/ignorable URL: ${asset.url}`);
532
+ continue;
533
+ }
534
+ const urlToQueue = resolvedUrlObj.href;
535
+ if (!processedOrQueuedUrls.has(urlToQueue)) {
498
536
  processedOrQueuedUrls.add(urlToQueue);
499
537
  const { assetType: guessedType } = guessMimeType(urlToQueue);
500
538
  const initialType = asset.type ?? guessedType;
501
539
  assetsToProcess.push({
502
540
  url: urlToQueue,
541
+ // Use the resolved URL
503
542
  type: initialType,
504
543
  content: void 0
544
+ // Content is initially undefined
505
545
  });
506
546
  logger?.debug(` -> Queued initial asset: ${urlToQueue} (Original raw: ${asset.url})`);
507
- } else if (urlToQueue.startsWith("data:")) {
508
- logger?.debug(` -> Skipping data URI: ${urlToQueue.substring(0, 50)}...`);
509
547
  } else {
510
- logger?.debug(` -> Skipping already queued initial asset: ${urlToQueue}`);
548
+ logger?.debug(` -> Skipping already processed/queued initial asset: ${urlToQueue}`);
511
549
  }
512
550
  }
513
551
  let iterationCount = 0;
@@ -540,7 +578,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
540
578
  let assetUrlObj = null;
541
579
  if (needsFetching) {
542
580
  try {
543
- assetUrlObj = new URL2(asset.url);
581
+ assetUrlObj = new import_url.URL(asset.url);
544
582
  } catch (urlError) {
545
583
  logger?.warn(`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
546
584
  finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
@@ -578,7 +616,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
578
616
  cssContentForParsing = textContent;
579
617
  }
580
618
  } else {
581
- logger?.warn(`Could not decode ${asset.type} ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`);
619
+ logger?.warn(`Could not decode ${asset.type} asset ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`);
582
620
  cssContentForParsing = void 0;
583
621
  if (embedAssets) {
584
622
  finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
@@ -625,6 +663,7 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
625
663
  const newlyDiscoveredAssets = extractUrlsFromCSS(
626
664
  cssContentForParsing,
627
665
  cssBaseContextUrl,
666
+ // Use the CSS file's own URL as the base
628
667
  logger
629
668
  );
630
669
  if (newlyDiscoveredAssets.length > 0) {
@@ -645,17 +684,22 @@ async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger)
645
684
  }
646
685
  }
647
686
  }
648
- const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? "MAX+" : iterationCount;
687
+ const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? `${MAX_ASSET_EXTRACTION_ITERATIONS}+ (limit hit)` : iterationCount;
649
688
  logger?.info(`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`);
650
689
  return {
651
690
  htmlContent: parsed.htmlContent,
652
691
  assets: Array.from(finalAssetsMap.values())
653
692
  };
654
693
  }
655
- var TEXT_ASSET_TYPES, BINARY_ASSET_TYPES, MAX_ASSET_EXTRACTION_ITERATIONS;
694
+ var import_promises, fs, import_path2, import_url, axiosNs, TEXT_ASSET_TYPES, BINARY_ASSET_TYPES, MAX_ASSET_EXTRACTION_ITERATIONS;
656
695
  var init_extractor = __esm({
657
696
  "src/core/extractor.ts"() {
658
697
  "use strict";
698
+ import_promises = require("fs/promises");
699
+ fs = __toESM(require("fs"), 1);
700
+ import_path2 = __toESM(require("path"), 1);
701
+ import_url = require("url");
702
+ axiosNs = __toESM(require("axios"), 1);
659
703
  init_mime();
660
704
  TEXT_ASSET_TYPES = /* @__PURE__ */ new Set(["css", "js"]);
661
705
  BINARY_ASSET_TYPES = /* @__PURE__ */ new Set(["image", "font", "video", "audio"]);
@@ -664,9 +708,6 @@ var init_extractor = __esm({
664
708
  });
665
709
 
666
710
  // 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
711
  async function minifyAssets(parsed, options = {}, logger) {
671
712
  const { htmlContent, assets } = parsed;
672
713
  const currentHtmlContent = htmlContent ?? "";
@@ -692,7 +733,7 @@ async function minifyAssets(parsed, options = {}, logger) {
692
733
  try {
693
734
  if (minifyFlags.minifyCss && processedAsset.type === "css") {
694
735
  logger?.debug(`Minifying CSS: ${assetIdentifier}`);
695
- const cssMinifier = new CleanCSS(CSS_MINIFY_OPTIONS);
736
+ const cssMinifier = new import_clean_css.default(CSS_MINIFY_OPTIONS);
696
737
  const result = cssMinifier.minify(processedAsset.content);
697
738
  if (result.errors && result.errors.length > 0) {
698
739
  logger?.warn(`\u26A0\uFE0F CleanCSS failed for ${assetIdentifier}: ${result.errors.join(", ")}`);
@@ -710,7 +751,7 @@ async function minifyAssets(parsed, options = {}, logger) {
710
751
  }
711
752
  if (minifyFlags.minifyJs && processedAsset.type === "js") {
712
753
  logger?.debug(`Minifying JS: ${assetIdentifier}`);
713
- const result = await jsMinify(processedAsset.content, JS_MINIFY_OPTIONS);
754
+ const result = await (0, import_terser.minify)(processedAsset.content, JS_MINIFY_OPTIONS);
714
755
  if (result.code) {
715
756
  newContent = result.code;
716
757
  logger?.debug(`JS minified successfully: ${assetIdentifier}`);
@@ -735,7 +776,7 @@ async function minifyAssets(parsed, options = {}, logger) {
735
776
  if (minifyFlags.minifyHtml && finalHtml.length > 0) {
736
777
  logger?.debug("Minifying HTML content...");
737
778
  try {
738
- finalHtml = await htmlMinify(finalHtml, {
779
+ finalHtml = await (0, import_html_minifier_terser.minify)(finalHtml, {
739
780
  ...HTML_MINIFY_OPTIONS,
740
781
  minifyCSS: minifyFlags.minifyCss,
741
782
  minifyJS: minifyFlags.minifyJs
@@ -754,10 +795,13 @@ async function minifyAssets(parsed, options = {}, logger) {
754
795
  // The array of processed asset copies
755
796
  };
756
797
  }
757
- var HTML_MINIFY_OPTIONS, CSS_MINIFY_OPTIONS, JS_MINIFY_OPTIONS;
798
+ var import_html_minifier_terser, import_clean_css, import_terser, HTML_MINIFY_OPTIONS, CSS_MINIFY_OPTIONS, JS_MINIFY_OPTIONS;
758
799
  var init_minifier = __esm({
759
800
  "src/core/minifier.ts"() {
760
801
  "use strict";
802
+ import_html_minifier_terser = require("html-minifier-terser");
803
+ import_clean_css = __toESM(require("clean-css"), 1);
804
+ import_terser = require("terser");
761
805
  HTML_MINIFY_OPTIONS = {
762
806
  collapseWhitespace: true,
763
807
  removeComments: true,
@@ -815,7 +859,6 @@ var init_minifier = __esm({
815
859
  });
816
860
 
817
861
  // src/core/packer.ts
818
- import * as cheerio2 from "cheerio";
819
862
  function escapeScriptContent(code) {
820
863
  return code.replace(/<\/(script)/gi, "<\\/$1");
821
864
  }
@@ -928,7 +971,7 @@ function packHTML(parsed, logger) {
928
971
  return '<!DOCTYPE html><html><head><base href="./"></head><body></body></html>';
929
972
  }
930
973
  logger?.debug("Loading HTML content into Cheerio for packing...");
931
- const $ = cheerio2.load(htmlContent);
974
+ const $ = cheerio.load(htmlContent);
932
975
  logger?.debug("Ensuring <base> tag exists...");
933
976
  ensureBaseTag($, logger);
934
977
  logger?.debug("Starting asset inlining...");
@@ -938,122 +981,11 @@ function packHTML(parsed, logger) {
938
981
  logger?.debug(`Packing complete. Final size: ${Buffer.byteLength(finalHtml)} bytes.`);
939
982
  return finalHtml;
940
983
  }
984
+ var cheerio;
941
985
  var init_packer = __esm({
942
986
  "src/core/packer.ts"() {
943
987
  "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
- };
988
+ cheerio = __toESM(require("cheerio"), 1);
1057
989
  }
1058
990
  });
1059
991
 
@@ -1093,9 +1025,11 @@ function bundleMultiPageHTML(pages, logger) {
1093
1025
  throw new Error(errorMsg);
1094
1026
  }
1095
1027
  logger?.info(`Bundling ${pages.length} pages into a multi-page HTML document.`);
1028
+ let pageIndex = 0;
1096
1029
  const validPages = pages.filter((page) => {
1097
1030
  const isValid = page && typeof page === "object" && typeof page.url === "string" && typeof page.html === "string";
1098
- if (!isValid) logger?.warn("Skipping invalid page entry");
1031
+ if (!isValid) logger?.warn(`Skipping invalid page entry at index ${pageIndex}`);
1032
+ pageIndex++;
1099
1033
  return isValid;
1100
1034
  });
1101
1035
  if (validPages.length === 0) {
@@ -1105,70 +1039,137 @@ function bundleMultiPageHTML(pages, logger) {
1105
1039
  }
1106
1040
  const slugMap = /* @__PURE__ */ new Map();
1107
1041
  const usedSlugs = /* @__PURE__ */ new Set();
1042
+ let firstValidSlug = void 0;
1043
+ let pageCounterForFallback = 1;
1108
1044
  for (const page of validPages) {
1109
- const baseSlug = sanitizeSlug(page.url);
1045
+ let baseSlug = sanitizeSlug(page.url);
1046
+ const isRootIndex = page.url === "/" || page.url === "index.html" || page.url.endsWith("/index.html");
1047
+ if (baseSlug === "index" && !isRootIndex) {
1048
+ logger?.debug(`URL "${page.url}" sanitized to "index", attempting to find alternative slug.`);
1049
+ const pathParts = page.url.replace(/\/$/, "").split("/").filter((p) => p && p.toLowerCase() !== "index.html" && p.toLowerCase() !== "index");
1050
+ if (pathParts.length > 0) {
1051
+ const lastPartSlug = sanitizeSlug(pathParts[pathParts.length - 1]);
1052
+ if (lastPartSlug && lastPartSlug !== "index") {
1053
+ baseSlug = lastPartSlug;
1054
+ logger?.debug(`Using last path part slug "${baseSlug}" instead.`);
1055
+ } else {
1056
+ baseSlug = "page";
1057
+ logger?.debug(`Last path part invalid ("${lastPartSlug}"), using fallback slug "page".`);
1058
+ }
1059
+ } else {
1060
+ baseSlug = "page";
1061
+ logger?.debug(`No valid path parts found, using fallback slug "page".`);
1062
+ }
1063
+ } else if (!baseSlug) {
1064
+ if (isRootIndex) {
1065
+ baseSlug = "index";
1066
+ logger?.debug(`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`);
1067
+ } else {
1068
+ baseSlug = "page";
1069
+ logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
1070
+ }
1071
+ }
1072
+ if (!baseSlug) {
1073
+ baseSlug = `page-${pageCounterForFallback++}`;
1074
+ logger?.warn(`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`);
1075
+ }
1110
1076
  let slug = baseSlug;
1111
- let counter = 1;
1077
+ let collisionCounter = 1;
1078
+ const originalBaseSlugForLog = baseSlug;
1112
1079
  while (usedSlugs.has(slug)) {
1113
- slug = `${baseSlug}-${counter++}`;
1114
- logger?.warn(`Slug collision detected for "${page.url}". Using "${slug}" instead.`);
1080
+ const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
1081
+ logger?.warn(`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`);
1082
+ slug = newSlug;
1115
1083
  }
1116
1084
  usedSlugs.add(slug);
1117
1085
  slugMap.set(page.url, slug);
1086
+ if (firstValidSlug === void 0) {
1087
+ firstValidSlug = slug;
1088
+ }
1118
1089
  }
1119
- const defaultPageSlug = slugMap.get(validPages[0].url);
1090
+ const defaultPageSlug = usedSlugs.has("index") ? "index" : firstValidSlug || "page";
1120
1091
  let output = `<!DOCTYPE html>
1121
1092
  <html lang="en">
1122
1093
  <head>
1123
1094
  <meta charset="UTF-8">
1124
1095
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1125
1096
  <title>Multi-Page Bundle</title>
1097
+ <style>
1098
+ body { font-family: sans-serif; margin: 0; }
1099
+ #main-nav { background-color: #f0f0f0; padding: 10px; border-bottom: 1px solid #ccc; }
1100
+ #main-nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
1101
+ #main-nav a.active { font-weight: bold; text-decoration: underline; }
1102
+ #page-container { padding: 20px; }
1103
+ template { display: none; }
1104
+ </style>
1126
1105
  </head>
1127
1106
  <body>
1128
1107
  <nav id="main-nav">
1129
1108
  ${validPages.map((p) => {
1130
1109
  const slug = slugMap.get(p.url);
1131
- const label = p.url.split("/").pop()?.split(".")[0] || "Page";
1110
+ const label = slug;
1132
1111
  return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
1133
- }).join("\n")}
1112
+ }).join("\n ")}
1134
1113
  </nav>
1135
1114
  <div id="page-container"></div>
1136
1115
  ${validPages.map((p) => {
1137
1116
  const slug = slugMap.get(p.url);
1138
1117
  return `<template id="page-${slug}">${p.html}</template>`;
1139
- }).join("\n")}
1118
+ }).join("\n ")}
1140
1119
  <script id="router-script">
1141
1120
  document.addEventListener('DOMContentLoaded', function() {
1121
+ const pageContainer = document.getElementById('page-container');
1122
+ const navLinks = document.querySelectorAll('#main-nav a');
1123
+
1142
1124
  function navigateTo(slug) {
1143
1125
  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');
1126
+ if (!template || !pageContainer) {
1127
+ console.warn('Navigation failed: Template or container not found for slug:', slug);
1128
+ // Maybe try navigating to default page? Or just clear container?
1129
+ if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
1130
+ return;
1131
+ }
1132
+ // Clear previous content and append new content
1133
+ pageContainer.innerHTML = ''; // Clear reliably
1134
+ pageContainer.appendChild(template.content.cloneNode(true));
1135
+
1136
+ // Update active link styling
1137
+ navLinks.forEach(link => {
1138
+ link.classList.toggle('active', link.getAttribute('data-page') === slug);
1151
1139
  });
1140
+
1141
+ // Update URL hash without triggering hashchange if already correct
1152
1142
  if (window.location.hash.substring(1) !== slug) {
1153
- history.pushState(null, '', '#' + slug);
1143
+ // Use pushState for cleaner history
1144
+ history.pushState({ slug: slug }, '', '#' + slug);
1154
1145
  }
1155
1146
  }
1156
1147
 
1157
- window.addEventListener('hashchange', () => {
1158
- const slug = window.location.hash.substring(1);
1159
- if (document.getElementById('page-' + slug)) navigateTo(slug);
1148
+ // Handle back/forward navigation
1149
+ window.addEventListener('popstate', (event) => {
1150
+ let slug = window.location.hash.substring(1);
1151
+ // If popstate event has state use it, otherwise fallback to hash or default
1152
+ if (event && event.state && event.state.slug) { // Check event exists
1153
+ slug = event.state.slug;
1154
+ }
1155
+ // Ensure the target page exists before navigating, fallback to default slug
1156
+ const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
1157
+ navigateTo(targetSlug);
1160
1158
  });
1161
1159
 
1162
- document.querySelectorAll('#main-nav a').forEach(link => {
1160
+ // Handle direct link clicks
1161
+ navLinks.forEach(link => {
1163
1162
  link.addEventListener('click', function(e) {
1164
1163
  e.preventDefault();
1165
1164
  const slug = this.getAttribute('data-page');
1166
- navigateTo(slug);
1165
+ if (slug) navigateTo(slug);
1167
1166
  });
1168
1167
  });
1169
1168
 
1170
- const initial = window.location.hash.substring(1);
1171
- navigateTo(document.getElementById('page-' + initial) ? initial : '${defaultPageSlug}');
1169
+ // Initial page load
1170
+ const initialHash = window.location.hash.substring(1);
1171
+ const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
1172
+ navigateTo(initialSlug);
1172
1173
  });
1173
1174
  </script>
1174
1175
  </body>
@@ -1182,58 +1183,69 @@ var init_bundler = __esm({
1182
1183
  init_extractor();
1183
1184
  init_minifier();
1184
1185
  init_packer();
1186
+ init_types();
1185
1187
  init_slugify();
1186
1188
  }
1187
1189
  });
1188
1190
 
1189
1191
  // 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) {
1192
+ async function fetchAndPackWebPage(url, logger, timeout = DEFAULT_PAGE_TIMEOUT, userAgent) {
1193
1193
  let browser = null;
1194
1194
  const start = Date.now();
1195
- logger?.debug(`Initiating fetch for single page: ${url}`);
1195
+ logger?.info(`Initiating fetch for single page: ${url}`);
1196
1196
  try {
1197
- browser = await puppeteer.launch({ headless: true });
1198
- logger?.debug(`Browser launched for ${url}`);
1197
+ logger?.debug("Launching browser...");
1198
+ browser = await puppeteer.launch(PUPPETEER_LAUNCH_OPTIONS);
1199
+ logger?.debug(`Browser launched successfully (PID: ${browser.process()?.pid}).`);
1199
1200
  const page = await browser.newPage();
1200
- logger?.debug(`Page created for ${url}`);
1201
+ logger?.debug(`New page created for ${url}`);
1202
+ if (userAgent) {
1203
+ await page.setUserAgent(userAgent);
1204
+ logger?.debug(`User-Agent set to: "${userAgent}"`);
1205
+ }
1201
1206
  try {
1202
1207
  logger?.debug(`Navigating to ${url} with timeout ${timeout}ms`);
1203
1208
  await page.goto(url, { waitUntil: "networkidle2", timeout });
1204
1209
  logger?.debug(`Navigation successful for ${url}`);
1205
1210
  const html = await page.content();
1206
- logger?.debug(`Content retrieved for ${url}`);
1211
+ logger?.debug(`Content retrieved for ${url} (${Buffer.byteLength(html, "utf-8")} bytes)`);
1207
1212
  const metadata = {
1208
1213
  input: url,
1209
1214
  outputSize: Buffer.byteLength(html, "utf-8"),
1210
1215
  assetCount: 0,
1211
- // Basic fetch doesn't track assets
1216
+ // Basic fetch doesn't track assets processed by *this* tool
1212
1217
  buildTimeMs: Date.now() - start,
1213
1218
  errors: []
1214
1219
  // No errors if we reached this point
1215
1220
  };
1216
1221
  await page.close();
1217
1222
  logger?.debug(`Page closed for ${url}`);
1223
+ await browser.close();
1218
1224
  logger?.debug(`Browser closed for ${url}`);
1219
1225
  browser = null;
1220
1226
  return { html, metadata };
1221
1227
  } catch (pageError) {
1222
1228
  logger?.error(`Error during page processing for ${url}: ${pageError.message}`);
1223
- try {
1224
- await page.close();
1225
- } catch (closeErr) {
1226
- throw closeErr;
1229
+ if (page && !page.isClosed()) {
1230
+ try {
1231
+ await page.close();
1232
+ logger?.debug(`Page closed after error for ${url}`);
1233
+ } catch (closeErr) {
1234
+ logger?.error(`Failed to close page after error for ${url}: ${closeErr.message}`);
1235
+ }
1227
1236
  }
1228
1237
  throw pageError;
1229
1238
  }
1230
1239
  } catch (launchError) {
1231
- logger?.error(`Critical error during browser launch or page creation for ${url}: ${launchError.message}`);
1240
+ logger?.error(`Critical error during browser launch or page setup for ${url}: ${launchError.message}`);
1232
1241
  if (browser) {
1233
1242
  try {
1234
1243
  await browser.close();
1244
+ logger?.debug("Browser closed after launch/setup error.");
1235
1245
  } catch (closeErr) {
1246
+ logger?.warn(`Failed to close browser after launch/setup error: ${closeErr.message}`);
1236
1247
  }
1248
+ browser = null;
1237
1249
  }
1238
1250
  throw launchError;
1239
1251
  } finally {
@@ -1246,99 +1258,123 @@ async function fetchAndPackWebPage(url, logger, timeout = 3e4) {
1246
1258
  }
1247
1259
  }
1248
1260
  }
1249
- async function crawlWebsite(startUrl, maxDepth, logger) {
1261
+ async function crawlWebsite(startUrl, options) {
1262
+ const {
1263
+ maxDepth = 1,
1264
+ timeout = DEFAULT_PAGE_TIMEOUT,
1265
+ // include = ['**'], // TODO: Implement glob filtering
1266
+ // exclude = [],
1267
+ userAgent,
1268
+ logger
1269
+ } = options;
1250
1270
  logger?.info(`Starting crawl for ${startUrl} with maxDepth ${maxDepth}`);
1251
1271
  if (maxDepth <= 0) {
1252
- logger?.info("maxDepth is 0 or negative, no pages will be crawled.");
1272
+ logger?.warn("maxDepth is 0 or negative, no pages will be crawled.");
1253
1273
  return [];
1254
1274
  }
1255
- const browser = await puppeteer.launch({ headless: true });
1275
+ let browser = null;
1256
1276
  const visited = /* @__PURE__ */ new Set();
1257
1277
  const results = [];
1258
1278
  const queue = [];
1259
1279
  let startOrigin;
1260
1280
  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
1281
  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 {
1282
+ startOrigin = new URL(startUrl).origin;
1283
+ } catch (e) {
1284
+ logger?.error(`Invalid start URL: ${startUrl}. ${e.message}`);
1285
+ throw new Error(`Invalid start URL: ${startUrl}`);
1286
+ }
1287
+ let normalizedStartUrl;
1288
+ try {
1289
+ const parsedStartUrl = new URL(startUrl);
1290
+ parsedStartUrl.hash = "";
1291
+ normalizedStartUrl = parsedStartUrl.href;
1292
+ } catch (e) {
1293
+ logger?.error(`Invalid start URL: ${startUrl}. ${e.message}`);
1294
+ throw new Error(`Invalid start URL: ${startUrl}`);
1295
+ }
1296
+ logger?.debug("Launching browser for crawl...");
1297
+ browser = await puppeteer.launch(PUPPETEER_LAUNCH_OPTIONS);
1298
+ logger?.debug(`Browser launched for crawl (PID: ${browser.process()?.pid}).`);
1299
+ visited.add(normalizedStartUrl);
1300
+ queue.push({ url: normalizedStartUrl, depth: 1 });
1301
+ logger?.debug(`Queued initial URL: ${normalizedStartUrl} (depth 1)`);
1302
+ while (queue.length > 0) {
1303
+ const { url, depth } = queue.shift();
1304
+ logger?.info(`Processing: ${url} (depth ${depth})`);
1305
+ let page = null;
1306
+ try {
1307
+ page = await browser.newPage();
1308
+ if (userAgent) {
1309
+ await page.setUserAgent(userAgent);
1310
+ }
1311
+ await page.goto(url, { waitUntil: "networkidle2", timeout });
1312
+ const html = await page.content();
1313
+ results.push({ url, html });
1314
+ logger?.debug(`Successfully fetched content for ${url}`);
1315
+ if (depth < maxDepth) {
1316
+ logger?.debug(`Discovering links on ${url} (depth ${depth}/${maxDepth})`);
1317
+ const hrefs = await page.evaluate(
1318
+ () => Array.from(document.querySelectorAll("a[href]"), (a) => a.getAttribute("href"))
1319
+ );
1320
+ logger?.debug(`Found ${hrefs.length} potential hrefs on ${url}`);
1321
+ let linksAdded = 0;
1322
+ for (const href of hrefs) {
1323
+ if (!href) continue;
1324
+ let absoluteUrl;
1325
+ try {
1326
+ const resolved = new URL(href, url);
1327
+ resolved.hash = "";
1328
+ absoluteUrl = resolved.href;
1329
+ } catch (e) {
1330
+ logger?.debug(`Ignoring invalid URL syntax: "${href}" on page ${url}`);
1331
+ continue;
1332
+ }
1333
+ if (absoluteUrl.startsWith(startOrigin) && !visited.has(absoluteUrl)) {
1334
+ visited.add(absoluteUrl);
1335
+ queue.push({ url: absoluteUrl, depth: depth + 1 });
1336
+ linksAdded++;
1337
+ }
1314
1338
  }
1339
+ logger?.debug(`Added ${linksAdded} new unique internal links to queue from ${url}`);
1340
+ } else {
1341
+ logger?.debug(`Max depth (${maxDepth}) reached, not discovering links on ${url}`);
1315
1342
  }
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}`);
1343
+ } catch (err) {
1344
+ logger?.warn(`\u274C Failed to process ${url}: ${err.message}`);
1345
+ } finally {
1346
+ if (page && !page.isClosed()) {
1347
+ try {
1348
+ await page.close();
1349
+ } catch (pageCloseError) {
1350
+ logger?.error(`Failed to close page for ${url}: ${pageCloseError.message}`);
1351
+ }
1328
1352
  }
1329
1353
  }
1330
1354
  }
1355
+ } catch (error) {
1356
+ logger?.error(`Critical crawl error: ${error instanceof Error ? error.message : error}`);
1357
+ throw error;
1358
+ } finally {
1359
+ if (browser) {
1360
+ logger?.info(`Crawl finished or errored. Closing browser.`);
1361
+ await browser.close();
1362
+ logger?.debug(`Browser closed after crawl.`);
1363
+ }
1331
1364
  }
1332
- logger?.info(`Crawl finished. Closing browser.`);
1333
- await browser.close();
1334
- logger?.info(`Found ${results.length} pages.`);
1365
+ logger?.info(`Crawl found ${results.length} pages.`);
1335
1366
  return results;
1336
1367
  }
1337
- async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1) {
1338
- const logger = new Logger();
1368
+ async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1, loggerInstance) {
1369
+ const logger = loggerInstance || new Logger();
1339
1370
  logger.info(`Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`);
1340
1371
  try {
1341
- const pages = await crawlWebsite(startUrl, maxDepth, logger);
1372
+ const crawlOptions = {
1373
+ maxDepth,
1374
+ logger
1375
+ /* Add other options like timeout, userAgent if needed */
1376
+ };
1377
+ const pages = await crawlWebsite(startUrl, crawlOptions);
1342
1378
  if (pages.length === 0) {
1343
1379
  logger.warn("Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle.");
1344
1380
  } else {
@@ -1361,11 +1397,98 @@ async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1) {
1361
1397
  throw error;
1362
1398
  }
1363
1399
  }
1400
+ var puppeteer, fs2, PUPPETEER_LAUNCH_OPTIONS, DEFAULT_PAGE_TIMEOUT;
1364
1401
  var init_web_fetcher = __esm({
1365
1402
  "src/core/web-fetcher.ts"() {
1366
1403
  "use strict";
1404
+ puppeteer = __toESM(require("puppeteer"), 1);
1405
+ fs2 = __toESM(require("fs/promises"), 1);
1367
1406
  init_logger();
1368
1407
  init_bundler();
1408
+ PUPPETEER_LAUNCH_OPTIONS = {
1409
+ headless: true,
1410
+ args: [
1411
+ "--no-sandbox",
1412
+ // Often required in containerized environments
1413
+ "--disable-setuid-sandbox",
1414
+ "--disable-dev-shm-usage"
1415
+ // Recommended for Docker/CI
1416
+ ]
1417
+ };
1418
+ DEFAULT_PAGE_TIMEOUT = 3e4;
1419
+ }
1420
+ });
1421
+
1422
+ // src/core/parser.ts
1423
+ async function parseHTML(entryFilePath, logger) {
1424
+ logger?.debug(`Parsing HTML file: ${entryFilePath}`);
1425
+ let htmlContent;
1426
+ try {
1427
+ htmlContent = await (0, import_promises2.readFile)(entryFilePath, "utf-8");
1428
+ logger?.debug(`Successfully read HTML file (${Buffer.byteLength(htmlContent)} bytes).`);
1429
+ } catch (err) {
1430
+ logger?.error(`Failed to read HTML file "${entryFilePath}": ${err.message}`);
1431
+ throw new Error(`Could not read input HTML file: ${entryFilePath}`, { cause: err });
1432
+ }
1433
+ const $ = cheerio2.load(htmlContent);
1434
+ const assets = [];
1435
+ const addedUrls = /* @__PURE__ */ new Set();
1436
+ const addAsset = (url, forcedType) => {
1437
+ if (!url || url.trim() === "" || url.startsWith("data:")) {
1438
+ return;
1439
+ }
1440
+ if (!addedUrls.has(url)) {
1441
+ addedUrls.add(url);
1442
+ const mimeInfo = guessMimeType(url);
1443
+ const type = forcedType ?? mimeInfo.assetType;
1444
+ assets.push({ type, url });
1445
+ logger?.debug(`Discovered asset: Type='${type}', URL='${url}'`);
1446
+ } else {
1447
+ logger?.debug(`Skipping duplicate asset URL: ${url}`);
1448
+ }
1449
+ };
1450
+ logger?.debug("Extracting assets from HTML tags...");
1451
+ $('link[rel="stylesheet"][href]').each((_, el) => {
1452
+ addAsset($(el).attr("href"), "css");
1453
+ });
1454
+ $("script[src]").each((_, el) => {
1455
+ addAsset($(el).attr("src"), "js");
1456
+ });
1457
+ $("img[src]").each((_, el) => addAsset($(el).attr("src"), "image"));
1458
+ $('input[type="image"][src]').each((_, el) => addAsset($(el).attr("src"), "image"));
1459
+ $("img[srcset], picture source[srcset]").each((_, el) => {
1460
+ const srcset = $(el).attr("srcset");
1461
+ srcset?.split(",").forEach((entry) => {
1462
+ const [url] = entry.trim().split(/\s+/);
1463
+ addAsset(url, "image");
1464
+ });
1465
+ });
1466
+ $("video[src]").each((_, el) => addAsset($(el).attr("src"), "video"));
1467
+ $("video[poster]").each((_, el) => addAsset($(el).attr("poster"), "image"));
1468
+ $("audio[src]").each((_, el) => addAsset($(el).attr("src"), "audio"));
1469
+ $("video > source[src]").each((_, el) => addAsset($(el).attr("src"), "video"));
1470
+ $("audio > source[src]").each((_, el) => addAsset($(el).attr("src"), "audio"));
1471
+ $("link[href]").filter((_, el) => {
1472
+ const rel = $(el).attr("rel")?.toLowerCase() ?? "";
1473
+ return ["icon", "shortcut icon", "apple-touch-icon", "manifest"].includes(rel);
1474
+ }).each((_, el) => {
1475
+ const rel = $(el).attr("rel")?.toLowerCase() ?? "";
1476
+ const isIcon = ["icon", "shortcut icon", "apple-touch-icon"].includes(rel);
1477
+ addAsset($(el).attr("href"), isIcon ? "image" : void 0);
1478
+ });
1479
+ $('link[rel="preload"][as="font"][href]').each((_, el) => {
1480
+ addAsset($(el).attr("href"), "font");
1481
+ });
1482
+ logger?.info(`HTML parsing complete. Discovered ${assets.length} unique asset links.`);
1483
+ return { htmlContent, assets };
1484
+ }
1485
+ var import_promises2, cheerio2;
1486
+ var init_parser = __esm({
1487
+ "src/core/parser.ts"() {
1488
+ "use strict";
1489
+ import_promises2 = require("fs/promises");
1490
+ cheerio2 = __toESM(require("cheerio"), 1);
1491
+ init_mime();
1369
1492
  }
1370
1493
  });
1371
1494
 
@@ -1455,117 +1578,91 @@ var init_meta = __esm({
1455
1578
  });
1456
1579
 
1457
1580
  // src/index.ts
1581
+ async function pack(input, options = {}) {
1582
+ const logger = options.loggerInstance || new Logger(options.logLevel);
1583
+ const isHttp = /^https?:\/\//i.test(input);
1584
+ if (!isHttp && /:\/\//.test(input) && !input.startsWith("file://")) {
1585
+ const errorMsg = `Unsupported protocol or input type: ${input}`;
1586
+ logger.error(errorMsg);
1587
+ throw new Error(errorMsg);
1588
+ }
1589
+ const isRemote = /^https?:\/\//i.test(input);
1590
+ const recursive = options.recursive === true || typeof options.recursive === "number";
1591
+ if (isRemote && recursive) {
1592
+ const depth = typeof options.recursive === "number" ? options.recursive : 1;
1593
+ logger.info(`Starting recursive fetch for ${input} up to depth ${depth}`);
1594
+ return generateRecursivePortableHTML(input, depth, options, logger);
1595
+ }
1596
+ logger.info(`Starting single page processing for: ${input}`);
1597
+ return generatePortableHTML(input, options, logger);
1598
+ }
1458
1599
  async function generatePortableHTML(input, options = {}, loggerInstance) {
1459
1600
  const logger = loggerInstance || new Logger(options.logLevel);
1460
- logger.info(`Generating portable HTML for: ${input}`);
1461
1601
  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...`);
1602
+ if (/^https?:\/\//i.test(input)) {
1603
+ logger.info(`Workspaceing remote page: ${input}`);
1465
1604
  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;
1605
+ const result = await fetchAndPackWebPage(input, logger);
1606
+ const metadata = timer.finish(result.html, result.metadata);
1607
+ logger.info(`Finished fetching and packing remote page: ${input}`);
1608
+ return { html: result.html, metadata };
1469
1609
  } catch (error) {
1470
- logger.error(`Failed to fetch remote URL ${input}: ${error.message}`);
1610
+ logger.error(`Error fetching remote page ${input}: ${error.message}`);
1471
1611
  throw error;
1472
1612
  }
1473
1613
  }
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}`);
1614
+ logger.info(`Processing local file: ${input}`);
1477
1615
  try {
1616
+ const baseUrl = options.baseUrl || input;
1478
1617
  const parsed = await parseHTML(input, logger);
1479
- const enriched = await extractAssets(parsed, options.embedAssets ?? true, basePath, logger);
1618
+ const enriched = await extractAssets(parsed, options.embedAssets ?? true, baseUrl, logger);
1480
1619
  const minified = await minifyAssets(enriched, options, logger);
1481
1620
  const finalHtml = packHTML(minified, logger);
1482
1621
  const metadata = timer.finish(finalHtml, {
1483
1622
  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
1623
  });
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
- }
1624
+ logger.info(`Finished processing local file: ${input}`);
1491
1625
  return { html: finalHtml, metadata };
1492
1626
  } catch (error) {
1493
- logger.error(`Error during local processing for ${input}: ${error.message}`);
1627
+ logger.error(`Error processing local file ${input}: ${error.message}`);
1494
1628
  throw error;
1495
1629
  }
1496
1630
  }
1497
1631
  async function generateRecursivePortableHTML(url, depth = 1, options = {}, loggerInstance) {
1498
1632
  const logger = loggerInstance || new Logger(options.logLevel);
1499
- logger.info(`Generating recursive portable HTML for: ${url}, Max Depth: ${depth}`);
1500
1633
  const timer = new BuildTimer(url);
1501
1634
  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);
1635
+ const errorMsg = `Invalid URL for recursive bundling. Must start with http:// or https://. Received: ${url}`;
1636
+ logger.error(errorMsg);
1637
+ throw new Error(errorMsg);
1505
1638
  }
1506
- const internalOutputPathPlaceholder = `${new URL(url).hostname}_recursive.html`;
1639
+ logger.info(`Starting recursive bundle for ${url} up to depth ${depth}`);
1507
1640
  try {
1508
- const { html, pages } = await recursivelyBundleSite(url, internalOutputPathPlaceholder, depth);
1509
- logger.info(`Recursive crawl complete. Discovered and bundled ${pages} pages.`);
1641
+ const { html, pages } = await recursivelyBundleSite(url, "output.html", depth, logger);
1510
1642
  timer.setPageCount(pages);
1511
1643
  const metadata = timer.finish(html, {
1512
1644
  assetCount: 0,
1513
- // NOTE: Asset count across multiple pages is not currently aggregated.
1514
1645
  pagesBundled: pages
1515
- // TODO: Potentially collect errors from the core function if it returns them
1516
1646
  });
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
- }
1647
+ logger.info(`Finished recursive bundle for ${url}. Bundled ${pages} pages.`);
1521
1648
  return { html, metadata };
1522
1649
  } 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}`);
1650
+ logger.error(`Error during recursive bundle for ${url}: ${error.message}`);
1554
1651
  throw error;
1555
1652
  }
1556
1653
  }
1557
1654
  var init_src = __esm({
1558
1655
  "src/index.ts"() {
1559
1656
  "use strict";
1657
+ init_web_fetcher();
1560
1658
  init_parser();
1561
1659
  init_extractor();
1562
1660
  init_minifier();
1563
1661
  init_packer();
1564
- init_web_fetcher();
1565
1662
  init_bundler();
1663
+ init_logger();
1566
1664
  init_meta();
1567
1665
  init_logger();
1568
- init_types();
1569
1666
  }
1570
1667
  });
1571
1668
 
@@ -1575,20 +1672,15 @@ __export(cli_exports, {
1575
1672
  main: () => main,
1576
1673
  runCli: () => runCli
1577
1674
  });
1578
- import fs3 from "fs";
1579
- import path3 from "path";
1580
- import { fileURLToPath as fileURLToPath2 } from "url";
1581
1675
  function getPackageJson() {
1582
1676
  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 (_) {
1677
+ const searchPath = typeof __dirname !== "undefined" ? import_path3.default.join(__dirname, "..", "..") : process.cwd();
1678
+ const pkgJsonPath = require.resolve("portapack/package.json", { paths: [searchPath] });
1679
+ return require(pkgJsonPath);
1680
+ } catch (err) {
1681
+ console.error("Warning: Could not dynamically load package.json for version.", err);
1682
+ return { version: "0.0.0-unknown" };
1590
1683
  }
1591
- return { version: "0.1.0" };
1592
1684
  }
1593
1685
  async function runCli(argv = process.argv) {
1594
1686
  let stdout = "";
@@ -1597,6 +1689,11 @@ async function runCli(argv = process.argv) {
1597
1689
  const originalLog = console.log;
1598
1690
  const originalErr = console.error;
1599
1691
  const originalWarn = console.warn;
1692
+ const restoreConsole = () => {
1693
+ console.log = originalLog;
1694
+ console.error = originalErr;
1695
+ console.warn = originalWarn;
1696
+ };
1600
1697
  console.log = (...args) => {
1601
1698
  stdout += args.join(" ") + "\n";
1602
1699
  };
@@ -1606,40 +1703,38 @@ async function runCli(argv = process.argv) {
1606
1703
  console.warn = (...args) => {
1607
1704
  stderr += args.join(" ") + "\n";
1608
1705
  };
1609
- let opts;
1706
+ let cliOptions;
1610
1707
  try {
1611
- opts = parseOptions(argv);
1612
- const version = getPackageJson().version || "0.1.0";
1613
- if (opts.verbose) {
1708
+ cliOptions = parseOptions(argv);
1709
+ const version = getPackageJson().version || "0.0.0";
1710
+ if (cliOptions.verbose) {
1614
1711
  console.log(`\u{1F4E6} PortaPack v${version}`);
1615
1712
  }
1616
- if (!opts.input) {
1713
+ if (!cliOptions.input) {
1617
1714
  console.error("\u274C Missing input file or URL");
1618
- console.log = originalLog;
1619
- console.error = originalErr;
1620
- console.warn = originalWarn;
1715
+ restoreConsole();
1621
1716
  return { stdout, stderr, exitCode: 1 };
1622
1717
  }
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}`);
1718
+ const inputBasename = import_path3.default.basename(cliOptions.input);
1719
+ const outputDefaultBase = inputBasename.includes(".") ? inputBasename.substring(0, inputBasename.lastIndexOf(".")) : inputBasename;
1720
+ const outputPath = cliOptions.output ?? `${outputDefaultBase || "output"}.packed.html`;
1721
+ if (cliOptions.verbose) {
1722
+ console.log(`\u{1F4E5} Input: ${cliOptions.input}`);
1626
1723
  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 */]}`);
1724
+ console.log(` Recursive: ${cliOptions.recursive ?? false}`);
1725
+ console.log(` Embed Assets: ${cliOptions.embedAssets}`);
1726
+ console.log(` Minify HTML: ${cliOptions.minifyHtml}`);
1727
+ console.log(` Minify CSS: ${cliOptions.minifyCss}`);
1728
+ console.log(` Minify JS: ${cliOptions.minifyJs}`);
1729
+ console.log(` Log Level: ${cliOptions.logLevel}`);
1633
1730
  }
1634
- if (opts.dryRun) {
1731
+ if (cliOptions.dryRun) {
1635
1732
  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;
1733
+ restoreConsole();
1639
1734
  return { stdout, stderr, exitCode: 0 };
1640
1735
  }
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");
1736
+ const result = await pack(cliOptions.input, cliOptions);
1737
+ import_fs.default.writeFileSync(outputPath, result.html, "utf-8");
1643
1738
  const meta = result.metadata;
1644
1739
  console.log(`\u2705 Packed: ${meta.input} \u2192 ${outputPath}`);
1645
1740
  console.log(`\u{1F4E6} Size: ${(meta.outputSize / 1024).toFixed(2)} KB`);
@@ -1648,7 +1743,7 @@ async function runCli(argv = process.argv) {
1648
1743
  if (meta.pagesBundled && meta.pagesBundled > 0) {
1649
1744
  console.log(`\u{1F9E9} Pages: ${meta.pagesBundled}`);
1650
1745
  }
1651
- if (meta.errors && meta.errors.length > 0) {
1746
+ if (meta.errors?.length) {
1652
1747
  console.warn(`
1653
1748
  \u26A0\uFE0F ${meta.errors.length} warning(s):`);
1654
1749
  for (const err of meta.errors) {
@@ -1658,37 +1753,49 @@ async function runCli(argv = process.argv) {
1658
1753
  } catch (err) {
1659
1754
  console.error(`
1660
1755
  \u{1F4A5} Error: ${err?.message || "Unknown failure"}`);
1661
- if (err?.stack && opts?.verbose) {
1756
+ if (err?.stack && cliOptions?.verbose) {
1662
1757
  console.error(err.stack);
1663
1758
  }
1664
1759
  exitCode = 1;
1665
1760
  } finally {
1666
- console.log = originalLog;
1667
- console.error = originalErr;
1668
- console.warn = originalWarn;
1761
+ restoreConsole();
1669
1762
  }
1670
1763
  return { stdout, stderr, exitCode };
1671
1764
  }
1672
- var main;
1765
+ var import_fs, import_path3, main;
1673
1766
  var init_cli = __esm({
1674
1767
  "src/cli/cli.ts"() {
1675
1768
  "use strict";
1769
+ import_fs = __toESM(require("fs"), 1);
1770
+ import_path3 = __toESM(require("path"), 1);
1676
1771
  init_options();
1677
1772
  init_src();
1678
- init_types();
1679
1773
  main = runCli;
1680
1774
  }
1681
1775
  });
1682
1776
 
1683
1777
  // src/cli/cli-entry.ts
1778
+ var cli_entry_exports = {};
1779
+ __export(cli_entry_exports, {
1780
+ startCLI: () => startCLI
1781
+ });
1782
+ module.exports = __toCommonJS(cli_entry_exports);
1684
1783
  var startCLI = async () => {
1685
1784
  const { main: main2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
1686
1785
  return await main2(process.argv);
1687
1786
  };
1688
- if (import.meta.url === `file://${process.argv[1]}`) {
1689
- startCLI().then(({ exitCode }) => process.exit(Number(exitCode)));
1787
+ if (require.main === module) {
1788
+ startCLI().then(({ stdout, stderr, exitCode }) => {
1789
+ if (stdout) process.stdout.write(stdout);
1790
+ if (stderr) process.stderr.write(stderr);
1791
+ process.exit(Number(exitCode));
1792
+ }).catch((err) => {
1793
+ console.error("\u{1F4A5} Unhandled CLI error:", err);
1794
+ process.exit(1);
1795
+ });
1690
1796
  }
1691
- export {
1797
+ // Annotate the CommonJS export names for ESM import in node:
1798
+ 0 && (module.exports = {
1692
1799
  startCLI
1693
- };
1694
- //# sourceMappingURL=cli-entry.js.map
1800
+ });
1801
+ //# sourceMappingURL=cli-entry.cjs.map