portapack 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +67 -8
- package/.releaserc.js +25 -27
- package/CHANGELOG.md +14 -22
- package/LICENSE.md +21 -0
- package/README.md +22 -53
- package/commitlint.config.js +30 -34
- package/dist/cli/cli-entry.cjs +183 -98
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +178 -97
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +38 -33
- package/docs/.vitepress/sidebar-generator.ts +89 -38
- package/docs/architecture.md +186 -0
- package/docs/cli.md +23 -23
- package/docs/code-of-conduct.md +7 -1
- package/docs/configuration.md +12 -11
- package/docs/contributing.md +6 -2
- package/docs/deployment.md +10 -5
- package/docs/development.md +8 -5
- package/docs/getting-started.md +13 -13
- package/docs/index.md +1 -1
- package/docs/public/android-chrome-192x192.png +0 -0
- package/docs/public/android-chrome-512x512.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/roadmap.md +233 -0
- package/docs/site.webmanifest +1 -0
- package/docs/troubleshooting.md +12 -1
- package/examples/main.ts +5 -30
- package/examples/sample-project/script.js +1 -1
- package/jest.config.ts +8 -13
- package/nodemon.json +5 -10
- package/package.json +2 -5
- package/src/cli/cli-entry.ts +2 -2
- package/src/cli/cli.ts +21 -16
- package/src/cli/options.ts +127 -113
- package/src/core/bundler.ts +253 -222
- package/src/core/extractor.ts +632 -565
- package/src/core/minifier.ts +173 -162
- package/src/core/packer.ts +141 -137
- package/src/core/parser.ts +74 -73
- package/src/core/web-fetcher.ts +270 -258
- package/src/index.ts +18 -17
- package/src/types.ts +9 -11
- package/src/utils/font.ts +12 -6
- package/src/utils/logger.ts +110 -105
- package/src/utils/meta.ts +75 -76
- package/src/utils/mime.ts +50 -50
- package/src/utils/slugify.ts +33 -34
- package/tests/unit/cli/cli-entry.test.ts +72 -70
- package/tests/unit/cli/cli.test.ts +314 -278
- package/tests/unit/cli/options.test.ts +294 -301
- package/tests/unit/core/bundler.test.ts +426 -329
- package/tests/unit/core/extractor.test.ts +793 -549
- package/tests/unit/core/minifier.test.ts +374 -274
- package/tests/unit/core/packer.test.ts +298 -264
- package/tests/unit/core/parser.test.ts +538 -150
- package/tests/unit/core/web-fetcher.test.ts +389 -359
- package/tests/unit/index.test.ts +238 -197
- package/tests/unit/utils/font.test.ts +26 -21
- package/tests/unit/utils/logger.test.ts +267 -260
- package/tests/unit/utils/meta.test.ts +29 -28
- package/tests/unit/utils/mime.test.ts +73 -74
- package/tests/unit/utils/slugify.test.ts +14 -12
- package/tsconfig.build.json +9 -10
- package/tsconfig.jest.json +1 -1
- package/tsconfig.json +2 -2
- package/tsup.config.ts +8 -9
- package/typedoc.json +5 -9
- /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
- /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
package/src/core/packer.ts
CHANGED
@@ -15,7 +15,7 @@ import { guessMimeType } from '../utils/mime'; // Assuming correct path
|
|
15
15
|
* Escapes characters potentially problematic within inline `<script>` tags.
|
16
16
|
*/
|
17
17
|
function escapeScriptContent(code: string): string {
|
18
|
-
|
18
|
+
return code.replace(/<\/(script)/gi, '<\\/$1');
|
19
19
|
}
|
20
20
|
|
21
21
|
/**
|
@@ -26,137 +26,141 @@ function escapeScriptContent(code: string): string {
|
|
26
26
|
* @param {Logger} [logger] - Optional logger instance.
|
27
27
|
*/
|
28
28
|
function ensureBaseTag($: CheerioAPI, logger?: Logger): void {
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
}
|
51
|
-
}
|
52
|
-
|
53
|
-
// Now head should represent the head element selection.
|
54
|
-
// Check if <base> exists within the guaranteed <head>.
|
55
|
-
// Use type guard just in case head couldn't be created properly
|
56
|
-
if (head && head.length > 0 && head.find('base[href]').length === 0) {
|
57
|
-
logger?.debug('Prepending <base href="./"> to <head>.');
|
58
|
-
head.prepend('<base href="./">');
|
29
|
+
let head = $('head');
|
30
|
+
|
31
|
+
// If <head> doesn't exist, create it, ensuring <html> exists first.
|
32
|
+
if (head.length === 0) {
|
33
|
+
logger?.debug('No <head> tag found. Creating <head> and ensuring <html> exists.');
|
34
|
+
let htmlElement = $('html');
|
35
|
+
|
36
|
+
// If <html> doesn't exist, create it and wrap the existing content.
|
37
|
+
if (htmlElement.length === 0) {
|
38
|
+
logger?.debug('No <html> tag found. Wrapping content in <html><body>...');
|
39
|
+
const bodyContent = $.root().html() || '';
|
40
|
+
$.root().empty();
|
41
|
+
// FIX: Use 'as any' for type assertion
|
42
|
+
htmlElement = $('<html>').appendTo($.root()) as any;
|
43
|
+
// FIX: Use 'as any' for type assertion
|
44
|
+
head = $('<head>').appendTo(htmlElement) as any;
|
45
|
+
$('<body>').html(bodyContent).appendTo(htmlElement);
|
46
|
+
} else {
|
47
|
+
// If <html> exists but <head> doesn't, prepend <head> to <html>
|
48
|
+
// FIX: Use 'as any' for type assertion
|
49
|
+
head = $('<head>').prependTo(htmlElement) as any;
|
59
50
|
}
|
51
|
+
}
|
52
|
+
|
53
|
+
// Now head should represent the head element selection.
|
54
|
+
// Check if <base> exists within the guaranteed <head>.
|
55
|
+
// Use type guard just in case head couldn't be created properly
|
56
|
+
if (head && head.length > 0 && head.find('base[href]').length === 0) {
|
57
|
+
logger?.debug('Prepending <base href="./"> to <head>.');
|
58
|
+
head.prepend('<base href="./">');
|
59
|
+
}
|
60
60
|
}
|
61
61
|
|
62
|
-
|
63
62
|
/**
|
64
63
|
* Inlines assets into the HTML document using Cheerio for safe DOM manipulation.
|
65
64
|
*/
|
66
65
|
function inlineAssets($: CheerioAPI, assets: Asset[], logger?: Logger): void {
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
}
|
66
|
+
logger?.debug(`Inlining ${assets.filter(a => a.content).length} assets with content...`);
|
67
|
+
const assetMap = new Map<string, Asset>(assets.map(asset => [asset.url, asset]));
|
68
|
+
|
69
|
+
// 1. Inline CSS (<link rel="stylesheet" href="...">)
|
70
|
+
$('link[rel="stylesheet"][href]').each((_, el) => {
|
71
|
+
const link = $(el);
|
72
|
+
const href = link.attr('href');
|
73
|
+
const asset = href ? assetMap.get(href) : undefined;
|
74
|
+
if (asset?.content && typeof asset.content === 'string') {
|
75
|
+
if (asset.content.startsWith('data:')) {
|
76
|
+
logger?.debug(`Replacing link with style tag using existing data URI: ${asset.url}`);
|
77
|
+
const styleTag = $('<style>').text(`@import url("${asset.content}");`);
|
78
|
+
link.replaceWith(styleTag);
|
79
|
+
} else {
|
80
|
+
logger?.debug(`Inlining CSS: ${asset.url}`);
|
81
|
+
const styleTag = $('<style>').text(asset.content);
|
82
|
+
link.replaceWith(styleTag);
|
83
|
+
}
|
84
|
+
} else if (href) {
|
85
|
+
logger?.warn(`Could not inline CSS: ${href}. Content missing or invalid.`);
|
86
|
+
}
|
87
|
+
});
|
88
|
+
|
89
|
+
// 2. Inline JS (<script src="...">)
|
90
|
+
$('script[src]').each((_, el) => {
|
91
|
+
const script = $(el);
|
92
|
+
const src = script.attr('src');
|
93
|
+
const asset = src ? assetMap.get(src) : undefined;
|
94
|
+
if (asset?.content && typeof asset.content === 'string') {
|
95
|
+
logger?.debug(`Inlining JS: ${asset.url}`);
|
96
|
+
const inlineScript = $('<script>');
|
97
|
+
inlineScript.text(escapeScriptContent(asset.content));
|
98
|
+
Object.entries(script.attr() || {}).forEach(([key, value]) => {
|
99
|
+
if (key.toLowerCase() !== 'src') inlineScript.attr(key, value);
|
100
|
+
});
|
101
|
+
script.replaceWith(inlineScript);
|
102
|
+
} else if (src) {
|
103
|
+
logger?.warn(`Could not inline JS: ${src}. Content missing or not string.`);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
|
107
|
+
// 3. Inline Images (<img src="...">, <video poster="...">, etc.)
|
108
|
+
$('img[src], video[poster], input[type="image"][src]').each((_, el) => {
|
109
|
+
const element = $(el);
|
110
|
+
const srcAttr = element.is('video') ? 'poster' : 'src';
|
111
|
+
const src = element.attr(srcAttr);
|
112
|
+
const asset = src ? assetMap.get(src) : undefined;
|
113
|
+
if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
|
114
|
+
logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
|
115
|
+
element.attr(srcAttr, asset.content);
|
116
|
+
} else if (src) {
|
117
|
+
logger?.warn(
|
118
|
+
`Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`
|
119
|
+
);
|
120
|
+
}
|
121
|
+
});
|
122
|
+
|
123
|
+
// 4. Inline srcset attributes (<img srcset="...">, <source srcset="...">)
|
124
|
+
$('img[srcset], source[srcset]').each((_, el) => {
|
125
|
+
const element = $(el);
|
126
|
+
const srcset = element.attr('srcset');
|
127
|
+
if (!srcset) return;
|
128
|
+
const newSrcsetParts: string[] = [];
|
129
|
+
let changed = false;
|
130
|
+
srcset.split(',').forEach(part => {
|
131
|
+
const trimmedPart = part.trim();
|
132
|
+
const [url, descriptor] = trimmedPart.split(/\s+/, 2);
|
133
|
+
const asset = url ? assetMap.get(url) : undefined;
|
134
|
+
if (
|
135
|
+
asset?.content &&
|
136
|
+
typeof asset.content === 'string' &&
|
137
|
+
asset.content.startsWith('data:')
|
138
|
+
) {
|
139
|
+
newSrcsetParts.push(`${asset.content}${descriptor ? ' ' + descriptor : ''}`);
|
140
|
+
changed = true;
|
141
|
+
} else {
|
142
|
+
newSrcsetParts.push(trimmedPart);
|
143
|
+
}
|
120
144
|
});
|
145
|
+
if (changed) {
|
146
|
+
element.attr('srcset', newSrcsetParts.join(', '));
|
147
|
+
}
|
148
|
+
});
|
149
|
+
|
150
|
+
// 5. Inline other asset types (video, audio sources)
|
151
|
+
$('video[src], audio[src], video > source[src], audio > source[src]').each((_, el) => {
|
152
|
+
const element = $(el);
|
153
|
+
const src = element.attr('src');
|
154
|
+
const asset = src ? assetMap.get(src) : undefined;
|
155
|
+
if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
|
156
|
+
logger?.debug(`Inlining media source: ${asset.url}`);
|
157
|
+
element.attr('src', asset.content);
|
158
|
+
}
|
159
|
+
});
|
121
160
|
|
122
|
-
|
123
|
-
$('img[srcset], source[srcset]').each((_, el) => {
|
124
|
-
const element = $(el);
|
125
|
-
const srcset = element.attr('srcset');
|
126
|
-
if (!srcset) return;
|
127
|
-
const newSrcsetParts: string[] = [];
|
128
|
-
let changed = false;
|
129
|
-
srcset.split(',').forEach(part => {
|
130
|
-
const trimmedPart = part.trim();
|
131
|
-
const [url, descriptor] = trimmedPart.split(/\s+/, 2);
|
132
|
-
const asset = url ? assetMap.get(url) : undefined;
|
133
|
-
if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
|
134
|
-
newSrcsetParts.push(`${asset.content}${descriptor ? ' ' + descriptor : ''}`);
|
135
|
-
changed = true;
|
136
|
-
} else {
|
137
|
-
newSrcsetParts.push(trimmedPart);
|
138
|
-
}
|
139
|
-
});
|
140
|
-
if (changed) {
|
141
|
-
element.attr('srcset', newSrcsetParts.join(', '));
|
142
|
-
}
|
143
|
-
});
|
144
|
-
|
145
|
-
// 5. Inline other asset types (video, audio sources)
|
146
|
-
$('video[src], audio[src], video > source[src], audio > source[src]').each((_, el) => {
|
147
|
-
const element = $(el);
|
148
|
-
const src = element.attr('src');
|
149
|
-
const asset = src ? assetMap.get(src) : undefined;
|
150
|
-
if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
|
151
|
-
logger?.debug(`Inlining media source: ${asset.url}`);
|
152
|
-
element.attr('src', asset.content);
|
153
|
-
}
|
154
|
-
});
|
155
|
-
|
156
|
-
logger?.debug('Asset inlining process complete.');
|
161
|
+
logger?.debug('Asset inlining process complete.');
|
157
162
|
}
|
158
163
|
|
159
|
-
|
160
164
|
/**
|
161
165
|
* Packs a ParsedHTML object into a single, self-contained HTML string.
|
162
166
|
* This involves ensuring a base tag exists and inlining all assets
|
@@ -168,24 +172,24 @@ function inlineAssets($: CheerioAPI, assets: Asset[], logger?: Logger): void {
|
|
168
172
|
* @returns {string} The packed HTML string with assets inlined. Returns a minimal HTML structure if input is invalid.
|
169
173
|
*/
|
170
174
|
export function packHTML(parsed: ParsedHTML, logger?: Logger): string {
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
175
|
+
const { htmlContent, assets } = parsed;
|
176
|
+
if (!htmlContent || typeof htmlContent !== 'string') {
|
177
|
+
logger?.warn('Packer received empty or invalid htmlContent. Returning minimal HTML shell.');
|
178
|
+
return '<!DOCTYPE html><html><head><base href="./"></head><body></body></html>';
|
179
|
+
}
|
176
180
|
|
177
|
-
|
178
|
-
|
181
|
+
logger?.debug('Loading HTML content into Cheerio for packing...');
|
182
|
+
const $ = cheerio.load(htmlContent);
|
179
183
|
|
180
|
-
|
181
|
-
|
184
|
+
logger?.debug('Ensuring <base> tag exists...');
|
185
|
+
ensureBaseTag($, logger); // Ensure base tag safely
|
182
186
|
|
183
|
-
|
184
|
-
|
187
|
+
logger?.debug('Starting asset inlining...');
|
188
|
+
inlineAssets($, assets, logger); // Inline assets safely
|
185
189
|
|
186
|
-
|
187
|
-
|
190
|
+
logger?.debug('Generating final packed HTML string...');
|
191
|
+
const finalHtml = $.html();
|
188
192
|
|
189
|
-
|
190
|
-
|
191
|
-
}
|
193
|
+
logger?.debug(`Packing complete. Final size: ${Buffer.byteLength(finalHtml)} bytes.`);
|
194
|
+
return finalHtml;
|
195
|
+
}
|
package/src/core/parser.ts
CHANGED
@@ -8,7 +8,6 @@
|
|
8
8
|
* and data URIs are ignored. Duplicate asset URLs are ignored.
|
9
9
|
*/
|
10
10
|
|
11
|
-
// FIX: Use only the named import for readFile
|
12
11
|
import { readFile } from 'fs/promises';
|
13
12
|
// NOTE: 'path' module was imported but not used, so removed. Add back if needed later.
|
14
13
|
// import path from 'path';
|
@@ -34,82 +33,84 @@ import { guessMimeType } from '../utils/mime.js';
|
|
34
33
|
* @throws {Error} Throws an error with cause if the file cannot be read.
|
35
34
|
*/
|
36
35
|
export async function parseHTML(entryFilePath: string, logger?: Logger): Promise<ParsedHTML> {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
36
|
+
logger?.debug(`Parsing HTML file: ${entryFilePath}`);
|
37
|
+
let htmlContent: string;
|
38
|
+
try {
|
39
|
+
// FIX: Use the correctly imported 'readFile' function directly
|
40
|
+
htmlContent = await readFile(entryFilePath, 'utf-8');
|
41
|
+
logger?.debug(`Successfully read HTML file (${Buffer.byteLength(htmlContent)} bytes).`);
|
42
|
+
} catch (err: any) {
|
43
|
+
logger?.error(`Failed to read HTML file "${entryFilePath}": ${err.message}`);
|
44
|
+
throw new Error(`Could not read input HTML file: ${entryFilePath}`, { cause: err });
|
45
|
+
}
|
47
46
|
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
const $: CheerioAPI = cheerio.load(htmlContent);
|
48
|
+
const assets: Asset[] = [];
|
49
|
+
const addedUrls = new Set<string>();
|
51
50
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
51
|
+
/** Helper to add unique assets */
|
52
|
+
const addAsset = (url?: string, forcedType?: Asset['type']): void => {
|
53
|
+
if (!url || url.trim() === '' || url.startsWith('data:')) {
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
if (!addedUrls.has(url)) {
|
57
|
+
addedUrls.add(url);
|
58
|
+
const mimeInfo = guessMimeType(url);
|
59
|
+
const type = forcedType ?? mimeInfo.assetType;
|
60
|
+
assets.push({ type, url });
|
61
|
+
logger?.debug(`Discovered asset: Type='${type}', URL='${url}'`);
|
62
|
+
} else {
|
63
|
+
logger?.debug(`Skipping duplicate asset URL: ${url}`);
|
64
|
+
}
|
65
|
+
};
|
67
66
|
|
68
|
-
|
67
|
+
logger?.debug('Extracting assets from HTML tags...');
|
69
68
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
});
|
69
|
+
// --- Extract Assets from Various Tags ---
|
70
|
+
// Stylesheets: <link rel="stylesheet" href="...">
|
71
|
+
$('link[rel="stylesheet"][href]').each((_, el) => {
|
72
|
+
addAsset($(el).attr('href'), 'css');
|
73
|
+
});
|
74
|
+
// JavaScript: <script src="...">
|
75
|
+
$('script[src]').each((_, el) => {
|
76
|
+
addAsset($(el).attr('src'), 'js');
|
77
|
+
});
|
78
|
+
// Images: <img src="...">, <input type="image" src="...">
|
79
|
+
$('img[src]').each((_, el) => addAsset($(el).attr('src'), 'image'));
|
80
|
+
$('input[type="image"][src]').each((_, el) => addAsset($(el).attr('src'), 'image'));
|
81
|
+
// Image srcset: <img srcset="...">, <source srcset="..."> (within picture)
|
82
|
+
$('img[srcset], picture source[srcset]').each((_, el) => {
|
83
|
+
const srcset = $(el).attr('srcset');
|
84
|
+
srcset?.split(',').forEach(entry => {
|
85
|
+
const [url] = entry.trim().split(/\s+/);
|
86
|
+
addAsset(url, 'image');
|
89
87
|
});
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
addAsset($(el).attr('href'), 'font');
|
88
|
+
});
|
89
|
+
// Video: <video src="...">, <video poster="...">
|
90
|
+
$('video[src]').each((_, el) => addAsset($(el).attr('src'), 'video'));
|
91
|
+
$('video[poster]').each((_, el) => addAsset($(el).attr('poster'), 'image'));
|
92
|
+
// Audio: <audio src="...">
|
93
|
+
$('audio[src]').each((_, el) => addAsset($(el).attr('src'), 'audio'));
|
94
|
+
// Media Sources: <source src="..."> within <video> or <audio>
|
95
|
+
$('video > source[src]').each((_, el) => addAsset($(el).attr('src'), 'video'));
|
96
|
+
$('audio > source[src]').each((_, el) => addAsset($(el).attr('src'), 'audio'));
|
97
|
+
// Icons and Manifest: <link rel="icon/shortcut icon/apple-touch-icon/manifest" href="...">
|
98
|
+
$('link[href]')
|
99
|
+
.filter((_, el) => {
|
100
|
+
const rel = $(el).attr('rel')?.toLowerCase() ?? '';
|
101
|
+
return ['icon', 'shortcut icon', 'apple-touch-icon', 'manifest'].includes(rel);
|
102
|
+
})
|
103
|
+
.each((_, el) => {
|
104
|
+
const rel = $(el).attr('rel')?.toLowerCase() ?? '';
|
105
|
+
const isIcon = ['icon', 'shortcut icon', 'apple-touch-icon'].includes(rel);
|
106
|
+
addAsset($(el).attr('href'), isIcon ? 'image' : undefined);
|
110
107
|
});
|
108
|
+
// Preloaded Fonts: <link rel="preload" as="font" href="...">
|
109
|
+
$('link[rel="preload"][as="font"][href]').each((_, el) => {
|
110
|
+
addAsset($(el).attr('href'), 'font');
|
111
|
+
});
|
111
112
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
}
|
113
|
+
// --- Parsing Complete ---
|
114
|
+
logger?.info(`HTML parsing complete. Discovered ${assets.length} unique asset links.`);
|
115
|
+
return { htmlContent, assets };
|
116
|
+
}
|