From f6f7fd77893133294427ab46a0d725b8feb83ef3 Mon Sep 17 00:00:00 2001 From: Ludovico Fischer Date: Mon, 24 Aug 2026 13:38:56 +0200 Subject: [PATCH 1/2] feat: add browser conditional export Expose the browser bundle from the root `svgo` entry when a resolver enables the `browser` condition, retaining Node-specific `import` and `require` entries under `node` and `default`. Document resolution, the `svgo/browser` fallback, and TypeScript `customConditions: ["browser"]`. Add runtime export coverage and a browser-specific typecheck, run by the existing test and typecheck package.json scripts. --- README.md | 10 ++++++++++ docs/02-usage/02-browser.mdx | 18 ++++++++++++++++-- package.json | 25 +++++++++++++++++------- test/exports.js | 34 +++++++++++++++++++++++++++++++++ test/tsconfig.browser.json | 7 +++++++ test/types/browser.test-d.ts | 4 ++++ test/types/lib/svgo.test-d.ts | 36 +++++++++++++++++++++++++++++++++++ 7 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 test/exports.js create mode 100644 test/tsconfig.browser.json create mode 100644 test/types/browser.test-d.ts create mode 100644 test/types/lib/svgo.test-d.ts diff --git a/README.md b/README.md index d0f70ecf0..cd19d437c 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,16 @@ You can also specify a path and customize the current working directory. const config = await loadConfig(configFile, cwd); ``` +## Module resolution + +`svgo` is exposed through three conditional exports: + +- `default` — the Node.js build, which additionally exposes `loadConfig`. +- `browser` — the bundled browser build, selected when a bundler targets the browser. +- `node` — same as the default build + +In most cases you can just `import { optimize } from 'svgo'` and let the runtime pick the right build. + ## Donors | [](https://sheetjs.com/) | [](https://fontello.com/) | diff --git a/docs/02-usage/02-browser.mdx b/docs/02-usage/02-browser.mdx index 7a1b30e62..d9e74f0c7 100644 --- a/docs/02-usage/02-browser.mdx +++ b/docs/02-usage/02-browser.mdx @@ -8,13 +8,21 @@ SVGO can run in the browser, but how to use it depends on the structure of your These instructions are for when you're writing your website in a Node.js environment, probably with tools like [webpack](https://webpack.js.org/) or [Rollup](https://rollupjs.org/) to build it. -Ensure you've installed the dependency by following the instructions in [Getting Started](../01-index.mdx#installation), then you can import `svgo/browser` to use SVGO on client-side. +Ensure you've installed the dependency by following the instructions in [Getting Started](../01-index.mdx#installation). + +### Module resolution + +SVGO resolves `svgo` through three conditional exports: + +- `default` — resolves to the Node.js build (which additionally exposes `loadConfig`). +- `browser` — resolves to the bundled browser build when a bundler enables the [`browser` condition](https://nodejs.org/api/packages.html#community-conditions-definitions). +- `node` — resolves to the Node.js build (which additionally exposes `loadConfig`). Here's a minimal example using [React](https://react.dev/): ```js import React from 'react'; -import { optimize } from 'svgo/browser'; +import { optimize } from 'svgo'; export default function SvgoDemo(props) { const { svg, svgoConfig } = props; @@ -29,6 +37,12 @@ export default function SvgoDemo(props) { } ``` +You can just import `optimize` from `svgo` and let the bundler resolve the browser version. This allows you to bundle for browser environments without changing the import. If for some reason this does not work, or you want to explicitly import the SVGO browser build, import `svgo/browser` instead of 'svgo'; + +If your bundler resolves `import 'svgo'` to the browser bundle, TypeScript might keep resolving the Node.js types. Add `"customConditions": ["browser"]` to select the matching browser types. See the [TypeScript `customConditions` documentation](https://www.typescriptlang.org/tsconfig/customConditions.html) for the supported `moduleResolution` values and configuration. + +To force the Node.js build when bundling for the browser, remove the `browser` condition from your resolver. Refer to your bundler's documentation for the exact configuration. + ## Without Build Tools These instructions are for when you want to fetch the SVGO browser bundle from client-side. For example, when you're building a static website with a templating engine like Handlebars, SSG like Jekyll, or just plain old HTML. diff --git a/package.json b/package.json index 3b20c9dce..68db912d0 100644 --- a/package.json +++ b/package.json @@ -54,13 +54,24 @@ "types": "./types/lib/svgo-node.d.ts", "exports": { ".": { - "import": "./lib/svgo-node.js", - "require": "./dist/svgo-node.cjs", - "types": "./types/lib/svgo-node.d.ts" + "browser": { + "types": "./types/lib/svgo.d.ts", + "default": "./dist/svgo.browser.js" + }, + "node": { + "types": "./types/lib/svgo-node.d.ts", + "import": "./lib/svgo-node.js", + "require": "./dist/svgo-node.cjs" + }, + "default": { + "types": "./types/lib/svgo-node.d.ts", + "import": "./lib/svgo-node.js", + "require": "./dist/svgo-node.cjs" + } }, "./browser": { - "import": "./dist/svgo.browser.js", - "types": "./types/lib/svgo.d.ts" + "types": "./types/lib/svgo.d.ts", + "import": "./dist/svgo.browser.js" } }, "files": [ @@ -80,8 +91,8 @@ "build:types": "pnpm clean:types && tsc -p tsconfig.build.json", "lint": "eslint . && prettier --check .", "lint:fix": "eslint --fix . && prettier --write .", - "typecheck": "tsc", - "test": "vitest run", + "typecheck": "tsc && tsc -p test/tsconfig.browser.json", + "test": "vitest run && node ./test/exports.js", "test:bundles": "pnpm build:bundles && node ./test/svgo.cjs && node ./test/browser.js", "test:regression": "node ./test/regression/extract.js && node ./test/regression/optimize.js && node ./test/regression/compare.js --no-diff", "qa": "pnpm lint && pnpm typecheck && pnpm test && pnpm test:bundles && pnpm test:regression", diff --git a/test/exports.js b/test/exports.js new file mode 100644 index 000000000..a87392ad9 --- /dev/null +++ b/test/exports.js @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import { createRequire, registerHooks } from 'node:module'; + +const require = createRequire(import.meta.url); + +const nodeSvgo = require('svgo'); +assert.equal(typeof nodeSvgo.optimize, 'function'); +assert.equal(typeof nodeSvgo.loadConfig, 'function'); + +// Add the browser condition to this import so Node resolves the browser branch +// of SVGO's root conditional export, just as a browser-targeting bundler would. +const hooks = registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier === 'svgo') { + return nextResolve(specifier, { + ...context, + conditions: [...context.conditions, 'browser'], + }); + } + return nextResolve(specifier, context); + }, +}); + +try { + const browserSvgo = await import('svgo'); + assert.equal(typeof browserSvgo.optimize, 'function'); + assert.equal('loadConfig' in browserSvgo, false); +} finally { + hooks.deregister(); +} + +const browserSubpathSvgo = await import('svgo/browser'); +assert.equal(typeof browserSubpathSvgo.optimize, 'function'); +assert.equal('loadConfig' in browserSubpathSvgo, false); diff --git a/test/tsconfig.browser.json b/test/tsconfig.browser.json new file mode 100644 index 000000000..54f6cde99 --- /dev/null +++ b/test/tsconfig.browser.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "customConditions": ["browser"] + }, + "include": ["types/browser.test-d.ts"] +} diff --git a/test/types/browser.test-d.ts b/test/types/browser.test-d.ts new file mode 100644 index 000000000..31b9346eb --- /dev/null +++ b/test/types/browser.test-d.ts @@ -0,0 +1,4 @@ +import { mapNodesToParents, optimize } from 'svgo'; + +optimize(''); +mapNodesToParents({ type: 'root', children: [] }); diff --git a/test/types/lib/svgo.test-d.ts b/test/types/lib/svgo.test-d.ts new file mode 100644 index 000000000..a3855cdbc --- /dev/null +++ b/test/types/lib/svgo.test-d.ts @@ -0,0 +1,36 @@ +import { assertType, expectTypeOf, test } from 'vitest'; +import { + BuiltinPlugin, + type Config, + type DataUri, + type Output, + builtinPlugins, + loadConfig, + optimize, +} from 'svgo'; + +// Resolving `svgo` by package name exercises the `types` export condition, +// which no other type test covers (svgo-node.test-d.ts imports by relative +// path). Under the default tsconfig this resolves to the node types entry. + +test('svgo types resolve by package name', () => { + expectTypeOf(optimize('')).toEqualTypeOf(); + assertType('enc'); + + expectTypeOf(loadConfig()).toEqualTypeOf>(); + expectTypeOf(loadConfig(undefined)).toEqualTypeOf>(); + expectTypeOf(loadConfig(null)).toEqualTypeOf>(); + expectTypeOf(loadConfig('svgo.config.js')).toEqualTypeOf>(); + + const presetDefault = builtinPlugins.find( + (plugin) => plugin.name === 'preset-default', + )!; + if (!presetDefault.isPreset) { + throw Error('Could not find preset-default.'); + } + + expectTypeOf(presetDefault.plugins).toEqualTypeOf< + ReadonlyArray> + >(); + expectTypeOf(presetDefault.name).toEqualTypeOf<'preset-default'>(); +}); From 5f13154995cd18e140737486cea88994516191a7 Mon Sep 17 00:00:00 2001 From: Ludovico Fischer Date: Mon, 24 Aug 2026 14:08:52 +0200 Subject: [PATCH 2/2] test: move exports test to test:bundle package.json script --- package.json | 4 ++-- tsconfig.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 68db912d0..e667365ca 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,8 @@ "lint": "eslint . && prettier --check .", "lint:fix": "eslint --fix . && prettier --write .", "typecheck": "tsc && tsc -p test/tsconfig.browser.json", - "test": "vitest run && node ./test/exports.js", - "test:bundles": "pnpm build:bundles && node ./test/svgo.cjs && node ./test/browser.js", + "test": "vitest run", + "test:bundles": "pnpm build:bundles && node ./test/svgo.cjs && node ./test/exports.js && node ./test/browser.js", "test:regression": "node ./test/regression/extract.js && node ./test/regression/optimize.js && node ./test/regression/compare.js --no-diff", "qa": "pnpm lint && pnpm typecheck && pnpm test && pnpm test:bundles && pnpm test:regression", "clean": "pnpm clean:build && pnpm clean:types", diff --git a/tsconfig.json b/tsconfig.json index eed424f34..d67231c55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "dist/", "node_modules/", "rollup.config.js", + "test/exports.js", "test/browser.js", "test/svgo.cjs" ]