Files
rspade_system/node_modules/svgo/lib/svgo/coa.js
root d523f0f600 Fix code quality violations and exclude Manifest from checks
Document application modes (development/debug/production)
Add global file drop handler, order column normalization, SPA hash fix
Serve CDN assets via /_vendor/ URLs instead of merging into bundles
Add production minification with license preservation
Improve JSON formatting for debugging and production optimization
Add CDN asset caching with CSS URL inlining for production builds
Add three-mode system (development, debug, production)
Update Manifest CLAUDE.md to reflect helper class architecture
Refactor Manifest.php into helper classes for better organization
Pre-manifest-refactor checkpoint: Add app_mode documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 10:38:22 +00:00

534 lines
14 KiB
JavaScript
Executable File

import fs from 'fs';
import path from 'path';
import colors from 'picocolors';
import { fileURLToPath } from 'url';
import { decodeSVGDatauri, encodeSVGDatauri } from './tools.js';
import { loadConfig, optimize } from '../svgo-node.js';
import { builtinPlugins } from '../builtin.js';
import { SvgoParserError } from '../parser.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkgPath = path.join(__dirname, '../../package.json');
const PKG = JSON.parse(await fs.promises.readFile(pkgPath, 'utf-8'));
/**
* Synchronously check if path is a directory. Tolerant to errors like ENOENT.
*
* @param {string} filePath
*/
export function checkIsDir(filePath) {
try {
return fs.lstatSync(filePath).isDirectory();
} catch {
return filePath.endsWith(path.sep);
}
}
/**
* @param {import('commander').Command} program
*/
export default function makeProgram(program) {
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')
.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, .mjs, and .cjs 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);
}
/**
* @param {ReadonlyArray<string>} args
* @param {any} opts
* @param {import('commander').Command} command
* @returns
*/
async function action(args, opts, command) {
const input = opts.input || args;
let output = opts.output;
/** @type {any} */
let config = {};
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 (process?.versions?.node && PKG.engines.node) {
// @ts-expect-error We control this and ensure it is never null.
const 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((/** @type {string} */ pattern) => RegExp(pattern))
: [];
// --precision
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 {
config.floatPrecision = Math.min(Math.max(0, number), 20);
}
}
// --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])) {
const dir = output[0];
for (let 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) {
const outputFolder = (output && output[0]) || opts.folder;
await optimizeFolder(config, opts.folder, outputFolder);
}
// --input
if (input.length !== 0) {
// STDIN
if (input[0] === '-') {
return new Promise((resolve, reject) => {
let data = '';
const file = output[0];
process.stdin
.on('data', (chunk) => (data += chunk))
.once('end', () =>
processSVGData(config, null, data, file).then(resolve, reject),
);
});
// file
} else {
await Promise.all(
input.map((/** @type {string} */ file, /** @type {number} */ n) =>
optimizeFile(config, file, output[n]),
),
);
}
// --string
} else if (opts.string) {
const data = decodeSVGDatauri(opts.string);
return processSVGData(config, null, data, output[0]);
}
}
/**
* Optimize SVG files in a directory.
*
* @param {any} config options
* @param {string} dir input directory
* @param {string} output output directory
* @return {Promise<any>}
*/
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 {any} config options
* @param {string} dir input directory
* @param {ReadonlyArray<string>} files list of file names in the directory
* @param {string} output output directory
* @return {Promise<any>}
*/
function processDirectory(config, dir, files, output) {
// take only *.svg files, recursively if necessary
const 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 {any} config options
* @param {string} dir input directory
* @param {ReadonlyArray<string>} files list of file names in the directory
* @param {string} output output directory
* @return {any[]}
*/
function getFilesDescriptions(config, dir, files, output) {
const filesInThisFolder = files
.filter(
(name) =>
name.slice(-4).toLowerCase() === '.svg' &&
!config.exclude.some((/** @type {RegExp} */ regExclude) =>
regExclude.test(name),
),
)
.map((name) => ({
inputPath: path.resolve(dir, name),
outputPath: path.resolve(output, name),
}));
if (!config.recursive) {
return filesInThisFolder;
}
return filesInThisFolder.concat(
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) => a.concat(b), []),
);
}
/**
* Read SVG file and pass to processing.
*
* @param {any} config options
* @param {string} file
* @param {string} output
* @return {Promise<any>}
*/
function optimizeFile(config, file, output) {
return fs.promises.readFile(file, 'utf8').then(
(data) => processSVGData(config, { path: file }, data, output, file),
(error) => checkOptimizeFileError(config, file, output, error),
);
}
/**
* Optimize SVG data.
*
* @param {any} config options
* @param {?{ path: string }} info
* @param {string} data SVG content to optimize
* @param {string} output where to write optimized file
* @param {any=} input input file name (being used if output is a directory)
* @return {Promise<any>}
*/
function processSVGData(config, info, data, output, input) {
const startTime = Date.now();
const prevFileSize = Buffer.byteLength(data, 'utf8');
let result;
try {
result = optimize(data, { ...config, ...info });
} catch (error) {
if (error instanceof SvgoParserError) {
console.error(colors.red(error.toString()));
process.exit(1);
} else {
throw error;
}
}
if (config.datauri) {
result.data = encodeSVGDatauri(result.data, config.datauri);
}
const resultFileSize = Buffer.byteLength(result.data, 'utf8');
const 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<void>}
*/
async function writeOutput(input, output, data) {
if (output == '-') {
process.stdout.write(data);
return Promise.resolve();
}
await fs.promises.mkdir(path.dirname(output), { recursive: true });
return fs.promises
.writeFile(output, data, 'utf8')
.catch((error) => checkWriteFileError(input, output, data, error));
}
/**
* Write time taken to optimize.
*
* @param {number} time time in milliseconds.
*/
function printTimeInfo(time) {
console.log(`Done in ${time} ms!`);
}
/**
* Write optimizing stats in a human-readable format.
*
* @param {number} inBytes size before optimization.
* @param {number} outBytes size after optimization.
*/
function printProfitInfo(inBytes, outBytes) {
const profitPercent = 100 - (outBytes * 100) / inBytes;
/** @type {[string, Function]} */
const ui = profitPercent < 0 ? ['+', colors.red] : ['-', colors.green];
console.log(
Math.round((inBytes / 1024) * 1000) / 1000 + ' KiB',
ui[0],
ui[1](Math.abs(Math.round(profitPercent * 10) / 10) + '%'),
'=',
Math.round((outBytes / 1024) * 1000) / 1000 + ' KiB',
);
}
/**
* Check for errors, if it's a dir optimize the dir.
*
* @param {any} config
* @param {string} input
* @param {string} output
* @param {Error & { code: string, path: string }} error
* @return {Promise<void>}
*/
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 & { code: string }} error
* @return {Promise<void>}
*/
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 = builtinPlugins
.map((plugin) => ` [ ${colors.green(plugin.name)} ] ${plugin.description}`)
.join('\n');
console.log('Currently available plugins:\n' + list);
}