1
0
mirror of https://github.com/svg/svgo.git synced 2025-08-01 18:46:52 +03:00

fix: improve jsdoc types and remove excludes (#2107)

This commit is contained in:
Seth Falco
2025-04-28 22:24:16 +01:00
committed by GitHub
parent 10c4287051
commit 71a1254895
17 changed files with 252 additions and 172 deletions

View File

@ -14,15 +14,15 @@
import SAX from 'sax'; import SAX from 'sax';
import { textElems } from '../plugins/_collections.js'; import { textElems } from '../plugins/_collections.js';
class SvgoParserError extends Error { export class SvgoParserError extends Error {
/** /**
* @param message {string} * @param {string} message
* @param line {number} * @param {number} line
* @param column {number} * @param {number} column
* @param source {string} * @param {string} source
* @param file {void | string} * @param {string|undefined} file
*/ */
constructor(message, line, column, source, file) { constructor(message, line, column, source, file = undefined) {
super(message); super(message);
this.name = 'SvgoParserError'; this.name = 'SvgoParserError';
this.message = `${file || '<input>'}:${line}:${column}: ${message}`; this.message = `${file || '<input>'}:${line}:${column}: ${message}`;
@ -34,6 +34,7 @@ class SvgoParserError extends Error {
Error.captureStackTrace(this, SvgoParserError); Error.captureStackTrace(this, SvgoParserError);
} }
} }
toString() { toString() {
const lines = this.source.split(/\r?\n/); const lines = this.source.split(/\r?\n/);
const startLine = Math.max(this.line - 3, 0); const startLine = Math.max(this.line - 3, 0);

View File

@ -67,12 +67,10 @@ const entities = {
/** /**
* convert XAST to SVG string * convert XAST to SVG string
* *
* @type {(data: XastRoot, config: StringifyOptions) => string} * @type {(data: XastRoot, userOptions?: StringifyOptions) => string}
*/ */
export const stringifySvg = (data, userOptions = {}) => { export const stringifySvg = (data, userOptions = {}) => {
/** /** @type {Options} */
* @type {Options}
*/
const config = { ...defaults, ...userOptions }; const config = { ...defaults, ...userOptions };
const indent = config.indent; const indent = config.indent;
let newIndent = ' '; let newIndent = ' ';
@ -81,9 +79,7 @@ export const stringifySvg = (data, userOptions = {}) => {
} else if (typeof indent === 'string') { } else if (typeof indent === 'string') {
newIndent = indent; newIndent = indent;
} }
/** /** @type {State} */
* @type {State}
*/
const state = { const state = {
indent: newIndent, indent: newIndent,
textContext: null, textContext: null,

4
lib/svgo-node.d.ts vendored
View File

@ -1,6 +1,6 @@
import { Config } from './svgo'; import type { Config } from './svgo.js';
export * from './svgo'; export * from './svgo.js';
/** /**
* If you write a tool on top of svgo you might need a way to load svgo config. * If you write a tool on top of svgo you might need a way to load svgo config.

View File

@ -1,6 +1,5 @@
import os from 'os'; import os from 'os';
import fs from 'fs'; import fs from 'fs';
import { pathToFileURL } from 'url';
import path from 'path'; import path from 'path';
import { import {
VERSION, VERSION,
@ -11,10 +10,17 @@ import {
_collections, _collections,
} from './svgo.js'; } from './svgo.js';
/**
* @typedef {import('./svgo.js').Config} Config
* @typedef {import('./svgo.js').Output} Output
*/
/**
* @param {string} configFile
* @returns {Promise<Config>}
*/
const importConfig = async (configFile) => { const importConfig = async (configFile) => {
// dynamic import expects file url instead of path and may fail const imported = await import(path.resolve(configFile));
// when windows path is provided
const imported = await import(pathToFileURL(configFile));
const config = imported.default; const config = imported.default;
if (config == null || typeof config !== 'object' || Array.isArray(config)) { if (config == null || typeof config !== 'object' || Array.isArray(config)) {
@ -23,6 +29,10 @@ const importConfig = async (configFile) => {
return config; return config;
}; };
/**
* @param {string} file
* @returns {Promise<boolean>}
*/
const isFile = async (file) => { const isFile = async (file) => {
try { try {
const stats = await fs.promises.stat(file); const stats = await fs.promises.stat(file);
@ -40,6 +50,11 @@ export {
_collections, _collections,
}; };
/**
* @param {string} configFile
* @param {string} cwd
* @returns {Promise<?Config>}
*/
export const loadConfig = async (configFile, cwd = process.cwd()) => { export const loadConfig = async (configFile, cwd = process.cwd()) => {
if (configFile != null) { if (configFile != null) {
if (path.isAbsolute(configFile)) { if (path.isAbsolute(configFile)) {
@ -71,6 +86,11 @@ export const loadConfig = async (configFile, cwd = process.cwd()) => {
} }
}; };
/**
* @param {string} input
* @param {Config} config
* @returns {Output}
*/
export const optimize = (input, config) => { export const optimize = (input, config) => {
if (config == null) { if (config == null) {
config = {}; config = {};

View File

@ -3,7 +3,7 @@ import path from 'path';
import { optimize, loadConfig } from './svgo-node.js'; import { optimize, loadConfig } from './svgo-node.js';
/** /**
* @typedef {import('../lib/types.js').Plugin} Plugin * @typedef {import('../lib/types.js').Plugin<?>} Plugin
*/ */
const describeLF = os.EOL === '\r\n' ? describe.skip : describe; const describeLF = os.EOL === '\r\n' ? describe.skip : describe;

2
lib/svgo.d.ts vendored
View File

@ -33,7 +33,7 @@ export type PluginConfig =
}[keyof BuiltinsWithRequiredParams] }[keyof BuiltinsWithRequiredParams]
| CustomPlugin; | CustomPlugin;
type BuiltinPlugin<Name, Params> = { export type BuiltinPlugin<Name, Params> = {
/** Name of the plugin, also known as the plugin ID. */ /** Name of the plugin, also known as the plugin ID. */
name: Name; name: Name;
description?: string; description?: string;

View File

@ -9,6 +9,9 @@ import _collections from '../plugins/_collections.js';
/** /**
* @typedef {import('./svgo.js').BuiltinPluginOrPreset<?, ?>} BuiltinPluginOrPreset * @typedef {import('./svgo.js').BuiltinPluginOrPreset<?, ?>} BuiltinPluginOrPreset
* @typedef {import('./svgo.js').Config} Config
* @typedef {import('./svgo.js').Output} Output
* @typedef {import('./svgo.js').PluginConfig} PluginConfig
*/ */
const pluginsMap = new Map(); const pluginsMap = new Map();
@ -31,6 +34,10 @@ function getPlugin(name) {
return pluginsMap.get(name); return pluginsMap.get(name);
} }
/**
* @param {string|PluginConfig} plugin
* @returns {?PluginConfig}
*/
const resolvePluginConfig = (plugin) => { const resolvePluginConfig = (plugin) => {
if (typeof plugin === 'string') { if (typeof plugin === 'string') {
// resolve builtin plugin specified as string // resolve builtin plugin specified as string
@ -49,6 +56,7 @@ const resolvePluginConfig = (plugin) => {
throw Error(`Plugin name must be specified`); throw Error(`Plugin name must be specified`);
} }
// use custom plugin implementation // use custom plugin implementation
// @ts-expect-error Checking for CustomPlugin with the presence of fn
let fn = plugin.fn; let fn = plugin.fn;
if (fn == null) { if (fn == null) {
// resolve builtin plugin implementation // resolve builtin plugin implementation
@ -75,6 +83,11 @@ export {
_collections, _collections,
}; };
/**
* @param {string} input
* @param {Config} config
* @returns {Output}
*/
export const optimize = (input, config) => { export const optimize = (input, config) => {
if (config == null) { if (config == null) {
config = {}; config = {};
@ -107,6 +120,8 @@ export const optimize = (input, config) => {
'Warning: plugins list includes null or undefined elements, these will be ignored.', 'Warning: plugins list includes null or undefined elements, these will be ignored.',
); );
} }
/** @type {Config} */
const globalOverrides = {}; const globalOverrides = {};
if (config.floatPrecision != null) { if (config.floatPrecision != null) {
globalOverrides.floatPrecision = config.floatPrecision; globalOverrides.floatPrecision = config.floatPrecision;

View File

@ -1,5 +1,10 @@
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import { optimize } from './svgo.js'; import { optimize } from './svgo.js';
import { SvgoParserError } from './parser.js';
/**
* @typedef {import('./svgo.js').CustomPlugin} CustomPlugin
*/
test('allow to setup default preset', () => { test('allow to setup default preset', () => {
const svg = ` const svg = `
@ -65,6 +70,7 @@ test('warn when user tries enable plugins in preset', () => {
const warn = jest.spyOn(console, 'warn'); const warn = jest.spyOn(console, 'warn');
optimize(svg, { optimize(svg, {
plugins: [ plugins: [
// @ts-expect-error Testing if we receive config that diverges from type definitions.
{ {
name: 'preset-default', name: 'preset-default',
params: { params: {
@ -138,6 +144,7 @@ describe('allow to configure EOL', () => {
</svg> </svg>
`; `;
const { data } = optimize(svg, { const { data } = optimize(svg, {
// @ts-expect-error Testing if we receive config that diverges from type definitions.
js2svg: { eol: 'invalid', pretty: true, indent: 2 }, js2svg: { eol: 'invalid', pretty: true, indent: 2 },
}); });
expect(data).toBe( expect(data).toBe(
@ -256,34 +263,37 @@ test('plugin precision should override preset precision', () => {
}); });
test('provides informative error in result', () => { test('provides informative error in result', () => {
expect.assertions(6);
const svg = `<svg viewBox="0 0 120 120"> const svg = `<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/> <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg> </svg>
`; `;
try { const error = new SvgoParserError(
optimize(svg, { path: 'test.svg' }); 'Unquoted attribute value',
} catch (error) { 2,
expect(error.name).toBe('SvgoParserError'); 33,
expect(error.message).toBe('test.svg:2:33: Unquoted attribute value'); svg,
expect(error.reason).toBe('Unquoted attribute value'); 'test.svg',
expect(error.line).toBe(2); );
expect(error.column).toBe(33); expect(() => optimize(svg, { path: 'test.svg' })).toThrow(error);
expect(error.source).toBe(svg); expect(error.name).toBe('SvgoParserError');
} expect(error.message).toBe('test.svg:2:33: Unquoted attribute value');
}); });
test('provides code snippet in rendered error', () => { test('provides code snippet in rendered error', () => {
expect.assertions(1);
const svg = `<svg viewBox="0 0 120 120"> const svg = `<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/> <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg> </svg>
`; `;
try { const error = new SvgoParserError(
optimize(svg, { path: 'test.svg' }); 'Unquoted attribute value',
} catch (error) { 2,
expect(error.toString()) 29,
.toBe(`SvgoParserError: test.svg:2:29: Unquoted attribute value svg,
'test.svg',
);
expect(() => optimize(svg, { path: 'test.svg' })).toThrow(error);
expect(error.toString())
.toBe(`SvgoParserError: test.svg:2:29: Unquoted attribute value
1 | <svg viewBox="0 0 120 120"> 1 | <svg viewBox="0 0 120 120">
> 2 | <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/> > 2 | <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
@ -291,11 +301,9 @@ test('provides code snippet in rendered error', () => {
3 | </svg> 3 | </svg>
4 | 4 |
`); `);
}
}); });
test('supports errors without path', () => { test('supports errors without path', () => {
expect.assertions(1);
const svg = `<svg viewBox="0 0 120 120"> const svg = `<svg viewBox="0 0 120 120">
<circle/> <circle/>
<circle/> <circle/>
@ -309,11 +317,10 @@ test('supports errors without path', () => {
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/> <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg> </svg>
`; `;
try { const error = new SvgoParserError('Unquoted attribute value', 11, 29, svg);
optimize(svg); expect(() => optimize(svg)).toThrow(error);
} catch (error) { expect(error.toString())
expect(error.toString()) .toBe(`SvgoParserError: <input>:11:29: Unquoted attribute value
.toBe(`SvgoParserError: <input>:11:29: Unquoted attribute value
9 | <circle/> 9 | <circle/>
10 | <circle/> 10 | <circle/>
@ -322,20 +329,18 @@ test('supports errors without path', () => {
12 | </svg> 12 | </svg>
13 | 13 |
`); `);
}
}); });
test('slices long line in error code snippet', () => { test('slices long line in error code snippet', () => {
expect.assertions(1);
const svg = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" viewBox="0 0 230 120"> const svg = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" viewBox="0 0 230 120">
<path d="M318.198 551.135 530.33 918.56l-289.778-77.646 38.823-144.889c77.646-289.778 294.98-231.543 256.156-86.655s178.51 203.124 217.334 58.235q58.234-217.334 250.955 222.534t579.555 155.292z stroke-width="1.5" fill="red" stroke="red" /> <path d="M318.198 551.135 530.33 918.56l-289.778-77.646 38.823-144.889c77.646-289.778 294.98-231.543 256.156-86.655s178.51 203.124 217.334 58.235q58.234-217.334 250.955 222.534t579.555 155.292z stroke-width="1.5" fill="red" stroke="red" />
</svg> </svg>
`; `;
try { const error = new SvgoParserError('Invalid attribute name', 2, 211, svg);
optimize(svg);
} catch (error) { expect(() => optimize(svg)).toThrow(error);
expect(error.toString()) expect(error.toString())
.toBe(`SvgoParserError: <input>:2:211: Invalid attribute name .toBe(`SvgoParserError: <input>:2:211: Invalid attribute name
1 | …-0.dtd" viewBox="0 0 230 120"> 1 | …-0.dtd" viewBox="0 0 230 120">
> 2 | …7.334 250.955 222.534t579.555 155.292z stroke-width="1.5" fill="red" strok… > 2 | …7.334 250.955 222.534t579.555 155.292z stroke-width="1.5" fill="red" strok…
@ -343,12 +348,13 @@ test('slices long line in error code snippet', () => {
3 | 3 |
4 | 4 |
`); `);
}
}); });
test('multipass option should trigger plugins multiple times', () => { test('multipass option should trigger plugins multiple times', () => {
const svg = `<svg id="abcdefghijklmnopqrstuvwxyz"></svg>`; const svg = `<svg id="abcdefghijklmnopqrstuvwxyz"></svg>`;
/** @type {number[]} */
const list = []; const list = [];
/** @type {CustomPlugin} */
const testPlugin = { const testPlugin = {
name: 'testPlugin', name: 'testPlugin',
fn: (_root, _params, info) => { fn: (_root, _params, info) => {

View File

@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
import { encodeSVGDatauri, decodeSVGDatauri } from './tools.js'; import { encodeSVGDatauri, decodeSVGDatauri } from './tools.js';
import { loadConfig, optimize } from '../svgo-node.js'; import { loadConfig, optimize } from '../svgo-node.js';
import { builtin } from '../builtin.js'; import { builtin } from '../builtin.js';
import { SvgoParserError } from '../parser.js';
/** /**
* @typedef {import('commander').Command} Command * @typedef {import('commander').Command} Command
@ -14,8 +15,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkgPath = path.join(__dirname, '../../package.json'); const pkgPath = path.join(__dirname, '../../package.json');
const PKG = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); const PKG = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
const regSVGFile = /\.svg$/i;
/** /**
* Synchronously check if path is a directory. Tolerant to errors like ENOENT. * Synchronously check if path is a directory. Tolerant to errors like ENOENT.
* *
@ -89,22 +88,17 @@ export default function makeProgram(program) {
.action(action); .action(action);
} }
/**
* @param {string[]} args
* @param {any} opts
* @param {Command} command
* @returns
*/
async function action(args, opts, command) { async function action(args, opts, command) {
var input = opts.input || args; const input = opts.input || args;
var output = opts.output; let output = opts.output;
var config = {}; /** @type {any} */
let 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 != null) {
if ( if (
@ -155,13 +149,8 @@ async function action(args, opts, command) {
return command.help(); return command.help();
} }
if ( if (process?.versions?.node && PKG.engines.node) {
typeof process == 'object' && // @ts-expect-error We control this and ensure it is never null.
process.versions &&
process.versions.node &&
PKG &&
PKG.engines.node
) {
var nodeVersion = String(PKG.engines.node).match(/\d*(\.\d+)*/)[0]; var nodeVersion = String(PKG.engines.node).match(/\d*(\.\d+)*/)[0];
if (parseFloat(process.versions.node) < parseFloat(nodeVersion)) { if (parseFloat(process.versions.node) < parseFloat(nodeVersion)) {
throw Error( throw Error(
@ -188,13 +177,20 @@ async function action(args, opts, command) {
// --exclude // --exclude
config.exclude = opts.exclude config.exclude = opts.exclude
? opts.exclude.map((pattern) => RegExp(pattern)) ? opts.exclude.map((/** @type {string} */ pattern) => RegExp(pattern))
: []; : [];
// --precision // --precision
if (opts.precision != null) { if (opts.precision != null) {
var precision = Math.min(Math.max(0, opts.precision), 20); const number = Number.parseInt(opts.precision, 10);
config.floatPrecision = precision; 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 // --multipass
@ -227,7 +223,7 @@ async function action(args, opts, command) {
if (output) { if (output) {
if (input.length && input[0] != '-') { if (input.length && input[0] != '-') {
if (output.length == 1 && checkIsDir(output[0])) { if (output.length == 1 && checkIsDir(output[0])) {
var dir = output[0]; const dir = output[0];
for (var i = 0; i < input.length; i++) { for (var i = 0; i < input.length; i++) {
output[i] = checkIsDir(input[i]) output[i] = checkIsDir(input[i])
? input[i] ? input[i]
@ -270,7 +266,9 @@ async function action(args, opts, command) {
// file // file
} else { } else {
await Promise.all( await Promise.all(
input.map((file, n) => optimizeFile(config, file, output[n])), input.map((/** @type {string} */ file, /** @type {number} */ n) =>
optimizeFile(config, file, output[n]),
),
); );
} }
@ -285,10 +283,10 @@ async function action(args, opts, command) {
/** /**
* Optimize SVG files in a directory. * Optimize SVG files in a directory.
* *
* @param {Object} config options * @param {any} config options
* @param {string} dir input directory * @param {string} dir input directory
* @param {string} output output directory * @param {string} output output directory
* @return {Promise} * @return {Promise<any>}
*/ */
function optimizeFolder(config, dir, output) { function optimizeFolder(config, dir, output) {
if (!config.quiet) { if (!config.quiet) {
@ -302,11 +300,11 @@ function optimizeFolder(config, dir, output) {
/** /**
* Process given files, take only SVG. * Process given files, take only SVG.
* *
* @param {Object} config options * @param {any} config options
* @param {string} dir input directory * @param {string} dir input directory
* @param {Array} files list of file names in the directory * @param {string[]} files list of file names in the directory
* @param {string} output output directory * @param {string} output output directory
* @return {Promise} * @return {Promise<any>}
*/ */
function processDirectory(config, dir, files, output) { function processDirectory(config, dir, files, output) {
// take only *.svg files, recursively if necessary // take only *.svg files, recursively if necessary
@ -330,43 +328,46 @@ function processDirectory(config, dir, files, output) {
/** /**
* Get SVG files descriptions. * Get SVG files descriptions.
* *
* @param {Object} config options * @param {any} config options
* @param {string} dir input directory * @param {string} dir input directory
* @param {Array} files list of file names in the directory * @param {string[]} files list of file names in the directory
* @param {string} output output directory * @param {string} output output directory
* @return {Array} * @return {any[]}
*/ */
function getFilesDescriptions(config, dir, files, output) { function getFilesDescriptions(config, dir, files, output) {
const filesInThisFolder = files const filesInThisFolder = files
.filter( .filter(
(name) => (name) =>
regSVGFile.test(name) && name.slice(-4).toLowerCase() === '.svg' &&
!config.exclude.some((regExclude) => regExclude.test(name)), !config.exclude.some((/** @type {RegExp} */ regExclude) =>
regExclude.test(name),
),
) )
.map((name) => ({ .map((name) => ({
inputPath: path.resolve(dir, name), inputPath: path.resolve(dir, name),
outputPath: path.resolve(output, name), outputPath: path.resolve(output, name),
})); }));
return config.recursive if (!config.recursive) {
? [].concat( return filesInThisFolder;
filesInThisFolder, }
files
.filter((name) => checkIsDir(path.resolve(dir, name))) return filesInThisFolder.concat(
.map((subFolderName) => { files
const subFolderPath = path.resolve(dir, subFolderName); .filter((name) => checkIsDir(path.resolve(dir, name)))
const subFolderFiles = fs.readdirSync(subFolderPath); .map((subFolderName) => {
const subFolderOutput = path.resolve(output, subFolderName); const subFolderPath = path.resolve(dir, subFolderName);
return getFilesDescriptions( const subFolderFiles = fs.readdirSync(subFolderPath);
config, const subFolderOutput = path.resolve(output, subFolderName);
subFolderPath, return getFilesDescriptions(
subFolderFiles, config,
subFolderOutput, subFolderPath,
); subFolderFiles,
}) subFolderOutput,
.reduce((a, b) => [].concat(a, b), []), );
) })
: filesInThisFolder; .reduce((a, b) => a.concat(b), []),
);
} }
/** /**
@ -375,7 +376,7 @@ function getFilesDescriptions(config, dir, files, output) {
* @param {Object} config options * @param {Object} config options
* @param {string} file * @param {string} file
* @param {string} output * @param {string} output
* @return {Promise} * @return {Promise<any>}
*/ */
function optimizeFile(config, file, output) { function optimizeFile(config, file, output) {
return fs.promises.readFile(file, 'utf8').then( return fs.promises.readFile(file, 'utf8').then(
@ -387,13 +388,14 @@ function optimizeFile(config, file, output) {
/** /**
* Optimize SVG data. * Optimize SVG data.
* *
* @param {Object} config options * @param {any} config options
* @param {?{ path: string }} info
* @param {string} data SVG content to optimize * @param {string} data SVG content to optimize
* @param {string} output where to write optimized file * @param {string} output where to write optimized file
* @param {string} [input] input file name (being used if output is a directory) * @param {any} input input file name (being used if output is a directory)
* @return {Promise} * @return {Promise<any>}
*/ */
function processSVGData(config, info, data, output, input) { function processSVGData(config, info, data, output, input = undefined) {
var startTime = Date.now(), var startTime = Date.now(),
prevFileSize = Buffer.byteLength(data, 'utf8'); prevFileSize = Buffer.byteLength(data, 'utf8');
@ -401,7 +403,7 @@ function processSVGData(config, info, data, output, input) {
try { try {
result = optimize(data, { ...config, ...info }); result = optimize(data, { ...config, ...info });
} catch (error) { } catch (error) {
if (error.name === 'SvgoParserError') { if (error instanceof SvgoParserError) {
console.error(colors.red(error.toString())); console.error(colors.red(error.toString()));
process.exit(1); process.exit(1);
} else { } else {
@ -441,7 +443,7 @@ function processSVGData(config, info, data, output, input) {
* @param {string} input * @param {string} input
* @param {string} output output file name. '-' for stdout * @param {string} output output file name. '-' for stdout
* @param {string} data data to write * @param {string} data data to write
* @return {Promise} * @return {Promise<void>}
*/ */
function writeOutput(input, output, data) { function writeOutput(input, output, data) {
if (output == '-') { if (output == '-') {
@ -488,11 +490,11 @@ function printProfitInfo(inBytes, outBytes) {
/** /**
* Check for errors, if it's a dir optimize the dir. * Check for errors, if it's a dir optimize the dir.
* *
* @param {Object} config * @param {any} config
* @param {string} input * @param {string} input
* @param {string} output * @param {string} output
* @param {Error} error * @param {Error & { code: string, path: string }} error
* @return {Promise} * @return {Promise<void>}
*/ */
function checkOptimizeFileError(config, input, output, error) { function checkOptimizeFileError(config, input, output, error) {
if (error.code == 'EISDIR') { if (error.code == 'EISDIR') {
@ -511,8 +513,8 @@ function checkOptimizeFileError(config, input, output, error) {
* @param {string} input * @param {string} input
* @param {string} output * @param {string} output
* @param {string} data * @param {string} data
* @param {Error} error * @param {Error & { code: string }} error
* @return {Promise} * @return {Promise<void>}
*/ */
function checkWriteFileError(input, output, data, error) { function checkWriteFileError(input, output, data, error) {
if (error.code == 'EISDIR' && input) { if (error.code == 'EISDIR' && input) {

View File

@ -1,49 +1,95 @@
/**
* @typedef {import('../types.js').XastChild} XastChild
* @typedef {import('../types.js').XastElement} XastElement
* @typedef {import('../types.js').XastNode} XastNode
* @typedef {import('../types.js').XastParent} XastParent
*/
/**
* @param {XastNode} node
* @returns {boolean}
*/
const isTag = (node) => { const isTag = (node) => {
return node.type === 'element'; return node.type === 'element';
}; };
/**
* @param {<T>(v: T) => boolean} test
* @param {XastNode[]} elems
* @returns {boolean}
*/
const existsOne = (test, elems) => { const existsOne = (test, elems) => {
return elems.some((elem) => { return elems.some((elem) => {
if (isTag(elem)) { return isTag(elem) && (test(elem) || existsOne(test, getChildren(elem)));
return test(elem) || existsOne(test, getChildren(elem));
} else {
return false;
}
}); });
}; };
/**
* @param {XastElement} elem
* @param {string} name
* @returns {?string}
*/
const getAttributeValue = (elem, name) => { const getAttributeValue = (elem, name) => {
return elem.attributes[name]; return elem.attributes[name];
}; };
/**
* @param {XastNode & { children?: XastChild[] }} node
* @returns {XastChild[]}
*/
const getChildren = (node) => { const getChildren = (node) => {
return node.children || []; return node.children || [];
}; };
/**
* @param {XastElement} elemAst
* @returns {string}
*/
const getName = (elemAst) => { const getName = (elemAst) => {
return elemAst.name; return elemAst.name;
}; };
/**
* @param {XastNode & { parentNode?: XastParent }} node
* @returns {?XastParent}
*/
const getParent = (node) => { const getParent = (node) => {
return node.parentNode || null; return node.parentNode || null;
}; };
/**
* @param {XastElement} elem
* @returns {XastChild[]}
*/
const getSiblings = (elem) => { const getSiblings = (elem) => {
var parent = getParent(elem); const parent = getParent(elem);
return parent ? getChildren(parent) : []; return parent ? getChildren(parent) : [];
}; };
/**
* @param {XastParent} node
* @returns {string}
*/
const getText = (node) => { const getText = (node) => {
if (node.children[0].type === 'text' && node.children[0].type === 'cdata') { if (node.children[0].type === 'text' || node.children[0].type === 'cdata') {
return node.children[0].value; return node.children[0].value;
} }
return ''; return '';
}; };
/**
* @param {XastElement} elem
* @param {string} name
* @returns {boolean}
*/
const hasAttrib = (elem, name) => { const hasAttrib = (elem, name) => {
return elem.attributes[name] !== undefined; return elem.attributes[name] !== undefined;
}; };
/**
* @param {Array<?XastNode>} nodes
* @returns {Array<?XastNode>}
*/
const removeSubsets = (nodes) => { const removeSubsets = (nodes) => {
let idx = nodes.length; let idx = nodes.length;
let node; let node;
@ -72,6 +118,11 @@ const removeSubsets = (nodes) => {
return nodes; return nodes;
}; };
/**
* @param {<T>(v: T) => boolean} test
* @param {XastNode[]} elems
* @returns {XastNode[]}
*/
const findAll = (test, elems) => { const findAll = (test, elems) => {
const result = []; const result = [];
for (const elem of elems) { for (const elem of elems) {
@ -85,6 +136,11 @@ const findAll = (test, elems) => {
return result; return result;
}; };
/**
* @param {<T>(v: T) => boolean} test
* @param {XastNode[]} elems
* @returns {?XastNode}
*/
const findOne = (test, elems) => { const findOne = (test, elems) => {
for (const elem of elems) { for (const elem of elems) {
if (isTag(elem)) { if (isTag(elem)) {
@ -100,7 +156,7 @@ const findOne = (test, elems) => {
return null; return null;
}; };
const svgoCssSelectAdapter = { export default {
isTag, isTag,
existsOne, existsOne,
getAttributeValue, getAttributeValue,
@ -114,5 +170,3 @@ const svgoCssSelectAdapter = {
findAll, findAll,
findOne, findOne,
}; };
export default svgoCssSelectAdapter;

View File

@ -1,8 +1,9 @@
import { visit } from '../xast.js'; import { visit } from '../xast.js';
/** /**
* @typedef {import('../svgo').BuiltinPlugin<string, Object>} BuiltinPlugin * @typedef {import('../svgo.js').BuiltinPlugin<string, ?>} BuiltinPlugin
* @typedef {import('../svgo').BuiltinPluginOrPreset<?, ?>} BuiltinPreset * @typedef {import('../svgo.js').BuiltinPluginOrPreset<?, ?>} BuiltinPreset
* @typedef {import('../types.js').XastNode} XastNode
*/ */
/** /**
@ -10,10 +11,11 @@ import { visit } from '../xast.js';
* *
* @module plugins * @module plugins
* *
* @param {Object} ast input ast * @param {XastNode} ast Input AST.
* @param {Object} info extra information * @param {Object} info Extra information.
* @param {Array} plugins plugins object from config * @param {Array<any>} plugins Plugins property from config.
* @return {Object} output ast * @param {any} overrides
* @param {any} globalOverrides
*/ */
export const invokePlugins = ( export const invokePlugins = (
ast, ast,

View File

@ -116,6 +116,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0", "@eslint/js": "^9.3.0",
"@jest/globals": "^29.7.0",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
@ -135,7 +136,7 @@
"rimraf": "^5.0.7", "rimraf": "^5.0.7",
"rollup": "^4.17.2", "rollup": "^4.17.2",
"tar-stream": "^3.1.7", "tar-stream": "^3.1.7",
"typescript": "^5.4.5" "typescript": "^5.8.3"
}, },
"resolutions": { "resolutions": {
"sax@^1.4.1": "patch:sax@npm%3A1.4.1#./.yarn/patches/sax-npm-1.4.1-503b1923cb.patch" "sax@^1.4.1": "patch:sax@npm%3A1.4.1#./.yarn/patches/sax-npm-1.4.1-503b1923cb.patch"

View File

@ -54,9 +54,7 @@ export const fn = (_root, params) => {
if (match) { if (match) {
// round it to the fixed precision // round it to the fixed precision
let num = Number(Number(match[1]).toFixed(floatPrecision)); let num = Number(Number(match[1]).toFixed(floatPrecision));
/** /** @type {any} */
* @type {any}
*/
let matchedUnit = match[3] || ''; let matchedUnit = match[3] || '';
/** /**
* @type{'' | keyof typeof absoluteLengths} * @type{'' | keyof typeof absoluteLengths}

View File

@ -60,13 +60,9 @@ export const fn = (_root, params) => {
if (match) { if (match) {
// round it to the fixed precision // round it to the fixed precision
let num = Number(Number(match[1]).toFixed(floatPrecision)); let num = Number(Number(match[1]).toFixed(floatPrecision));
/** /** @type {any} */
* @type {any}
*/
let matchedUnit = match[3] || ''; let matchedUnit = match[3] || '';
/** /** @type {'' | keyof typeof absoluteLengths} */
* @type{'' | keyof typeof absoluteLengths}
*/
let units = matchedUnit; let units = matchedUnit;
// convert absolute values to pixels // convert absolute values to pixels

View File

@ -78,7 +78,6 @@ export const fn = (root, params) => {
const cssText = node.children const cssText = node.children
.filter((child) => child.type === 'text' || child.type === 'cdata') .filter((child) => child.type === 'text' || child.type === 'cdata')
// @ts-expect-error
.map((child) => child.value) .map((child) => child.value)
.join(''); .join('');
@ -368,7 +367,6 @@ export const fn = (root, params) => {
}, },
}); });
// csstree v2 changed this type
if (style.cssAst.children.isEmpty) { if (style.cssAst.children.isEmpty) {
// remove empty style element // remove empty style element
detachNodeFromParent(style.node, style.parentNode); detachNodeFromParent(style.node, style.parentNode);

View File

@ -9,15 +9,5 @@
"strict": true, "strict": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["lib/**/*.js", "plugins/**/*", "test/cli/**/*"], "include": ["lib/**/*.js", "plugins/**/*", "test/cli/**/*"]
"exclude": [
"lib/svgo-node.js",
"lib/svgo-node.test.js",
"lib/svgo.js",
"lib/builtin.js",
"lib/svgo.test.js",
"lib/svgo/**/*.js",
"plugins/plugins.js",
"plugins/preset-default.js"
]
} }

View File

@ -4491,6 +4491,7 @@ __metadata:
resolution: "svgo@workspace:." resolution: "svgo@workspace:."
dependencies: dependencies:
"@eslint/js": ^9.3.0 "@eslint/js": ^9.3.0
"@jest/globals": ^29.7.0
"@rollup/plugin-commonjs": ^25.0.7 "@rollup/plugin-commonjs": ^25.0.7
"@rollup/plugin-node-resolve": ^15.2.3 "@rollup/plugin-node-resolve": ^15.2.3
"@rollup/plugin-terser": ^0.4.4 "@rollup/plugin-terser": ^0.4.4
@ -4517,7 +4518,7 @@ __metadata:
rollup: ^4.17.2 rollup: ^4.17.2
sax: ^1.4.1 sax: ^1.4.1
tar-stream: ^3.1.7 tar-stream: ^3.1.7
typescript: ^5.4.5 typescript: ^5.8.3
bin: bin:
svgo: ./bin/svgo.js svgo: ./bin/svgo.js
languageName: unknown languageName: unknown
@ -4626,23 +4627,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@npm:^5.4.5": "typescript@npm:^5.8.3":
version: 5.4.5 version: 5.8.3
resolution: "typescript@npm:5.4.5" resolution: "typescript@npm:5.8.3"
bin: bin:
tsc: bin/tsc tsc: bin/tsc
tsserver: bin/tsserver tsserver: bin/tsserver
checksum: 53c879c6fa1e3bcb194b274d4501ba1985894b2c2692fa079db03c5a5a7140587a1e04e1ba03184605d35f439b40192d9e138eb3279ca8eee313c081c8bcd9b0 checksum: cb1d081c889a288b962d3c8ae18d337ad6ee88a8e81ae0103fa1fecbe923737f3ba1dbdb3e6d8b776c72bc73bfa6d8d850c0306eed1a51377d2fccdfd75d92c4
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@patch:typescript@^5.4.5#~builtin<compat/typescript>": "typescript@patch:typescript@^5.8.3#~builtin<compat/typescript>":
version: 5.4.5 version: 5.8.3
resolution: "typescript@patch:typescript@npm%3A5.4.5#~builtin<compat/typescript>::version=5.4.5&hash=5adc0c" resolution: "typescript@patch:typescript@npm%3A5.8.3#~builtin<compat/typescript>::version=5.8.3&hash=5786d5"
bin: bin:
tsc: bin/tsc tsc: bin/tsc
tsserver: bin/tsserver tsserver: bin/tsserver
checksum: d59e26e74f6b444517d0ba16e0ee16e75c652c2b49a59f2ebdbeb16647a855fd50c7fc786a58987e45f03bce0677092e2dd3333649fd53b11d0b0d271455837c checksum: f1743b6850976b3debf7cbd53d2bc0b67e75d47eb6410db564c8bb475e92a8a48f8a3fcd14d89cf93426835281c31a8f8a94dad90be4dc899279a898532ba97f
languageName: node languageName: node
linkType: hard linkType: hard