portapack 0.2.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.
- package/.eslintrc.json +9 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/deploy-pages.yml +56 -0
- package/.prettierrc +9 -0
- package/.releaserc.js +29 -0
- package/CHANGELOG.md +21 -0
- package/README.md +288 -0
- package/commitlint.config.js +36 -0
- package/dist/cli/cli-entry.js +1694 -0
- package/dist/cli/cli-entry.js.map +1 -0
- package/dist/index.d.ts +275 -0
- package/dist/index.js +1405 -0
- package/dist/index.js.map +1 -0
- package/docs/.vitepress/config.ts +89 -0
- package/docs/.vitepress/sidebar-generator.ts +73 -0
- package/docs/cli.md +117 -0
- package/docs/code-of-conduct.md +65 -0
- package/docs/configuration.md +151 -0
- package/docs/contributing.md +107 -0
- package/docs/demo.md +46 -0
- package/docs/deployment.md +132 -0
- package/docs/development.md +168 -0
- package/docs/getting-started.md +106 -0
- package/docs/index.md +40 -0
- package/docs/portapack-transparent.png +0 -0
- package/docs/portapack.jpg +0 -0
- package/docs/troubleshooting.md +107 -0
- package/examples/main.ts +118 -0
- package/examples/sample-project/index.html +12 -0
- package/examples/sample-project/logo.png +1 -0
- package/examples/sample-project/script.js +1 -0
- package/examples/sample-project/styles.css +1 -0
- package/jest.config.ts +124 -0
- package/jest.setup.cjs +211 -0
- package/nodemon.json +11 -0
- package/output.html +1 -0
- package/package.json +161 -0
- package/site-packed.html +1 -0
- package/src/cli/cli-entry.ts +28 -0
- package/src/cli/cli.ts +139 -0
- package/src/cli/options.ts +151 -0
- package/src/core/bundler.ts +201 -0
- package/src/core/extractor.ts +618 -0
- package/src/core/minifier.ts +233 -0
- package/src/core/packer.ts +191 -0
- package/src/core/parser.ts +115 -0
- package/src/core/web-fetcher.ts +292 -0
- package/src/index.ts +262 -0
- package/src/types.ts +163 -0
- package/src/utils/font.ts +41 -0
- package/src/utils/logger.ts +139 -0
- package/src/utils/meta.ts +100 -0
- package/src/utils/mime.ts +90 -0
- package/src/utils/slugify.ts +70 -0
- package/test-output.html +0 -0
- package/tests/__fixtures__/sample-project/index.html +5 -0
- package/tests/unit/cli/cli-entry.test.ts +104 -0
- package/tests/unit/cli/cli.test.ts +230 -0
- package/tests/unit/cli/options.test.ts +316 -0
- package/tests/unit/core/bundler.test.ts +287 -0
- package/tests/unit/core/extractor.test.ts +1129 -0
- package/tests/unit/core/minifier.test.ts +414 -0
- package/tests/unit/core/packer.test.ts +193 -0
- package/tests/unit/core/parser.test.ts +540 -0
- package/tests/unit/core/web-fetcher.test.ts +374 -0
- package/tests/unit/index.test.ts +339 -0
- package/tests/unit/utils/font.test.ts +81 -0
- package/tests/unit/utils/logger.test.ts +275 -0
- package/tests/unit/utils/meta.test.ts +70 -0
- package/tests/unit/utils/mime.test.ts +96 -0
- package/tests/unit/utils/slugify.test.ts +71 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.jest.json +17 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +71 -0
- package/typedoc.json +28 -0
@@ -0,0 +1,1694 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
var __defProp = Object.defineProperty;
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
4
|
+
var __esm = (fn, res) => function __init() {
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
6
|
+
};
|
7
|
+
var __export = (target, all) => {
|
8
|
+
for (var name in all)
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
10
|
+
};
|
11
|
+
|
12
|
+
// src/types.ts
|
13
|
+
var LogLevel;
|
14
|
+
var init_types = __esm({
|
15
|
+
"src/types.ts"() {
|
16
|
+
"use strict";
|
17
|
+
LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
18
|
+
LogLevel2[LogLevel2["NONE"] = 0] = "NONE";
|
19
|
+
LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
|
20
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
21
|
+
LogLevel2[LogLevel2["INFO"] = 3] = "INFO";
|
22
|
+
LogLevel2[LogLevel2["DEBUG"] = 4] = "DEBUG";
|
23
|
+
return LogLevel2;
|
24
|
+
})(LogLevel || {});
|
25
|
+
}
|
26
|
+
});
|
27
|
+
|
28
|
+
// src/cli/options.ts
|
29
|
+
import { Command, Option } from "commander";
|
30
|
+
function parseRecursiveValue(val) {
|
31
|
+
if (val === void 0) return true;
|
32
|
+
const parsed = parseInt(val, 10);
|
33
|
+
return isNaN(parsed) || parsed < 0 ? true : parsed;
|
34
|
+
}
|
35
|
+
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));
|
38
|
+
program.parse(argv);
|
39
|
+
const opts = program.opts();
|
40
|
+
const inputArg = program.args.length > 0 ? program.args[0] : void 0;
|
41
|
+
let finalLogLevel;
|
42
|
+
const cliLogLevel = opts.logLevel;
|
43
|
+
if (cliLogLevel) {
|
44
|
+
switch (cliLogLevel) {
|
45
|
+
case "debug":
|
46
|
+
finalLogLevel = 4 /* DEBUG */;
|
47
|
+
break;
|
48
|
+
case "info":
|
49
|
+
finalLogLevel = 3 /* INFO */;
|
50
|
+
break;
|
51
|
+
case "warn":
|
52
|
+
finalLogLevel = 2 /* WARN */;
|
53
|
+
break;
|
54
|
+
case "error":
|
55
|
+
finalLogLevel = 1 /* ERROR */;
|
56
|
+
break;
|
57
|
+
case "silent":
|
58
|
+
case "none":
|
59
|
+
finalLogLevel = 0 /* NONE */;
|
60
|
+
break;
|
61
|
+
default:
|
62
|
+
finalLogLevel = 3 /* INFO */;
|
63
|
+
}
|
64
|
+
} else if (opts.verbose) {
|
65
|
+
finalLogLevel = 4 /* DEBUG */;
|
66
|
+
} else {
|
67
|
+
finalLogLevel = 3 /* INFO */;
|
68
|
+
}
|
69
|
+
let embedAssets = true;
|
70
|
+
if (argv.includes("--no-embed-assets")) {
|
71
|
+
embedAssets = false;
|
72
|
+
} else if (opts.embedAssets === true) {
|
73
|
+
embedAssets = true;
|
74
|
+
}
|
75
|
+
let minifyHtml = opts.minifyHtml !== false;
|
76
|
+
let minifyCss = opts.minifyCss !== false;
|
77
|
+
let minifyJs = opts.minifyJs !== false;
|
78
|
+
if (opts.minify === false) {
|
79
|
+
minifyHtml = false;
|
80
|
+
minifyCss = false;
|
81
|
+
minifyJs = false;
|
82
|
+
}
|
83
|
+
let recursiveOpt = opts.recursive;
|
84
|
+
if (opts.maxDepth !== void 0 && !isNaN(opts.maxDepth) && opts.maxDepth >= 0) {
|
85
|
+
recursiveOpt = opts.maxDepth;
|
86
|
+
}
|
87
|
+
return {
|
88
|
+
// Pass through directly parsed options
|
89
|
+
baseUrl: opts.baseUrl,
|
90
|
+
dryRun: opts.dryRun ?? false,
|
91
|
+
// Ensure boolean, default false
|
92
|
+
output: opts.output,
|
93
|
+
verbose: opts.verbose ?? false,
|
94
|
+
// Ensure boolean, default false
|
95
|
+
// Set calculated/processed options
|
96
|
+
input: inputArg,
|
97
|
+
logLevel: finalLogLevel,
|
98
|
+
recursive: recursiveOpt,
|
99
|
+
// Final calculated value for recursion
|
100
|
+
embedAssets,
|
101
|
+
// Final calculated value
|
102
|
+
minifyHtml,
|
103
|
+
// Final calculated value
|
104
|
+
minifyCss,
|
105
|
+
// Final calculated value
|
106
|
+
minifyJs
|
107
|
+
// Final calculated value
|
108
|
+
// Exclude intermediate commander properties like:
|
109
|
+
// minify, logLevel (string version), maxDepth,
|
110
|
+
// minifyHtml, minifyCss, minifyJs (commander's raw boolean flags)
|
111
|
+
};
|
112
|
+
}
|
113
|
+
var logLevels;
|
114
|
+
var init_options = __esm({
|
115
|
+
"src/cli/options.ts"() {
|
116
|
+
"use strict";
|
117
|
+
init_types();
|
118
|
+
logLevels = ["debug", "info", "warn", "error", "silent", "none"];
|
119
|
+
}
|
120
|
+
});
|
121
|
+
|
122
|
+
// src/utils/mime.ts
|
123
|
+
import path from "path";
|
124
|
+
function guessMimeType(urlOrPath) {
|
125
|
+
if (!urlOrPath) {
|
126
|
+
return DEFAULT_MIME_TYPE;
|
127
|
+
}
|
128
|
+
let ext = "";
|
129
|
+
try {
|
130
|
+
const parsedUrl = new URL(urlOrPath);
|
131
|
+
ext = path.extname(parsedUrl.pathname).toLowerCase();
|
132
|
+
} catch {
|
133
|
+
ext = path.extname(urlOrPath).toLowerCase();
|
134
|
+
}
|
135
|
+
return MIME_MAP[ext] || DEFAULT_MIME_TYPE;
|
136
|
+
}
|
137
|
+
var MIME_MAP, DEFAULT_MIME_TYPE;
|
138
|
+
var init_mime = __esm({
|
139
|
+
"src/utils/mime.ts"() {
|
140
|
+
"use strict";
|
141
|
+
MIME_MAP = {
|
142
|
+
// CSS
|
143
|
+
".css": { mime: "text/css", assetType: "css" },
|
144
|
+
// JavaScript
|
145
|
+
".js": { mime: "application/javascript", assetType: "js" },
|
146
|
+
".mjs": { mime: "application/javascript", assetType: "js" },
|
147
|
+
// Images
|
148
|
+
".png": { mime: "image/png", assetType: "image" },
|
149
|
+
".jpg": { mime: "image/jpeg", assetType: "image" },
|
150
|
+
".jpeg": { mime: "image/jpeg", assetType: "image" },
|
151
|
+
".gif": { mime: "image/gif", assetType: "image" },
|
152
|
+
".svg": { mime: "image/svg+xml", assetType: "image" },
|
153
|
+
".webp": { mime: "image/webp", assetType: "image" },
|
154
|
+
".ico": { mime: "image/x-icon", assetType: "image" },
|
155
|
+
".avif": { mime: "image/avif", assetType: "image" },
|
156
|
+
// Fonts
|
157
|
+
".woff": { mime: "font/woff", assetType: "font" },
|
158
|
+
".woff2": { mime: "font/woff2", assetType: "font" },
|
159
|
+
".ttf": { mime: "font/ttf", assetType: "font" },
|
160
|
+
".otf": { mime: "font/otf", assetType: "font" },
|
161
|
+
".eot": { mime: "application/vnd.ms-fontobject", assetType: "font" },
|
162
|
+
// Audio/Video (add more as needed)
|
163
|
+
".mp3": { mime: "audio/mpeg", assetType: "other" },
|
164
|
+
".ogg": { mime: "audio/ogg", assetType: "other" },
|
165
|
+
".wav": { mime: "audio/wav", assetType: "other" },
|
166
|
+
".mp4": { mime: "video/mp4", assetType: "other" },
|
167
|
+
".webm": { mime: "video/webm", assetType: "other" },
|
168
|
+
// Other common web types
|
169
|
+
".json": { mime: "application/json", assetType: "other" },
|
170
|
+
".webmanifest": { mime: "application/manifest+json", assetType: "other" },
|
171
|
+
".xml": { mime: "application/xml", assetType: "other" },
|
172
|
+
".html": { mime: "text/html", assetType: "other" },
|
173
|
+
// Usually not needed as asset, but for completeness
|
174
|
+
".txt": { mime: "text/plain", assetType: "other" }
|
175
|
+
};
|
176
|
+
DEFAULT_MIME_TYPE = {
|
177
|
+
mime: "application/octet-stream",
|
178
|
+
assetType: "other"
|
179
|
+
// Explicit cast needed
|
180
|
+
};
|
181
|
+
}
|
182
|
+
});
|
183
|
+
|
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
|
+
// 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
|
+
function isUtf8DecodingLossy(originalBuffer, decodedString) {
|
263
|
+
try {
|
264
|
+
const reEncodedBuffer = Buffer.from(decodedString, "utf-8");
|
265
|
+
return !originalBuffer.equals(reEncodedBuffer);
|
266
|
+
} catch (e) {
|
267
|
+
return true;
|
268
|
+
}
|
269
|
+
}
|
270
|
+
function determineBaseUrl(inputPathOrUrl, logger) {
|
271
|
+
logger?.debug(`Determining base URL for input: ${inputPathOrUrl}`);
|
272
|
+
if (!inputPathOrUrl) {
|
273
|
+
logger?.warn("Cannot determine base URL: inputPathOrUrl is empty or invalid.");
|
274
|
+
return void 0;
|
275
|
+
}
|
276
|
+
try {
|
277
|
+
if (/^https?:\/\//i.test(inputPathOrUrl)) {
|
278
|
+
const url = new URL2(inputPathOrUrl);
|
279
|
+
url.pathname = url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1);
|
280
|
+
url.search = "";
|
281
|
+
url.hash = "";
|
282
|
+
const baseUrl = url.href;
|
283
|
+
logger?.debug(`Determined remote base URL: ${baseUrl}`);
|
284
|
+
return baseUrl;
|
285
|
+
} else if (inputPathOrUrl.includes("://") && !inputPathOrUrl.startsWith("file:")) {
|
286
|
+
logger?.warn(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
|
287
|
+
return void 0;
|
288
|
+
} else {
|
289
|
+
let absolutePath;
|
290
|
+
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
|
+
}
|
297
|
+
} 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.`);
|
308
|
+
}
|
309
|
+
isDirectory = false;
|
310
|
+
}
|
311
|
+
const dirPath = isDirectory ? absolutePath : path2.dirname(absolutePath);
|
312
|
+
let normalizedPathForURL = dirPath.replace(/\\/g, "/");
|
313
|
+
if (/^[A-Z]:\//i.test(normalizedPathForURL) && !normalizedPathForURL.startsWith("/")) {
|
314
|
+
normalizedPathForURL = "/" + normalizedPathForURL;
|
315
|
+
}
|
316
|
+
const fileUrl = new URL2("file://" + normalizedPathForURL);
|
317
|
+
let fileUrlString = fileUrl.href;
|
318
|
+
if (!fileUrlString.endsWith("/")) {
|
319
|
+
fileUrlString += "/";
|
320
|
+
}
|
321
|
+
logger?.debug(`Determined local base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved dir: ${dirPath}, isDir: ${isDirectory})`);
|
322
|
+
return fileUrlString;
|
323
|
+
}
|
324
|
+
} catch (error) {
|
325
|
+
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}` : ""}`);
|
327
|
+
return void 0;
|
328
|
+
}
|
329
|
+
}
|
330
|
+
function resolveAssetUrl(assetUrl, baseContextUrl, logger) {
|
331
|
+
const trimmedUrl = assetUrl?.trim();
|
332
|
+
if (!trimmedUrl || trimmedUrl.startsWith("data:") || trimmedUrl.startsWith("#")) {
|
333
|
+
return null;
|
334
|
+
}
|
335
|
+
let resolvableUrl = trimmedUrl;
|
336
|
+
if (resolvableUrl.startsWith("//") && baseContextUrl) {
|
337
|
+
try {
|
338
|
+
const base = new URL2(baseContextUrl);
|
339
|
+
resolvableUrl = base.protocol + resolvableUrl;
|
340
|
+
} catch (e) {
|
341
|
+
logger?.warn(`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`);
|
342
|
+
return null;
|
343
|
+
}
|
344
|
+
}
|
345
|
+
try {
|
346
|
+
const resolved = new URL2(resolvableUrl, baseContextUrl);
|
347
|
+
return resolved;
|
348
|
+
} catch (error) {
|
349
|
+
const message = error instanceof Error ? error.message : String(error);
|
350
|
+
if (!/^[a-z]+:/i.test(resolvableUrl) && !resolvableUrl.startsWith("/") && !baseContextUrl) {
|
351
|
+
logger?.warn(`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`);
|
352
|
+
} else {
|
353
|
+
logger?.warn(`\u26A0\uFE0F Failed to parse/resolve URL "${resolvableUrl}" ${baseContextUrl ? 'against base "' + baseContextUrl + '"' : "(no base provided)"}: ${message}`);
|
354
|
+
}
|
355
|
+
return null;
|
356
|
+
}
|
357
|
+
}
|
358
|
+
function resolveCssRelativeUrl(relativeUrl, cssBaseContextUrl, logger) {
|
359
|
+
if (!relativeUrl || relativeUrl.startsWith("data:")) {
|
360
|
+
return null;
|
361
|
+
}
|
362
|
+
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
|
+
}
|
385
|
+
} catch (error) {
|
386
|
+
logger?.warn(
|
387
|
+
`Failed to resolve CSS URL: "${relativeUrl}" against "${cssBaseContextUrl}": ${String(error)}`
|
388
|
+
);
|
389
|
+
return null;
|
390
|
+
}
|
391
|
+
}
|
392
|
+
async function fetchAsset(resolvedUrl, logger, timeout = 1e4) {
|
393
|
+
logger?.debug(`Attempting to fetch asset: ${resolvedUrl.href}`);
|
394
|
+
const protocol = resolvedUrl.protocol;
|
395
|
+
try {
|
396
|
+
if (protocol === "http:" || protocol === "https:") {
|
397
|
+
const response = await axios.default.get(resolvedUrl.href, {
|
398
|
+
responseType: "arraybuffer",
|
399
|
+
timeout
|
400
|
+
});
|
401
|
+
logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers["content-type"] || "N/A"}, Size: ${response.data.byteLength} bytes)`);
|
402
|
+
return Buffer.from(response.data);
|
403
|
+
} else if (protocol === "file:") {
|
404
|
+
let filePath;
|
405
|
+
try {
|
406
|
+
filePath = fileURLToPath(resolvedUrl);
|
407
|
+
} catch (e) {
|
408
|
+
logger?.error(`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`);
|
409
|
+
return null;
|
410
|
+
}
|
411
|
+
const data = await readFile2(filePath);
|
412
|
+
logger?.debug(`Read local file ${filePath} (${data.byteLength} bytes)`);
|
413
|
+
return data;
|
414
|
+
} else {
|
415
|
+
logger?.warn(`Unsupported protocol "${protocol}" in URL: ${resolvedUrl.href}`);
|
416
|
+
return null;
|
417
|
+
}
|
418
|
+
} 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}`;
|
425
|
+
logger?.warn(logMessage);
|
426
|
+
} else if (protocol === "file:") {
|
427
|
+
let failedPath = resolvedUrl.href;
|
428
|
+
try {
|
429
|
+
failedPath = fileURLToPath(resolvedUrl);
|
430
|
+
} catch {
|
431
|
+
}
|
432
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
433
|
+
logger?.warn(`\u26A0\uFE0F File not found (ENOENT) for asset: ${failedPath}.`);
|
434
|
+
} else if (error instanceof Error && error.code === "EACCES") {
|
435
|
+
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
|
+
} else {
|
439
|
+
logger?.warn(`\u26A0\uFE0F An unknown error occurred while reading local asset ${failedPath}: ${String(error)}`);
|
440
|
+
}
|
441
|
+
} else if (error instanceof Error) {
|
442
|
+
logger?.warn(`\u26A0\uFE0F An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`);
|
443
|
+
} else {
|
444
|
+
logger?.warn(`\u26A0\uFE0F An unknown and unexpected error occurred processing asset ${resolvedUrl.href}: ${String(error)}`);
|
445
|
+
}
|
446
|
+
return null;
|
447
|
+
}
|
448
|
+
}
|
449
|
+
function extractUrlsFromCSS(cssContent, cssBaseContextUrl, logger) {
|
450
|
+
const newlyDiscovered = [];
|
451
|
+
const processedInThisParse = /* @__PURE__ */ new Set();
|
452
|
+
const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
453
|
+
const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
|
454
|
+
const processFoundUrl = (rawUrl, ruleType) => {
|
455
|
+
if (!rawUrl || rawUrl.trim() === "" || rawUrl.startsWith("data:")) return;
|
456
|
+
const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
|
457
|
+
if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
|
458
|
+
processedInThisParse.add(resolvedUrl);
|
459
|
+
const { assetType } = guessMimeType(resolvedUrl);
|
460
|
+
newlyDiscovered.push({
|
461
|
+
type: assetType,
|
462
|
+
url: resolvedUrl,
|
463
|
+
// The resolved URL string
|
464
|
+
content: void 0
|
465
|
+
});
|
466
|
+
logger?.debug(`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`);
|
467
|
+
}
|
468
|
+
};
|
469
|
+
urlRegex.lastIndex = 0;
|
470
|
+
importRegex.lastIndex = 0;
|
471
|
+
let match;
|
472
|
+
while ((match = urlRegex.exec(cssContent)) !== null) {
|
473
|
+
processFoundUrl(match[2], "url()");
|
474
|
+
}
|
475
|
+
importRegex.lastIndex = 0;
|
476
|
+
while ((match = importRegex.exec(cssContent)) !== null) {
|
477
|
+
processFoundUrl(match[2] || match[4], "@import");
|
478
|
+
}
|
479
|
+
return newlyDiscovered;
|
480
|
+
}
|
481
|
+
async function extractAssets(parsed, embedAssets = true, inputPathOrUrl, logger) {
|
482
|
+
logger?.info(`\u{1F680} Starting asset extraction! Embed: ${embedAssets}. Input: ${inputPathOrUrl || "(HTML content only)"}`);
|
483
|
+
const initialAssets = parsed.assets || [];
|
484
|
+
const finalAssetsMap = /* @__PURE__ */ new Map();
|
485
|
+
let assetsToProcess = [];
|
486
|
+
const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || "", logger);
|
487
|
+
if (!htmlBaseContextUrl && initialAssets.some((a) => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith("data:") && !a.url.startsWith("#") && !a.url.startsWith("/"))) {
|
488
|
+
logger?.warn("\u{1F6A8} No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail.");
|
489
|
+
} else if (htmlBaseContextUrl) {
|
490
|
+
logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
|
491
|
+
}
|
492
|
+
const processedOrQueuedUrls = /* @__PURE__ */ new Set();
|
493
|
+
logger?.debug(`Queueing ${initialAssets.length} initial assets parsed from HTML...`);
|
494
|
+
for (const asset of initialAssets) {
|
495
|
+
const resolvedUrlObj = resolveAssetUrl(asset.url, htmlBaseContextUrl, logger);
|
496
|
+
const urlToQueue = resolvedUrlObj ? resolvedUrlObj.href : asset.url;
|
497
|
+
if (!urlToQueue.startsWith("data:") && !processedOrQueuedUrls.has(urlToQueue)) {
|
498
|
+
processedOrQueuedUrls.add(urlToQueue);
|
499
|
+
const { assetType: guessedType } = guessMimeType(urlToQueue);
|
500
|
+
const initialType = asset.type ?? guessedType;
|
501
|
+
assetsToProcess.push({
|
502
|
+
url: urlToQueue,
|
503
|
+
type: initialType,
|
504
|
+
content: void 0
|
505
|
+
});
|
506
|
+
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
|
+
} else {
|
510
|
+
logger?.debug(` -> Skipping already queued initial asset: ${urlToQueue}`);
|
511
|
+
}
|
512
|
+
}
|
513
|
+
let iterationCount = 0;
|
514
|
+
while (assetsToProcess.length > 0) {
|
515
|
+
iterationCount++;
|
516
|
+
if (iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS) {
|
517
|
+
logger?.error(`\u{1F6D1} Asset extraction loop limit hit (${MAX_ASSET_EXTRACTION_ITERATIONS})! Aborting.`);
|
518
|
+
const remainingUrls = assetsToProcess.map((a) => a.url).slice(0, 10).join(", ");
|
519
|
+
logger?.error(`Remaining queue sample (${assetsToProcess.length} items): ${remainingUrls}...`);
|
520
|
+
assetsToProcess.forEach((asset) => {
|
521
|
+
if (!finalAssetsMap.has(asset.url)) {
|
522
|
+
finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
|
523
|
+
}
|
524
|
+
});
|
525
|
+
assetsToProcess = [];
|
526
|
+
break;
|
527
|
+
}
|
528
|
+
const currentBatch = [...assetsToProcess];
|
529
|
+
assetsToProcess = [];
|
530
|
+
logger?.debug(`--- Processing batch ${iterationCount}: ${currentBatch.length} asset(s) ---`);
|
531
|
+
for (const asset of currentBatch) {
|
532
|
+
if (finalAssetsMap.has(asset.url)) {
|
533
|
+
logger?.debug(`Skipping asset already in final map: ${asset.url}`);
|
534
|
+
continue;
|
535
|
+
}
|
536
|
+
let assetContentBuffer = null;
|
537
|
+
let finalContent = void 0;
|
538
|
+
let cssContentForParsing = void 0;
|
539
|
+
const needsFetching = embedAssets || asset.type === "css";
|
540
|
+
let assetUrlObj = null;
|
541
|
+
if (needsFetching) {
|
542
|
+
try {
|
543
|
+
assetUrlObj = new URL2(asset.url);
|
544
|
+
} catch (urlError) {
|
545
|
+
logger?.warn(`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
|
546
|
+
finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
|
547
|
+
continue;
|
548
|
+
}
|
549
|
+
if (assetUrlObj) {
|
550
|
+
assetContentBuffer = await fetchAsset(assetUrlObj, logger);
|
551
|
+
}
|
552
|
+
}
|
553
|
+
if (needsFetching && assetContentBuffer === null) {
|
554
|
+
logger?.debug(`Storing asset ${asset.url} without content due to fetch failure.`);
|
555
|
+
finalAssetsMap.set(asset.url, { ...asset, content: void 0 });
|
556
|
+
continue;
|
557
|
+
}
|
558
|
+
if (assetContentBuffer) {
|
559
|
+
const mimeInfo = guessMimeType(asset.url);
|
560
|
+
const effectiveMime = mimeInfo.mime || "application/octet-stream";
|
561
|
+
if (TEXT_ASSET_TYPES.has(asset.type)) {
|
562
|
+
let textContent;
|
563
|
+
let wasLossy = false;
|
564
|
+
try {
|
565
|
+
textContent = assetContentBuffer.toString("utf-8");
|
566
|
+
wasLossy = isUtf8DecodingLossy(assetContentBuffer, textContent);
|
567
|
+
} catch (e) {
|
568
|
+
textContent = void 0;
|
569
|
+
wasLossy = true;
|
570
|
+
}
|
571
|
+
if (!wasLossy && textContent !== void 0) {
|
572
|
+
if (embedAssets) {
|
573
|
+
finalContent = textContent;
|
574
|
+
} else {
|
575
|
+
finalContent = void 0;
|
576
|
+
}
|
577
|
+
if (asset.type === "css") {
|
578
|
+
cssContentForParsing = textContent;
|
579
|
+
}
|
580
|
+
} else {
|
581
|
+
logger?.warn(`Could not decode ${asset.type} ${asset.url} as valid UTF-8 text.${embedAssets ? " Falling back to base64 data URI." : ""}`);
|
582
|
+
cssContentForParsing = void 0;
|
583
|
+
if (embedAssets) {
|
584
|
+
finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
|
585
|
+
} else {
|
586
|
+
finalContent = void 0;
|
587
|
+
}
|
588
|
+
}
|
589
|
+
} else if (BINARY_ASSET_TYPES.has(asset.type)) {
|
590
|
+
if (embedAssets) {
|
591
|
+
finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString("base64")}`;
|
592
|
+
} else {
|
593
|
+
finalContent = void 0;
|
594
|
+
}
|
595
|
+
cssContentForParsing = void 0;
|
596
|
+
} else {
|
597
|
+
cssContentForParsing = void 0;
|
598
|
+
if (embedAssets) {
|
599
|
+
try {
|
600
|
+
const attemptedTextContent = assetContentBuffer.toString("utf-8");
|
601
|
+
if (isUtf8DecodingLossy(assetContentBuffer, attemptedTextContent)) {
|
602
|
+
logger?.warn(`Couldn't embed unclassified asset ${asset.url} as text due to invalid UTF-8 sequences. Falling back to base64 (octet-stream).`);
|
603
|
+
finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
|
604
|
+
} else {
|
605
|
+
finalContent = attemptedTextContent;
|
606
|
+
logger?.debug(`Successfully embedded unclassified asset ${asset.url} as text.`);
|
607
|
+
}
|
608
|
+
} catch (decodeError) {
|
609
|
+
logger?.warn(`Error during text decoding for unclassified asset ${asset.url}: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}. Falling back to base64.`);
|
610
|
+
finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString("base64")}`;
|
611
|
+
}
|
612
|
+
} else {
|
613
|
+
finalContent = void 0;
|
614
|
+
}
|
615
|
+
}
|
616
|
+
} else {
|
617
|
+
finalContent = void 0;
|
618
|
+
cssContentForParsing = void 0;
|
619
|
+
}
|
620
|
+
finalAssetsMap.set(asset.url, { ...asset, url: asset.url, content: finalContent });
|
621
|
+
if (asset.type === "css" && cssContentForParsing) {
|
622
|
+
const cssBaseContextUrl = determineBaseUrl(asset.url, logger);
|
623
|
+
logger?.debug(`CSS base context for resolving nested assets within ${asset.url}: ${cssBaseContextUrl}`);
|
624
|
+
if (cssBaseContextUrl) {
|
625
|
+
const newlyDiscoveredAssets = extractUrlsFromCSS(
|
626
|
+
cssContentForParsing,
|
627
|
+
cssBaseContextUrl,
|
628
|
+
logger
|
629
|
+
);
|
630
|
+
if (newlyDiscoveredAssets.length > 0) {
|
631
|
+
logger?.debug(`Discovered ${newlyDiscoveredAssets.length} nested assets in CSS ${asset.url}. Checking against queue...`);
|
632
|
+
for (const newAsset of newlyDiscoveredAssets) {
|
633
|
+
if (!processedOrQueuedUrls.has(newAsset.url)) {
|
634
|
+
processedOrQueuedUrls.add(newAsset.url);
|
635
|
+
assetsToProcess.push(newAsset);
|
636
|
+
logger?.debug(` -> Queued new nested asset: ${newAsset.url}`);
|
637
|
+
} else {
|
638
|
+
logger?.debug(` -> Skipping already processed/queued nested asset: ${newAsset.url}`);
|
639
|
+
}
|
640
|
+
}
|
641
|
+
}
|
642
|
+
} else {
|
643
|
+
logger?.warn(`Could not determine base URL context for CSS file ${asset.url}. Cannot resolve nested relative paths within it.`);
|
644
|
+
}
|
645
|
+
}
|
646
|
+
}
|
647
|
+
}
|
648
|
+
const finalIterationCount = iterationCount > MAX_ASSET_EXTRACTION_ITERATIONS ? "MAX+" : iterationCount;
|
649
|
+
logger?.info(`\u2705 Asset extraction COMPLETE! Found ${finalAssetsMap.size} unique assets in ${finalIterationCount} iterations.`);
|
650
|
+
return {
|
651
|
+
htmlContent: parsed.htmlContent,
|
652
|
+
assets: Array.from(finalAssetsMap.values())
|
653
|
+
};
|
654
|
+
}
|
655
|
+
var TEXT_ASSET_TYPES, BINARY_ASSET_TYPES, MAX_ASSET_EXTRACTION_ITERATIONS;
|
656
|
+
var init_extractor = __esm({
|
657
|
+
"src/core/extractor.ts"() {
|
658
|
+
"use strict";
|
659
|
+
init_mime();
|
660
|
+
TEXT_ASSET_TYPES = /* @__PURE__ */ new Set(["css", "js"]);
|
661
|
+
BINARY_ASSET_TYPES = /* @__PURE__ */ new Set(["image", "font", "video", "audio"]);
|
662
|
+
MAX_ASSET_EXTRACTION_ITERATIONS = 1e3;
|
663
|
+
}
|
664
|
+
});
|
665
|
+
|
666
|
+
// 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
|
+
async function minifyAssets(parsed, options = {}, logger) {
|
671
|
+
const { htmlContent, assets } = parsed;
|
672
|
+
const currentHtmlContent = htmlContent ?? "";
|
673
|
+
const currentAssets = assets ?? [];
|
674
|
+
if (!currentHtmlContent && currentAssets.length === 0) {
|
675
|
+
logger?.debug("Minification skipped: No content.");
|
676
|
+
return { htmlContent: currentHtmlContent, assets: currentAssets };
|
677
|
+
}
|
678
|
+
const minifyFlags = {
|
679
|
+
minifyHtml: options.minifyHtml !== false,
|
680
|
+
minifyCss: options.minifyCss !== false,
|
681
|
+
minifyJs: options.minifyJs !== false
|
682
|
+
};
|
683
|
+
logger?.debug(`Minification flags: ${JSON.stringify(minifyFlags)}`);
|
684
|
+
const minifiedAssets = await Promise.all(
|
685
|
+
currentAssets.map(async (asset) => {
|
686
|
+
let processedAsset = { ...asset };
|
687
|
+
if (typeof processedAsset.content !== "string" || processedAsset.content.length === 0) {
|
688
|
+
return processedAsset;
|
689
|
+
}
|
690
|
+
let newContent = processedAsset.content;
|
691
|
+
const assetIdentifier = processedAsset.url || `inline ${processedAsset.type}`;
|
692
|
+
try {
|
693
|
+
if (minifyFlags.minifyCss && processedAsset.type === "css") {
|
694
|
+
logger?.debug(`Minifying CSS: ${assetIdentifier}`);
|
695
|
+
const cssMinifier = new CleanCSS(CSS_MINIFY_OPTIONS);
|
696
|
+
const result = cssMinifier.minify(processedAsset.content);
|
697
|
+
if (result.errors && result.errors.length > 0) {
|
698
|
+
logger?.warn(`\u26A0\uFE0F CleanCSS failed for ${assetIdentifier}: ${result.errors.join(", ")}`);
|
699
|
+
} else {
|
700
|
+
if (result.warnings && result.warnings.length > 0) {
|
701
|
+
logger?.debug(`CleanCSS warnings for ${assetIdentifier}: ${result.warnings.join(", ")}`);
|
702
|
+
}
|
703
|
+
if (result.styles) {
|
704
|
+
newContent = result.styles;
|
705
|
+
logger?.debug(`CSS minified successfully: ${assetIdentifier}`);
|
706
|
+
} else {
|
707
|
+
logger?.warn(`\u26A0\uFE0F CleanCSS produced no styles but reported no errors for ${assetIdentifier}. Keeping original.`);
|
708
|
+
}
|
709
|
+
}
|
710
|
+
}
|
711
|
+
if (minifyFlags.minifyJs && processedAsset.type === "js") {
|
712
|
+
logger?.debug(`Minifying JS: ${assetIdentifier}`);
|
713
|
+
const result = await jsMinify(processedAsset.content, JS_MINIFY_OPTIONS);
|
714
|
+
if (result.code) {
|
715
|
+
newContent = result.code;
|
716
|
+
logger?.debug(`JS minified successfully: ${assetIdentifier}`);
|
717
|
+
} else {
|
718
|
+
const terserError = result.error;
|
719
|
+
if (terserError) {
|
720
|
+
logger?.warn(`\u26A0\uFE0F Terser failed for ${assetIdentifier}: ${terserError.message || terserError}`);
|
721
|
+
} else {
|
722
|
+
logger?.warn(`\u26A0\uFE0F Terser produced no code but reported no errors for ${assetIdentifier}. Keeping original.`);
|
723
|
+
}
|
724
|
+
}
|
725
|
+
}
|
726
|
+
} catch (err) {
|
727
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
728
|
+
logger?.warn(`\u26A0\uFE0F Failed to minify asset ${assetIdentifier} (${processedAsset.type}): ${errorMessage}`);
|
729
|
+
}
|
730
|
+
processedAsset.content = newContent;
|
731
|
+
return processedAsset;
|
732
|
+
})
|
733
|
+
);
|
734
|
+
let finalHtml = currentHtmlContent;
|
735
|
+
if (minifyFlags.minifyHtml && finalHtml.length > 0) {
|
736
|
+
logger?.debug("Minifying HTML content...");
|
737
|
+
try {
|
738
|
+
finalHtml = await htmlMinify(finalHtml, {
|
739
|
+
...HTML_MINIFY_OPTIONS,
|
740
|
+
minifyCSS: minifyFlags.minifyCss,
|
741
|
+
minifyJS: minifyFlags.minifyJs
|
742
|
+
});
|
743
|
+
logger?.debug("HTML minified successfully.");
|
744
|
+
} catch (err) {
|
745
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
746
|
+
logger?.warn(`\u26A0\uFE0F HTML minification failed: ${errorMessage}`);
|
747
|
+
}
|
748
|
+
} else if (finalHtml.length > 0) {
|
749
|
+
logger?.debug("HTML minification skipped (disabled).");
|
750
|
+
}
|
751
|
+
return {
|
752
|
+
htmlContent: finalHtml,
|
753
|
+
assets: minifiedAssets
|
754
|
+
// The array of processed asset copies
|
755
|
+
};
|
756
|
+
}
|
757
|
+
var HTML_MINIFY_OPTIONS, CSS_MINIFY_OPTIONS, JS_MINIFY_OPTIONS;
|
758
|
+
var init_minifier = __esm({
|
759
|
+
"src/core/minifier.ts"() {
|
760
|
+
"use strict";
|
761
|
+
HTML_MINIFY_OPTIONS = {
|
762
|
+
collapseWhitespace: true,
|
763
|
+
removeComments: true,
|
764
|
+
conservativeCollapse: true,
|
765
|
+
minifyCSS: false,
|
766
|
+
// Handled separately
|
767
|
+
minifyJS: false,
|
768
|
+
// Handled separately
|
769
|
+
removeAttributeQuotes: false,
|
770
|
+
removeRedundantAttributes: true,
|
771
|
+
removeScriptTypeAttributes: true,
|
772
|
+
removeStyleLinkTypeAttributes: true,
|
773
|
+
useShortDoctype: true
|
774
|
+
};
|
775
|
+
CSS_MINIFY_OPTIONS = {
|
776
|
+
returnPromise: false,
|
777
|
+
// <<< *** Ensures sync operation at runtime ***
|
778
|
+
level: {
|
779
|
+
1: {
|
780
|
+
// Level 1 optimizations (safe transformations)
|
781
|
+
optimizeBackground: true,
|
782
|
+
optimizeBorderRadius: true,
|
783
|
+
optimizeFilter: true,
|
784
|
+
optimizeFontWeight: true,
|
785
|
+
optimizeOutline: true
|
786
|
+
},
|
787
|
+
2: {
|
788
|
+
// Level 2 optimizations (structural changes, generally safe)
|
789
|
+
mergeMedia: true,
|
790
|
+
mergeNonAdjacentRules: true,
|
791
|
+
removeDuplicateFontRules: true,
|
792
|
+
removeDuplicateMediaBlocks: true,
|
793
|
+
removeDuplicateRules: true,
|
794
|
+
restructureRules: true
|
795
|
+
}
|
796
|
+
}
|
797
|
+
// Note: Type checking based on these options seems problematic with current @types/clean-css
|
798
|
+
};
|
799
|
+
JS_MINIFY_OPTIONS = {
|
800
|
+
compress: {
|
801
|
+
dead_code: true,
|
802
|
+
drop_console: false,
|
803
|
+
drop_debugger: true,
|
804
|
+
ecma: 2020,
|
805
|
+
keep_classnames: true,
|
806
|
+
keep_fnames: true
|
807
|
+
},
|
808
|
+
mangle: {
|
809
|
+
keep_classnames: true,
|
810
|
+
keep_fnames: true
|
811
|
+
},
|
812
|
+
format: { comments: false }
|
813
|
+
};
|
814
|
+
}
|
815
|
+
});
|
816
|
+
|
817
|
+
// src/core/packer.ts
|
818
|
+
import * as cheerio2 from "cheerio";
|
819
|
+
function escapeScriptContent(code) {
|
820
|
+
return code.replace(/<\/(script)/gi, "<\\/$1");
|
821
|
+
}
|
822
|
+
function ensureBaseTag($, logger) {
|
823
|
+
let head = $("head");
|
824
|
+
if (head.length === 0) {
|
825
|
+
logger?.debug("No <head> tag found. Creating <head> and ensuring <html> exists.");
|
826
|
+
let htmlElement = $("html");
|
827
|
+
if (htmlElement.length === 0) {
|
828
|
+
logger?.debug("No <html> tag found. Wrapping content in <html><body>...");
|
829
|
+
const bodyContent = $.root().html() || "";
|
830
|
+
$.root().empty();
|
831
|
+
htmlElement = $("<html>").appendTo($.root());
|
832
|
+
head = $("<head>").appendTo(htmlElement);
|
833
|
+
$("<body>").html(bodyContent).appendTo(htmlElement);
|
834
|
+
} else {
|
835
|
+
head = $("<head>").prependTo(htmlElement);
|
836
|
+
}
|
837
|
+
}
|
838
|
+
if (head && head.length > 0 && head.find("base[href]").length === 0) {
|
839
|
+
logger?.debug('Prepending <base href="./"> to <head>.');
|
840
|
+
head.prepend('<base href="./">');
|
841
|
+
}
|
842
|
+
}
|
843
|
+
function inlineAssets($, assets, logger) {
|
844
|
+
logger?.debug(`Inlining ${assets.filter((a) => a.content).length} assets with content...`);
|
845
|
+
const assetMap = new Map(assets.map((asset) => [asset.url, asset]));
|
846
|
+
$('link[rel="stylesheet"][href]').each((_, el) => {
|
847
|
+
const link = $(el);
|
848
|
+
const href = link.attr("href");
|
849
|
+
const asset = href ? assetMap.get(href) : void 0;
|
850
|
+
if (asset?.content && typeof asset.content === "string") {
|
851
|
+
if (asset.content.startsWith("data:")) {
|
852
|
+
logger?.debug(`Replacing link with style tag using existing data URI: ${asset.url}`);
|
853
|
+
const styleTag = $("<style>").text(`@import url("${asset.content}");`);
|
854
|
+
link.replaceWith(styleTag);
|
855
|
+
} else {
|
856
|
+
logger?.debug(`Inlining CSS: ${asset.url}`);
|
857
|
+
const styleTag = $("<style>").text(asset.content);
|
858
|
+
link.replaceWith(styleTag);
|
859
|
+
}
|
860
|
+
} else if (href) {
|
861
|
+
logger?.warn(`Could not inline CSS: ${href}. Content missing or invalid.`);
|
862
|
+
}
|
863
|
+
});
|
864
|
+
$("script[src]").each((_, el) => {
|
865
|
+
const script = $(el);
|
866
|
+
const src = script.attr("src");
|
867
|
+
const asset = src ? assetMap.get(src) : void 0;
|
868
|
+
if (asset?.content && typeof asset.content === "string") {
|
869
|
+
logger?.debug(`Inlining JS: ${asset.url}`);
|
870
|
+
const inlineScript = $("<script>");
|
871
|
+
inlineScript.text(escapeScriptContent(asset.content));
|
872
|
+
Object.entries(script.attr() || {}).forEach(([key, value]) => {
|
873
|
+
if (key.toLowerCase() !== "src") inlineScript.attr(key, value);
|
874
|
+
});
|
875
|
+
script.replaceWith(inlineScript);
|
876
|
+
} else if (src) {
|
877
|
+
logger?.warn(`Could not inline JS: ${src}. Content missing or not string.`);
|
878
|
+
}
|
879
|
+
});
|
880
|
+
$('img[src], video[poster], input[type="image"][src]').each((_, el) => {
|
881
|
+
const element = $(el);
|
882
|
+
const srcAttr = element.is("video") ? "poster" : "src";
|
883
|
+
const src = element.attr(srcAttr);
|
884
|
+
const asset = src ? assetMap.get(src) : void 0;
|
885
|
+
if (asset?.content && typeof asset.content === "string" && asset.content.startsWith("data:")) {
|
886
|
+
logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
|
887
|
+
element.attr(srcAttr, asset.content);
|
888
|
+
} else if (src) {
|
889
|
+
logger?.warn(`Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`);
|
890
|
+
}
|
891
|
+
});
|
892
|
+
$("img[srcset], source[srcset]").each((_, el) => {
|
893
|
+
const element = $(el);
|
894
|
+
const srcset = element.attr("srcset");
|
895
|
+
if (!srcset) return;
|
896
|
+
const newSrcsetParts = [];
|
897
|
+
let changed = false;
|
898
|
+
srcset.split(",").forEach((part) => {
|
899
|
+
const trimmedPart = part.trim();
|
900
|
+
const [url, descriptor] = trimmedPart.split(/\s+/, 2);
|
901
|
+
const asset = url ? assetMap.get(url) : void 0;
|
902
|
+
if (asset?.content && typeof asset.content === "string" && asset.content.startsWith("data:")) {
|
903
|
+
newSrcsetParts.push(`${asset.content}${descriptor ? " " + descriptor : ""}`);
|
904
|
+
changed = true;
|
905
|
+
} else {
|
906
|
+
newSrcsetParts.push(trimmedPart);
|
907
|
+
}
|
908
|
+
});
|
909
|
+
if (changed) {
|
910
|
+
element.attr("srcset", newSrcsetParts.join(", "));
|
911
|
+
}
|
912
|
+
});
|
913
|
+
$("video[src], audio[src], video > source[src], audio > source[src]").each((_, el) => {
|
914
|
+
const element = $(el);
|
915
|
+
const src = element.attr("src");
|
916
|
+
const asset = src ? assetMap.get(src) : void 0;
|
917
|
+
if (asset?.content && typeof asset.content === "string" && asset.content.startsWith("data:")) {
|
918
|
+
logger?.debug(`Inlining media source: ${asset.url}`);
|
919
|
+
element.attr("src", asset.content);
|
920
|
+
}
|
921
|
+
});
|
922
|
+
logger?.debug("Asset inlining process complete.");
|
923
|
+
}
|
924
|
+
function packHTML(parsed, logger) {
|
925
|
+
const { htmlContent, assets } = parsed;
|
926
|
+
if (!htmlContent || typeof htmlContent !== "string") {
|
927
|
+
logger?.warn("Packer received empty or invalid htmlContent. Returning minimal HTML shell.");
|
928
|
+
return '<!DOCTYPE html><html><head><base href="./"></head><body></body></html>';
|
929
|
+
}
|
930
|
+
logger?.debug("Loading HTML content into Cheerio for packing...");
|
931
|
+
const $ = cheerio2.load(htmlContent);
|
932
|
+
logger?.debug("Ensuring <base> tag exists...");
|
933
|
+
ensureBaseTag($, logger);
|
934
|
+
logger?.debug("Starting asset inlining...");
|
935
|
+
inlineAssets($, assets, logger);
|
936
|
+
logger?.debug("Generating final packed HTML string...");
|
937
|
+
const finalHtml = $.html();
|
938
|
+
logger?.debug(`Packing complete. Final size: ${Buffer.byteLength(finalHtml)} bytes.`);
|
939
|
+
return finalHtml;
|
940
|
+
}
|
941
|
+
var init_packer = __esm({
|
942
|
+
"src/core/packer.ts"() {
|
943
|
+
"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
|
+
};
|
1057
|
+
}
|
1058
|
+
});
|
1059
|
+
|
1060
|
+
// src/utils/slugify.ts
|
1061
|
+
function slugify(url) {
|
1062
|
+
if (!url || typeof url !== "string") return "index";
|
1063
|
+
let cleaned = url.trim();
|
1064
|
+
let pathAndSearch = "";
|
1065
|
+
try {
|
1066
|
+
const urlObj = new URL(url, "https://placeholder.base");
|
1067
|
+
pathAndSearch = (urlObj.pathname ?? "") + (urlObj.search ?? "");
|
1068
|
+
} catch {
|
1069
|
+
pathAndSearch = cleaned.split("#")[0];
|
1070
|
+
}
|
1071
|
+
try {
|
1072
|
+
cleaned = decodeURIComponent(pathAndSearch);
|
1073
|
+
} catch (e) {
|
1074
|
+
cleaned = pathAndSearch;
|
1075
|
+
}
|
1076
|
+
cleaned = cleaned.replace(/\.(html?|php|aspx?|jsp)$/i, "").replace(/[\s/?=&\\]+/g, "-").replace(/[^\w._-]+/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
1077
|
+
return cleaned || "index";
|
1078
|
+
}
|
1079
|
+
function sanitizeSlug(rawUrl) {
|
1080
|
+
return slugify(rawUrl);
|
1081
|
+
}
|
1082
|
+
var init_slugify = __esm({
|
1083
|
+
"src/utils/slugify.ts"() {
|
1084
|
+
"use strict";
|
1085
|
+
}
|
1086
|
+
});
|
1087
|
+
|
1088
|
+
// src/core/bundler.ts
|
1089
|
+
function bundleMultiPageHTML(pages, logger) {
|
1090
|
+
if (!Array.isArray(pages)) {
|
1091
|
+
const errorMsg = "Input pages must be an array of PageEntry objects";
|
1092
|
+
logger?.error(errorMsg);
|
1093
|
+
throw new Error(errorMsg);
|
1094
|
+
}
|
1095
|
+
logger?.info(`Bundling ${pages.length} pages into a multi-page HTML document.`);
|
1096
|
+
const validPages = pages.filter((page) => {
|
1097
|
+
const isValid = page && typeof page === "object" && typeof page.url === "string" && typeof page.html === "string";
|
1098
|
+
if (!isValid) logger?.warn("Skipping invalid page entry");
|
1099
|
+
return isValid;
|
1100
|
+
});
|
1101
|
+
if (validPages.length === 0) {
|
1102
|
+
const errorMsg = "No valid page entries found in input array";
|
1103
|
+
logger?.error(errorMsg);
|
1104
|
+
throw new Error(errorMsg);
|
1105
|
+
}
|
1106
|
+
const slugMap = /* @__PURE__ */ new Map();
|
1107
|
+
const usedSlugs = /* @__PURE__ */ new Set();
|
1108
|
+
for (const page of validPages) {
|
1109
|
+
const baseSlug = sanitizeSlug(page.url);
|
1110
|
+
let slug = baseSlug;
|
1111
|
+
let counter = 1;
|
1112
|
+
while (usedSlugs.has(slug)) {
|
1113
|
+
slug = `${baseSlug}-${counter++}`;
|
1114
|
+
logger?.warn(`Slug collision detected for "${page.url}". Using "${slug}" instead.`);
|
1115
|
+
}
|
1116
|
+
usedSlugs.add(slug);
|
1117
|
+
slugMap.set(page.url, slug);
|
1118
|
+
}
|
1119
|
+
const defaultPageSlug = slugMap.get(validPages[0].url);
|
1120
|
+
let output = `<!DOCTYPE html>
|
1121
|
+
<html lang="en">
|
1122
|
+
<head>
|
1123
|
+
<meta charset="UTF-8">
|
1124
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
1125
|
+
<title>Multi-Page Bundle</title>
|
1126
|
+
</head>
|
1127
|
+
<body>
|
1128
|
+
<nav id="main-nav">
|
1129
|
+
${validPages.map((p) => {
|
1130
|
+
const slug = slugMap.get(p.url);
|
1131
|
+
const label = p.url.split("/").pop()?.split(".")[0] || "Page";
|
1132
|
+
return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
|
1133
|
+
}).join("\n")}
|
1134
|
+
</nav>
|
1135
|
+
<div id="page-container"></div>
|
1136
|
+
${validPages.map((p) => {
|
1137
|
+
const slug = slugMap.get(p.url);
|
1138
|
+
return `<template id="page-${slug}">${p.html}</template>`;
|
1139
|
+
}).join("\n")}
|
1140
|
+
<script id="router-script">
|
1141
|
+
document.addEventListener('DOMContentLoaded', function() {
|
1142
|
+
function navigateTo(slug) {
|
1143
|
+
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');
|
1151
|
+
});
|
1152
|
+
if (window.location.hash.substring(1) !== slug) {
|
1153
|
+
history.pushState(null, '', '#' + slug);
|
1154
|
+
}
|
1155
|
+
}
|
1156
|
+
|
1157
|
+
window.addEventListener('hashchange', () => {
|
1158
|
+
const slug = window.location.hash.substring(1);
|
1159
|
+
if (document.getElementById('page-' + slug)) navigateTo(slug);
|
1160
|
+
});
|
1161
|
+
|
1162
|
+
document.querySelectorAll('#main-nav a').forEach(link => {
|
1163
|
+
link.addEventListener('click', function(e) {
|
1164
|
+
e.preventDefault();
|
1165
|
+
const slug = this.getAttribute('data-page');
|
1166
|
+
navigateTo(slug);
|
1167
|
+
});
|
1168
|
+
});
|
1169
|
+
|
1170
|
+
const initial = window.location.hash.substring(1);
|
1171
|
+
navigateTo(document.getElementById('page-' + initial) ? initial : '${defaultPageSlug}');
|
1172
|
+
});
|
1173
|
+
</script>
|
1174
|
+
</body>
|
1175
|
+
</html>`;
|
1176
|
+
logger?.info(`Multi-page bundle generated. Size: ${Buffer.byteLength(output, "utf-8")} bytes.`);
|
1177
|
+
return output;
|
1178
|
+
}
|
1179
|
+
var init_bundler = __esm({
|
1180
|
+
"src/core/bundler.ts"() {
|
1181
|
+
"use strict";
|
1182
|
+
init_extractor();
|
1183
|
+
init_minifier();
|
1184
|
+
init_packer();
|
1185
|
+
init_slugify();
|
1186
|
+
}
|
1187
|
+
});
|
1188
|
+
|
1189
|
+
// 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) {
|
1193
|
+
let browser = null;
|
1194
|
+
const start = Date.now();
|
1195
|
+
logger?.debug(`Initiating fetch for single page: ${url}`);
|
1196
|
+
try {
|
1197
|
+
browser = await puppeteer.launch({ headless: true });
|
1198
|
+
logger?.debug(`Browser launched for ${url}`);
|
1199
|
+
const page = await browser.newPage();
|
1200
|
+
logger?.debug(`Page created for ${url}`);
|
1201
|
+
try {
|
1202
|
+
logger?.debug(`Navigating to ${url} with timeout ${timeout}ms`);
|
1203
|
+
await page.goto(url, { waitUntil: "networkidle2", timeout });
|
1204
|
+
logger?.debug(`Navigation successful for ${url}`);
|
1205
|
+
const html = await page.content();
|
1206
|
+
logger?.debug(`Content retrieved for ${url}`);
|
1207
|
+
const metadata = {
|
1208
|
+
input: url,
|
1209
|
+
outputSize: Buffer.byteLength(html, "utf-8"),
|
1210
|
+
assetCount: 0,
|
1211
|
+
// Basic fetch doesn't track assets
|
1212
|
+
buildTimeMs: Date.now() - start,
|
1213
|
+
errors: []
|
1214
|
+
// No errors if we reached this point
|
1215
|
+
};
|
1216
|
+
await page.close();
|
1217
|
+
logger?.debug(`Page closed for ${url}`);
|
1218
|
+
logger?.debug(`Browser closed for ${url}`);
|
1219
|
+
browser = null;
|
1220
|
+
return { html, metadata };
|
1221
|
+
} catch (pageError) {
|
1222
|
+
logger?.error(`Error during page processing for ${url}: ${pageError.message}`);
|
1223
|
+
try {
|
1224
|
+
await page.close();
|
1225
|
+
} catch (closeErr) {
|
1226
|
+
throw closeErr;
|
1227
|
+
}
|
1228
|
+
throw pageError;
|
1229
|
+
}
|
1230
|
+
} catch (launchError) {
|
1231
|
+
logger?.error(`Critical error during browser launch or page creation for ${url}: ${launchError.message}`);
|
1232
|
+
if (browser) {
|
1233
|
+
try {
|
1234
|
+
await browser.close();
|
1235
|
+
} catch (closeErr) {
|
1236
|
+
}
|
1237
|
+
}
|
1238
|
+
throw launchError;
|
1239
|
+
} finally {
|
1240
|
+
if (browser) {
|
1241
|
+
logger?.warn(`Closing browser in final cleanup for ${url}. This might indicate an unusual error path.`);
|
1242
|
+
try {
|
1243
|
+
await browser.close();
|
1244
|
+
} catch (closeErr) {
|
1245
|
+
}
|
1246
|
+
}
|
1247
|
+
}
|
1248
|
+
}
|
1249
|
+
async function crawlWebsite(startUrl, maxDepth, logger) {
|
1250
|
+
logger?.info(`Starting crawl for ${startUrl} with maxDepth ${maxDepth}`);
|
1251
|
+
if (maxDepth <= 0) {
|
1252
|
+
logger?.info("maxDepth is 0 or negative, no pages will be crawled.");
|
1253
|
+
return [];
|
1254
|
+
}
|
1255
|
+
const browser = await puppeteer.launch({ headless: true });
|
1256
|
+
const visited = /* @__PURE__ */ new Set();
|
1257
|
+
const results = [];
|
1258
|
+
const queue = [];
|
1259
|
+
let startOrigin;
|
1260
|
+
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
|
+
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 {
|
1314
|
+
}
|
1315
|
+
}
|
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}`);
|
1328
|
+
}
|
1329
|
+
}
|
1330
|
+
}
|
1331
|
+
}
|
1332
|
+
logger?.info(`Crawl finished. Closing browser.`);
|
1333
|
+
await browser.close();
|
1334
|
+
logger?.info(`Found ${results.length} pages.`);
|
1335
|
+
return results;
|
1336
|
+
}
|
1337
|
+
async function recursivelyBundleSite(startUrl, outputFile, maxDepth = 1) {
|
1338
|
+
const logger = new Logger();
|
1339
|
+
logger.info(`Starting recursive site bundle for ${startUrl} to ${outputFile} (maxDepth: ${maxDepth})`);
|
1340
|
+
try {
|
1341
|
+
const pages = await crawlWebsite(startUrl, maxDepth, logger);
|
1342
|
+
if (pages.length === 0) {
|
1343
|
+
logger.warn("Crawl completed but found 0 pages. Output file may be empty or reflect an empty bundle.");
|
1344
|
+
} else {
|
1345
|
+
logger.info(`Crawl successful, found ${pages.length} pages. Starting bundling.`);
|
1346
|
+
}
|
1347
|
+
const bundledHtml = bundleMultiPageHTML(pages, logger);
|
1348
|
+
logger.info(`Bundling complete. Output size: ${Buffer.byteLength(bundledHtml, "utf-8")} bytes.`);
|
1349
|
+
logger.info(`Writing bundled HTML to ${outputFile}`);
|
1350
|
+
await fs2.writeFile(outputFile, bundledHtml, "utf-8");
|
1351
|
+
logger.info(`Successfully wrote bundled output to ${outputFile}`);
|
1352
|
+
return {
|
1353
|
+
pages: pages.length,
|
1354
|
+
html: bundledHtml
|
1355
|
+
};
|
1356
|
+
} catch (error) {
|
1357
|
+
logger.error(`Error during recursive site bundle: ${error.message}`);
|
1358
|
+
if (error.stack) {
|
1359
|
+
logger.error(`Stack trace: ${error.stack}`);
|
1360
|
+
}
|
1361
|
+
throw error;
|
1362
|
+
}
|
1363
|
+
}
|
1364
|
+
var init_web_fetcher = __esm({
|
1365
|
+
"src/core/web-fetcher.ts"() {
|
1366
|
+
"use strict";
|
1367
|
+
init_logger();
|
1368
|
+
init_bundler();
|
1369
|
+
}
|
1370
|
+
});
|
1371
|
+
|
1372
|
+
// src/utils/meta.ts
|
1373
|
+
var BuildTimer;
|
1374
|
+
var init_meta = __esm({
|
1375
|
+
"src/utils/meta.ts"() {
|
1376
|
+
"use strict";
|
1377
|
+
BuildTimer = class {
|
1378
|
+
startTime;
|
1379
|
+
input;
|
1380
|
+
pagesBundled;
|
1381
|
+
// Tracks pages for recursive bundles
|
1382
|
+
assetCount = 0;
|
1383
|
+
// Tracks discovered/processed assets
|
1384
|
+
errors = [];
|
1385
|
+
// Collects warnings/errors
|
1386
|
+
/**
|
1387
|
+
* Creates and starts a build timer session for a given input.
|
1388
|
+
*
|
1389
|
+
* @param {string} input - The source file path or URL being processed.
|
1390
|
+
*/
|
1391
|
+
constructor(input) {
|
1392
|
+
this.startTime = Date.now();
|
1393
|
+
this.input = input;
|
1394
|
+
}
|
1395
|
+
/**
|
1396
|
+
* Explicitly sets the number of assets discovered or processed.
|
1397
|
+
* This might be called after asset extraction/minification.
|
1398
|
+
*
|
1399
|
+
* @param {number} count - The total number of assets.
|
1400
|
+
*/
|
1401
|
+
setAssetCount(count) {
|
1402
|
+
this.assetCount = count;
|
1403
|
+
}
|
1404
|
+
/**
|
1405
|
+
* Records a warning or error message encountered during the build.
|
1406
|
+
* These are added to the final metadata.
|
1407
|
+
*
|
1408
|
+
* @param {string} message - The warning or error description.
|
1409
|
+
*/
|
1410
|
+
addError(message) {
|
1411
|
+
this.errors.push(message);
|
1412
|
+
}
|
1413
|
+
/**
|
1414
|
+
* Sets the number of pages bundled, typically used in multi-page
|
1415
|
+
* or recursive bundling scenarios.
|
1416
|
+
*
|
1417
|
+
* @param {number} count - The number of HTML pages included in the bundle.
|
1418
|
+
*/
|
1419
|
+
setPageCount(count) {
|
1420
|
+
this.pagesBundled = count;
|
1421
|
+
}
|
1422
|
+
/**
|
1423
|
+
* Stops the timer, calculates final metrics, and returns the complete
|
1424
|
+
* BundleMetadata object. Merges any explicitly provided metadata
|
1425
|
+
* (like assetCount calculated elsewhere) with the timer's tracked data.
|
1426
|
+
*
|
1427
|
+
* @param {string} finalHtml - The final generated HTML string, used to calculate output size.
|
1428
|
+
* @param {Partial<BundleMetadata>} [extra] - Optional object containing metadata fields
|
1429
|
+
* (like assetCount or pre-calculated errors) that should override the timer's internal values.
|
1430
|
+
* @returns {BundleMetadata} The finalized metadata object for the build process.
|
1431
|
+
*/
|
1432
|
+
finish(html, extra) {
|
1433
|
+
const buildTimeMs = Date.now() - this.startTime;
|
1434
|
+
const outputSize = Buffer.byteLength(html || "", "utf-8");
|
1435
|
+
const combinedErrors = Array.from(/* @__PURE__ */ new Set([...this.errors, ...extra?.errors ?? []]));
|
1436
|
+
const finalMetadata = {
|
1437
|
+
input: this.input,
|
1438
|
+
outputSize,
|
1439
|
+
buildTimeMs,
|
1440
|
+
assetCount: extra?.assetCount ?? this.assetCount,
|
1441
|
+
pagesBundled: extra?.pagesBundled ?? this.pagesBundled,
|
1442
|
+
// Assign the combined errors array
|
1443
|
+
errors: combinedErrors
|
1444
|
+
};
|
1445
|
+
if (finalMetadata.pagesBundled === void 0) {
|
1446
|
+
delete finalMetadata.pagesBundled;
|
1447
|
+
}
|
1448
|
+
if (finalMetadata.errors?.length === 0) {
|
1449
|
+
delete finalMetadata.errors;
|
1450
|
+
}
|
1451
|
+
return finalMetadata;
|
1452
|
+
}
|
1453
|
+
};
|
1454
|
+
}
|
1455
|
+
});
|
1456
|
+
|
1457
|
+
// src/index.ts
|
1458
|
+
async function generatePortableHTML(input, options = {}, loggerInstance) {
|
1459
|
+
const logger = loggerInstance || new Logger(options.logLevel);
|
1460
|
+
logger.info(`Generating portable HTML for: ${input}`);
|
1461
|
+
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...`);
|
1465
|
+
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;
|
1469
|
+
} catch (error) {
|
1470
|
+
logger.error(`Failed to fetch remote URL ${input}: ${error.message}`);
|
1471
|
+
throw error;
|
1472
|
+
}
|
1473
|
+
}
|
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}`);
|
1477
|
+
try {
|
1478
|
+
const parsed = await parseHTML(input, logger);
|
1479
|
+
const enriched = await extractAssets(parsed, options.embedAssets ?? true, basePath, logger);
|
1480
|
+
const minified = await minifyAssets(enriched, options, logger);
|
1481
|
+
const finalHtml = packHTML(minified, logger);
|
1482
|
+
const metadata = timer.finish(finalHtml, {
|
1483
|
+
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
|
+
});
|
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
|
+
}
|
1491
|
+
return { html: finalHtml, metadata };
|
1492
|
+
} catch (error) {
|
1493
|
+
logger.error(`Error during local processing for ${input}: ${error.message}`);
|
1494
|
+
throw error;
|
1495
|
+
}
|
1496
|
+
}
|
1497
|
+
async function generateRecursivePortableHTML(url, depth = 1, options = {}, loggerInstance) {
|
1498
|
+
const logger = loggerInstance || new Logger(options.logLevel);
|
1499
|
+
logger.info(`Generating recursive portable HTML for: ${url}, Max Depth: ${depth}`);
|
1500
|
+
const timer = new BuildTimer(url);
|
1501
|
+
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);
|
1505
|
+
}
|
1506
|
+
const internalOutputPathPlaceholder = `${new URL(url).hostname}_recursive.html`;
|
1507
|
+
try {
|
1508
|
+
const { html, pages } = await recursivelyBundleSite(url, internalOutputPathPlaceholder, depth);
|
1509
|
+
logger.info(`Recursive crawl complete. Discovered and bundled ${pages} pages.`);
|
1510
|
+
timer.setPageCount(pages);
|
1511
|
+
const metadata = timer.finish(html, {
|
1512
|
+
assetCount: 0,
|
1513
|
+
// NOTE: Asset count across multiple pages is not currently aggregated.
|
1514
|
+
pagesBundled: pages
|
1515
|
+
// TODO: Potentially collect errors from the core function if it returns them
|
1516
|
+
});
|
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
|
+
}
|
1521
|
+
return { html, metadata };
|
1522
|
+
} 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}`);
|
1554
|
+
throw error;
|
1555
|
+
}
|
1556
|
+
}
|
1557
|
+
var init_src = __esm({
|
1558
|
+
"src/index.ts"() {
|
1559
|
+
"use strict";
|
1560
|
+
init_parser();
|
1561
|
+
init_extractor();
|
1562
|
+
init_minifier();
|
1563
|
+
init_packer();
|
1564
|
+
init_web_fetcher();
|
1565
|
+
init_bundler();
|
1566
|
+
init_meta();
|
1567
|
+
init_logger();
|
1568
|
+
init_types();
|
1569
|
+
}
|
1570
|
+
});
|
1571
|
+
|
1572
|
+
// src/cli/cli.ts
|
1573
|
+
var cli_exports = {};
|
1574
|
+
__export(cli_exports, {
|
1575
|
+
main: () => main,
|
1576
|
+
runCli: () => runCli
|
1577
|
+
});
|
1578
|
+
import fs3 from "fs";
|
1579
|
+
import path3 from "path";
|
1580
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
1581
|
+
function getPackageJson() {
|
1582
|
+
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 (_) {
|
1590
|
+
}
|
1591
|
+
return { version: "0.1.0" };
|
1592
|
+
}
|
1593
|
+
async function runCli(argv = process.argv) {
|
1594
|
+
let stdout = "";
|
1595
|
+
let stderr = "";
|
1596
|
+
let exitCode = 0;
|
1597
|
+
const originalLog = console.log;
|
1598
|
+
const originalErr = console.error;
|
1599
|
+
const originalWarn = console.warn;
|
1600
|
+
console.log = (...args) => {
|
1601
|
+
stdout += args.join(" ") + "\n";
|
1602
|
+
};
|
1603
|
+
console.error = (...args) => {
|
1604
|
+
stderr += args.join(" ") + "\n";
|
1605
|
+
};
|
1606
|
+
console.warn = (...args) => {
|
1607
|
+
stderr += args.join(" ") + "\n";
|
1608
|
+
};
|
1609
|
+
let opts;
|
1610
|
+
try {
|
1611
|
+
opts = parseOptions(argv);
|
1612
|
+
const version = getPackageJson().version || "0.1.0";
|
1613
|
+
if (opts.verbose) {
|
1614
|
+
console.log(`\u{1F4E6} PortaPack v${version}`);
|
1615
|
+
}
|
1616
|
+
if (!opts.input) {
|
1617
|
+
console.error("\u274C Missing input file or URL");
|
1618
|
+
console.log = originalLog;
|
1619
|
+
console.error = originalErr;
|
1620
|
+
console.warn = originalWarn;
|
1621
|
+
return { stdout, stderr, exitCode: 1 };
|
1622
|
+
}
|
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}`);
|
1626
|
+
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 */]}`);
|
1633
|
+
}
|
1634
|
+
if (opts.dryRun) {
|
1635
|
+
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;
|
1639
|
+
return { stdout, stderr, exitCode: 0 };
|
1640
|
+
}
|
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");
|
1643
|
+
const meta = result.metadata;
|
1644
|
+
console.log(`\u2705 Packed: ${meta.input} \u2192 ${outputPath}`);
|
1645
|
+
console.log(`\u{1F4E6} Size: ${(meta.outputSize / 1024).toFixed(2)} KB`);
|
1646
|
+
console.log(`\u23F1\uFE0F Time: ${meta.buildTimeMs} ms`);
|
1647
|
+
console.log(`\u{1F5BC}\uFE0F Assets: ${meta.assetCount}`);
|
1648
|
+
if (meta.pagesBundled && meta.pagesBundled > 0) {
|
1649
|
+
console.log(`\u{1F9E9} Pages: ${meta.pagesBundled}`);
|
1650
|
+
}
|
1651
|
+
if (meta.errors && meta.errors.length > 0) {
|
1652
|
+
console.warn(`
|
1653
|
+
\u26A0\uFE0F ${meta.errors.length} warning(s):`);
|
1654
|
+
for (const err of meta.errors) {
|
1655
|
+
console.warn(` - ${err}`);
|
1656
|
+
}
|
1657
|
+
}
|
1658
|
+
} catch (err) {
|
1659
|
+
console.error(`
|
1660
|
+
\u{1F4A5} Error: ${err?.message || "Unknown failure"}`);
|
1661
|
+
if (err?.stack && opts?.verbose) {
|
1662
|
+
console.error(err.stack);
|
1663
|
+
}
|
1664
|
+
exitCode = 1;
|
1665
|
+
} finally {
|
1666
|
+
console.log = originalLog;
|
1667
|
+
console.error = originalErr;
|
1668
|
+
console.warn = originalWarn;
|
1669
|
+
}
|
1670
|
+
return { stdout, stderr, exitCode };
|
1671
|
+
}
|
1672
|
+
var main;
|
1673
|
+
var init_cli = __esm({
|
1674
|
+
"src/cli/cli.ts"() {
|
1675
|
+
"use strict";
|
1676
|
+
init_options();
|
1677
|
+
init_src();
|
1678
|
+
init_types();
|
1679
|
+
main = runCli;
|
1680
|
+
}
|
1681
|
+
});
|
1682
|
+
|
1683
|
+
// src/cli/cli-entry.ts
|
1684
|
+
var startCLI = async () => {
|
1685
|
+
const { main: main2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
1686
|
+
return await main2(process.argv);
|
1687
|
+
};
|
1688
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
1689
|
+
startCLI().then(({ exitCode }) => process.exit(Number(exitCode)));
|
1690
|
+
}
|
1691
|
+
export {
|
1692
|
+
startCLI
|
1693
|
+
};
|
1694
|
+
//# sourceMappingURL=cli-entry.js.map
|