Skip to content

fix: improve jsdoc types and remove excludes #2107

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

Merged
merged 1 commit into from
Apr 28, 2025
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
15 changes: 8 additions & 7 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
import SAX from 'sax';
import { textElems } from '../plugins/_collections.js';

class SvgoParserError extends Error {
export class SvgoParserError extends Error {
/**
* @param message {string}
* @param line {number}
* @param column {number}
* @param source {string}
* @param file {void | string}
* @param {string} message
* @param {number} line
* @param {number} column
* @param {string} source
* @param {string|undefined} file
*/
constructor(message, line, column, source, file) {
constructor(message, line, column, source, file = undefined) {
super(message);
this.name = 'SvgoParserError';
this.message = `${file || '<input>'}:${line}:${column}: ${message}`;
Expand All @@ -34,6 +34,7 @@ class SvgoParserError extends Error {
Error.captureStackTrace(this, SvgoParserError);
}
}

toString() {
const lines = this.source.split(/\r?\n/);
const startLine = Math.max(this.line - 3, 0);
Expand Down
10 changes: 3 additions & 7 deletions lib/stringifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,10 @@ const entities = {
/**
* convert XAST to SVG string
*
* @type {(data: XastRoot, config: StringifyOptions) => string}
* @type {(data: XastRoot, userOptions?: StringifyOptions) => string}
*/
export const stringifySvg = (data, userOptions = {}) => {
/**
* @type {Options}
*/
/** @type {Options} */
const config = { ...defaults, ...userOptions };
const indent = config.indent;
let newIndent = ' ';
Expand All @@ -81,9 +79,7 @@ export const stringifySvg = (data, userOptions = {}) => {
} else if (typeof indent === 'string') {
newIndent = indent;
}
/**
* @type {State}
*/
/** @type {State} */
const state = {
indent: newIndent,
textContext: null,
Expand Down
4 changes: 2 additions & 2 deletions lib/svgo-node.d.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
28 changes: 24 additions & 4 deletions lib/svgo-node.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os from 'os';
import fs from 'fs';
import { pathToFileURL } from 'url';
import path from 'path';
import {
VERSION,
Expand All @@ -11,10 +10,17 @@ import {
_collections,
} 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) => {
// dynamic import expects file url instead of path and may fail
// when windows path is provided
const imported = await import(pathToFileURL(configFile));
const imported = await import(path.resolve(configFile));
const config = imported.default;

if (config == null || typeof config !== 'object' || Array.isArray(config)) {
Expand All @@ -23,6 +29,10 @@ const importConfig = async (configFile) => {
return config;
};

/**
* @param {string} file
* @returns {Promise<boolean>}
*/
const isFile = async (file) => {
try {
const stats = await fs.promises.stat(file);
Expand All @@ -40,6 +50,11 @@ export {
_collections,
};

/**
* @param {string} configFile
* @param {string} cwd
* @returns {Promise<?Config>}
*/
export const loadConfig = async (configFile, cwd = process.cwd()) => {
if (configFile != null) {
if (path.isAbsolute(configFile)) {
Expand Down Expand Up @@ -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) => {
if (config == null) {
config = {};
Expand Down
2 changes: 1 addition & 1 deletion lib/svgo-node.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
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;
Expand Down
2 changes: 1 addition & 1 deletion lib/svgo.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type PluginConfig =
}[keyof BuiltinsWithRequiredParams]
| CustomPlugin;

type BuiltinPlugin<Name, Params> = {
export type BuiltinPlugin<Name, Params> = {
/** Name of the plugin, also known as the plugin ID. */
name: Name;
description?: string;
Expand Down
15 changes: 15 additions & 0 deletions lib/svgo.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import _collections from '../plugins/_collections.js';

/**
* @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();
Expand All @@ -31,6 +34,10 @@ function getPlugin(name) {
return pluginsMap.get(name);
}

/**
* @param {string|PluginConfig} plugin
* @returns {?PluginConfig}
*/
const resolvePluginConfig = (plugin) => {
if (typeof plugin === 'string') {
// resolve builtin plugin specified as string
Expand All @@ -49,6 +56,7 @@ const resolvePluginConfig = (plugin) => {
throw Error(`Plugin name must be specified`);
}
// use custom plugin implementation
// @ts-expect-error Checking for CustomPlugin with the presence of fn
let fn = plugin.fn;
if (fn == null) {
// resolve builtin plugin implementation
Expand All @@ -75,6 +83,11 @@ export {
_collections,
};

/**
* @param {string} input
* @param {Config} config
* @returns {Output}
*/
export const optimize = (input, config) => {
if (config == null) {
config = {};
Expand Down Expand Up @@ -107,6 +120,8 @@ export const optimize = (input, config) => {
'Warning: plugins list includes null or undefined elements, these will be ignored.',
);
}

/** @type {Config} */
const globalOverrides = {};
if (config.floatPrecision != null) {
globalOverrides.floatPrecision = config.floatPrecision;
Expand Down
70 changes: 38 additions & 32 deletions lib/svgo.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { jest } from '@jest/globals';
import { optimize } from './svgo.js';
import { SvgoParserError } from './parser.js';

/**
* @typedef {import('./svgo.js').CustomPlugin} CustomPlugin
*/

test('allow to setup default preset', () => {
const svg = `
Expand Down Expand Up @@ -65,6 +70,7 @@ test('warn when user tries enable plugins in preset', () => {
const warn = jest.spyOn(console, 'warn');
optimize(svg, {
plugins: [
// @ts-expect-error Testing if we receive config that diverges from type definitions.
{
name: 'preset-default',
params: {
Expand Down Expand Up @@ -138,6 +144,7 @@ describe('allow to configure EOL', () => {
</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 },
});
expect(data).toBe(
Expand Down Expand Up @@ -256,46 +263,47 @@ test('plugin precision should override preset precision', () => {
});

test('provides informative error in result', () => {
expect.assertions(6);
const svg = `<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg>
`;
try {
optimize(svg, { path: 'test.svg' });
} catch (error) {
expect(error.name).toBe('SvgoParserError');
expect(error.message).toBe('test.svg:2:33: Unquoted attribute value');
expect(error.reason).toBe('Unquoted attribute value');
expect(error.line).toBe(2);
expect(error.column).toBe(33);
expect(error.source).toBe(svg);
}
const error = new SvgoParserError(
'Unquoted attribute value',
2,
33,
svg,
'test.svg',
);
expect(() => optimize(svg, { path: 'test.svg' })).toThrow(error);
expect(error.name).toBe('SvgoParserError');
expect(error.message).toBe('test.svg:2:33: Unquoted attribute value');
});

test('provides code snippet in rendered error', () => {
expect.assertions(1);
const svg = `<svg viewBox="0 0 120 120">
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg>
`;
try {
optimize(svg, { path: 'test.svg' });
} catch (error) {
expect(error.toString())
.toBe(`SvgoParserError: test.svg:2:29: Unquoted attribute value
const error = new SvgoParserError(
'Unquoted attribute value',
2,
29,
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">
> 2 | <circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
| ^
3 | </svg>
4 |
`);
}
});

test('supports errors without path', () => {
expect.assertions(1);
const svg = `<svg viewBox="0 0 120 120">
<circle/>
<circle/>
Expand All @@ -309,11 +317,10 @@ test('supports errors without path', () => {
<circle fill="#ff0000" cx=60.444444" cy="60" r="50"/>
</svg>
`;
try {
optimize(svg);
} catch (error) {
expect(error.toString())
.toBe(`SvgoParserError: <input>:11:29: Unquoted attribute value
const error = new SvgoParserError('Unquoted attribute value', 11, 29, svg);
expect(() => optimize(svg)).toThrow(error);
expect(error.toString())
.toBe(`SvgoParserError: <input>:11:29: Unquoted attribute value

9 | <circle/>
10 | <circle/>
Expand All @@ -322,33 +329,32 @@ test('supports errors without path', () => {
12 | </svg>
13 |
`);
}
});

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">
<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>
`;
try {
optimize(svg);
} catch (error) {
expect(error.toString())
.toBe(`SvgoParserError: <input>:2:211: Invalid attribute name
const error = new SvgoParserError('Invalid attribute name', 2, 211, svg);

expect(() => optimize(svg)).toThrow(error);
expect(error.toString())
.toBe(`SvgoParserError: <input>:2:211: Invalid attribute name

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…
| ^
3 |
4 |
`);
}
});

test('multipass option should trigger plugins multiple times', () => {
const svg = `<svg id="abcdefghijklmnopqrstuvwxyz"></svg>`;
/** @type {number[]} */
const list = [];
/** @type {CustomPlugin} */
const testPlugin = {
name: 'testPlugin',
fn: (_root, _params, info) => {
Expand Down
Loading