Skip to content

Commit

Permalink
Add --options command line option. (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnkenny54 authored Oct 16, 2024
1 parent 789836c commit 9947fb4
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 125 deletions.
4 changes: 2 additions & 2 deletions bin/svgo.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env node

import colors from 'picocolors';
import { program } from 'commander';
import makeProgram from '../lib/svgo/coa.js';
makeProgram(program);

const program = makeProgram();
program.parseAsync(process.argv).catch((error) => {
console.error(colors.red(error.stack));
process.exit(1);
Expand Down
2 changes: 2 additions & 0 deletions lib/svgo.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export type Config = {
preset?: 'default' | 'next' | 'none';
enable?: string[];
disable?: string[];
// Configuration parameters for plugins.
options?: Record<string, {}>;
/** Options for rendering optimized SVG from AST. */
js2svg?: StringifyOptions;
/** Output as Data URI string. */
Expand Down
102 changes: 62 additions & 40 deletions lib/svgo.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,42 +33,56 @@ function getPlugin(name) {

/**
* @param {import('./svgo.js').PluginConfig} plugin
* @param {string[]} disabled
* @param {Record<string,{}>|undefined} options
*/
const resolvePluginConfig = (plugin) => {
function resolvePluginConfig(plugin, disabled, options = {}) {
let resolvedPlugin;
let builtinPlugin;
if (typeof plugin === 'string') {
// resolve builtin plugin specified as string
const builtinPlugin = getPlugin(plugin);
if (builtinPlugin == null) {
throw Error(`Unknown builtin plugin "${plugin}" specified.`);
}
return {
builtinPlugin = getPlugin(plugin);
resolvedPlugin = {
name: plugin,
params: {},
fn: builtinPlugin.fn,
};
}
if (typeof plugin === 'object' && plugin != null) {
if (plugin.name == null) {
throw Error(`Plugin name must be specified`);
}
} else if (typeof plugin === 'object') {
// use custom plugin implementation
let fn = 'fn' in plugin ? plugin.fn : undefined;
if (!fn) {
// resolve builtin plugin implementation
const builtinPlugin = getPlugin(plugin.name);
if (builtinPlugin == null) {
throw Error(`Unknown builtin plugin "${plugin.name}" specified.`);
}
builtinPlugin = getPlugin(plugin.name);
fn = builtinPlugin.fn;
}
return {
resolvedPlugin = {
name: plugin.name,
params: plugin.params,
fn,
};
} else {
throw new Error();
}

// Override with command line options.
if (options[resolvedPlugin.name]) {
resolvedPlugin.params = options[resolvedPlugin.name];
} else if (builtinPlugin && builtinPlugin.isPreset && builtinPlugin.plugins) {
/** @type {Object<string,{}>} */
const overrides = resolvedPlugin.params.overrides ?? {};
for (const { name } of builtinPlugin.plugins) {
if (options[name] !== undefined) {
overrides[name] = options[name];
}
if (disabled.includes(name)) {
overrides[name] = false;
}
}
resolvedPlugin.params = { overrides: overrides };
}
return null;
};

return resolvedPlugin;
}

export { VERSION, builtin as builtinPlugins, _collections };

Expand All @@ -77,7 +91,7 @@ export { VERSION, builtin as builtinPlugins, _collections };
* @param {import('svgo-ll').Config} config
* @returns {import('svgo-ll').Output}
*/
export const optimize = (input, config) => {
export function optimize(input, config) {
/**
* @param {import('svgo-ll').Config} config
* @returns {import('svgo-ll').PluginConfig[]}
Expand All @@ -99,31 +113,51 @@ export const optimize = (input, config) => {
return ['preset-default'];
}

const presets = getPreset();
const plugins = getPreset();
if (config.enable) {
for (const builtinName of config.enable) {
const builtin = pluginsMap.get(builtinName);
if (builtin) {
presets.push(builtin);
plugins.push(builtin);
} else {
console.warn(`plugin "${builtinName}" not found`);
}
}
}
return presets;

return plugins;
}

if (!config) {
config = {};
}
if (typeof config !== 'object') {
throw Error('Config should be an object');

const disabled = config.disable ?? [];
let plugins = getPlugins(config);
if (disabled.length > 0) {
plugins = plugins.filter(
(p) => typeof p === 'string' && !disabled.includes(p),
);
}
const resolvedPlugins = plugins.map((p) =>
resolvePluginConfig(p, disabled, config.options),
);

return optimizeResolved(input, config, resolvedPlugins);
}

/**
* @param {string} input
* @param {{path?:string,maxPasses?:number,floatPrecision?:number}&{js2svg?:import('./types.js').StringifyOptions, datauri?:import('./types.js').DataUri}} config
* @param {import('svgo-ll').CustomPlugin[]} resolvedPlugins
* @returns {import('svgo-ll').Output}
*/
function optimizeResolved(input, config, resolvedPlugins) {
let prevResultSize = Number.POSITIVE_INFINITY;
let output = '';
/** @type {import('./types.js').PluginInfo} */
const info = {};
if (config.path != null) {
if (config.path) {
info.path = config.path;
}

Expand All @@ -136,23 +170,11 @@ export const optimize = (input, config) => {
const maxPasses = config.maxPasses
? Math.max(Math.min(config.maxPasses, 10), 1)
: 10;
const plugins = getPlugins(config);

for (let i = 0; i < maxPasses; i += 1) {
info.passNumber = i;
if (!Array.isArray(plugins)) {
throw Error('malformed config, `plugins` property must be an array.');
}
const resolvedPlugins = plugins
.filter((plugin) => plugin != null)
.map(resolvePluginConfig);

if (resolvedPlugins.length < plugins.length) {
console.warn(
'Warning: plugins list includes null or undefined elements, these will be ignored.',
);
}
/** @type {import('./svgo.js').Config} */
const globalOverrides = { disable: config.disable };
const globalOverrides = {};
if (config.floatPrecision != null) {
globalOverrides.floatPrecision = config.floatPrecision;
}
Expand Down Expand Up @@ -181,7 +203,7 @@ export const optimize = (input, config) => {
data: output,
ast: ast,
};
};
}

export default {
VERSION,
Expand Down
93 changes: 51 additions & 42 deletions lib/svgo/coa.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { decodeSVGDatauri } from './tools.js';
import { loadConfig, optimize } from '../svgo-node.js';
import { builtin, builtinPresets } from '../builtin.js';
import { SvgoParserError } from '../parser.js';
import { Command, Option } from 'commander';
import { readJSONFile } from './tools-node.js';

/**
* @typedef {import('commander').Command} Command
*/
/**
* @typedef {{quiet?:boolean,recursive?:boolean,exclude:RegExp[]}} CommandConfig
* @typedef {import('../svgo.js').Config&CommandConfig} ExtendedConfig
Expand All @@ -36,13 +35,14 @@ export function checkIsDir(filePath) {
}

/**
* @param {Command} program
* @returns {Command}
*/
export default function makeProgram(program) {
export default function makeProgram() {
const program = new Command();

program
.name(PKG.name)
.description(PKG.description)
.version(PKG.version, '-v, --version')
.argument('[INPUT...]', 'Alias to --input')
.option('-i, --input <INPUT...>', 'Input files, "-" for STDIN')
.option('-s, --string <STRING>', 'Input SVG data string')
Expand All @@ -51,73 +51,86 @@ export default function makeProgram(program) {
'Input folder, optimize and rewrite all *.svg files',
)
.option(
'-o, --output <OUTPUT...>',
'Output file or folder (by default the same as the input), "-" for STDOUT',
'-r, --recursive',
"Use with '--folder'. Optimizes *.svg files in folders recursively.",
)
.option(
'--exclude <PATTERN...>',
"Use with '--folder'. Exclude files matching regular expression pattern.",
)
.option(
'--preset <default | next | none>',
'Specify which set of predefined plugins to use',
'default',
)
.option(
'--config <CONFIG>',
'Custom config file, only .js, .mjs, and .cjs is supported',
)
.option(
'--enable <plugin...>',
'Specify one or more builtin plugins to run in addition to those in the preset or config',
)
.option(
'--options <FILENAME>',
'Path to a JSON file containing configuration parameters for enabled plugins',
)
.option(
'--disable <plugin...>',
'Specify one or more plugins from the preset or config which should not be run ',
'Specify one or more plugins from the preset or config which should not be run',
)
.option(
'--datauri <base64 | enc | unenc>',
'Output as Data URI string (base64), URI encoded (enc) or unencoded (unenc)',
'--config <CONFIG>',
'Custom config file, only .js, .mjs, and .cjs is supported',
)
.option(
'--max-passes <INTEGER>',
'Maximum number of iterations over the plugins.',
'10',
)
.option('--multipass', 'DEPRECATED - use --max-passes')
.option(
'-o, --output <OUTPUT...>',
'Output file or folder (by default the same as the input), "-" for STDOUT',
)
.option(
'--datauri <base64 | enc | unenc>',
'Output as Data URI string (base64), URI encoded (enc) or unencoded (unenc)',
)
.option('--pretty', 'Add line breaks and indentation to output')
.option(
'--indent <INTEGER>',
'Number of spaces to indent if --pretty is specified',
'4',
)
.option(
'--eol <lf | crlf>',
'Line break to use when outputting SVG. If unspecified, the platform default is used',
.addOption(
new Option(
'--eol <lf | crlf>',
'Line break to use when outputting SVG. If unspecified, the platform default is used',
).choices(['lf', 'crlf']),
)
.option('--final-newline', 'Ensure output ends with a line break')
.option(
'-r, --recursive',
"Use with '--folder'. Optimizes *.svg files in folders recursively.",
)
.option(
'--exclude <PATTERN...>',
"Use with '--folder'. Exclude files matching regular expression pattern.",
)
// used by picocolors internally
.option('--no-color', 'Output plain text without color')
.option(
'-q, --quiet',
'Only output error messages, not regular status messages',
)
.option('--show-plugins', 'Show available plugins and exit')
// used by picocolors internally
.option('--no-color', 'Output plain text without color')
.option(
'-p, --precision <INTEGER>',
'DEPRECATED. Set number of digits in the fractional part, overrides plugins params',
.version(PKG.version, '-v, --version')
.addOption(
new Option('--multipass', 'DEPRECATED - use --max-passes').hideHelp(),
)
.addOption(
new Option(
'-p, --precision <INTEGER>',
'DEPRECATED. Set number of digits in the fractional part, overrides plugins params',
).hideHelp(),
)
.action(action);

return program;
}

/**
* @param {string[]} args
* @param {import("commander").OptionValues} opts
* @param {import('commander').Command} command
* @param {Command} command
*/
async function action(args, opts, command) {
/** @type {string[]} */
Expand Down Expand Up @@ -163,13 +176,6 @@ async function action(args, opts, command) {
}
}

if (opts.eol != null && opts.eol !== 'lf' && opts.eol !== 'crlf') {
console.error(
"error: option '--eol' must have one of the following values: 'lf' or 'crlf'",
);
process.exit(1);
}

// --show-plugins
if (opts.showPlugins) {
showAvailablePlugins();
Expand All @@ -188,7 +194,7 @@ async function action(args, opts, command) {
}

if (
typeof process == 'object' &&
typeof process === 'object' &&
process.versions &&
process.versions.node &&
PKG &&
Expand All @@ -208,7 +214,7 @@ async function action(args, opts, command) {

// --config
const loadedConfig = await loadConfig(opts.config);
if (loadedConfig != null) {
if (loadedConfig) {
config = { ...loadedConfig, exclude: [] };
}

Expand Down Expand Up @@ -263,6 +269,9 @@ async function action(args, opts, command) {
if (opts.disable) {
config.disable = opts.disable;
}
if (opts.options && !loadedConfig) {
config.options = readJSONFile(opts.options);
}

// --pretty
if (opts.pretty) {
Expand Down
Loading

0 comments on commit 9947fb4

Please sign in to comment.