Skip to content
Open
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

| [<img src="https://sheetjs.com/sketch128.png" width="80">](https://sheetjs.com/) | [<img src="https://raw.githubusercontent.com/fontello/fontello/8.0.0/fontello-image.svg" width="80">](https://fontello.com/) |
Expand Down
18 changes: 16 additions & 2 deletions docs/02-usage/02-browser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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, <abbr title="Static Site Generator">SSG</abbr> like Jekyll, or just plain old HTML.
Expand Down
25 changes: 18 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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",
Expand Down
34 changes: 34 additions & 0 deletions test/exports.js
Original file line number Diff line number Diff line change
@@ -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({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not very convinced about this test, since it is assuming something about how the bundler works internally, rather than testing a real bundler.

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);
7 changes: 7 additions & 0 deletions test/tsconfig.browser.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"customConditions": ["browser"]
},
"include": ["types/browser.test-d.ts"]
}
4 changes: 4 additions & 0 deletions test/types/browser.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { mapNodesToParents, optimize } from 'svgo';

optimize('<svg />');
mapNodesToParents({ type: 'root', children: [] });
36 changes: 36 additions & 0 deletions test/types/lib/svgo.test-d.ts
Original file line number Diff line number Diff line change
@@ -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('<svg></svg>')).toEqualTypeOf<Output>();
assertType<DataUri>('enc');

expectTypeOf(loadConfig()).toEqualTypeOf<Promise<Config | null>>();
expectTypeOf(loadConfig(undefined)).toEqualTypeOf<Promise<Config | null>>();
expectTypeOf(loadConfig(null)).toEqualTypeOf<Promise<Config | null>>();
expectTypeOf(loadConfig('svgo.config.js')).toEqualTypeOf<Promise<Config>>();

const presetDefault = builtinPlugins.find(
(plugin) => plugin.name === 'preset-default',
)!;
if (!presetDefault.isPreset) {
throw Error('Could not find preset-default.');
}

expectTypeOf(presetDefault.plugins).toEqualTypeOf<
ReadonlyArray<BuiltinPlugin<string, Object>>
>();
expectTypeOf(presetDefault.name).toEqualTypeOf<'preset-default'>();
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dist/",
"node_modules/",
"rollup.config.js",
"test/exports.js",
"test/browser.js",
"test/svgo.cjs"
]
Expand Down
Loading