Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --options command line option. #60

Merged
merged 4 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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