123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- const colors = require('picocolors');
- const { loadConfig, optimize } = require('../svgo-node.js');
- const pluginsMap = require('../../plugins/plugins.js');
- const PKG = require('../../package.json');
- const { encodeSVGDatauri, decodeSVGDatauri } = require('./tools.js');
- const regSVGFile = /\.svg$/i;
- /**
- * Synchronously check if path is a directory. Tolerant to errors like ENOENT.
- * @param {string} path
- */
- function checkIsDir(path) {
- try {
- return fs.lstatSync(path).isDirectory();
- } catch (e) {
- return false;
- }
- }
- module.exports = function makeProgram(program) {
- program
- .name(PKG.name)
- .description(PKG.description, {
- INPUT: 'Alias to --input',
- })
- .version(PKG.version, '-v, --version')
- .arguments('[INPUT...]')
- .option('-i, --input <INPUT...>', 'Input files, "-" for STDIN')
- .option('-s, --string <STRING>', 'Input SVG data string')
- .option(
- '-f, --folder <FOLDER>',
- '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'
- )
- .option(
- '-p, --precision <INTEGER>',
- 'Set number of digits in the fractional part, overrides plugins params'
- )
- .option('--config <CONFIG>', 'Custom config file, only .js is supported')
- .option(
- '--datauri <FORMAT>',
- 'Output as Data URI string (base64), URI encoded (enc) or unencoded (unenc)'
- )
- .option(
- '--multipass',
- 'Pass over SVGs multiple times to ensure all optimizations are applied'
- )
- .option('--pretty', 'Make SVG pretty printed')
- .option('--indent <INTEGER>', 'Indent number when pretty printing SVGs')
- .option(
- '--eol <EOL>',
- 'Line break to use when outputting SVG: lf, crlf. If unspecified, uses platform default.'
- )
- .option('--final-newline', 'Ensure SVG 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."
- )
- .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')
- .action(action);
- };
- async function action(args, opts, command) {
- var input = opts.input || args;
- var output = opts.output;
- var config = {};
- if (opts.precision != null) {
- const number = Number.parseInt(opts.precision, 10);
- if (Number.isNaN(number)) {
- console.error(
- "error: option '-p, --precision' argument must be an integer number"
- );
- process.exit(1);
- } else {
- opts.precision = number;
- }
- }
- if (opts.datauri != null) {
- if (
- opts.datauri !== 'base64' &&
- opts.datauri !== 'enc' &&
- opts.datauri !== 'unenc'
- ) {
- console.error(
- "error: option '--datauri' must have one of the following values: 'base64', 'enc' or 'unenc'"
- );
- process.exit(1);
- }
- }
- if (opts.indent != null) {
- const number = Number.parseInt(opts.indent, 10);
- if (Number.isNaN(number)) {
- console.error(
- "error: option '--indent' argument must be an integer number"
- );
- process.exit(1);
- } else {
- opts.indent = number;
- }
- }
- 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();
- return;
- }
- // w/o anything
- if (
- (input.length === 0 || input[0] === '-') &&
- !opts.string &&
- !opts.stdin &&
- !opts.folder &&
- process.stdin.isTTY === true
- ) {
- return command.help();
- }
- if (
- typeof process == 'object' &&
- process.versions &&
- process.versions.node &&
- PKG &&
- PKG.engines.node
- ) {
- var nodeVersion = String(PKG.engines.node).match(/\d*(\.\d+)*/)[0];
- if (parseFloat(process.versions.node) < parseFloat(nodeVersion)) {
- throw Error(
- `${PKG.name} requires Node.js version ${nodeVersion} or higher.`
- );
- }
- }
- // --config
- const loadedConfig = await loadConfig(opts.config);
- if (loadedConfig != null) {
- config = loadedConfig;
- }
- // --quiet
- if (opts.quiet) {
- config.quiet = opts.quiet;
- }
- // --recursive
- if (opts.recursive) {
- config.recursive = opts.recursive;
- }
- // --exclude
- config.exclude = opts.exclude
- ? opts.exclude.map((pattern) => RegExp(pattern))
- : [];
- // --precision
- if (opts.precision != null) {
- var precision = Math.min(Math.max(0, opts.precision), 20);
- config.floatPrecision = precision;
- }
- // --multipass
- if (opts.multipass) {
- config.multipass = true;
- }
- // --pretty
- if (opts.pretty) {
- config.js2svg = config.js2svg || {};
- config.js2svg.pretty = true;
- if (opts.indent != null) {
- config.js2svg.indent = opts.indent;
- }
- }
- // --eol
- if (opts.eol) {
- config.js2svg = config.js2svg || {};
- config.js2svg.eol = opts.eol;
- }
- // --final-newline
- if (opts.finalNewline) {
- config.js2svg = config.js2svg || {};
- config.js2svg.finalNewline = true;
- }
- // --output
- if (output) {
- if (input.length && input[0] != '-') {
- if (output.length == 1 && checkIsDir(output[0])) {
- var dir = output[0];
- for (var i = 0; i < input.length; i++) {
- output[i] = checkIsDir(input[i])
- ? input[i]
- : path.resolve(dir, path.basename(input[i]));
- }
- } else if (output.length < input.length) {
- output = output.concat(input.slice(output.length));
- }
- }
- } else if (input.length) {
- output = input;
- } else if (opts.string) {
- output = '-';
- }
- if (opts.datauri) {
- config.datauri = opts.datauri;
- }
- // --folder
- if (opts.folder) {
- var ouputFolder = (output && output[0]) || opts.folder;
- await optimizeFolder(config, opts.folder, ouputFolder);
- }
- // --input
- if (input.length !== 0) {
- // STDIN
- if (input[0] === '-') {
- return new Promise((resolve, reject) => {
- var data = '',
- file = output[0];
- process.stdin
- .on('data', (chunk) => (data += chunk))
- .once('end', () =>
- processSVGData(config, { input: 'string' }, data, file).then(
- resolve,
- reject
- )
- );
- });
- // file
- } else {
- await Promise.all(
- input.map((file, n) => optimizeFile(config, file, output[n]))
- );
- }
- // --string
- } else if (opts.string) {
- var data = decodeSVGDatauri(opts.string);
- return processSVGData(config, { input: 'string' }, data, output[0]);
- }
- }
- /**
- * Optimize SVG files in a directory.
- * @param {Object} config options
- * @param {string} dir input directory
- * @param {string} output output directory
- * @return {Promise}
- */
- function optimizeFolder(config, dir, output) {
- if (!config.quiet) {
- console.log(`Processing directory '${dir}':\n`);
- }
- return fs.promises
- .readdir(dir)
- .then((files) => processDirectory(config, dir, files, output));
- }
- /**
- * Process given files, take only SVG.
- * @param {Object} config options
- * @param {string} dir input directory
- * @param {Array} files list of file names in the directory
- * @param {string} output output directory
- * @return {Promise}
- */
- function processDirectory(config, dir, files, output) {
- // take only *.svg files, recursively if necessary
- var svgFilesDescriptions = getFilesDescriptions(config, dir, files, output);
- return svgFilesDescriptions.length
- ? Promise.all(
- svgFilesDescriptions.map((fileDescription) =>
- optimizeFile(
- config,
- fileDescription.inputPath,
- fileDescription.outputPath
- )
- )
- )
- : Promise.reject(
- new Error(`No SVG files have been found in '${dir}' directory.`)
- );
- }
- /**
- * Get svg files descriptions
- * @param {Object} config options
- * @param {string} dir input directory
- * @param {Array} files list of file names in the directory
- * @param {string} output output directory
- * @return {Array}
- */
- function getFilesDescriptions(config, dir, files, output) {
- const filesInThisFolder = files
- .filter(
- (name) =>
- regSVGFile.test(name) &&
- !config.exclude.some((regExclude) => regExclude.test(name))
- )
- .map((name) => ({
- inputPath: path.resolve(dir, name),
- outputPath: path.resolve(output, name),
- }));
- return config.recursive
- ? [].concat(
- filesInThisFolder,
- files
- .filter((name) => checkIsDir(path.resolve(dir, name)))
- .map((subFolderName) => {
- const subFolderPath = path.resolve(dir, subFolderName);
- const subFolderFiles = fs.readdirSync(subFolderPath);
- const subFolderOutput = path.resolve(output, subFolderName);
- return getFilesDescriptions(
- config,
- subFolderPath,
- subFolderFiles,
- subFolderOutput
- );
- })
- .reduce((a, b) => [].concat(a, b), [])
- )
- : filesInThisFolder;
- }
- /**
- * Read SVG file and pass to processing.
- * @param {Object} config options
- * @param {string} file
- * @param {string} output
- * @return {Promise}
- */
- function optimizeFile(config, file, output) {
- return fs.promises.readFile(file, 'utf8').then(
- (data) =>
- processSVGData(config, { input: 'file', path: file }, data, output, file),
- (error) => checkOptimizeFileError(config, file, output, error)
- );
- }
- /**
- * Optimize SVG data.
- * @param {Object} config options
- * @param {string} data SVG content to optimize
- * @param {string} output where to write optimized file
- * @param {string} [input] input file name (being used if output is a directory)
- * @return {Promise}
- */
- function processSVGData(config, info, data, output, input) {
- var startTime = Date.now(),
- prevFileSize = Buffer.byteLength(data, 'utf8');
- const result = optimize(data, { ...config, ...info });
- if (result.modernError) {
- console.error(colors.red(result.modernError.toString()));
- process.exit(1);
- }
- if (config.datauri) {
- result.data = encodeSVGDatauri(result.data, config.datauri);
- }
- var resultFileSize = Buffer.byteLength(result.data, 'utf8'),
- processingTime = Date.now() - startTime;
- return writeOutput(input, output, result.data).then(
- function () {
- if (!config.quiet && output != '-') {
- if (input) {
- console.log(`\n${path.basename(input)}:`);
- }
- printTimeInfo(processingTime);
- printProfitInfo(prevFileSize, resultFileSize);
- }
- },
- (error) =>
- Promise.reject(
- new Error(
- error.code === 'ENOTDIR'
- ? `Error: output '${output}' is not a directory.`
- : error
- )
- )
- );
- }
- /**
- * Write result of an optimization.
- * @param {string} input
- * @param {string} output output file name. '-' for stdout
- * @param {string} data data to write
- * @return {Promise}
- */
- function writeOutput(input, output, data) {
- if (output == '-') {
- console.log(data);
- return Promise.resolve();
- }
- fs.mkdirSync(path.dirname(output), { recursive: true });
- return fs.promises
- .writeFile(output, data, 'utf8')
- .catch((error) => checkWriteFileError(input, output, data, error));
- }
- /**
- * Write a time taken by optimization.
- * @param {number} time time in milliseconds.
- */
- function printTimeInfo(time) {
- console.log(`Done in ${time} ms!`);
- }
- /**
- * Write optimizing information in human readable format.
- * @param {number} inBytes size before optimization.
- * @param {number} outBytes size after optimization.
- */
- function printProfitInfo(inBytes, outBytes) {
- var profitPercents = 100 - (outBytes * 100) / inBytes;
- console.log(
- Math.round((inBytes / 1024) * 1000) / 1000 +
- ' KiB' +
- (profitPercents < 0 ? ' + ' : ' - ') +
- colors.green(Math.abs(Math.round(profitPercents * 10) / 10) + '%') +
- ' = ' +
- Math.round((outBytes / 1024) * 1000) / 1000 +
- ' KiB'
- );
- }
- /**
- * Check for errors, if it's a dir optimize the dir.
- * @param {Object} config
- * @param {string} input
- * @param {string} output
- * @param {Error} error
- * @return {Promise}
- */
- function checkOptimizeFileError(config, input, output, error) {
- if (error.code == 'EISDIR') {
- return optimizeFolder(config, input, output);
- } else if (error.code == 'ENOENT') {
- return Promise.reject(
- new Error(`Error: no such file or directory '${error.path}'.`)
- );
- }
- return Promise.reject(error);
- }
- /**
- * Check for saving file error. If the output is a dir, then write file there.
- * @param {string} input
- * @param {string} output
- * @param {string} data
- * @param {Error} error
- * @return {Promise}
- */
- function checkWriteFileError(input, output, data, error) {
- if (error.code == 'EISDIR' && input) {
- return fs.promises.writeFile(
- path.resolve(output, path.basename(input)),
- data,
- 'utf8'
- );
- } else {
- return Promise.reject(error);
- }
- }
- /**
- * Show list of available plugins with short description.
- */
- function showAvailablePlugins() {
- const list = Object.entries(pluginsMap)
- .sort(([a], [b]) => a.localeCompare(b))
- .map(([name, plugin]) => ` [ ${colors.green(name)} ] ${plugin.description}`)
- .join('\n');
- console.log('Currently available plugins:\n' + list);
- }
- module.exports.checkIsDir = checkIsDir;
|