single-file-cli 2.1.1 → 2.1.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/build.sh +1 -1
- package/compile.sh +5 -5
- package/deno.json +3 -3
- package/deno.lock +14 -14
- package/lib/browser.js +7 -2
- package/lib/cdp-client.js +18 -9
- package/lib/deno-polyfill.js +19 -2
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/options.js +192 -94
- package/package.json +5 -5
- package/single-file-cli-api.js +39 -19
- package/single-file-launcher.js +5 -93
- package/test/e2e/errors-file.test.js +31 -0
- package/test/e2e/frame-gate.test.js +2 -1
- package/test/e2e/timeouts.test.js +70 -0
- package/test/e2e/urls-file.test.js +37 -0
- package/test/unit/cli-usage.test.js +56 -0
- package/test/unit/options.test.js +136 -0
- package/test/unit/write-file.test.js +37 -0
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "2.1.
|
|
1
|
+
export const version = "2.1.3";
|
package/options.js
CHANGED
|
@@ -203,76 +203,155 @@ const OPTIONS_INFO = [{
|
|
|
203
203
|
}];
|
|
204
204
|
|
|
205
205
|
const { args, exit } = Deno;
|
|
206
|
-
|
|
207
|
-
export { options, parseArgs };
|
|
206
|
+
export { getOptions, parseArgs, applySettings, parseUrlsFile };
|
|
208
207
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const optionDescription = optionInfo.description;
|
|
231
|
-
const optionDefaultValue = optionInfo.defaultValue === undefined ? "" : `(default: ${JSON.stringify(optionInfo.defaultValue)})`;
|
|
232
|
-
console.log(` --${optionName}: ${optionDescription} <${optionType}> ${optionDefaultValue}`); // eslint-disable-line no-console
|
|
233
|
-
});
|
|
234
|
-
console.log(""); // eslint-disable-line no-console
|
|
208
|
+
function parseUrlsFile(content) {
|
|
209
|
+
return content.split("\n")
|
|
210
|
+
.map(line => line.trim())
|
|
211
|
+
.filter(line => line)
|
|
212
|
+
.map(line => {
|
|
213
|
+
let optionPosition = line.indexOf(" --");
|
|
214
|
+
if (optionPosition < 0) {
|
|
215
|
+
optionPosition = line.indexOf("\t--");
|
|
216
|
+
}
|
|
217
|
+
if (optionPosition > 0) {
|
|
218
|
+
const url = line.substring(0, optionPosition).trim();
|
|
219
|
+
const { options, positionals, invalidOptions } = parseArgs(tokenizeArgs(line.substring(optionPosition + 1).trim()), false);
|
|
220
|
+
positionals.filter(positional => positional.startsWith("--")).forEach(option =>
|
|
221
|
+
console.warn(`Warning: Unknown option ${option} (${url})`)); // eslint-disable-line no-console
|
|
222
|
+
invalidOptions.forEach(({ name, value }) => console.warn(value === undefined ? // eslint-disable-line no-console
|
|
223
|
+
`Warning: Missing value for --${name} (${url})` :
|
|
224
|
+
`Warning: Invalid value for --${name}: ${JSON.stringify(value)} (${url})`));
|
|
225
|
+
return [url, options];
|
|
226
|
+
} else {
|
|
227
|
+
return line;
|
|
228
|
+
}
|
|
235
229
|
});
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function tokenizeArgs(argsString) {
|
|
233
|
+
const args = [];
|
|
234
|
+
let previousCharacter, previousPreviousCharacter, lastQuoteCharacter;
|
|
235
|
+
let lastCharIndex = 0;
|
|
236
|
+
for (let currentCharIndex = 0; currentCharIndex < argsString.length; currentCharIndex++) {
|
|
237
|
+
const character = argsString[currentCharIndex];
|
|
238
|
+
if (character == lastQuoteCharacter && (previousCharacter != "\\" || previousPreviousCharacter == "\\")) {
|
|
239
|
+
args.push(argsString.substring(lastCharIndex, currentCharIndex));
|
|
240
|
+
lastQuoteCharacter = null;
|
|
241
|
+
lastCharIndex = currentCharIndex + 1;
|
|
242
|
+
} else if (!lastQuoteCharacter) {
|
|
243
|
+
if (character == "'" || character == "\"") {
|
|
244
|
+
lastQuoteCharacter = character;
|
|
245
|
+
lastCharIndex = currentCharIndex + 1;
|
|
246
|
+
} else if (character == " " || character == "\t" || character == "=") {
|
|
247
|
+
if (lastCharIndex < currentCharIndex) {
|
|
248
|
+
args.push(argsString.substring(lastCharIndex, currentCharIndex));
|
|
249
|
+
}
|
|
250
|
+
lastCharIndex = currentCharIndex + 1;
|
|
251
|
+
}
|
|
239
252
|
}
|
|
253
|
+
previousPreviousCharacter = previousCharacter;
|
|
254
|
+
previousCharacter = character;
|
|
255
|
+
}
|
|
256
|
+
if (lastCharIndex < argsString.length) {
|
|
257
|
+
args.push(argsString.substring(lastCharIndex).trim());
|
|
258
|
+
}
|
|
259
|
+
return args;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function applySettings(options, settings, explicitOptions = parseArgs(Array.from(args), false).options) {
|
|
263
|
+
const profiles = settings.profiles || {};
|
|
264
|
+
let profileName = options.settingsFileProfile;
|
|
265
|
+
if (profileName == "default" || profileName === undefined) {
|
|
266
|
+
profileName = "__Default_Settings__";
|
|
267
|
+
} else if (!profiles[profileName]) {
|
|
268
|
+
const profileNames = Object.keys(profiles).filter(name => name != "__Default_Settings__");
|
|
269
|
+
throw new Error(`Unknown profile ${JSON.stringify(profileName)}, available profiles: ${profileNames.join(", ")}`);
|
|
270
|
+
}
|
|
271
|
+
Object.assign(options, profiles[profileName], explicitOptions);
|
|
272
|
+
delete options.settingsFile;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function getOptions() {
|
|
276
|
+
const { positionals, options, invalidOptions } = parseArgs(Array.from(args));
|
|
277
|
+
const unknownOptions = positionals.filter(positional => positional.startsWith("--"));
|
|
278
|
+
const urls = positionals.filter(positional => !positional.startsWith("--"));
|
|
279
|
+
if (options.help) {
|
|
280
|
+
printUsage();
|
|
240
281
|
exit(0);
|
|
241
282
|
}
|
|
242
283
|
if (options.version) {
|
|
243
284
|
console.log(version); // eslint-disable-line no-console
|
|
244
285
|
exit(0);
|
|
245
286
|
}
|
|
246
|
-
|
|
287
|
+
const errorMessages = [];
|
|
288
|
+
unknownOptions.forEach(option => errorMessages.push(`Unknown option ${option}`));
|
|
289
|
+
invalidOptions.forEach(({ name, value }) => errorMessages.push(value === undefined ?
|
|
290
|
+
`Missing value for --${name}` :
|
|
291
|
+
`Invalid value for --${name}: ${JSON.stringify(value)}`));
|
|
292
|
+
if (!urls.length && !options.urlsFile) {
|
|
293
|
+
errorMessages.push("The URL or path of the page to save is required");
|
|
294
|
+
}
|
|
295
|
+
if (urls.length > 2) {
|
|
296
|
+
errorMessages.push(`Unexpected arguments: ${urls.slice(2).join(", ")}`);
|
|
297
|
+
}
|
|
298
|
+
if (errorMessages.length) {
|
|
299
|
+
printUsage();
|
|
300
|
+
errorMessages.forEach(message => console.error(`Error: ${message}`)); // eslint-disable-line no-console
|
|
301
|
+
exit(1);
|
|
302
|
+
}
|
|
303
|
+
return { ...options, url: urls[0], output: urls[1] };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function printUsage() {
|
|
307
|
+
console.log(USAGE_TEXT + "\n"); // eslint-disable-line no-console
|
|
308
|
+
console.log("Options:"); // eslint-disable-line no-console
|
|
309
|
+
OPTIONS_INFO.forEach(category => {
|
|
310
|
+
const categoryName = CATEGORIES[OPTIONS_INFO.indexOf(category)];
|
|
311
|
+
console.log(` * ${categoryName}:`); // eslint-disable-line no-console
|
|
312
|
+
Object.keys(category).forEach(optionName => {
|
|
313
|
+
const optionInfo = category[optionName];
|
|
314
|
+
let optionType = optionInfo.type;
|
|
315
|
+
if (isArray(optionType)) {
|
|
316
|
+
optionType = optionType.replace("[]", "*");
|
|
317
|
+
}
|
|
318
|
+
const optionDescription = optionInfo.description;
|
|
319
|
+
const optionDefaultValue = optionInfo.defaultValue === undefined ? "" : `(default: ${JSON.stringify(optionInfo.defaultValue)})`;
|
|
320
|
+
console.log(` --${optionName}: ${optionDescription} <${optionType}> ${optionDefaultValue}`); // eslint-disable-line no-console
|
|
321
|
+
});
|
|
322
|
+
console.log(""); // eslint-disable-line no-console
|
|
323
|
+
});
|
|
247
324
|
}
|
|
248
325
|
|
|
249
326
|
function parseArgs(args, setDefaultValues = true) {
|
|
250
327
|
const positionals = [];
|
|
251
328
|
const options = {};
|
|
252
|
-
const
|
|
329
|
+
const invalidOptions = [];
|
|
330
|
+
const result = { positionals, options: {}, invalidOptions };
|
|
253
331
|
let argIndex = 0;
|
|
254
332
|
while (argIndex < args.length) {
|
|
255
333
|
const arg = args[argIndex];
|
|
256
|
-
const {
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
334
|
+
const { argValue, option } = parseArg(arg);
|
|
335
|
+
if (option) {
|
|
336
|
+
const optionName = option.name;
|
|
337
|
+
if (options[optionName] === undefined) {
|
|
338
|
+
options[optionName] = [];
|
|
260
339
|
}
|
|
261
|
-
let nextArgName;
|
|
262
340
|
if (argValue === undefined) {
|
|
263
|
-
if (
|
|
264
|
-
argIndex + 1
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
(
|
|
269
|
-
|
|
270
|
-
|
|
341
|
+
if (argIndex + 1 < args.length && !parseArg(args[argIndex + 1]).option) {
|
|
342
|
+
const nextArg = args[argIndex + 1];
|
|
343
|
+
if (isValid(option.info.type, nextArg)) {
|
|
344
|
+
options[optionName].push(nextArg);
|
|
345
|
+
argIndex++;
|
|
346
|
+
} else if (option.info.type.startsWith("number")) {
|
|
347
|
+
invalidOptions.push({ name: optionName, value: nextArg });
|
|
348
|
+
argIndex++;
|
|
349
|
+
}
|
|
271
350
|
}
|
|
272
|
-
} else if (isValid(
|
|
273
|
-
options[
|
|
351
|
+
} else if (isValid(option.info.type, argValue)) {
|
|
352
|
+
options[optionName].push(argValue);
|
|
274
353
|
} else {
|
|
275
|
-
|
|
354
|
+
invalidOptions.push({ name: optionName, value: argValue });
|
|
276
355
|
}
|
|
277
356
|
} else {
|
|
278
357
|
positionals.push(arg);
|
|
@@ -280,34 +359,38 @@ function parseArgs(args, setDefaultValues = true) {
|
|
|
280
359
|
argIndex++;
|
|
281
360
|
}
|
|
282
361
|
Object.keys(options).forEach(optionName => {
|
|
283
|
-
const optionInfo = getOptionInfo(optionName);
|
|
362
|
+
const optionInfo = getOptionInfo(optionName).info;
|
|
284
363
|
const optionKey = getOptionKey(optionName, optionInfo);
|
|
285
364
|
let optionValue = options[optionName];
|
|
286
365
|
const isArrayType = isArray(optionInfo.type);
|
|
366
|
+
if (!optionInfo.type.startsWith("boolean") && !optionValue.length &&
|
|
367
|
+
!invalidOptions.some(invalidOption => invalidOption.name == optionName)) {
|
|
368
|
+
invalidOptions.push({ name: optionName });
|
|
369
|
+
}
|
|
287
370
|
if (optionInfo.type.startsWith("boolean")) {
|
|
288
371
|
optionValue = optionValue.map(value => value == "true");
|
|
289
372
|
optionValue = isArrayType ?
|
|
290
373
|
optionValue.length ? optionValue : true :
|
|
291
|
-
optionValue.length ? optionValue[
|
|
374
|
+
optionValue.length ? optionValue[optionValue.length - 1] : true;
|
|
292
375
|
} else if (optionInfo.type.startsWith("number")) {
|
|
293
376
|
optionValue = optionValue.map(value => Number(value));
|
|
294
377
|
optionValue = isArrayType ?
|
|
295
378
|
optionValue.length ? optionValue : optionInfo.defaultValue || 0 :
|
|
296
|
-
optionValue.length ? optionValue[
|
|
379
|
+
optionValue.length ? optionValue[optionValue.length - 1] : optionInfo.defaultValue || 0;
|
|
297
380
|
} else {
|
|
298
381
|
optionValue = isArrayType ?
|
|
299
382
|
optionValue.length ? optionValue : optionInfo.defaultValue || "" :
|
|
300
|
-
optionValue.length ? optionValue[
|
|
383
|
+
optionValue.length ? optionValue[optionValue.length - 1] : optionInfo.defaultValue || "";
|
|
301
384
|
}
|
|
302
385
|
result.options[optionKey] = optionValue;
|
|
303
386
|
});
|
|
304
387
|
if (setDefaultValues) {
|
|
305
388
|
OPTIONS_INFO.forEach(categoryOptions => {
|
|
306
389
|
Object.keys(categoryOptions).forEach(optionName => {
|
|
307
|
-
const optionInfo =
|
|
390
|
+
const optionInfo = categoryOptions[optionName];
|
|
308
391
|
const optionKey = getOptionKey(optionName, optionInfo);
|
|
309
392
|
if (result.options[optionKey] === undefined && optionInfo.defaultValue !== undefined) {
|
|
310
|
-
result.options[optionKey] =
|
|
393
|
+
result.options[optionKey] = optionInfo.defaultValue;
|
|
311
394
|
}
|
|
312
395
|
});
|
|
313
396
|
});
|
|
@@ -332,7 +415,11 @@ function parseArgs(args, setDefaultValues = true) {
|
|
|
332
415
|
}
|
|
333
416
|
if (result.options.browserArgs) {
|
|
334
417
|
const browserArguments = result.options.browserArguments || [];
|
|
335
|
-
|
|
418
|
+
try {
|
|
419
|
+
browserArguments.push(...JSON.parse(result.options.browserArgs));
|
|
420
|
+
} catch {
|
|
421
|
+
invalidOptions.push({ name: "browser-args", value: result.options.browserArgs });
|
|
422
|
+
}
|
|
336
423
|
result.options.browserArgs = browserArguments;
|
|
337
424
|
delete result.options.browserArguments;
|
|
338
425
|
}
|
|
@@ -340,46 +427,14 @@ function parseArgs(args, setDefaultValues = true) {
|
|
|
340
427
|
result.options.browserArgs = result.options.browserArguments;
|
|
341
428
|
delete result.options.browserArguments;
|
|
342
429
|
}
|
|
343
|
-
if (result.options.errorFile) {
|
|
430
|
+
if (result.options.errorFile !== undefined) {
|
|
344
431
|
result.options.errorsFile = result.options.errorFile;
|
|
345
432
|
delete result.options.errorFile;
|
|
346
433
|
}
|
|
347
|
-
if (result.options.errorTracesDisabled) {
|
|
434
|
+
if (result.options.errorTracesDisabled !== undefined) {
|
|
348
435
|
result.options.errorsTracesDisabled = result.options.errorTracesDisabled;
|
|
349
436
|
delete result.options.errorTracesDisabled;
|
|
350
437
|
}
|
|
351
|
-
if (result.options.crawlReplaceUrls) {
|
|
352
|
-
result.options.crawlReplaceURLs = result.options.crawlReplaceUrls;
|
|
353
|
-
delete result.options.crawlReplaceUrls;
|
|
354
|
-
}
|
|
355
|
-
if (result.options.saveOriginalUrls) {
|
|
356
|
-
result.options.saveOriginalURLs = result.options.saveOriginalUrls;
|
|
357
|
-
delete result.options.saveOriginalUrls;
|
|
358
|
-
}
|
|
359
|
-
if (result.options.browserRemoteDebuggingUrl) {
|
|
360
|
-
result.options.browserRemoteDebuggingURL = result.options.browserRemoteDebuggingUrl;
|
|
361
|
-
delete result.options.browserRemoteDebuggingUrl;
|
|
362
|
-
}
|
|
363
|
-
if (result.options.crawlRemoveUrlFragment) {
|
|
364
|
-
result.options.crawlRemoveURLFragment = result.options.crawlRemoveUrlFragment;
|
|
365
|
-
delete result.options.crawlRemoveUrlFragment;
|
|
366
|
-
}
|
|
367
|
-
if (result.options.compressCss) {
|
|
368
|
-
result.options.compressCSS = result.options.compressCss;
|
|
369
|
-
delete result.options.compressCss;
|
|
370
|
-
}
|
|
371
|
-
if (result.options.compressHtml) {
|
|
372
|
-
result.options.compressHTML = result.options.compressHtml;
|
|
373
|
-
delete result.options.compressHtml;
|
|
374
|
-
}
|
|
375
|
-
if (result.options.insertMetaCsp) {
|
|
376
|
-
result.options.insertMetaCSP = result.options.insertMetaCsp;
|
|
377
|
-
delete result.options.insertMetaCsp;
|
|
378
|
-
}
|
|
379
|
-
if (result.options.blockedUrlPatterns) {
|
|
380
|
-
result.options.blockedURLPatterns = result.options.blockedUrlPatterns;
|
|
381
|
-
delete result.options.blockedUrlPatterns;
|
|
382
|
-
}
|
|
383
438
|
if (result.options.filenameReplacedCharacters) {
|
|
384
439
|
const filenameReplacedCharacters = result.options.filenameReplacedCharacters;
|
|
385
440
|
result.options.filenameReplacedCharacters = [];
|
|
@@ -404,6 +459,49 @@ function parseArgs(args, setDefaultValues = true) {
|
|
|
404
459
|
}
|
|
405
460
|
});
|
|
406
461
|
}
|
|
462
|
+
if (result.options.httpHeaders) {
|
|
463
|
+
const headers = {};
|
|
464
|
+
result.options.httpHeaders.forEach(header => {
|
|
465
|
+
const separatorIndex = header.indexOf("=");
|
|
466
|
+
if (separatorIndex <= 0) {
|
|
467
|
+
invalidOptions.push({ name: "http-header", value: header });
|
|
468
|
+
} else {
|
|
469
|
+
headers[header.substring(0, separatorIndex).trim()] = header.substring(separatorIndex + 1).trim();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
result.options.httpHeaders = headers;
|
|
473
|
+
}
|
|
474
|
+
if (result.options.emulateMediaFeatures) {
|
|
475
|
+
result.options.emulateMediaFeatures = result.options.emulateMediaFeatures
|
|
476
|
+
.map(feature => {
|
|
477
|
+
const separatorIndex = feature.indexOf(":");
|
|
478
|
+
if (separatorIndex <= 0) {
|
|
479
|
+
invalidOptions.push({ name: "emulate-media-feature", value: feature });
|
|
480
|
+
} else {
|
|
481
|
+
return {
|
|
482
|
+
name: feature.substring(0, separatorIndex),
|
|
483
|
+
value: feature.substring(separatorIndex + 1)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
.filter(feature => feature);
|
|
488
|
+
}
|
|
489
|
+
if (result.options.browserCookies) {
|
|
490
|
+
result.options.browserCookies = result.options.browserCookies.map(cookie => {
|
|
491
|
+
const [name, value, domain, path, expires, httpOnly, secure, sameSite, url] = cookie.split(",");
|
|
492
|
+
return {
|
|
493
|
+
name,
|
|
494
|
+
value,
|
|
495
|
+
url,
|
|
496
|
+
domain,
|
|
497
|
+
path,
|
|
498
|
+
secure: secure === "true",
|
|
499
|
+
httpOnly: httpOnly === "true",
|
|
500
|
+
sameSite,
|
|
501
|
+
expires: expires && !isNaN(Number(expires)) ? Number(expires) : undefined
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}
|
|
407
505
|
return result;
|
|
408
506
|
}
|
|
409
507
|
|
|
@@ -423,13 +521,13 @@ function parseArg(arg) {
|
|
|
423
521
|
const parsedArg = arg.match(ARGS_REGEX);
|
|
424
522
|
if (parsedArg && parsedArg.length) {
|
|
425
523
|
let [_, argName, argValue] = parsedArg; // eslint-disable-line no-unused-vars
|
|
426
|
-
const
|
|
524
|
+
const option = getOptionInfo(argName);
|
|
427
525
|
if (argValue !== undefined &&
|
|
428
526
|
((argValue.startsWith("\"") && argValue.endsWith("\"")) ||
|
|
429
527
|
(argValue.startsWith("'") && argValue.endsWith("'")))) {
|
|
430
528
|
argValue = argValue.substring(1, argValue.length - 1);
|
|
431
529
|
}
|
|
432
|
-
return { argName, argValue,
|
|
530
|
+
return { argName, argValue, option };
|
|
433
531
|
} else {
|
|
434
532
|
return {};
|
|
435
533
|
}
|
|
@@ -440,7 +538,7 @@ function getOptionInfo(optionName) {
|
|
|
440
538
|
OPTIONS_INFO.forEach(categoryOptions => {
|
|
441
539
|
Object.keys(categoryOptions).forEach(keyName => {
|
|
442
540
|
if (keyName.toLowerCase() == optionName.toLowerCase() || categoryOptions[keyName].alias == optionName.toLowerCase()) {
|
|
443
|
-
result = categoryOptions[keyName];
|
|
541
|
+
result = { name: keyName, info: categoryOptions[keyName] };
|
|
444
542
|
}
|
|
445
543
|
});
|
|
446
544
|
});
|
|
@@ -455,7 +553,7 @@ function isValid(type, value) {
|
|
|
455
553
|
if (type.startsWith("boolean")) {
|
|
456
554
|
return value == "true" || value == "false";
|
|
457
555
|
} else if (type.startsWith("number")) {
|
|
458
|
-
return !isNaN(value);
|
|
556
|
+
return value.trim() != "" && !isNaN(value);
|
|
459
557
|
} else {
|
|
460
558
|
return true;
|
|
461
559
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "single-file-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "SingleFile CLI",
|
|
5
5
|
"author": "Gildas Lormeau",
|
|
6
6
|
"engines": {
|
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node --test \"test/**/*.test.js\"",
|
|
13
|
+
"test": "node --test --test-concurrency=2 \"test/**/*.test.js\"",
|
|
14
14
|
"lint": "eslint ."
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
17
|
"single-file": "single-file-node.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"simple-cdp": "^1.10.
|
|
20
|
+
"simple-cdp": "^1.10.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@eslint/js": "^9.39.
|
|
24
|
-
"esbuild": "^0.27.
|
|
23
|
+
"@eslint/js": "^9.39.5",
|
|
24
|
+
"esbuild": "^0.27.7",
|
|
25
25
|
"eslint": "^10.8.1"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/single-file-cli-api.js
CHANGED
|
@@ -106,7 +106,6 @@ async function initialize(options) {
|
|
|
106
106
|
|
|
107
107
|
async function capture(urls, options) {
|
|
108
108
|
let newTasks;
|
|
109
|
-
const taskUrls = tasks.map(task => task.url);
|
|
110
109
|
newTasks = await Promise.all(urls.map(value => {
|
|
111
110
|
let url, taskOptions;
|
|
112
111
|
if (Array.isArray(value)) {
|
|
@@ -118,7 +117,7 @@ async function capture(urls, options) {
|
|
|
118
117
|
}
|
|
119
118
|
return createTask(url, taskOptions);
|
|
120
119
|
}));
|
|
121
|
-
newTasks = newTasks.filter(task => task && !
|
|
120
|
+
newTasks = newTasks.filter((task, taskIndex) => task && !mergeDuplicateTask(task, tasks.concat(newTasks.slice(0, taskIndex))));
|
|
122
121
|
if (newTasks.length) {
|
|
123
122
|
tasks = tasks.concat(newTasks);
|
|
124
123
|
await saveTasks();
|
|
@@ -300,37 +299,27 @@ async function capturePage(options) {
|
|
|
300
299
|
pageData.comment = undefined;
|
|
301
300
|
content = JSON.stringify(pageData, null, 2);
|
|
302
301
|
}
|
|
303
|
-
if (options.output) {
|
|
304
|
-
filename = await getFilename(options.output, options);
|
|
305
|
-
} else if (options.dumpContent) {
|
|
302
|
+
if (options.dumpContent && !options.output) {
|
|
306
303
|
if (options.compressContent) {
|
|
307
304
|
await stdout.write(content);
|
|
308
305
|
} else {
|
|
309
306
|
console.log(content || ""); // eslint-disable-line no-console
|
|
310
307
|
}
|
|
311
308
|
} else {
|
|
312
|
-
|
|
309
|
+
let outputFilename = options.output || pageData.filename;
|
|
310
|
+
if (options.outputJson && !outputFilename.endsWith(".json")) {
|
|
311
|
+
outputFilename += ".json";
|
|
312
|
+
}
|
|
313
|
+
filename = await writeOutputFile(outputFilename, content, options);
|
|
313
314
|
}
|
|
314
315
|
if (filename) {
|
|
315
|
-
if (options.outputJson) {
|
|
316
|
-
filename += filename.endsWith(".json") ? "" : ".json";
|
|
317
|
-
}
|
|
318
|
-
const directoryName = path.dirname(filename);
|
|
319
|
-
if (directoryName !== ".") {
|
|
320
|
-
await mkdir(directoryName, { recursive: true });
|
|
321
|
-
}
|
|
322
|
-
if (content instanceof Uint8Array) {
|
|
323
|
-
await writeFile(filename, content);
|
|
324
|
-
} else {
|
|
325
|
-
await writeTextFile(filename, content);
|
|
326
|
-
}
|
|
327
316
|
const outputDirectory = getOutputDirectory(options);
|
|
328
317
|
pageData.filename = filename.startsWith(outputDirectory) ? filename.substring(outputDirectory.length) : filename;
|
|
329
318
|
}
|
|
330
319
|
return pageData;
|
|
331
320
|
} catch (error) {
|
|
332
321
|
const date = new Date();
|
|
333
|
-
let message = `[${date.toISOString()}] URL: ${options.url}`;
|
|
322
|
+
let message = `[${date.toISOString()}] URL: ${options.url} Error: ${error.message || error}`;
|
|
334
323
|
if (!options.errorsTracesDisabled) {
|
|
335
324
|
message += "\nStack: " + error.stack;
|
|
336
325
|
}
|
|
@@ -366,6 +355,37 @@ function getOutputDirectory(options) {
|
|
|
366
355
|
return outputDirectory;
|
|
367
356
|
}
|
|
368
357
|
|
|
358
|
+
async function writeOutputFile(outputFilename, content, options) {
|
|
359
|
+
while (true) {
|
|
360
|
+
const filename = await getFilename(outputFilename, options);
|
|
361
|
+
if (!filename) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const directoryName = path.dirname(filename);
|
|
365
|
+
if (directoryName !== ".") {
|
|
366
|
+
await mkdir(directoryName, { recursive: true });
|
|
367
|
+
}
|
|
368
|
+
// exclusive creation prevents parallel tasks writing the same filename
|
|
369
|
+
// from silently overwriting each other
|
|
370
|
+
const writeOptions = { createNew: options.filenameConflictAction != "overwrite" };
|
|
371
|
+
try {
|
|
372
|
+
if (content instanceof Uint8Array) {
|
|
373
|
+
await writeFile(filename, content, writeOptions);
|
|
374
|
+
} else {
|
|
375
|
+
await writeTextFile(filename, content, writeOptions);
|
|
376
|
+
}
|
|
377
|
+
return filename;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (!(error instanceof errors.AlreadyExists)) {
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
if (options.filenameConflictAction == "skip") {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
369
389
|
async function getFilename(filename, options, index = 1) {
|
|
370
390
|
let newFilename = getOutputDirectory(options) + filename;
|
|
371
391
|
if (options.filenameConflictAction == "overwrite") {
|
package/single-file-launcher.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { initialize } from "./single-file-cli-api.js";
|
|
25
25
|
import { closeBrowser } from "./lib/browser.js";
|
|
26
26
|
import { Deno } from "./lib/deno-polyfill.js";
|
|
27
|
-
import {
|
|
27
|
+
import { getOptions, applySettings, parseUrlsFile } from "./options.js";
|
|
28
28
|
|
|
29
29
|
const { readTextFile, readFile, exit, addSignalListener } = Deno;
|
|
30
30
|
|
|
@@ -43,39 +43,17 @@ export { run };
|
|
|
43
43
|
|
|
44
44
|
async function run() {
|
|
45
45
|
try {
|
|
46
|
+
const options = getOptions();
|
|
46
47
|
let urls;
|
|
47
48
|
if (options.settingsFile) {
|
|
48
|
-
const settings = JSON.parse(await
|
|
49
|
-
|
|
50
|
-
if (profileName == "default" || !settings.profiles[profileName]) {
|
|
51
|
-
profileName = "__Default_Settings__";
|
|
52
|
-
}
|
|
53
|
-
Object.assign(options, settings.profiles[profileName]);
|
|
54
|
-
delete options.settingsFile;
|
|
49
|
+
const settings = JSON.parse(await readTextFile(options.settingsFile));
|
|
50
|
+
applySettings(options, settings);
|
|
55
51
|
}
|
|
56
52
|
if (options.urlsFile) {
|
|
57
53
|
urls = await getUrlsFile(options.urlsFile);
|
|
58
54
|
} else {
|
|
59
55
|
urls = [options.url];
|
|
60
56
|
}
|
|
61
|
-
if (options.browserCookies) {
|
|
62
|
-
const cookies = [];
|
|
63
|
-
for (const cookie of options.browserCookies) {
|
|
64
|
-
const [name, value, domain, path, expires, httpOnly, secure, sameSite, url] = cookie.split(",");
|
|
65
|
-
cookies.push({
|
|
66
|
-
name,
|
|
67
|
-
value,
|
|
68
|
-
url,
|
|
69
|
-
domain,
|
|
70
|
-
path,
|
|
71
|
-
secure: secure === "true",
|
|
72
|
-
httpOnly: httpOnly === "true",
|
|
73
|
-
sameSite,
|
|
74
|
-
expires: isNaN(Number(expires)) ? undefined : Number(expires)
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
options.browserCookies = cookies;
|
|
78
|
-
}
|
|
79
57
|
if (options.browserCookiesFile) {
|
|
80
58
|
const cookiesContent = await readTextFile(options.browserCookiesFile);
|
|
81
59
|
try {
|
|
@@ -84,23 +62,6 @@ async function run() {
|
|
|
84
62
|
options.browserCookies = parseCookies(cookiesContent);
|
|
85
63
|
}
|
|
86
64
|
}
|
|
87
|
-
if (options.emulateMediaFeatures) {
|
|
88
|
-
options.emulateMediaFeatures = options.emulateMediaFeatures.map(feature => {
|
|
89
|
-
const colonIndex = feature.indexOf(":");
|
|
90
|
-
return {
|
|
91
|
-
name: feature.substring(0, colonIndex),
|
|
92
|
-
value: feature.substring(colonIndex + 1)
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
if (options.httpHeaders) {
|
|
97
|
-
const headers = {};
|
|
98
|
-
for (const header of options.httpHeaders) {
|
|
99
|
-
const [name, value] = header.split("=");
|
|
100
|
-
headers[name] = value.trim();
|
|
101
|
-
}
|
|
102
|
-
options.httpHeaders = headers;
|
|
103
|
-
}
|
|
104
65
|
if (options.embeddedImage) {
|
|
105
66
|
options.embeddedImage = Array.from(await readFile(options.embeddedImage));
|
|
106
67
|
}
|
|
@@ -148,54 +109,5 @@ async function closeBrowserAndExit(code) {
|
|
|
148
109
|
}
|
|
149
110
|
|
|
150
111
|
async function getUrlsFile(urlsFile) {
|
|
151
|
-
|
|
152
|
-
urls = urls.map(value => {
|
|
153
|
-
value = value.trim();
|
|
154
|
-
let optionPosition = value.indexOf(" --");
|
|
155
|
-
if (optionPosition < 0) {
|
|
156
|
-
optionPosition = value.indexOf("\t--");
|
|
157
|
-
}
|
|
158
|
-
if (optionPosition > 0) {
|
|
159
|
-
const url = value.substring(0, optionPosition).trim();
|
|
160
|
-
const argsString = value.substring(optionPosition + 1).trim();
|
|
161
|
-
const args = [];
|
|
162
|
-
let previousCharacter, previousPreviousCharacter, lastQuoteCharacter;
|
|
163
|
-
let lastCharIndex = 0;
|
|
164
|
-
for (let currentCharIndex = 0; currentCharIndex < argsString.length; currentCharIndex++) {
|
|
165
|
-
const character = argsString[currentCharIndex];
|
|
166
|
-
if (character == lastQuoteCharacter && (previousCharacter != "\\" || previousPreviousCharacter == "\\")) {
|
|
167
|
-
args.push(argsString.substring(lastCharIndex, currentCharIndex));
|
|
168
|
-
lastQuoteCharacter = null;
|
|
169
|
-
lastCharIndex = currentCharIndex + 1;
|
|
170
|
-
} else if (!lastQuoteCharacter) {
|
|
171
|
-
if (character == "'" || character == "\"") {
|
|
172
|
-
lastQuoteCharacter = argsString[currentCharIndex];
|
|
173
|
-
lastCharIndex = currentCharIndex + 1;
|
|
174
|
-
} else {
|
|
175
|
-
const isSpaceCharacter = character == " " || character == "\t";
|
|
176
|
-
if (isSpaceCharacter || character == "=") {
|
|
177
|
-
if (isSpaceCharacter && (currentCharIndex == lastCharIndex + 1)) {
|
|
178
|
-
lastCharIndex++;
|
|
179
|
-
} else if (lastCharIndex < currentCharIndex) {
|
|
180
|
-
args.push(argsString.substring(lastCharIndex, currentCharIndex));
|
|
181
|
-
lastCharIndex = currentCharIndex + 1;
|
|
182
|
-
} else {
|
|
183
|
-
lastCharIndex = currentCharIndex + 1;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
previousPreviousCharacter = previousCharacter;
|
|
189
|
-
previousCharacter = character;
|
|
190
|
-
}
|
|
191
|
-
if (lastCharIndex < argsString.length) {
|
|
192
|
-
args.push(argsString.substring(lastCharIndex).trim());
|
|
193
|
-
}
|
|
194
|
-
const { options } = parseArgs(args, false);
|
|
195
|
-
return [url, options];
|
|
196
|
-
} else {
|
|
197
|
-
return value;
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
return urls;
|
|
112
|
+
return parseUrlsFile(await readTextFile(urlsFile));
|
|
201
113
|
}
|