portapack 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +83 -216
- package/dist/cli/{cli-entry.js → cli-entry.cjs} +626 -498
- package/dist/cli/cli-entry.cjs.map +1 -0
- package/dist/index.d.ts +51 -56
- package/dist/index.js +523 -443
- package/dist/index.js.map +1 -1
- package/docs/cli.md +158 -42
- package/jest.config.ts +18 -8
- package/jest.setup.cjs +66 -146
- package/package.json +5 -5
- package/src/cli/cli-entry.ts +15 -15
- package/src/cli/cli.ts +130 -119
- package/src/core/bundler.ts +174 -63
- package/src/core/extractor.ts +243 -203
- package/src/core/web-fetcher.ts +205 -141
- package/src/index.ts +161 -224
- package/tests/unit/cli/cli-entry.test.ts +66 -77
- package/tests/unit/cli/cli.test.ts +243 -145
- package/tests/unit/core/bundler.test.ts +334 -258
- package/tests/unit/core/extractor.test.ts +391 -1051
- package/tests/unit/core/minifier.test.ts +130 -221
- package/tests/unit/core/packer.test.ts +255 -106
- package/tests/unit/core/parser.test.ts +89 -458
- package/tests/unit/core/web-fetcher.test.ts +330 -285
- package/tests/unit/index.test.ts +206 -300
- package/tests/unit/utils/logger.test.ts +32 -28
- package/tsconfig.jest.json +7 -7
- package/tsup.config.ts +34 -29
- package/dist/cli/cli-entry.js.map +0 -1
- package/output.html +0 -1
- package/site-packed.html +0 -1
- package/test-output.html +0 -0
package/src/core/extractor.ts
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
* @file src/core/extractor.ts
|
3
3
|
* @description Handles discovery, resolution, fetching, and optional embedding of assets
|
4
4
|
* linked from HTML and recursively within CSS (@import, url()). This is the heart of finding EVERYTHING.
|
5
|
-
* @version 1.1.
|
5
|
+
* @version 1.1.4 - Added console logs for debugging path/URL resolution. Refined determineBaseUrl.
|
6
6
|
*/
|
7
7
|
|
8
8
|
// === Node.js Core Imports ===
|
@@ -13,13 +13,15 @@ import path from 'path';
|
|
13
13
|
import { fileURLToPath, URL } from 'url'; // Crucial for file path/URL conversion
|
14
14
|
|
15
15
|
// === External Dependencies ===
|
16
|
-
|
16
|
+
// Using requireNamespace avoids potential ESM/CJS interop issues with mocks if they arise
|
17
|
+
// const axios = require('axios'); // Alternative if import * causes issues with mocks
|
18
|
+
import * as axiosNs from 'axios'; // Using namespace import for clarity
|
17
19
|
import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
18
20
|
|
19
21
|
// === Project Imports ===
|
20
|
-
import type { Asset, ParsedHTML } from '../types';
|
21
|
-
import { guessMimeType } from '../utils/mime';
|
22
|
-
import { Logger } from '../utils/logger';
|
22
|
+
import type { Asset, ParsedHTML } from '../types'; // Adjust path if needed
|
23
|
+
import { guessMimeType } from '../utils/mime'; // Adjust path if needed
|
24
|
+
import { Logger } from '../utils/logger'; // Adjust path if needed
|
23
25
|
|
24
26
|
// === Constants ===
|
25
27
|
/** Set of asset types defined in Asset['type'] generally considered text-based */
|
@@ -31,6 +33,11 @@ const MAX_ASSET_EXTRACTION_ITERATIONS = 1000;
|
|
31
33
|
|
32
34
|
// === Helper Functions ===
|
33
35
|
|
36
|
+
/**
|
37
|
+
* Custom type for Node.js error objects with a `code` property.
|
38
|
+
*/
|
39
|
+
type NodeJSErrnoException = Error & { code?: string };
|
40
|
+
|
34
41
|
/**
|
35
42
|
* Checks if decoding a buffer as UTF-8 and re-encoding is lossy.
|
36
43
|
* @param {Buffer} originalBuffer The original binary buffer.
|
@@ -42,17 +49,21 @@ function isUtf8DecodingLossy(originalBuffer: Buffer, decodedString: string): boo
|
|
42
49
|
const reEncodedBuffer = Buffer.from(decodedString, 'utf-8');
|
43
50
|
return !originalBuffer.equals(reEncodedBuffer);
|
44
51
|
} catch (e) {
|
52
|
+
// Error during re-encoding likely means original wasn't valid UTF-8
|
45
53
|
return true;
|
46
54
|
}
|
47
55
|
}
|
48
56
|
|
49
57
|
/**
|
50
58
|
* Determines the absolute base directory URL (http://, https://, or file:///) ending in '/'.
|
59
|
+
* This is crucial for resolving relative links found in the source document.
|
51
60
|
* @param {string} inputPathOrUrl - The original source HTML file path or a full HTTP/HTTPS URL.
|
52
61
|
* @param {Logger} [logger] - Optional logger instance.
|
53
62
|
* @returns {string | undefined} The absolute base URL string ending in '/', or undefined if determination fails.
|
54
63
|
*/
|
55
64
|
function determineBaseUrl(inputPathOrUrl: string, logger?: Logger): string | undefined {
|
65
|
+
// [DEBUG LOG] Added for diagnostics
|
66
|
+
console.log(`[DEBUG determineBaseUrl] Input: "${inputPathOrUrl}"`);
|
56
67
|
logger?.debug(`Determining base URL for input: ${inputPathOrUrl}`);
|
57
68
|
if (!inputPathOrUrl) {
|
58
69
|
logger?.warn('Cannot determine base URL: inputPathOrUrl is empty or invalid.');
|
@@ -60,228 +71,265 @@ function determineBaseUrl(inputPathOrUrl: string, logger?: Logger): string | und
|
|
60
71
|
}
|
61
72
|
|
62
73
|
try {
|
74
|
+
// Handle non-file URLs (HTTP, HTTPS)
|
63
75
|
if (/^https?:\/\//i.test(inputPathOrUrl)) {
|
64
76
|
const url = new URL(inputPathOrUrl);
|
77
|
+
// Get URL up to the last slash in the path
|
65
78
|
url.pathname = url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1);
|
66
|
-
url.search = ''; url.hash = '';
|
79
|
+
url.search = ''; url.hash = ''; // Clear query params/fragments
|
67
80
|
const baseUrl = url.href;
|
68
81
|
logger?.debug(`Determined remote base URL: ${baseUrl}`);
|
69
|
-
|
82
|
+
// [DEBUG LOG] Added for diagnostics
|
83
|
+
console.log(`[DEBUG determineBaseUrl] Determined Remote URL: "${baseUrl}"`);
|
84
|
+
return baseUrl; // URLs from constructor usually end in '/' if path ends in '/'
|
70
85
|
}
|
86
|
+
// Handle other protocols (warn and return undefined)
|
71
87
|
else if (inputPathOrUrl.includes('://') && !inputPathOrUrl.startsWith('file:')) {
|
72
88
|
logger?.warn(`Input "${inputPathOrUrl}" looks like a URL but uses an unsupported protocol. Cannot determine base URL.`);
|
89
|
+
// [DEBUG LOG] Added for diagnostics
|
90
|
+
console.log(`[DEBUG determineBaseUrl] Unsupported protocol.`);
|
73
91
|
return undefined;
|
74
92
|
}
|
93
|
+
// Handle file paths and file: URLs
|
75
94
|
else {
|
76
|
-
let
|
95
|
+
let resourcePath: string; // Path to the actual file or dir input
|
96
|
+
let isInputLikelyDirectory = false;
|
97
|
+
|
98
|
+
// Convert input to an absolute path
|
77
99
|
if (inputPathOrUrl.startsWith('file:')) {
|
78
|
-
|
79
|
-
|
100
|
+
resourcePath = fileURLToPath(inputPathOrUrl);
|
101
|
+
// file: URLs ending in / strongly suggest a directory
|
102
|
+
isInputLikelyDirectory = inputPathOrUrl.endsWith('/');
|
80
103
|
} else {
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
isDirectory = false;
|
104
|
+
resourcePath = path.resolve(inputPathOrUrl); // Resolve relative/absolute file paths
|
105
|
+
// Check if the resolved path *actually* exists and is a directory
|
106
|
+
// This distinguishes 'C:\path\to\dir' from 'C:\path\to\file.html'
|
107
|
+
try {
|
108
|
+
// Use statSync carefully - assumes it's available and works (or mocked)
|
109
|
+
isInputLikelyDirectory = fs.statSync(resourcePath).isDirectory();
|
110
|
+
} catch {
|
111
|
+
// If stat fails (ENOENT, EACCES), assume it refers to a file path
|
112
|
+
isInputLikelyDirectory = false;
|
113
|
+
}
|
92
114
|
}
|
93
|
-
|
94
|
-
|
115
|
+
// [DEBUG LOG] Added for diagnostics
|
116
|
+
console.log(`[DEBUG determineBaseUrl] resourcePath: "${resourcePath}", isInputLikelyDirectory: ${isInputLikelyDirectory}`);
|
117
|
+
|
118
|
+
// The base directory is the directory containing the resourcePath,
|
119
|
+
// OR resourcePath itself if it was identified as a directory.
|
120
|
+
const baseDirPath = isInputLikelyDirectory ? resourcePath : path.dirname(resourcePath);
|
121
|
+
// [DEBUG LOG] Added for diagnostics
|
122
|
+
console.log(`[DEBUG determineBaseUrl] Calculated baseDirPath: "${baseDirPath}"`);
|
123
|
+
|
124
|
+
// Convert base directory path back to a file URL ending in '/'
|
125
|
+
let normalizedPathForURL = baseDirPath.replace(/\\/g, '/'); // Use forward slashes
|
126
|
+
// Ensure leading slash for Windows file URLs (e.g., /C:/...)
|
95
127
|
if (/^[A-Z]:\//i.test(normalizedPathForURL) && !normalizedPathForURL.startsWith('/')) {
|
96
128
|
normalizedPathForURL = '/' + normalizedPathForURL;
|
97
129
|
}
|
130
|
+
// Ensure trailing slash for the directory URL
|
131
|
+
if (!normalizedPathForURL.endsWith('/')) {
|
132
|
+
normalizedPathForURL += '/';
|
133
|
+
}
|
134
|
+
|
98
135
|
const fileUrl = new URL('file://' + normalizedPathForURL);
|
99
|
-
|
100
|
-
|
101
|
-
logger?.debug(`Determined
|
136
|
+
const fileUrlString = fileUrl.href;
|
137
|
+
|
138
|
+
logger?.debug(`Determined base URL: ${fileUrlString} (from: ${inputPathOrUrl}, resolved base dir: ${baseDirPath})`);
|
139
|
+
// [DEBUG LOG] Added for diagnostics
|
140
|
+
console.log(`[DEBUG determineBaseUrl] Determined File URL: "${fileUrlString}"`);
|
102
141
|
return fileUrlString;
|
142
|
+
|
103
143
|
}
|
104
144
|
} catch (error: unknown) {
|
105
145
|
const message = error instanceof Error ? error.message : String(error);
|
106
|
-
|
146
|
+
// [DEBUG LOG] Added for diagnostics
|
147
|
+
console.error(`[DEBUG determineBaseUrl] Error determining base URL: ${message}`);
|
148
|
+
logger?.error(`💀 Failed to determine base URL for "${inputPathOrUrl}": ${message}${error instanceof Error && error.stack ? ` - Stack: ${error.stack}` : ''}`);
|
107
149
|
return undefined;
|
108
150
|
}
|
109
151
|
}
|
110
152
|
|
111
153
|
/**
|
112
154
|
* Resolves an asset URL relative to a base URL context.
|
113
|
-
*
|
114
|
-
* @param {string}
|
155
|
+
* Handles data URIs, fragments, protocol-relative URLs.
|
156
|
+
* @param {string} assetUrl - The raw URL string found in the source (e.g., href, src).
|
157
|
+
* @param {string} [baseContextUrl] - The absolute base URL of the containing document (HTML or CSS).
|
115
158
|
* @param {Logger} [logger] - Optional logger instance.
|
116
|
-
* @returns {URL | null} A validated, absolute URL object or null.
|
159
|
+
* @returns {URL | null} A validated, absolute URL object, or null if invalid/ignorable.
|
117
160
|
*/
|
118
161
|
function resolveAssetUrl(assetUrl: string, baseContextUrl?: string, logger?: Logger): URL | null {
|
119
162
|
const trimmedUrl = assetUrl?.trim();
|
163
|
+
// Ignore empty, data URIs, or fragment-only URLs
|
120
164
|
if (!trimmedUrl || trimmedUrl.startsWith('data:') || trimmedUrl.startsWith('#')) {
|
121
165
|
return null;
|
122
166
|
}
|
167
|
+
|
123
168
|
let resolvableUrl = trimmedUrl;
|
169
|
+
|
170
|
+
// Handle protocol-relative URLs (e.g., //example.com/image.png)
|
124
171
|
if (resolvableUrl.startsWith('//') && baseContextUrl) {
|
125
172
|
try {
|
126
173
|
const base = new URL(baseContextUrl);
|
127
|
-
resolvableUrl = base.protocol + resolvableUrl;
|
174
|
+
resolvableUrl = base.protocol + resolvableUrl; // Prepend the base protocol (http: or https:)
|
128
175
|
} catch (e) {
|
129
176
|
logger?.warn(`Could not extract protocol from base "${baseContextUrl}" for protocol-relative URL "${trimmedUrl}". Skipping.`);
|
130
177
|
return null;
|
131
178
|
}
|
132
179
|
}
|
180
|
+
|
133
181
|
try {
|
182
|
+
// Use URL constructor for resolution. Handles absolute, relative paths, ../ etc.
|
183
|
+
// baseContextUrl provides the context for resolving relative URLs.
|
134
184
|
const resolved = new URL(resolvableUrl, baseContextUrl);
|
185
|
+
// Don't attempt to fetch ws://, mailto:, etc. Add protocols as needed.
|
186
|
+
if (!['http:', 'https:', 'file:'].includes(resolved.protocol)) {
|
187
|
+
logger?.debug(`Skipping asset with unsupported protocol: ${resolved.href}`);
|
188
|
+
return null;
|
189
|
+
}
|
135
190
|
return resolved;
|
136
191
|
} catch (error: unknown) {
|
192
|
+
// Log errors during URL parsing/resolution but don't halt the process
|
137
193
|
const message = error instanceof Error ? error.message : String(error);
|
194
|
+
// Avoid warning for relative paths when no base was provided (e.g., direct HTML string input)
|
138
195
|
if (!/^[a-z]+:/i.test(resolvableUrl) && !resolvableUrl.startsWith('/') && !baseContextUrl) {
|
139
196
|
logger?.warn(`Cannot resolve relative URL "${resolvableUrl}" - Base context URL was not provided or determined.`);
|
140
197
|
} else {
|
141
198
|
logger?.warn(`⚠️ Failed to parse/resolve URL "${resolvableUrl}" ${baseContextUrl ? 'against base "' + baseContextUrl + '"' : '(no base provided)'}: ${message}`);
|
142
199
|
}
|
143
|
-
return null;
|
200
|
+
return null; // Return null if resolution fails
|
144
201
|
}
|
145
202
|
}
|
146
203
|
|
147
204
|
/**
|
148
|
-
* Properly resolves CSS relative paths, handling "../" correctly.
|
149
|
-
*
|
150
|
-
*
|
151
|
-
* @param {string}
|
152
|
-
* @param {
|
153
|
-
* @
|
154
|
-
* @returns {string | null} The resolved absolute URL or null if resolution fails
|
205
|
+
* Properly resolves CSS relative paths (like url("../images/bg.png")), handling "../" correctly.
|
206
|
+
* Uses the CSS file's own location as the base for resolution.
|
207
|
+
* @param {string} relativeUrl - The relative URL string from CSS (e.g., "../images/bg.png").
|
208
|
+
* @param {string} cssBaseContextUrl - The absolute URL of the CSS file containing the relative URL.
|
209
|
+
* @param {Logger} [logger] - Optional logger instance.
|
210
|
+
* @returns {string | null} The resolved absolute URL string, or null if resolution fails/invalid.
|
155
211
|
*/
|
156
212
|
function resolveCssRelativeUrl(
|
157
213
|
relativeUrl: string,
|
158
|
-
cssBaseContextUrl: string,
|
214
|
+
cssBaseContextUrl: string, // e.g., file:///C:/mock/base/dir/css/deep.css or https://.../style.css
|
159
215
|
logger?: Logger
|
160
216
|
): string | null {
|
161
|
-
|
162
|
-
|
163
|
-
|
217
|
+
// [DEBUG LOG] Added for diagnostics
|
218
|
+
console.log(`[DEBUG resolveCssRelativeUrl] Input: relative="${relativeUrl}", base="${cssBaseContextUrl}"`);
|
219
|
+
|
220
|
+
if (!relativeUrl || relativeUrl.startsWith('data:') || relativeUrl.startsWith('#')) {
|
221
|
+
return null; // Ignore empty, data URIs, or fragments
|
164
222
|
}
|
165
223
|
|
166
224
|
try {
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
// If that base path is actually a directory, use it directly;
|
172
|
-
// otherwise, use its dirname. This prevents us from dropping
|
173
|
-
// the final directory name when we already have a trailing slash.
|
174
|
-
let cssDir: string;
|
175
|
-
try {
|
176
|
-
const stat = fs.statSync(basePath);
|
177
|
-
if (stat.isDirectory()) {
|
178
|
-
cssDir = basePath;
|
179
|
-
} else {
|
180
|
-
cssDir = path.dirname(basePath);
|
181
|
-
}
|
182
|
-
} catch {
|
183
|
-
// If stat fails, assume it's a file path
|
184
|
-
cssDir = path.dirname(basePath);
|
185
|
-
}
|
225
|
+
// Use the URL constructor which correctly handles relative paths including ../
|
226
|
+
// relative to the base URL provided.
|
227
|
+
const resolvedUrl = new URL(relativeUrl, cssBaseContextUrl);
|
186
228
|
|
187
|
-
|
188
|
-
|
189
|
-
|
229
|
+
// [DEBUG LOG] Added for diagnostics
|
230
|
+
console.log(`[DEBUG resolveCssRelativeUrl] Resolved URL object href: "${resolvedUrl.href}"`);
|
231
|
+
return resolvedUrl.href; // Return the resolved absolute URL string
|
190
232
|
|
191
|
-
// On Windows, ensure file:///C:/something
|
192
|
-
if (/^[A-Z]:/i.test(resolvedPath) && !resolvedPath.startsWith('/')) {
|
193
|
-
resolvedPath = '/' + resolvedPath;
|
194
|
-
}
|
195
|
-
return `file://${resolvedPath}`;
|
196
|
-
} else {
|
197
|
-
// For http/https etc., do standard resolution
|
198
|
-
return new URL(relativeUrl, cssBaseContextUrl).href;
|
199
|
-
}
|
200
233
|
} catch (error) {
|
234
|
+
// Log warning if URL resolution fails for some reason
|
201
235
|
logger?.warn(
|
202
|
-
`Failed to resolve CSS URL: "${relativeUrl}"
|
236
|
+
`Failed to resolve CSS URL: "${relativeUrl}" relative to "${cssBaseContextUrl}": ${String(error)}`
|
203
237
|
);
|
238
|
+
// [DEBUG LOG] Added for diagnostics
|
239
|
+
console.error(`[DEBUG resolveCssRelativeUrl] Error resolving: ${String(error)}`);
|
204
240
|
return null;
|
205
241
|
}
|
206
242
|
}
|
207
243
|
|
208
244
|
|
209
245
|
/**
|
210
|
-
* Asynchronously fetches the content of a resolved asset URL.
|
211
|
-
* @async
|
212
|
-
* @param {URL} resolvedUrl - The absolute URL object of the asset to fetch.
|
213
|
-
* @param {Logger} [logger] - Optional logger instance.
|
214
|
-
* @param {number} [timeout=10000] - Network timeout in milliseconds.
|
215
|
-
* @returns {Promise<Buffer | null>} Asset content as a Buffer, or null on failure.
|
216
|
-
*/
|
217
|
-
/**
|
218
|
-
* Asynchronously fetches the content of a resolved asset URL.
|
246
|
+
* Asynchronously fetches the content of a resolved asset URL (http, https, file).
|
219
247
|
* @async
|
220
248
|
* @param {URL} resolvedUrl - The absolute URL object of the asset to fetch.
|
221
249
|
* @param {Logger} [logger] - Optional logger instance.
|
222
|
-
* @param {number} [timeout=10000] - Network timeout in milliseconds.
|
250
|
+
* @param {number} [timeout=10000] - Network timeout in milliseconds for HTTP(S) requests.
|
223
251
|
* @returns {Promise<Buffer | null>} Asset content as a Buffer, or null on failure.
|
224
252
|
*/
|
225
253
|
async function fetchAsset(resolvedUrl: URL, logger?: Logger, timeout: number = 10000): Promise<Buffer | null> {
|
254
|
+
// [DEBUG LOG] Added for diagnostics
|
255
|
+
console.log(`[DEBUG fetchAsset] Attempting fetch for URL: ${resolvedUrl.href}`);
|
226
256
|
logger?.debug(`Attempting to fetch asset: ${resolvedUrl.href}`);
|
227
257
|
const protocol = resolvedUrl.protocol;
|
228
258
|
|
229
259
|
try {
|
230
260
|
if (protocol === 'http:' || protocol === 'https:') {
|
231
|
-
|
261
|
+
// Use axios namespace import's default property
|
262
|
+
const response: AxiosResponse<ArrayBuffer> = await axiosNs.default.get(resolvedUrl.href, {
|
232
263
|
responseType: 'arraybuffer', timeout: timeout,
|
233
264
|
});
|
234
|
-
|
235
|
-
|
265
|
+
logger?.debug(`Workspaceed remote asset ${resolvedUrl.href} (Status: ${response.status}, Type: ${response.headers['content-type'] || 'N/A'}, Size: ${response.data?.byteLength ?? 0} bytes)`);
|
266
|
+
// [DEBUG LOG] Added for diagnostics
|
267
|
+
console.log(`[DEBUG fetchAsset] HTTP fetch SUCCESS for: ${resolvedUrl.href}, Status: ${response.status}`);
|
268
|
+
return Buffer.from(response.data);
|
236
269
|
} else if (protocol === 'file:') {
|
237
270
|
let filePath: string;
|
238
271
|
try {
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
272
|
+
// Convert file URL to path. IMPORTANT: This strips query params and fragments.
|
273
|
+
filePath = fileURLToPath(resolvedUrl);
|
274
|
+
} catch (e: any) {
|
275
|
+
// [DEBUG LOG] Added for diagnostics
|
276
|
+
console.error(`[DEBUG fetchAsset] fileURLToPath FAILED for: ${resolvedUrl.href}`, e);
|
277
|
+
logger?.error(`Could not convert file URL to path: ${resolvedUrl.href}. Error: ${e.message}`);
|
278
|
+
return null;
|
279
|
+
}
|
280
|
+
|
281
|
+
const normalizedForLog = path.normalize(filePath);
|
282
|
+
// [DEBUG LOG] Added for diagnostics
|
283
|
+
console.log(`[DEBUG fetchAsset] Attempting readFile with path: "${normalizedForLog}" (Original from URL: "${filePath}")`);
|
284
|
+
|
285
|
+
// Read file using fs/promises
|
286
|
+
const data = await readFile(filePath); // This call uses the mock in tests
|
287
|
+
|
288
|
+
// [DEBUG LOG] Added for diagnostics
|
289
|
+
console.log(`[DEBUG fetchAsset] readFile call SUCCEEDED for path: "${normalizedForLog}". Data length: ${data?.byteLength}`);
|
290
|
+
logger?.debug(`Read local file ${filePath} (${data.byteLength} bytes)`);
|
248
291
|
return data;
|
249
292
|
} else {
|
250
|
-
|
293
|
+
// [DEBUG LOG] Added for diagnostics
|
294
|
+
console.log(`[DEBUG fetchAsset] Unsupported protocol: ${protocol}`);
|
295
|
+
logger?.warn(`Unsupported protocol "${protocol}" in URL: ${resolvedUrl.href}`);
|
251
296
|
return null;
|
252
297
|
}
|
253
298
|
} catch (error: unknown) {
|
254
|
-
|
299
|
+
// [DEBUG LOG] Added for diagnostics
|
300
|
+
const failedId = protocol === 'file:' ? path.normalize(fileURLToPath(resolvedUrl)) : resolvedUrl.href;
|
301
|
+
console.error(`[DEBUG fetchAsset] fetch/read FAILED for: "${failedId}". Error:`, error);
|
255
302
|
|
256
|
-
//
|
257
|
-
|
303
|
+
// --- Handle Errors Based on Protocol/Context ---
|
304
|
+
// Use the imported namespace directly for isAxiosError check
|
305
|
+
if ((protocol === 'http:' || protocol === 'https:') && axiosNs.isAxiosError(error)) {
|
258
306
|
const status = error.response?.status ?? 'N/A';
|
259
307
|
const statusText = error.response?.statusText ?? 'Error';
|
260
308
|
const code = error.code ?? 'N/A';
|
261
309
|
const message = error.message;
|
262
|
-
//
|
310
|
+
// Format consistent with test expectations
|
263
311
|
const logMessage = `⚠️ Failed to fetch remote asset ${resolvedUrl.href}: Status ${status} - ${statusText}. Code: ${code}, Message: ${message}`;
|
264
312
|
logger?.warn(logMessage);
|
265
313
|
}
|
266
314
|
// Check for specific FS errors (only relevant if protocol was file:)
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
315
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
316
|
+
let failedPath = resolvedUrl.href; // Fallback path for logging if conversion fails
|
317
|
+
try { failedPath = fileURLToPath(resolvedUrl); } catch { /* ignore */ }
|
318
|
+
failedPath = path.normalize(failedPath); // Normalize for consistent logging
|
271
319
|
|
272
|
-
if (error instanceof Error && (error as
|
320
|
+
if (error instanceof Error && (error as NodeJSErrnoException).code === 'ENOENT') {
|
273
321
|
logger?.warn(`⚠️ File not found (ENOENT) for asset: ${failedPath}.`);
|
274
|
-
} else if (error instanceof Error && (error as
|
275
|
-
|
276
|
-
|
322
|
+
} else if (error instanceof Error && (error as NodeJSErrnoException).code === 'EACCES') {
|
323
|
+
// Log EACCES specifically for tests to catch if needed
|
324
|
+
logger?.warn(`⚠️ Permission denied (EACCES) reading asset: ${failedPath}.`);
|
325
|
+
// Also log the more generic message that the test currently expects
|
277
326
|
logger?.warn(`⚠️ Failed to read local asset ${failedPath}: ${error.message}`);
|
327
|
+
} else if (error instanceof Error) {
|
328
|
+
logger?.warn(`⚠️ Failed to read local asset ${failedPath}: ${error.message}`);
|
278
329
|
} else {
|
279
|
-
|
330
|
+
logger?.warn(`⚠️ An unknown error occurred while reading local asset ${failedPath}: ${String(error)}`);
|
280
331
|
}
|
281
332
|
}
|
282
|
-
// Check for other specific errors like invalid URL types if necessary (ERR_INVALID_URL handled above mostly)
|
283
|
-
// else if (error instanceof TypeError && error.message.includes('ERR_INVALID_URL')) { ... }
|
284
|
-
|
285
333
|
// Generic fallback for truly unexpected errors during fetch/read
|
286
334
|
else if (error instanceof Error) {
|
287
335
|
logger?.warn(`⚠️ An unexpected error occurred processing asset ${resolvedUrl.href}: ${error.message}`);
|
@@ -293,79 +341,71 @@ async function fetchAsset(resolvedUrl: URL, logger?: Logger, timeout: number = 1
|
|
293
341
|
}
|
294
342
|
|
295
343
|
/**
|
296
|
-
* Extracts URLs from CSS content and resolves them
|
297
|
-
*
|
298
|
-
* @param {string}
|
299
|
-
* @param {
|
300
|
-
* @param {
|
301
|
-
* @
|
302
|
-
*/
|
303
|
-
/**
|
304
|
-
* Extracts URLs from CSS content and resolves them against the CSS base URL.
|
305
|
-
* Returns an array of *potentially* new Asset objects with resolved URLs.
|
344
|
+
* Extracts URLs from CSS content using regex and resolves them.
|
345
|
+
* Finds `url(...)` and `@import` rules.
|
346
|
+
* @param {string} cssContent - The CSS content string to parse.
|
347
|
+
* @param {string} cssBaseContextUrl - The absolute URL of the CSS file (used for resolving relative paths).
|
348
|
+
* @param {Logger} [logger] - Optional logger instance.
|
349
|
+
* @returns {Asset[]} An array of newly discovered Asset objects (type, resolved URL, content initially undefined).
|
306
350
|
*/
|
307
351
|
function extractUrlsFromCSS(
|
308
352
|
cssContent: string,
|
309
353
|
cssBaseContextUrl: string,
|
310
|
-
// discoveredAssets: Asset[], // REMOVE: This function will now RETURN the assets
|
311
|
-
// visitedUrls: Set<string>, // REMOVE
|
312
354
|
logger?: Logger
|
313
|
-
): Asset[] {
|
314
|
-
const newlyDiscovered: Asset[] = [];
|
315
|
-
|
355
|
+
): Asset[] {
|
356
|
+
const newlyDiscovered: Asset[] = [];
|
357
|
+
// Track URLs processed within this specific CSS file to avoid adding duplicates from the same file
|
358
|
+
const processedInThisParse = new Set<string>();
|
316
359
|
|
360
|
+
// Regex for url(...) patterns, handling optional quotes
|
317
361
|
const urlRegex = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
|
362
|
+
// Regex for @import rules, handling url() or bare string, optional quotes
|
318
363
|
const importRegex = /@import\s+(?:url\(\s*(['"]?)(.*?)\1\s*\)|(['"])(.*?)\3)\s*;/gi;
|
319
364
|
|
365
|
+
/** Internal helper to process a found URL string */
|
320
366
|
const processFoundUrl = (rawUrl: string | undefined, ruleType: '@import' | 'url()') => {
|
321
367
|
if (!rawUrl || rawUrl.trim() === '' || rawUrl.startsWith('data:')) return;
|
322
368
|
|
323
369
|
const resolvedUrl = resolveCssRelativeUrl(rawUrl, cssBaseContextUrl, logger);
|
324
370
|
|
325
|
-
//
|
371
|
+
// If successfully resolved and not already found in *this* CSS file
|
326
372
|
if (resolvedUrl && !processedInThisParse.has(resolvedUrl)) {
|
327
|
-
processedInThisParse.add(resolvedUrl);
|
328
|
-
const { assetType } = guessMimeType(resolvedUrl);
|
373
|
+
processedInThisParse.add(resolvedUrl);
|
374
|
+
const { assetType } = guessMimeType(resolvedUrl); // Guess type based on resolved URL
|
329
375
|
|
330
|
-
// Add to the list
|
376
|
+
// Add to the list of assets discovered in this pass
|
331
377
|
newlyDiscovered.push({
|
332
378
|
type: assetType,
|
333
|
-
url: resolvedUrl, // The resolved URL string
|
334
|
-
content: undefined
|
379
|
+
url: resolvedUrl, // The resolved absolute URL string
|
380
|
+
content: undefined // Content will be fetched later if needed
|
335
381
|
});
|
336
|
-
|
382
|
+
logger?.debug(`Discovered nested ${assetType} asset (${ruleType}) in CSS ${cssBaseContextUrl}: ${resolvedUrl}`);
|
337
383
|
}
|
338
384
|
};
|
339
385
|
|
340
|
-
//
|
341
|
-
urlRegex.lastIndex = 0;
|
342
|
-
importRegex.lastIndex = 0;
|
386
|
+
// Execute regex for url(...)
|
343
387
|
let match;
|
344
388
|
while ((match = urlRegex.exec(cssContent)) !== null) {
|
345
|
-
processFoundUrl(match[2], 'url()');
|
389
|
+
processFoundUrl(match[2], 'url()'); // Group 2 captures the URL part
|
346
390
|
}
|
347
|
-
|
391
|
+
|
392
|
+
// Execute regex for @import
|
393
|
+
// Reset lastIndex as we're using the same regex instance implicitly if defined outside loop
|
394
|
+
importRegex.lastIndex = 0; // Explicitly reset
|
348
395
|
while ((match = importRegex.exec(cssContent)) !== null) {
|
396
|
+
// Group 2 captures url('...'), Group 4 captures bare "..."
|
349
397
|
processFoundUrl(match[2] || match[4], '@import');
|
350
398
|
}
|
351
399
|
|
352
|
-
return newlyDiscovered;
|
400
|
+
return newlyDiscovered;
|
353
401
|
}
|
354
402
|
|
355
|
-
/**
|
356
|
-
* Extracts all discoverable assets recursively from HTML and CSS.
|
357
|
-
* @async
|
358
|
-
* @export
|
359
|
-
* @param {ParsedHTML} parsed - Initial parsed HTML data.
|
360
|
-
* @param {boolean} [embedAssets=true] - Whether to embed content.
|
361
|
-
* @param {string} [inputPathOrUrl] - Original HTML source location.
|
362
|
-
* @param {Logger} [logger] - Optional logger instance.
|
363
|
-
* @returns {Promise<ParsedHTML>} Processed data with all assets.
|
364
|
-
*/
|
365
403
|
/**
|
366
404
|
* Extracts all discoverable assets recursively from HTML and CSS.
|
367
405
|
* Fetches assets if embedAssets is true or if the asset is CSS (to parse for more assets).
|
368
406
|
* Resolves URLs relative to their context (HTML base or CSS file location).
|
407
|
+
* Handles potential infinite loops with an iteration limit.
|
408
|
+
*
|
369
409
|
* @async
|
370
410
|
* @export
|
371
411
|
* @param {ParsedHTML} parsed - Initial parsed HTML data containing `htmlContent` and an initial `assets` array.
|
@@ -387,8 +427,10 @@ export async function extractAssets(
|
|
387
427
|
const finalAssetsMap = new Map<string, Asset>();
|
388
428
|
// Queue holds assets to be processed: { url: string (resolved), type: ..., content?: ... }
|
389
429
|
let assetsToProcess: Asset[] = [];
|
430
|
+
// Set to track URLs that are already processed (in finalAssetsMap) OR currently in the queue (assetsToProcess)
|
431
|
+
const processedOrQueuedUrls = new Set<string>();
|
390
432
|
|
391
|
-
// Determine
|
433
|
+
// --- Determine Base URL Context ---
|
392
434
|
const htmlBaseContextUrl = determineBaseUrl(inputPathOrUrl || '', logger);
|
393
435
|
if (!htmlBaseContextUrl && initialAssets.some(a => !/^[a-z]+:/i.test(a.url) && !a.url.startsWith('data:') && !a.url.startsWith('#') && !a.url.startsWith('/'))) {
|
394
436
|
logger?.warn("🚨 No valid base path/URL determined for the HTML source! Resolution of relative asset paths from HTML may fail.");
|
@@ -396,18 +438,16 @@ export async function extractAssets(
|
|
396
438
|
logger?.debug(`Using HTML base context URL: ${htmlBaseContextUrl}`);
|
397
439
|
}
|
398
440
|
|
399
|
-
// --- CORRECTED: Define processedOrQueuedUrls HERE in the main function scope ---
|
400
|
-
// Set to track URLs that are already processed (in finalAssetsMap) OR currently in the queue (assetsToProcess)
|
401
|
-
// This prevents adding the same asset to the queue multiple times.
|
402
|
-
const processedOrQueuedUrls = new Set<string>();
|
403
|
-
|
404
441
|
// --- Initial Queue Population ---
|
405
442
|
logger?.debug(`Queueing ${initialAssets.length} initial assets parsed from HTML...`);
|
406
443
|
for (const asset of initialAssets) {
|
407
444
|
// Resolve the initial asset URL against the HTML base context
|
408
445
|
const resolvedUrlObj = resolveAssetUrl(asset.url, htmlBaseContextUrl, logger);
|
409
|
-
|
410
|
-
|
446
|
+
if (!resolvedUrlObj) {
|
447
|
+
logger?.debug(` -> Skipping initial asset with unresolvable/ignorable URL: ${asset.url}`);
|
448
|
+
continue; // Skip if URL is invalid or data URI etc.
|
449
|
+
}
|
450
|
+
const urlToQueue = resolvedUrlObj.href; // Use the resolved absolute URL string
|
411
451
|
|
412
452
|
// Skip data URIs and check if this URL is already tracked
|
413
453
|
if (!urlToQueue.startsWith('data:') && !processedOrQueuedUrls.has(urlToQueue)) {
|
@@ -419,15 +459,15 @@ export async function extractAssets(
|
|
419
459
|
|
420
460
|
// Add to the processing queue
|
421
461
|
assetsToProcess.push({
|
422
|
-
url: urlToQueue,
|
462
|
+
url: urlToQueue, // Use the resolved URL
|
423
463
|
type: initialType,
|
424
464
|
content: undefined
|
425
465
|
});
|
426
|
-
|
466
|
+
logger?.debug(` -> Queued initial asset: ${urlToQueue} (Original raw: ${asset.url})`);
|
427
467
|
} else if (urlToQueue.startsWith('data:')) {
|
428
|
-
|
468
|
+
logger?.debug(` -> Skipping data URI: ${urlToQueue.substring(0, 50)}...`);
|
429
469
|
} else {
|
430
|
-
logger?.debug(` -> Skipping already queued initial asset: ${urlToQueue}`);
|
470
|
+
logger?.debug(` -> Skipping already processed/queued initial asset: ${urlToQueue}`);
|
431
471
|
}
|
432
472
|
}
|
433
473
|
|
@@ -449,16 +489,16 @@ export async function extractAssets(
|
|
449
489
|
break; // Exit loop
|
450
490
|
}
|
451
491
|
|
452
|
-
// Process assets in batches
|
492
|
+
// Process assets in batches for clarity in logs
|
453
493
|
const currentBatch = [...assetsToProcess];
|
454
494
|
assetsToProcess = []; // Clear queue for the next batch discovered in this iteration
|
455
495
|
|
456
496
|
logger?.debug(`--- Processing batch ${iterationCount}: ${currentBatch.length} asset(s) ---`);
|
457
497
|
|
458
498
|
for (const asset of currentBatch) {
|
459
|
-
// Skip if already fully processed
|
499
|
+
// Skip if already fully processed (e.g., added in a previous batch)
|
460
500
|
if (finalAssetsMap.has(asset.url)) {
|
461
|
-
|
501
|
+
logger?.debug(`Skipping asset already in final map: ${asset.url}`);
|
462
502
|
continue;
|
463
503
|
}
|
464
504
|
|
@@ -467,36 +507,38 @@ export async function extractAssets(
|
|
467
507
|
let cssContentForParsing: string | undefined = undefined; // For CSS parsing
|
468
508
|
|
469
509
|
// --- Determine if fetching is needed ---
|
510
|
+
// Fetch if embedding everything OR if it's CSS (need content for parsing)
|
470
511
|
const needsFetching = embedAssets || asset.type === 'css';
|
471
|
-
let assetUrlObj: URL | null = null;
|
512
|
+
let assetUrlObj: URL | null = null; // URL object needed for fetchAsset
|
472
513
|
|
473
514
|
if (needsFetching) {
|
474
515
|
// --- Create URL object for fetching ---
|
475
516
|
try {
|
476
|
-
assetUrlObj = new URL(asset.url);
|
517
|
+
assetUrlObj = new URL(asset.url); // Asset URL should be absolute here
|
477
518
|
} catch (urlError) {
|
478
|
-
|
479
|
-
finalAssetsMap.set(asset.url, { ...asset, content: undefined });
|
519
|
+
logger?.warn(`Cannot create URL object for "${asset.url}", skipping fetch. Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
|
520
|
+
finalAssetsMap.set(asset.url, { ...asset, content: undefined }); // Store asset without content
|
480
521
|
continue; // Skip to next asset in batch
|
481
522
|
}
|
482
523
|
|
483
524
|
// --- Fetch Asset ---
|
484
525
|
if (assetUrlObj) {
|
485
526
|
assetContentBuffer = await fetchAsset(assetUrlObj, logger);
|
527
|
+
// fetchAsset returns null on failure
|
486
528
|
}
|
487
529
|
} // End if(needsFetching)
|
488
530
|
|
489
|
-
// --- If fetching was needed but failed,
|
531
|
+
// --- If fetching was needed but failed, store asset without content and skip ---
|
490
532
|
if (needsFetching && assetContentBuffer === null) {
|
491
|
-
|
533
|
+
logger?.debug(`Storing asset ${asset.url} without content due to fetch failure.`);
|
492
534
|
finalAssetsMap.set(asset.url, { ...asset, content: undefined });
|
493
535
|
continue; // Skip to next asset in batch
|
494
536
|
}
|
495
537
|
|
496
538
|
// --- Prepare Content for Storing/Embedding (if fetched successfully) ---
|
497
539
|
if (assetContentBuffer) { // Only proceed if content was fetched
|
498
|
-
const mimeInfo = guessMimeType(asset.url);
|
499
|
-
const effectiveMime = mimeInfo.mime || 'application/octet-stream';
|
540
|
+
const mimeInfo = guessMimeType(asset.url); // Guess MIME based on URL extension
|
541
|
+
const effectiveMime = mimeInfo.mime || 'application/octet-stream'; // Fallback MIME
|
500
542
|
|
501
543
|
// Try to decode TEXT types as UTF-8
|
502
544
|
if (TEXT_ASSET_TYPES.has(asset.type)) {
|
@@ -508,29 +550,29 @@ export async function extractAssets(
|
|
508
550
|
} catch (e) { textContent = undefined; wasLossy = true; }
|
509
551
|
|
510
552
|
if (!wasLossy && textContent !== undefined) {
|
511
|
-
//
|
553
|
+
// If embedding, store the text content
|
512
554
|
if (embedAssets) {
|
513
555
|
finalContent = textContent;
|
514
556
|
} else {
|
515
557
|
finalContent = undefined; // Not embedding text
|
516
558
|
}
|
517
|
-
// If it's CSS, store
|
559
|
+
// If it's CSS, store its text content for parsing regardless of embedding
|
518
560
|
if (asset.type === 'css') {
|
519
561
|
cssContentForParsing = textContent;
|
520
562
|
}
|
521
563
|
} else {
|
522
564
|
// Decoding failed or was lossy
|
523
|
-
logger?.warn(`Could not decode ${asset.type} ${asset.url} as valid UTF-8 text.${embedAssets ? ' Falling back to base64 data URI.' : ''}`);
|
565
|
+
logger?.warn(`Could not decode ${asset.type} asset ${asset.url} as valid UTF-8 text.${embedAssets ? ' Falling back to base64 data URI.' : ''}`);
|
524
566
|
cssContentForParsing = undefined; // Cannot parse if decoding failed
|
525
|
-
// Embed as base64 if requested
|
567
|
+
// Embed as base64 data URI if requested
|
526
568
|
if (embedAssets) {
|
527
569
|
finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString('base64')}`;
|
528
570
|
} else {
|
529
|
-
finalContent = undefined;
|
571
|
+
finalContent = undefined;
|
530
572
|
}
|
531
573
|
}
|
532
574
|
}
|
533
|
-
// Embed BINARY types as base64 if requested
|
575
|
+
// Embed BINARY types as base64 data URI if requested
|
534
576
|
else if (BINARY_ASSET_TYPES.has(asset.type)) {
|
535
577
|
if (embedAssets) {
|
536
578
|
finalContent = `data:${effectiveMime};base64,${assetContentBuffer.toString('base64')}`;
|
@@ -539,18 +581,18 @@ export async function extractAssets(
|
|
539
581
|
}
|
540
582
|
cssContentForParsing = undefined; // Not CSS
|
541
583
|
}
|
542
|
-
// Handle 'other' types:
|
584
|
+
// Handle 'other' types: attempt text decode, fallback to base64 if embedding
|
543
585
|
else { // asset.type === 'other' or unknown
|
544
|
-
|
586
|
+
cssContentForParsing = undefined; // Not CSS
|
545
587
|
if (embedAssets) {
|
546
588
|
try {
|
547
589
|
const attemptedTextContent = assetContentBuffer.toString('utf-8');
|
548
590
|
if (isUtf8DecodingLossy(assetContentBuffer, attemptedTextContent)) {
|
549
|
-
|
550
|
-
|
591
|
+
logger?.warn(`Couldn't embed unclassified asset ${asset.url} as text due to invalid UTF-8 sequences. Falling back to base64 (octet-stream).`);
|
592
|
+
finalContent = `data:application/octet-stream;base64,${assetContentBuffer.toString('base64')}`;
|
551
593
|
} else {
|
552
|
-
|
553
|
-
|
594
|
+
finalContent = attemptedTextContent;
|
595
|
+
logger?.debug(`Successfully embedded unclassified asset ${asset.url} as text.`);
|
554
596
|
}
|
555
597
|
} catch (decodeError) {
|
556
598
|
logger?.warn(`Error during text decoding for unclassified asset ${asset.url}: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}. Falling back to base64.`);
|
@@ -560,8 +602,7 @@ export async function extractAssets(
|
|
560
602
|
finalContent = undefined; // Not embedding
|
561
603
|
}
|
562
604
|
}
|
563
|
-
} else {
|
564
|
-
// Content was not fetched
|
605
|
+
} else { // Content was not fetched (e.g., embedAssets=false and not CSS)
|
565
606
|
finalContent = undefined;
|
566
607
|
cssContentForParsing = undefined;
|
567
608
|
}
|
@@ -569,35 +610,34 @@ export async function extractAssets(
|
|
569
610
|
// --- Store the final asset ---
|
570
611
|
// Use the resolved URL as the key and in the asset object itself
|
571
612
|
finalAssetsMap.set(asset.url, { ...asset, url: asset.url, content: finalContent });
|
572
|
-
// Note: URL
|
613
|
+
// Note: URL was already added to processedOrQueuedUrls when initially queued or discovered
|
573
614
|
|
574
615
|
// --- Process CSS for nested assets ---
|
575
616
|
// Only if it's CSS and we successfully decoded its content for parsing
|
576
617
|
if (asset.type === 'css' && cssContentForParsing) {
|
577
|
-
// Determine the base URL *for this specific CSS file*
|
578
|
-
|
618
|
+
// Determine the base URL *for this specific CSS file* to resolve its relative links
|
619
|
+
const cssBaseContextUrl = determineBaseUrl(asset.url, logger); // CSS URL is absolute here
|
579
620
|
logger?.debug(`CSS base context for resolving nested assets within ${asset.url}: ${cssBaseContextUrl}`);
|
580
621
|
|
581
622
|
if (cssBaseContextUrl) {
|
582
|
-
// Get the list of *potentially* new assets discovered in this CSS
|
623
|
+
// Get the list of *potentially* new assets discovered in this CSS file's content
|
583
624
|
const newlyDiscoveredAssets = extractUrlsFromCSS(
|
584
625
|
cssContentForParsing,
|
585
|
-
cssBaseContextUrl,
|
626
|
+
cssBaseContextUrl, // Use CSS file's base URL
|
586
627
|
logger
|
587
628
|
);
|
588
629
|
|
589
630
|
if (newlyDiscoveredAssets.length > 0) {
|
590
|
-
|
631
|
+
logger?.debug(`Discovered ${newlyDiscoveredAssets.length} nested assets in CSS ${asset.url}. Checking against queue...`);
|
591
632
|
for (const newAsset of newlyDiscoveredAssets) {
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
assetsToProcess.push(newAsset); // Add to the main queue for the *next* iteration
|
633
|
+
// CHECK: Add to queue only if this resolved URL hasn't been processed OR queued before.
|
634
|
+
if (!processedOrQueuedUrls.has(newAsset.url)) {
|
635
|
+
processedOrQueuedUrls.add(newAsset.url); // Mark as queued now
|
636
|
+
assetsToProcess.push(newAsset); // Add to the main queue for the *next* iteration
|
597
637
|
logger?.debug(` -> Queued new nested asset: ${newAsset.url}`);
|
598
|
-
|
599
|
-
|
600
|
-
|
638
|
+
} else {
|
639
|
+
logger?.debug(` -> Skipping already processed/queued nested asset: ${newAsset.url}`);
|
640
|
+
}
|
601
641
|
}
|
602
642
|
}
|
603
643
|
} else {
|