From 92ee2fbda345c6eeb4c01921b33eb328ad9e531e Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:30:45 -0800 Subject: [PATCH] fix(svgrloader): isolate SVG transforms from discovered SVGR configuration The default SVGR options now set runtimeConfig to false, which skips svgr.config.js in the site directory and in ancestor directories. Sites that still want file discovery pass svgrConfig with runtimeConfig true. The plugin docs record the new default and point configuration at the svgrConfig option. Loader tests cover a parent TypeScript config, that opt-in, a config inside the site, and a parent file that does not parse. Fixes #8646 --- .../src/__tests__/svgrLoader.test.ts | 78 +++++++++++++++++++ .../docusaurus-plugin-svgr/src/svgrLoader.ts | 3 + website/docs/api/plugins/plugin-svgr.mdx | 3 + 3 files changed, 84 insertions(+) diff --git a/packages/docusaurus-plugin-svgr/src/__tests__/svgrLoader.test.ts b/packages/docusaurus-plugin-svgr/src/__tests__/svgrLoader.test.ts index b022e076eecf..7e19bb3f5f54 100644 --- a/packages/docusaurus-plugin-svgr/src/__tests__/svgrLoader.test.ts +++ b/packages/docusaurus-plugin-svgr/src/__tests__/svgrLoader.test.ts @@ -6,6 +6,8 @@ */ import {describe, expect, it} from 'vitest'; +import {mkdir, mkdtempDisposable, realpath, writeFile} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; import path from 'node:path'; import {transform} from '@svgr/core'; import svgo from '@svgr/plugin-svgo'; @@ -15,6 +17,8 @@ import type {SVGRConfig} from '../options'; const siteDir = path.join(__dirname, 'site'); +const typescriptSvgrConfig = 'module.exports = {typescript: true};\n'; + function gradientSvg(color: string) { return ` Gradient @@ -53,6 +57,35 @@ function getIds(code: string): string[] { ); } +// SVGR searches from the SVG file upward, so each case needs its own directory. +// Cosmiconfig caches a search path for the process lifetime. +async function withSvgrConfigFile( + configSource: string, + location: 'parent' | 'site', +): Promise<{siteDirectory: string; filePath: string} & AsyncDisposable> { + const parent = await mkdtempDisposable( + path.join(await realpath(tmpdir()), 'docusaurus-svgr-'), + ); + const siteDirectory = path.join(parent.path, 'site'); + const filePath = 'src/icon.svg'; + await mkdir(path.join(siteDirectory, 'src'), {recursive: true}); + await writeFile(path.join(siteDirectory, filePath), gradientSvg('red')); + const configDirectory = location === 'parent' ? parent.path : siteDirectory; + await writeFile(path.join(configDirectory, 'svgr.config.js'), configSource); + return { + siteDirectory, + filePath, + [Symbol.asyncDispose]: parent[Symbol.asyncDispose], + }; +} + +function expectPlainReactSvg(code: string) { + expect(code).toContain('export default'); + expect(code).toContain('viewBox="0 0 10 10"'); + expect(code).not.toContain('interface SVGRProps'); + expect(code).not.toContain('import type { SVGProps }'); +} + describe('createSVGRConfig', () => { it('prefixes SVG ids to avoid clashes between inline SVGs', async () => { const codeA = await transformSvg({ @@ -122,4 +155,49 @@ describe('createSVGRConfig', () => { expect(getIds(code)).toEqual(['a']); expect(code).toContain('fill="url(#a)"'); }); + + it('ignores a parent svgr config that enables TypeScript output', async () => { + await using site = await withSvgrConfigFile(typescriptSvgrConfig, 'parent'); + const code = await transformSvg({ + svg: gradientSvg('red'), + filePath: site.filePath, + siteDirectory: site.siteDirectory, + }); + expectPlainReactSvg(code); + }); + + it('loads a parent svgr config when runtimeConfig is enabled', async () => { + await using site = await withSvgrConfigFile(typescriptSvgrConfig, 'parent'); + const code = await transformSvg({ + svg: gradientSvg('red'), + filePath: site.filePath, + siteDirectory: site.siteDirectory, + svgrConfig: {runtimeConfig: true}, + }); + expect(code).toContain('interface SVGRProps'); + expect(code).toContain('import type { SVGProps }'); + }); + + it('ignores an svgr config in the site directory', async () => { + await using site = await withSvgrConfigFile(typescriptSvgrConfig, 'site'); + const code = await transformSvg({ + svg: gradientSvg('red'), + filePath: site.filePath, + siteDirectory: site.siteDirectory, + }); + expectPlainReactSvg(code); + }); + + it('ignores a malformed svgr config discovered from a parent directory', async () => { + await using site = await withSvgrConfigFile( + 'module.exports = {\n', + 'parent', + ); + const code = await transformSvg({ + svg: gradientSvg('red'), + filePath: site.filePath, + siteDirectory: site.siteDirectory, + }); + expectPlainReactSvg(code); + }); }); diff --git a/packages/docusaurus-plugin-svgr/src/svgrLoader.ts b/packages/docusaurus-plugin-svgr/src/svgrLoader.ts index c9363db21e7a..a3ac0dd0ce41 100644 --- a/packages/docusaurus-plugin-svgr/src/svgrLoader.ts +++ b/packages/docusaurus-plugin-svgr/src/svgrLoader.ts @@ -68,6 +68,9 @@ function createDefaultSVGRConfig({siteDir}: {siteDir: string}): SVGRConfig { svgo: true, svgoConfig: createDefaultSVGOConfig({siteDir}), titleProp: true, + // Skip svgr.config.js discovered in the site directory or an ancestor. + // See https://github.com/meta/docusaurus/issues/8646 + runtimeConfig: false, }; } diff --git a/website/docs/api/plugins/plugin-svgr.mdx b/website/docs/api/plugins/plugin-svgr.mdx index 53a11bed7ad2..6dca55b54376 100644 --- a/website/docs/api/plugins/plugin-svgr.mdx +++ b/website/docs/api/plugins/plugin-svgr.mdx @@ -61,6 +61,7 @@ const defaultSvgrConfig = { prettier: false, svgo: true, titleProp: true, + runtimeConfig: false, svgoConfig: { plugins: [ { @@ -88,4 +89,6 @@ const defaultSvgrConfig = { The `prefixIds` plugin prefixes SVG ids (for example, `id="a"` becomes `id="svg-3f2a1c-a"`) to avoid clashes between multiple SVGs inlined in the same page. Class names are not prefixed. +`runtimeConfig: false` disables discovery of [SVGR configuration files](https://react-svgr.com/docs/configuration-files/) (for example `svgr.config.js`) in the site directory and ancestor directories. Configure SVGR through the `svgrConfig` option. + Providing `svgoConfig` replaces the default [SVGO](https://svgo.dev/) config entirely. Use `svgoConfig: undefined` to read your own [`svgo.config.js` file](https://svgo.dev/docs/introduction/#configuration) instead.