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..e667365ca 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,9 +91,9 @@
"build:types": "pnpm clean:types && tsc -p tsconfig.build.json",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint --fix . && prettier --write .",
- "typecheck": "tsc",
+ "typecheck": "tsc && tsc -p test/tsconfig.browser.json",
"test": "vitest run",
- "test:bundles": "pnpm build:bundles && node ./test/svgo.cjs && node ./test/browser.js",
+ "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/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