single-file-cli 2.1.0 → 2.1.2

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/options.js CHANGED
@@ -203,76 +203,155 @@ const OPTIONS_INFO = [{
203
203
  }];
204
204
 
205
205
  const { args, exit } = Deno;
206
- const options = getOptions();
207
- export { options, parseArgs };
206
+ export { getOptions, parseArgs, applySettings, parseUrlsFile };
208
207
 
209
- function getOptions() {
210
- const { positionals, options } = parseArgs(Array.from(args));
211
- const unknownOptions = [];
212
- positionals.forEach(positional => {
213
- if (positional.startsWith("--")) {
214
- unknownOptions.push(positional);
215
- positionals.splice(positionals.indexOf(positional), 1);
216
- }
217
- });
218
- if ((!positionals.length || positionals.length > 2 || options.help || unknownOptions.length) && (!options.version && !options.urlsFile)) {
219
- console.log(USAGE_TEXT + "\n"); // eslint-disable-line no-console
220
- console.log("Options:"); // eslint-disable-line no-console
221
- OPTIONS_INFO.forEach(category => {
222
- const categoryName = CATEGORIES[OPTIONS_INFO.indexOf(category)];
223
- console.log(` * ${categoryName}:`); // eslint-disable-line no-console
224
- Object.keys(category).forEach(optionName => {
225
- const optionInfo = getOptionInfo(optionName);
226
- let optionType = optionInfo.type;
227
- if (isArray(optionType)) {
228
- optionType = optionType.replace("[]", "*");
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
- if (unknownOptions.length) {
237
- console.log(`Error: Unknown option${unknownOptions.length > 1 ? "s" : ""} ${unknownOptions.join(", ")}`); // eslint-disable-line no-console
238
- console.log(""); // eslint-disable-line no-console
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
- return { ...options, url: positionals[0], output: positionals[1] };
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 result = { positionals, options: {} };
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 { argName, argValue, optionInfo } = parseArg(arg);
257
- if (optionInfo) {
258
- if (options[argName] === undefined) {
259
- options[argName] = [];
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 < args.length &&
265
- ({ argName: nextArgName } = parseArg(args[argIndex + 1])) &&
266
- (nextArgName === undefined || !getOptionInfo(nextArgName)) &&
267
- isValid(optionInfo.type, args[argIndex + 1]) &&
268
- (isArray(optionInfo.type) || !options[argName].length)) {
269
- options[argName].push(args[argIndex + 1]);
270
- argIndex++;
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(optionInfo.type, argValue) && (isArray(optionInfo.type) || !options[argName].length)) {
273
- options[argName].push(argValue);
351
+ } else if (isValid(option.info.type, argValue)) {
352
+ options[optionName].push(argValue);
274
353
  } else {
275
- positionals.push(arg);
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[0] : true;
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[0] : optionInfo.defaultValue || 0;
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[0] : optionInfo.defaultValue || "";
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 = getOptionInfo(optionName);
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] = categoryOptions[optionName].defaultValue;
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
- browserArguments.push(...JSON.parse(result.options.browserArgs));
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 optionInfo = getOptionInfo(argName);
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, optionInfo };
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.0",
3
+ "version": "2.1.2",
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.0"
20
+ "simple-cdp": "^1.10.1"
21
21
  },
22
22
  "devDependencies": {
23
- "@eslint/js": "^9.39.1",
24
- "esbuild": "^0.27.4",
23
+ "@eslint/js": "^9.39.5",
24
+ "esbuild": "^0.27.7",
25
25
  "eslint": "^10.8.1"
26
26
  }
27
27
  }
@@ -23,6 +23,7 @@
23
23
 
24
24
  /* global URL */
25
25
 
26
+ import { Buffer } from "node:buffer";
26
27
  import * as backend from "./lib/cdp-client.js";
27
28
  import { getZipScriptSource } from "./lib/single-file-script.js";
28
29
  import { Deno, path } from "./lib/deno-polyfill.js";
@@ -105,7 +106,6 @@ async function initialize(options) {
105
106
 
106
107
  async function capture(urls, options) {
107
108
  let newTasks;
108
- const taskUrls = tasks.map(task => task.url);
109
109
  newTasks = await Promise.all(urls.map(value => {
110
110
  let url, taskOptions;
111
111
  if (Array.isArray(value)) {
@@ -117,7 +117,7 @@ async function capture(urls, options) {
117
117
  }
118
118
  return createTask(url, taskOptions);
119
119
  }));
120
- newTasks = newTasks.filter(task => task && !taskUrls.includes(task.url));
120
+ newTasks = newTasks.filter((task, taskIndex) => task && !mergeDuplicateTask(task, tasks.concat(newTasks.slice(0, taskIndex))));
121
121
  if (newTasks.length) {
122
122
  tasks = tasks.concat(newTasks);
123
123
  await saveTasks();
@@ -292,44 +292,34 @@ async function capturePage(options) {
292
292
  if (options.outputJson) {
293
293
  if (content instanceof Uint8Array) {
294
294
  pageData.content = undefined;
295
- pageData.binaryContent = content.toBase64();
295
+ pageData.binaryContent = Buffer.from(content).toString("base64");
296
296
  }
297
297
  pageData.doctype = undefined;
298
298
  pageData.viewport = undefined;
299
299
  pageData.comment = undefined;
300
300
  content = JSON.stringify(pageData, null, 2);
301
301
  }
302
- if (options.output) {
303
- filename = await getFilename(options.output, options);
304
- } else if (options.dumpContent) {
302
+ if (options.dumpContent && !options.output) {
305
303
  if (options.compressContent) {
306
304
  await stdout.write(content);
307
305
  } else {
308
306
  console.log(content || ""); // eslint-disable-line no-console
309
307
  }
310
308
  } else {
311
- filename = await getFilename(pageData.filename, options);
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);
312
314
  }
313
315
  if (filename) {
314
- if (options.outputJson) {
315
- filename += filename.endsWith(".json") ? "" : ".json";
316
- }
317
- const directoryName = path.dirname(filename);
318
- if (directoryName !== ".") {
319
- await mkdir(directoryName, { recursive: true });
320
- }
321
- if (content instanceof Uint8Array) {
322
- await writeFile(filename, content);
323
- } else {
324
- await writeTextFile(filename, content);
325
- }
326
316
  const outputDirectory = getOutputDirectory(options);
327
317
  pageData.filename = filename.startsWith(outputDirectory) ? filename.substring(outputDirectory.length) : filename;
328
318
  }
329
319
  return pageData;
330
320
  } catch (error) {
331
321
  const date = new Date();
332
- let message = `[${date.toISOString()}] URL: ${options.url}`;
322
+ let message = `[${date.toISOString()}] URL: ${options.url} Error: ${error.message || error}`;
333
323
  if (!options.errorsTracesDisabled) {
334
324
  message += "\nStack: " + error.stack;
335
325
  }
@@ -365,6 +355,37 @@ function getOutputDirectory(options) {
365
355
  return outputDirectory;
366
356
  }
367
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
+
368
389
  async function getFilename(filename, options, index = 1) {
369
390
  let newFilename = getOutputDirectory(options) + filename;
370
391
  if (options.filenameConflictAction == "overwrite") {
@@ -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 { options, parseArgs } from "./options.js";
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 Deno.readTextFile(options.settingsFile));
49
- let profileName = options.settingsProfile || "default";
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
- let urls = (await readTextFile(urlsFile)).split("\n");
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
  }